Compare commits

..

4 Commits

Author SHA1 Message Date
Quentin McGaw
4b86f29f59 fix(docker): openvpn 2.4.12-r0 install 2022-03-31 20:52:31 +00:00
Quentin McGaw
12eafa0f65 fix(env): OPENVPN_FLAGS functionality 2022-03-31 20:45:40 +00:00
Quentin McGaw
7acca7bc10 fix(ci): docker metadata image tags
- Move metata as top step in publish workflow
- Simplify `v0.x.x` check
- Dynamically determine base branch
2022-03-22 08:56:56 +00:00
Quentin McGaw
5cc55c92fa 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:56:33 +00:00
809 changed files with 88416 additions and 274908 deletions

View File

@@ -1,2 +1,2 @@
FROM qmcgaw/godevcontainer FROM qmcgaw/godevcontainer
RUN apk add wireguard-tools htop openssl RUN apk add wireguard-tools

View File

@@ -9,21 +9,17 @@ It works on Linux, Windows and OSX.
- [VS code](https://code.visualstudio.com/download) installed - [VS code](https://code.visualstudio.com/download) installed
- [VS code remote 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 - [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 on your host if you don't have them:
```sh
touch ~/.gitconfig ~/.zsh_history
```
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P). 1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Remote-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
@@ -33,9 +29,13 @@ You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image.
```Dockerfile ```Dockerfile
FROM qmcgaw/godevcontainer FROM qmcgaw/godevcontainer
USER root
RUN apk add curl RUN apk add curl
USER vscode
``` ```
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
To rebuild the image, either: To rebuild the image, either:
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container` - With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
@@ -47,11 +47,11 @@ You can customize **settings** and **extensions** in the [devcontainer.json](dev
### Entrypoint script ### Entrypoint script
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
### Publish a port ### Publish a port
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml). You can also now do it directly with VSCode without restarting the container. To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
### Run other services ### Run other services

View File

@@ -1,73 +1,81 @@
{ {
"name": "gluetun-dev", "name": "gluetun-dev",
"dockerComposeFile": [ "dockerComposeFile": [
"docker-compose.yml" "docker-compose.yml"
], ],
"service": "vscode", "service": "vscode",
"runServices": [ "runServices": [
"vscode" "vscode"
], ],
"shutdownAction": "stopCompose", "shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy", "postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
// "overrideCommand": "", "extensions": [
"customizations": { "golang.go",
"vscode": { "eamodio.gitlens", // IDE Git information
"extensions": [ "davidanson.vscode-markdownlint",
"golang.go", "ms-azuretools.vscode-docker", // Docker integration and linting
"eamodio.gitlens", // IDE Git information "shardulm94.trailing-spaces", // Show trailing spaces
"davidanson.vscode-markdownlint", "Gruntfuggly.todo-tree", // Highlights TODO comments
"ms-azuretools.vscode-docker", // Docker integration and linting "bierner.emojisense", // Emoji sense for markdown
"shardulm94.trailing-spaces", // Show trailing spaces "stkb.rewrap", // rewrap comments after n characters on one line
"Gruntfuggly.todo-tree", // Highlights TODO comments "vscode-icons-team.vscode-icons", // Better file extension icons
"bierner.emojisense", // Emoji sense for markdown "github.vscode-pull-request-github", // Github interaction
"stkb.rewrap", // rewrap comments after n characters on one line "redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"vscode-icons-team.vscode-icons", // Better file extension icons "bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"github.vscode-pull-request-github", // Github interaction "IBM.output-colorizer", // Colorize your output/test logs
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting "mohsen1.prettify-json", // Prettify JSON data
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked ],
"IBM.output-colorizer", // Colorize your output/test logs "settings": {
"github.copilot" // AI code completion "files.eol": "\n",
], "remote.extensionKind": {
"settings": { "ms-azuretools.vscode-docker": "workspace"
"files.eol": "\n", },
"remote.extensionKind": { "editor.codeActionsOnSaveTimeout": 3000,
"ms-azuretools.vscode-docker": "workspace" "go.useLanguageServer": true,
}, "[go]": {
"go.useLanguageServer": true, "editor.formatOnSave": true,
"[go]": { "editor.codeActionsOnSave": {
"editor.codeActionsOnSave": { "source.organizeImports": true,
"source.organizeImports": true },
} // Optional: Disable snippets, as they conflict with completion ranking.
}, "editor.snippetSuggestions": "none"
"[go.mod]": { },
"editor.codeActionsOnSave": { "[go.mod]": {
"source.organizeImports": true "editor.formatOnSave": true,
} "editor.codeActionsOnSave": {
}, "source.organizeImports": true,
"gopls": { },
"usePlaceholders": false, },
"staticcheck": true "gopls": {
}, "usePlaceholders": false,
"go.lintTool": "golangci-lint", "staticcheck": true
"go.lintOnSave": "package", },
"editor.formatOnSave": true, "go.autocompleteUnimportedPackages": true,
"go.buildTags": "linux", "go.gotoSymbol.includeImports": true,
"go.toolsEnvVars": { "go.gotoSymbol.includeGoroot": true,
"CGO_ENABLED": "0" "go.lintTool": "golangci-lint",
}, "go.buildOnSave": "workspace",
"go.testEnvVars": { "go.lintOnSave": "workspace",
"CGO_ENABLED": "1" "go.vetOnSave": "workspace",
}, "editor.formatOnSave": true,
"go.testFlags": [ "go.toolsEnvVars": {
"-v", "GOFLAGS": "-tags=",
"-race" // "CGO_ENABLED": 1 // for the race detector
], },
"go.testTimeout": "10s", "gopls.env": {
"go.coverOnSingleTest": true, "GOFLAGS": "-tags="
"go.coverOnSingleTestFile": true, },
"go.coverOnTestPackage": true "go.testEnvVars": {
} "": ""
} },
} "go.testFlags": [
"-v",
// "-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true
}
} }

View File

@@ -1,28 +1,33 @@
version: "3.7" version: "3.7"
services: services:
vscode: vscode:
build: . build: .
volumes: image: godevcontainer
- ../:/workspace devices:
# Docker socket to access Docker server - /dev/net/tun:/dev/net/tun
- /var/run/docker.sock:/var/run/docker.sock volumes:
# SSH directory for Linux, OSX and WSL - ../:/workspace
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is # Docker socket to access Docker server
# created in the container. On Windows, files are copied - /var/run/docker.sock:/var/run/docker.sock
# from /mnt/ssh to ~/.ssh to fix permissions. # Docker configuration
- ~/.ssh:/mnt/ssh - ~/.docker:/root/.docker:z
# Shell history persistence # SSH directory for Linux, OSX and WSL
- ~/.zsh_history:/root/.zsh_history - ~/.ssh:/root/.ssh:z
# Git config # For Windows without WSL, a copy will be made
- ~/.gitconfig:/root/.gitconfig # from /tmp/.ssh to ~/.ssh to fix permissions
environment: #- ~/.ssh:/tmp/.ssh:ro
- TZ= # Shell history persistence
cap_add: - ~/.zsh_history:/root/.zsh_history:z
# For debugging with dlv # Git config
- SYS_PTRACE - ~/.gitconfig:/root/.gitconfig:z
- NET_ADMIN environment:
security_opt: - TZ=
# For debugging with dlv cap_add:
- seccomp:unconfined # For debugging with dlv
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ] # - 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,7 +40,6 @@ body:
attributes: attributes:
label: VPN service provider label: VPN service provider
options: options:
- AirVPN
- Custom - Custom
- Cyberghost - Cyberghost
- ExpressVPN - ExpressVPN
@@ -60,10 +54,8 @@ body:
- PrivateVPN - PrivateVPN
- ProtonVPN - ProtonVPN
- PureVPN - PureVPN
- SlickVPN
- Surfshark - Surfshark
- TorGuard - TorGuard
- VPNSecure.me
- VPNUnlimited - VPNUnlimited
- VyprVPN - VyprVPN
- WeVPN - WeVPN
@@ -80,7 +72,6 @@ body:
- Portainer - Portainer
- Kubernetes - Kubernetes
- Podman - Podman
- Unraid
- Other - Other
validations: validations:
required: true required: true
@@ -90,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
@@ -103,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,7 +1,4 @@
contact_links: contact_links:
- name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help? - name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new url: https://github.com/qdm12/gluetun/discussions/new
about: Please create a Github discussion. about: Please create a Github discussion.

View File

@@ -14,4 +14,4 @@ One of the following is required:
If the list of servers requires to login **or** is hidden behind an interactive configurator, If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file. you can only use a custom Openvpn configuration file.
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so. [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

51
.github/labels.yml vendored
View File

@@ -1,13 +1,18 @@
# Temporary status - name: "Bug :bug:"
- name: "🗯️ Waiting for feedback" color: "b60205"
color: "aadefa" description: ""
- name: "Feature request :bulb:"
color: "0e8a16"
description: ""
- name: "Help wanted :pray:"
color: "4caf50"
description: ""
- name: "Documentation :memo:"
color: "c5def5"
description: ""
- name: "Needs more info :thinking:"
color: "795548"
description: "" description: ""
- name: "🔴 Blocked"
color: "ff3f14"
description: "Blocked by another issue or pull request"
- name: "🔒 After next release"
color: "e8f274"
description: "Will be done after the next release"
# Priority # Priority
- name: "🚨 Urgent" - name: "🚨 Urgent"
@@ -17,18 +22,7 @@
color: "4285f4" color: "4285f4"
description: "" description: ""
# Complexity
- name: "☣️ Hard to do"
color: "7d0008"
description: ""
- name: "🟩 Easy to do"
color: "34cf43"
description: ""
# VPN providers # VPN providers
- name: ":cloud: AirVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Cyberghost" - name: ":cloud: Cyberghost"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
@@ -70,17 +64,12 @@
- name: ":cloud: PureVPN" - name: ":cloud: PureVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
- name: ":cloud: SlickVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Surfshark" - name: ":cloud: Surfshark"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
- name: ":cloud: Torguard" - name: ":cloud: Torguard"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
- name: ":cloud: VPNSecure.me"
color: "cfe8d4"
- name: ":cloud: VPNUnlimited" - name: ":cloud: VPNUnlimited"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
@@ -95,9 +84,6 @@
description: "" description: ""
# Problem category # Problem category
- name: "Config problem"
color: "ffc7ea"
description: ""
- name: "Openvpn" - name: "Openvpn"
color: "ffc7ea" color: "ffc7ea"
description: "" description: ""
@@ -110,15 +96,6 @@
- name: "Firewall" - name: "Firewall"
color: "ffc7ea" color: "ffc7ea"
description: "" description: ""
- name: "Routing"
color: "ffc7ea"
description: ""
- name: "IPv6"
color: "ffc7ea"
description: ""
- name: "Port forwarding"
color: "ffc7ea"
description: ""
- name: "HTTP proxy" - name: "HTTP proxy"
color: "ffc7ea" color: "ffc7ea"
description: "" description: ""

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@v4 - uses: actions/checkout@v3
- 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 .
@@ -63,79 +65,65 @@ jobs:
-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 .
codeql: # - name: Image security analysis
runs-on: ubuntu-latest # uses: snyk/actions/docker@master
permissions: # env:
actions: read # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
contents: read # with:
security-events: write # image: final-image
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
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, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
# 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@v5 uses: docker/build-push-action@v2.9.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@v4
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.

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@v4 - 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,46 +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@v4
- uses: DavidAnson/markdownlint-cli2-action@v14
with:
globs: "**.md"
config: .markdownlint.json
- 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
- uses: peter-evans/dockerhub-description@v3
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

15
.github/workflows/misspell.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Misspells
on:
pull_request:
branches: [master]
push:
branches: [master]
jobs:
misspell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error

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

@@ -7,69 +7,52 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- dupl - dupl
- maligned
- goerr113 - goerr113
- containedctx - path: internal/server/
- goconst
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters: linters:
- dupl - dupl
- path: "internal\\/configuration\\/settings\\/.+\\.go" - path: internal/configuration/
linters: linters:
- dupl - dupl
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$" - path: internal/constants/
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)" linters:
- dupl
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
linters: linters:
- gomnd - gomnd
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$" - text: "mnd: Magic number: 0400*"
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
linters: linters:
- gomnd - gomnd
- linters: - text: "variable 'mssFix' is only used in the if-statement*"
- lll path: "openvpnconf.go"
source: "^//go:generate .+$"
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters: linters:
- ireturn - ifshort
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go" - text: "variable 'auth' is only used in the if-statement*"
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)" path: "openvpnconf.go"
linters: linters:
- ireturn - ifshort
- path: "internal\\/firewall\\/.*\\.go"
text: "string `-i ` has [1-9][0-9]* occurrences, make it a constant"
linters:
- goconst
- path: "internal\\/provider\\/ipvanish\\/updater\\/servers.go"
text: "string ` in ` has 3 occurrences, make it a constant"
linters:
- goconst
- path: "internal\\/vpn\\/portforward.go"
text: 'directive `//nolint:ireturn` is unused for linter "ireturn"'
linters:
- nolintlint
linters: linters:
enable: enable:
# - cyclop # - cyclop
# - errorlint # - errorlint
- asasalint # - ireturn
# - varnamelen
# - wrapcheck
- asciicheck - asciicheck
- bidichk - bidichk
- bodyclose - bodyclose
- containedctx
- decorder
- dogsled - dogsled
- dupl - dupl
- dupword
- durationcheck - durationcheck
- errchkjson
- errname - errname
- execinquery
- exhaustive - exhaustive
- exportloopref - exportloopref
- forcetypeassert - forcetypeassert
- gci - gci
- gocheckcompilerdirectives
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- gocognit - gocognit
@@ -84,42 +67,31 @@ linters:
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosmopolitan - ifshort
- grouper
- importas - importas
- interfacebloat
- ireturn
- lll - lll
- maintidx
- makezero - makezero
- mirror
- misspell - misspell
- 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 - tenv
- thelper - thelper
- tparallel - tparallel
- unconvert - unconvert
- unparam - unparam
- usestdlibvars
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint
run: run:
skip-dirs: skip-dirs:

View File

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

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

35
.vscode/launch.json vendored
View File

@@ -1,35 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Update a VPN provider servers data",
"type": "go",
"request": "launch",
"cwd": "${workspaceFolder}",
"program": "cmd/gluetun/main.go",
"args": [
"update",
"${input:updateMode}",
"-providers",
"${input:provider}"
],
}
],
"inputs": [
{
"id": "provider",
"type": "promptString",
"description": "Please enter a provider (or comma separated list of providers)",
},
{
"id": "updateMode",
"type": "pickString",
"description": "Update mode to use",
"options": [
"-maintainer",
"-enduser"
],
"default": "-maintainer"
},
]
}

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": true
}
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package"
}

View File

@@ -1,22 +1,18 @@
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.15
ARG GO_ALPINE_VERSION=3.18 ARG GO_ALPINE_VERSION=3.15
ARG GO_VERSION=1.21 ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.6.0 ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.56.2 ARG GOLANGCI_LINT_VERSION=v1.43.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64 ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base 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
@@ -34,17 +30,14 @@ FROM --platform=${BUILDPLATFORM} base AS lint
COPY .golangci.yml ./ COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m 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
@@ -73,12 +66,8 @@ LABEL \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \ org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \ org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux" org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
ENV VPN_SERVICE_PROVIDER=pia \ ENV VPNSP=pia \
VPN_TYPE=openvpn \ VPN_TYPE=openvpn \
# Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \
# OpenVPN # OpenVPN
OPENVPN_PROTOCOL=udp \ OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \ OPENVPN_USER= \
@@ -88,59 +77,45 @@ ENV VPN_SERVICE_PROVIDER=pia \
OPENVPN_VERSION=2.5 \ OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \ OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \ OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \ OPENVPN_CIPHER= \
OPENVPN_AUTH= \ OPENVPN_AUTH= \
OPENVPN_PROCESS_USER=root \ OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \ OPENVPN_CUSTOM_CONFIG= \
OPENVPN_INTERFACE=tun0 \
OPENVPN_PORT= \
# Wireguard # Wireguard
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_ADDRESS= \
WIREGUARD_ADDRESSES= \ WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \ WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_MTU=1400 \ WIREGUARD_INTERFACE=wg0 \
WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering # VPN server filtering
SERVER_REGIONS= \ REGION= \
SERVER_COUNTRIES= \ COUNTRY= \
SERVER_CITIES= \ CITY= \
SERVER_HOSTNAMES= \ SERVER_HOSTNAME= \
SERVER_CATEGORIES= \
# # Mullvad only: # # Mullvad only:
ISP= \ ISP= \
OWNED_ONLY=no \ OWNED=no \
# # Private Internet Access only: # # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \ PIA_ENCRYPTION= \
VPN_PORT_FORWARDING=off \ PORT_FORWARDING=off \
VPN_PORT_FORWARDING_LISTENING_PORT=0 \ PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# # 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_NAME= \
# # ProtonVPN only: # # ProtonVPN only:
FREE_ONLY= \ FREE_ONLY= \
# # Surfshark only: # # Surfshark only:
MULTIHOP_ONLY= \ MULTIHOP_ONLY= \
# # VPN Secure only:
PREMIUM_ONLY= \
# # PIA only:
PORT_FORWARD_ONLY= \
# Firewall # Firewall
FIREWALL=on \ FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_VPN_INPUT_PORTS= \
@@ -151,8 +126,7 @@ 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_ADDRESS=cloudflare.com:443 \ HEALTH_TARGET_ADDRESS=github.com:443 \
HEALTH_SUCCESS_WAIT_DURATION=5s \
HEALTH_VPN_DURATION_INITIAL=6s \ HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \ HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS # DNS over TLS
@@ -169,13 +143,12 @@ ENV VPN_SERVICE_PROVIDER=pia \
BLOCK_ADS=off \ BLOCK_ADS=off \
UNBLOCK= \ UNBLOCK= \
DNS_UPDATE_PERIOD=24h \ DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \ DNS_PLAINTEXT_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \ DNS_KEEP_NAMESERVER=off \
# HTTP proxy # HTTP proxy
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 \
@@ -187,23 +160,11 @@ ENV VPN_SERVICE_PROVIDER=pia \
SHADOWSOCKS_PASSWORD= \ SHADOWSOCKS_PASSWORD= \
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
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
# Server data updater # Server data updater
UPDATER_PERIOD=0 \ UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP # Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \ PUBLICIP_PERIOD=12h \
PUBLICIP_API=ipinfo \
PUBLICIP_API_TOKEN= \
# Pprof
PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \
PPROF_MUTEX_PROFILE_RATE=0 \
PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras # Extras
VERSION_INFORMATION=on \ VERSION_INFORMATION=on \
TZ= \ TZ= \
@@ -213,12 +174,12 @@ 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=1 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 ip6tables unbound 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 && \
# Fix vulnerability issue # Fix vulnerability issue
apk add --no-cache --update busybox && \ 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 && \ rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \

257
README.md
View File

@@ -1,130 +1,127 @@
# Gluetun VPN client # Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN service providers *Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg) 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*
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
[![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) ![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
[![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.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/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.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)
![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release) [![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 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) [![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date) [![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
![Last release](https://img.shields.io/github/release/qdm12/gluetun?label=Last%20release)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags) ![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)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master) ![GitHub last release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors) ![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![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) [![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 issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun) [![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun) [![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 repo size](https://img.shields.io/github/repo-size/qdm12/gluetun) [![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
![Go version](https://img.shields.io/github/go-mod/go-version/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)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme) [![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
## Quick links ![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)
- [Setup](#setup)
- [Features](#features) ![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
- Problem?
- 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) ## Quick links
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550) - [Setup](#Setup)
- Suggestion? - [Features](#Features)
- [Create an issue](https://github.com/qdm12/gluetun/issues) - Problem?
- Happy? - [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12) - [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw) - [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com) - Suggestion?
- **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) - [Create an issue](https://github.com/qdm12/gluetun/issues)
- Video: - [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Happy?
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4) - Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- [Substack Console interview](https://console.substack.com/p/console-72) - Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- Video:
## Features
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- Based on Alpine 3.18 for a small Docker image of 35.6MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - [Substack Console interview](https://console.substack.com/p/console-72)
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace ## Features
- For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Surfshark** and **Windscribe**
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - Based on Alpine 3.15 for a small Docker image of 29MB
- For custom Wireguard configurations 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
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - Supports OpenVPN for all providers listed
- DNS over TLS baked in with service provider(s) of your choice - Supports Wireguard both kernelspace and userspace
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours - For **Mullvad**, **Ivpn** and **Windscribe**
- Choose the vpn network protocol, `udp` or `tcp` - For **Torguard**, **VPN Unlimited** and **WeVPN** 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 - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP) - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP) - DNS over TLS baked in with service provider(s) of your choice
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md) - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md) - Choose the vpn network protocol, `udp` or `tcp`
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆 - Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding) - Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers - Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- Unbound subprogram drops root privileges once launched - [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)
🎉 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
version: "3" [🐛 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: - VPNSP=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_ADDRESS=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

@@ -16,10 +16,10 @@ import (
"github.com/qdm12/dns/pkg/unbound" "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/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration/sources/env" "github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files" "github.com/qdm12/gluetun/internal/configuration/sources/files"
mux "github.com/qdm12/gluetun/internal/configuration/sources/merge" "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,22 @@ 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/provider"
"github.com/qdm12/gluetun/internal/publicip" "github.com/qdm12/gluetun/internal/publicip"
pubipapi "github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/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/golibs/command" "github.com/qdm12/golibs/command"
"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" "github.com/qdm12/updated/pkg/dnscrypto"
) )
@@ -61,6 +55,11 @@ var (
created = "an unknown date" created = "an unknown date"
) )
var (
errSetupRouting = errors.New("cannot setup routing")
errCreateUser = errors.New("cannot create user")
)
func main() { func main() {
buildInfo := models.BuildInformation{ buildInfo := models.BuildInformation{
Version: version, Version: version,
@@ -69,36 +68,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.NewCmder() cmder := command.NewCmder()
secretsReader := secrets.New()
filesReader := files.New()
envReader := env.New(logger) envReader := env.New(logger)
muxReader := mux.New(secretsReader, filesReader, envReader) filesReader := files.New()
secretsReader := secrets.New()
muxReader := mux.New(envReader, filesReader, secretsReader)
errorCh := make(chan error) errorCh := make(chan error)
go func() { go func() {
errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli) errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli)
}() }()
var err error
select { select {
case signal := <-signalCh: case <-signalCtx.Done():
stop()
fmt.Println("") fmt.Println("")
logger.Warn("Caught OS signal " + signal.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)
@@ -110,38 +110,27 @@ func main() {
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:
if !timer.Stop() { if !timer.Stop() {
<-timer.C <-timer.C
} }
if shutdownErr != nil {
logger.Warnf("Shutdown not completed gracefully: %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)
case signal := <-signalCh:
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
os.Exit(1)
} }
os.Exit(1)
} }
var ( var (
errCommandUnknown = errors.New("command is unknown") errCommandUnknown = errors.New("command is unknown")
) )
//nolint:gocognit,gocyclo,maintidx //nolint:gocognit,gocyclo
func _main(ctx context.Context, buildInfo models.BuildInformation, func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, source Source, args []string, logger logging.ParentLogger, source sources.Source,
tun Tun, netLinker netLinker, cmder command.RunStarter, tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
cli clier) error { cli cli.CLIer) error {
if len(args) > 1 { // cli operation if len(args) > 1 { // cli operation
switch args[1] { switch args[1] {
case "healthcheck": case "healthcheck":
@@ -149,7 +138,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
case "clientkey": case "clientkey":
return cli.ClientKey(args[2:]) return cli.ClientKey(args[2:])
case "openvpnconfig": case "openvpnconfig":
return cli.OpenvpnConfig(logger, source, 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":
@@ -159,7 +148,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
} }
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z") announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
if err != nil { if err != nil {
return err return err
} }
@@ -170,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version, Version: buildInfo.Version,
Commit: buildInfo.Commit, Commit: buildInfo.Commit,
BuildDate: buildInfo.Created, BuildDate: buildInfo.Created,
Announcement: "Wiki moved to https://github.com/qdm12/gluetun-wiki", 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",
@@ -185,68 +174,21 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
// Note: no need to validate minimal settings for the firewall:
// - global log level is parsed from source
// - firewall Debug and Enabled are booleans parsed from source
logger.Patch(log.SetLevel(*allSettings.Log.Level))
netLinker.PatchLoggerLevel(*allSettings.Log.Level)
routingLogger := logger.New(log.SetComponent("routing"))
if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.Patch(log.SetLevel(log.LevelDebug))
}
routingConf := routing.New(netLinker, routingLogger)
defaultRoutes, err := routingConf.DefaultRoutes()
if err != nil {
return err
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
firewallLogger := logger.New(log.SetComponent("firewall"))
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 {
return err
}
if *allSettings.Firewall.Enabled {
err = firewallConf.SetEnabled(ctx, true)
if err != nil {
return err
}
}
// 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, constants.ServersData) 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) err = allSettings.Validate(allServers)
if err != nil { if err != nil {
return err return err
} }
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof")) logger.PatchLevel(*allSettings.Log.Level)
pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil {
return fmt.Errorf("creating Pprof server: %w", err)
}
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
@@ -255,7 +197,7 @@ 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, "", "") dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt" const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
@@ -264,8 +206,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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: "Unbound", getVersion: dnsConf.Version},
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) { {name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
return firewall.Version(ctx, cmder) return firewall.Version(ctx, cmder)
@@ -277,10 +219,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Info(allSettings.String()) logger.Info(allSettings.String())
for _, warning := range allSettings.Warnings() {
logger.Warn(warning)
}
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil { if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
return err return err
} }
@@ -291,7 +229,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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("%w: %s", errCreateUser, 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))
@@ -299,22 +237,54 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
// set it for Unbound // set it for Unbound
// TODO remove this when migrating to qdm12/dns v2 // TODO remove this when migrating to qdm12/dns v2
allSettings.DNS.DoT.Unbound.Username = nonRootUsername allSettings.DNS.DoT.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername allSettings.VPN.OpenVPN.ProcUser = nonRootUsername
if err := os.Chown("/etc/unbound", puid, pgid); err != nil { if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err return err
} }
firewallLogLevel := *allSettings.Log.Level
if *allSettings.Firewall.Debug {
firewallLogLevel = logging.LevelDebug
}
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
Level: firewallLogLevel,
})
routingConf := routing.New(netLinker, routingLogger)
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil {
return err
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
defaultIP, err := routingConf.DefaultIP()
if err != nil {
return err
}
firewallLogger := logger.NewChild(logging.Settings{
Prefix: "firewall: ",
Level: firewallLogLevel,
})
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
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("%w: %s", errSetupRouting, 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())
} }
}() }()
@@ -325,26 +295,25 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
err = routingConf.AddLocalRules(localNetworks) if err := tun.Check(constants.TunnelDevice); err != nil {
if err != nil { logger.Info(err.Error() + "; creating it...")
return fmt.Errorf("adding local rules: %w", err) err = tun.Create(constants.TunnelDevice)
if err != nil {
return err
}
} }
const tunDevice = "/dev/net/tun" if *allSettings.Firewall.Enabled {
if err := tun.Check(tunDevice); err != nil { err := firewallConf.SetEnabled(ctx, true) // disabled by default
logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice)
if err != nil { if err != nil {
return 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?
@@ -365,24 +334,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...) tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...) otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
if *allSettings.Pprof.Enabled { portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
// TODO run in run loop so this can be patched at runtime
pprofReady := make(chan struct{})
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
}
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding, portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
routingConf, httpClient, firewallConf, portForwardLogger, 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)
}
unboundLogger := logger.New(log.SetComponent("dns")) unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient, unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger) unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler( dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
@@ -396,38 +355,31 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler) controlGroupHandler.Add(dnsTickerHandler)
publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API) publicIPLooper := publicip.NewLoop(httpClient,
ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken) logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
if err != nil {
return fmt.Errorf("creating public IP API client: %w", err)
}
publicIPLooper := publicip.NewLoop(ipFetcher,
logger.New(log.SetComponent("ip getter")),
allSettings.PublicIP, puid, pgid) allSettings.PublicIP, puid, pgid)
publicIPRunError, err := publicIPLooper.Start(ctx) pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
if err != nil { "public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
return fmt.Errorf("starting public ip loop: %w", err) go publicIPLooper.Run(pubIPCtx, pubIPDone)
} otherGroupHandler.Add(pubIPHandler)
updaterLogger := logger.New(log.SetComponent("updater")) pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
unzipper := unzip.New(httpClient) vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress) vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
openvpnFileExtractor := extract.New() allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient, cmder, publicIPLooper, unboundLooper, 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
@@ -440,37 +392,31 @@ 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 controlServerAddress := fmt.Sprintf(":%d", *allSettings.ControlServer.Port)
controlServerLogging := *allSettings.ControlServer.Log 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, controlServerAddress, controlServerLogging, httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")), logger.NewChild(logging.Settings{Prefix: "http server: "}),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, 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.New(log.SetComponent("healthcheck")) healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper) healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout)) "HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
@@ -481,31 +427,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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())
} }
@@ -528,68 +456,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, source cli.Source, ipv6Checker cli.IPv6Checker) error
HealthCheck(ctx context.Context, source cli.Source, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type Source interface {
Read() (settings settings.Settings, err error)
ReadHealth() (health settings.Health, err error)
String() string
}

56
go.mod
View File

@@ -1,55 +1,43 @@
module github.com/qdm12/gluetun module github.com/qdm12/gluetun
go 1.21 go 1.17
require ( require (
github.com/breml/rootcerts v0.2.16 github.com/breml/rootcerts v0.2.1
github.com/fatih/color v1.16.0 github.com/fatih/color v1.13.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.4
github.com/klauspost/pgzip v1.2.6
github.com/qdm12/dns v1.11.0 github.com/qdm12/dns v1.11.0
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/gosettings v0.4.0-rc1
github.com/qdm12/goshutdown v0.3.0 github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0 github.com/qdm12/gosplash v0.1.0
github.com/qdm12/gotree v0.2.0 github.com/qdm12/gotree v0.2.0
github.com/qdm12/govalid v0.2.0-rc1 github.com/qdm12/govalid v0.1.0
github.com/qdm12/log v0.1.0 github.com/qdm12/ss-server v0.4.0
github.com/qdm12/ss-server v0.5.0
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.7.0
github.com/ulikunitz/xz v0.5.11 github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
github.com/vishvananda/netlink v1.2.1-beta.2 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.org/x/net v0.19.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
golang.org/x/sys v0.15.0 inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
golang.org/x/text v0.14.0
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
gopkg.in/ini.v1 v1.67.0
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.5 // indirect
github.com/josharian/native v1.0.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.2.0 // indirect github.com/mdlayher/genetlink v1.0.0 // indirect
github.com/mdlayher/netlink v1.6.2 // indirect github.com/mdlayher/netlink v1.4.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect
github.com/miekg/dns v1.1.40 // indirect github.com/miekg/dns v1.1.40 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
golang.org/x/crypto v0.17.0 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
golang.org/x/sync v0.1.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

185
go.sum
View File

@@ -4,8 +4,8 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/g
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.16 h1:yN1TGvicfHx8dKz3OQRIrx/5nE/iN3XT1ibqGbd6urc= github.com/breml/rootcerts v0.2.1 h1:GZMVDXOs945764NFck0vtHSjktKYubOFM0kjf5HAuwc=
github.com/breml/rootcerts v0.2.16/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/breml/rootcerts v0.2.1/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -14,8 +14,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= 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/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.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 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/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/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/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -36,24 +36,29 @@ github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3K
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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU= github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -63,20 +68,25 @@ 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/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/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.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ= github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v1.6.2 h1:D2zGSkvYsJ6NreeED3JiVTu1lj2sIYATqSaZlhPzUgQ= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= 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 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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=
@@ -97,20 +107,16 @@ github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/gosettings v0.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38=
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
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.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g= github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw= github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c= github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4= github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
github.com/qdm12/govalid v0.2.0-rc1 h1:4iYQvU4ibrASgzelsEgZX4JyKX3UTB/DcHObzQ7BXtw= github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
github.com/qdm12/govalid v0.2.0-rc1/go.mod h1:/uWzVWMuS71wmbsVnlUxpQiy6EAXqm8eQ2RbyA72roQ= github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw= github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI= github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
github.com/qdm12/ss-server v0.5.0 h1:ARAqJayohDM51BmJ/R5Yplkpo+Qxgp7xizBF1HWd7uQ=
github.com/qdm12/ss-server v0.5.0/go.mod h1:eFd8PL/uy0ZvJ4KeSUzToruJctVQoYqXk+LRy9vcOiI=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g= 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/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=
@@ -122,101 +128,97 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 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/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/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.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=
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/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= go4.org/unsafe/assume-no-moving-gc v0.0.0-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= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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.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/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190503192946-f4e77d36d62c/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-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-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-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-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
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-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-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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-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-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-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-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-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-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-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-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-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-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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220128215802-99c3d69c2c27/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-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-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=
@@ -226,29 +228,24 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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-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-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde h1:ybF7AMzIUikL9x4LgwEmzhXtzRpKNqngme1VGDWz+Nk= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU= inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= 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

@@ -12,6 +12,10 @@ var (
ErrUserAlreadyExists = errors.New("user already exists") 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) {
UIDStr := strconv.Itoa(uid) UIDStr := strconv.Itoa(uid)

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

@@ -7,12 +7,16 @@ import (
"os" "os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files" "github.com/qdm12/gluetun/internal/constants"
) )
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)
filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file") filepath := flagSet.String("path", constants.ClientKey, "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
} }

View File

@@ -6,49 +6,51 @@ import (
"fmt" "fmt"
"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 (
ErrFormatNotRecognized = errors.New("format is not recognized") ErrFormatNotRecognized = errors.New("format is not recognized")
ErrProviderUnspecified = errors.New("VPN provider to format was not specified") ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified") ErrOpenOutputFile = errors.New("cannot open output file")
ErrWriteOutput = errors.New("cannot write to output file")
ErrCloseOutputFile = errors.New("cannot close output file")
) )
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)
}
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'") flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to") 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
} }
@@ -57,53 +59,74 @@ func (c *CLI) FormatServers(args []string) error {
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format) 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("%w: %s", ErrNewStorage, err)
} }
currentServers := storage.GetServers()
formatted := storage.FormatToMarkdown(providerToFormat) var formatted string
switch {
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)
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, 0644)
if err != nil { if err != nil {
return fmt.Errorf("opening output file: %w", err) return fmt.Errorf("%w: %s", ErrOpenOutputFile, 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("%w: %s", ErrWriteOutput, err)
} }
err = file.Close() err = file.Close()
if err != nil { if err != nil {
return fmt.Errorf("closing output file: %w", err) return fmt.Errorf("%w: %s", ErrCloseOutputFile, err)
} }
return nil return nil

View File

@@ -6,18 +6,21 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/healthcheck" "github.com/qdm12/gluetun/internal/healthcheck"
) )
func (c *CLI) HealthCheck(ctx context.Context, source Source, _ Warner) 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.
config, err := source.ReadHealth() config, err := source.ReadHealth()
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

@@ -8,9 +8,9 @@ func newNoopLogger() *noopLogger {
return new(noopLogger) return new(noopLogger)
} }
func (l *noopLogger) Debug(string) {} func (l *noopLogger) Debug(s string) {}
func (l *noopLogger) Info(string) {} func (l *noopLogger) Info(s string) {}
func (l *noopLogger) Warn(string) {} func (l *noopLogger) Warn(s string) {}
func (l *noopLogger) Error(string) {} func (l *noopLogger) Error(s string) {}
func (l *noopLogger) PatchLevel(logging.Level) {} func (l *noopLogger) PatchLevel(level logging.Level) {}
func (l *noopLogger) PatchPrefix(string) {} func (l *noopLogger) PatchPrefix(prefix string) {}

View File

@@ -1,85 +1,51 @@
package cli package cli
import ( import (
"context"
"fmt" "fmt"
"net/http"
"net/netip"
"strings" "strings"
"time" "time"
"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"
) )
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 {
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, source Source,
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()
allSettings, err := source.Read() allSettings, err := source.Read()
if err != nil { if err != nil {
return err return err
} }
ipv6Supported, err := ipv6Checker.IsIPv6Supported() if err = allSettings.Validate(allServers); err != nil {
return err
}
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil { if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err) return err
} }
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err = allSettings.Validate(storage, ipv6Supported); 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)
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,
allSettings.VPN.OpenVPN, ipv6Supported)
fmt.Println(strings.Join(lines, "\n")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil
} }

View File

@@ -2,105 +2,131 @@ package cli
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"net"
"net/http" "net/http"
"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")
ErrNoProviderSpecified = errors.New("no provider was specified") ErrDNSAddress = errors.New("DNS address is not valid")
ErrNoProviderSpecified = errors.New("no provider was specified")
ErrNewStorage = errors.New("cannot create storage")
ErrUpdateServerInformation = errors.New("cannot update server information")
ErrWriteToFile = errors.New("cannot write updated information to file")
) )
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 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")
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, ",")
} }
options.SetDefaults(options.Providers[0]) options.SetDefaults()
err := options.Validate() err := options.Validate()
if err != nil { if err != nil {
return fmt.Errorf("options validation failed: %w", err) return fmt.Errorf("options validation failed: %w", err)
} }
storage, err := storage.New(logger, constants.ServersData)
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)
ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken)
if err != nil { if err != nil {
return fmt.Errorf("creating public IP API client: %w", err) return fmt.Errorf("%w: %s", ErrNewStorage, err)
} }
openvpnFileExtractor := extract.New() currentServers := storage.GetServers()
providers := provider.NewProviders(storage, time.Now, logger, httpClient, updater := updater.New(options, httpClient, currentServers, logger)
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) allServers, err := updater.UpdateServers(ctx)
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("%w: %s", ErrUpdateServerInformation, err)
}
if endUserMode {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
}
} }
if maintainerMode { if maintainerMode {
err := storage.FlushToFile(c.repoServersPath) if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
if err != nil { return fmt.Errorf("%w: %s", ErrWriteToFile, 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

@@ -2,9 +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/gotree" "github.com/qdm12/gotree"
) )
@@ -13,17 +13,13 @@ type DNS struct {
// 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
// DoT 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 over TLS 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
@@ -35,7 +31,7 @@ type DNS struct {
func (d DNS) validate() (err error) { func (d DNS) validate() (err error) {
err = d.DoT.validate() err = d.DoT.validate()
if err != nil { if err != nil {
return fmt.Errorf("validating DoT settings: %w", err) return fmt.Errorf("failed validating DoT settings: %w", err)
} }
return nil return nil
@@ -43,8 +39,8 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) { func (d *DNS) Copy() (copied DNS) {
return DNS{ return DNS{
ServerAddress: d.ServerAddress, ServerAddress: helpers.CopyIP(d.ServerAddress),
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver), KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
DoT: d.DoT.copy(), DoT: d.DoT.copy(),
} }
} }
@@ -52,8 +48,8 @@ func (d *DNS) Copy() (copied DNS) {
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) { func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = gosettings.MergeWithValidator(d.ServerAddress, other.ServerAddress) d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.MergeWithPointer(d.KeepNameserver, other.KeepNameserver) d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.mergeWith(other.DoT) d.DoT.mergeWith(other.DoT)
} }
@@ -61,15 +57,15 @@ func (d *DNS) mergeWith(other DNS) {
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DNS) overrideWith(other DNS) { func (d *DNS) overrideWith(other DNS) {
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress) d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver) d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT) d.DoT.overrideWith(other.DoT)
} }
func (d *DNS) setDefaults() { func (d *DNS) setDefaults() {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1}) localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost) d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false) d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
d.DoT.setDefaults() d.DoT.setDefaults()
} }
@@ -79,11 +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.AppendNode(d.DoT.toLinesNode()) node.AppendNode(d.DoT.toLinesNode())
return node return node
} }

View File

@@ -3,12 +3,12 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"regexp" "regexp"
"github.com/qdm12/dns/pkg/blacklist" "github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"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.
@@ -18,14 +18,14 @@ 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
} }
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
@@ -53,34 +53,34 @@ func (b DNSBlacklist) validate() (err error) {
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),
} }
} }
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) { func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.MergeWithPointer(b.BlockMalicious, other.BlockMalicious) b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.MergeWithPointer(b.BlockAds, other.BlockAds) b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.MergeWithPointer(b.BlockSurveillance, other.BlockSurveillance) b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.MergeWithSlice(b.AllowedHosts, other.AllowedHosts) b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.MergeWithSlice(b.AddBlockedHosts, other.AddBlockedHosts) b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.MergeWithSlice(b.AddBlockedIPs, other.AddBlockedIPs) b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.MergeWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
} }
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) { func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious) b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds) b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance) b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts) b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts) b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs) b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
} }
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) { func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
@@ -90,8 +90,8 @@ func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, e
BlockSurveillance: *b.BlockSurveillance, BlockSurveillance: *b.BlockSurveillance,
AllowedHosts: b.AllowedHosts, AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts, AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: netipAddressesToNetaddrIPs(b.AddBlockedIPs), AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes), AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}, nil }, nil
} }
@@ -102,9 +102,9 @@ 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.Appendf("Allowed hosts:") allowedHostsNode := node.Appendf("Allowed hosts:")

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -54,8 +54,8 @@ func (d DoT) validate() (err error) {
func (d *DoT) copy() (copied DoT) { func (d *DoT) copy() (copied DoT) {
return DoT{ return DoT{
Enabled: gosettings.CopyPointer(d.Enabled), Enabled: helpers.CopyBoolPtr(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
Unbound: d.Unbound.copy(), Unbound: d.Unbound.copy(),
Blacklist: d.Blacklist.copy(), Blacklist: d.Blacklist.copy(),
} }
@@ -64,8 +64,8 @@ func (d *DoT) copy() (copied DoT) {
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (d *DoT) mergeWith(other DoT) { func (d *DoT) mergeWith(other DoT) {
d.Enabled = gosettings.MergeWithPointer(d.Enabled, other.Enabled) d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.MergeWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound) d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist) d.Blacklist.mergeWith(other.Blacklist)
} }
@@ -74,16 +74,16 @@ func (d *DoT) mergeWith(other DoT) {
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DoT) overrideWith(other DoT) { func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled) d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound) d.Unbound.overrideWith(other.Unbound)
d.Blacklist.overrideWith(other.Blacklist) d.Blacklist.overrideWith(other.Blacklist)
} }
func (d *DoT) setDefaults() { func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true) d.Enabled = helpers.DefaultBool(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
d.Unbound.setDefaults() d.Unbound.setDefaults()
d.Blacklist.setDefaults() d.Blacklist.setDefaults()
} }
@@ -95,12 +95,12 @@ func (d DoT) String() string {
func (d DoT) toLinesNode() (node *gotree.Node) { func (d DoT) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS over TLS settings:") node = gotree.New("DNS over TLS settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled)) node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled))
if !*d.Enabled { if !*d.Enabled {
return node return node
} }
update := "disabled" //nolint:goconst update := "disabled"
if *d.UpdatePeriod > 0 { if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String() update = "every " + d.UpdatePeriod.String()
} }

View File

@@ -5,21 +5,19 @@ import "errors"
var ( var (
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") ErrFirewallZeroPort = errors.New("cannot have a zero port to block")
ErrFirewallZeroPort = errors.New("cannot have a zero port")
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")
ErrNameNotValid = errors.New("the server name specified is not valid") ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientCertMissing = errors.New("client certificate is missing")
ErrOpenVPNClientCertNotValid = errors.New("client certificate is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing") ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNClientKeyNotValid = errors.New("client key is not valid")
ErrOpenVPNConfigFile = errors.New("custom configuration file error")
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")
@@ -27,27 +25,27 @@ 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")
ErrPortForwardingFilepathNotValid = errors.New("port forwarding filepath given is not valid")
ErrPublicIPFilepathNotValid = errors.New("public IP address file path is not valid")
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short") ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
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")
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")
ErrWireguardPreSharedKeyNotValid = errors.New("pre-shared key is not valid")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set") ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPrivateKeyNotValid = errors.New("private key is not valid")
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")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
) )

View File

@@ -2,9 +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/gotree" "github.com/qdm12/gotree"
) )
@@ -12,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
} }
@@ -26,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
} }
@@ -46,11 +40,11 @@ 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),
} }
} }
@@ -59,27 +53,27 @@ func (f *Firewall) copy() (copied Firewall) {
// It merges values of slices together, even if they // It merges values of slices together, even if they
// are set in the receiver settings. // are set in the receiver settings.
func (f *Firewall) mergeWith(other Firewall) { func (f *Firewall) mergeWith(other Firewall) {
f.VPNInputPorts = gosettings.MergeWithSlice(f.VPNInputPorts, other.VPNInputPorts) f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = gosettings.MergeWithSlice(f.InputPorts, other.InputPorts) f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.MergeWithSlice(f.OutboundSubnets, other.OutboundSubnets) f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.MergeWithPointer(f.Enabled, other.Enabled) f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
f.Debug = gosettings.MergeWithPointer(f.Debug, other.Debug) 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 {
@@ -89,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
} }
@@ -115,8 +109,7 @@ 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 {
subnet := subnet outboundSubnets.Appendf("%s", subnet)
outboundSubnets.Appendf("%s", &subnet)
} }
} }

View File

@@ -1,74 +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 {
testCase := testCase
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

@@ -3,9 +3,8 @@ package settings
import ( import (
"fmt" "fmt"
"os" "os"
"time"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address" "github.com/qdm12/govalid/address"
) )
@@ -16,36 +15,25 @@ 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
// ReadHeaderTimeout is the HTTP server header read timeout
// duration of the HTTP server. It defaults to 100 milliseconds.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration of the
// HTTP server. It defaults to 500 milliseconds.
ReadTimeout time.Duration
// TargetAddress is the address (host or host:port) // TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check. // to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state. // It cannot be the empty string in the internal state.
TargetAddress string TargetAddress string
// SuccessWait is the duration to wait to re-run the VPN HealthyWait
// healthcheck after a successful healthcheck.
// It defaults to 5 seconds and cannot be zero in
// the internal state.
SuccessWait time.Duration
// VPN has health settings specific to the VPN loop.
VPN HealthyWait
} }
func (h Health) Validate() (err error) { func (h Health) Validate() (err error) {
uid := os.Getuid() uid := os.Getuid()
err = address.Validate(h.ServerAddress, _, err = address.Validate(h.ServerAddress,
address.OptionListening(uid)) address.OptionListening(uid))
if err != nil { if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err) return fmt.Errorf("%w: %s",
ErrServerAddressNotValid, err)
} }
err = h.VPN.validate() err = h.VPN.validate()
if err != nil { if err != nil {
return fmt.Errorf("health VPN settings: %w", err) return fmt.Errorf("health VPN settings validation failed: %w", err)
} }
return nil return nil
@@ -53,23 +41,17 @@ 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,
ReadHeaderTimeout: h.ReadHeaderTimeout, TargetAddress: h.TargetAddress,
ReadTimeout: h.ReadTimeout, VPN: h.VPN.copy(),
TargetAddress: h.TargetAddress,
SuccessWait: h.SuccessWait,
VPN: h.VPN.copy(),
} }
} }
// MergeWith merges the other settings into any // MergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) { func (h *Health) MergeWith(other Health) {
h.ServerAddress = gosettings.MergeWithString(h.ServerAddress, other.ServerAddress) h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.MergeWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout) h.TargetAddress = helpers.MergeWithString(h.TargetAddress, other.TargetAddress)
h.ReadTimeout = gosettings.MergeWithNumber(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.MergeWithString(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.MergeWithNumber(h.SuccessWait, other.SuccessWait)
h.VPN.mergeWith(other.VPN) h.VPN.mergeWith(other.VPN)
} }
@@ -77,23 +59,14 @@ func (h *Health) MergeWith(other Health) {
// 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.OverrideWithString(h.ServerAddress, other.ServerAddress) h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.OverrideWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout) h.TargetAddress = helpers.OverrideWithString(h.TargetAddress, other.TargetAddress)
h.ReadTimeout = gosettings.OverrideWithNumber(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.OverrideWithString(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.OverrideWithNumber(h.SuccessWait, other.SuccessWait)
h.VPN.overrideWith(other.VPN) h.VPN.overrideWith(other.VPN)
} }
func (h *Health) SetDefaults() { func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultString(h.ServerAddress, "127.0.0.1:9999") h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
const defaultReadHeaderTimeout = 100 * time.Millisecond h.TargetAddress = helpers.DefaultString(h.TargetAddress, "github.com:443")
h.ReadHeaderTimeout = gosettings.DefaultNumber(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 500 * time.Millisecond
h.ReadTimeout = gosettings.DefaultNumber(h.ReadTimeout, defaultReadTimeout)
h.TargetAddress = gosettings.DefaultString(h.TargetAddress, "cloudflare.com:443")
const defaultSuccessWait = 5 * time.Second
h.SuccessWait = gosettings.DefaultNumber(h.SuccessWait, defaultSuccessWait)
h.VPN.setDefaults() h.VPN.setDefaults()
} }
@@ -105,9 +78,6 @@ func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:") node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress) node.Appendf("Server listening address: %s", h.ServerAddress)
node.Appendf("Target address: %s", h.TargetAddress) node.Appendf("Target address: %s", h.TargetAddress)
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
node.AppendNode(h.VPN.toLinesNode("VPN")) node.AppendNode(h.VPN.toLinesNode("VPN"))
return node return node
} }

View File

@@ -3,7 +3,7 @@ package settings
import ( import (
"time" "time"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -27,31 +27,31 @@ func (h HealthyWait) validate() (err error) {
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) { func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{ return HealthyWait{
Initial: gosettings.CopyPointer(h.Initial), Initial: helpers.CopyDurationPtr(h.Initial),
Addition: gosettings.CopyPointer(h.Addition), Addition: helpers.CopyDurationPtr(h.Addition),
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) { func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = gosettings.MergeWithPointer(h.Initial, other.Initial) h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
h.Addition = gosettings.MergeWithPointer(h.Addition, other.Addition) h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
} }
// 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 *HealthyWait) overrideWith(other HealthyWait) { func (h *HealthyWait) overrideWith(other HealthyWait) {
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial) h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition) h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
} }
func (h *HealthyWait) setDefaults() { func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second const additionDurationDefault = 5 * time.Second
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault) h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault) h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
} }
func (h HealthyWait) String() string { func (h HealthyWait) String() string {

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,32 @@ func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
} }
return false return false
} }
var ErrValueNotOneOf = errors.New("value is not one of the possible choices")
func AreAllOneOf(values, choices []string) (err error) {
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,226 @@
package helpers
import (
"net"
"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 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 MergeWithInt(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 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,133 @@
package helpers
import (
"net"
"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 OverrideWithStringPtr(existing, other *string) (result *string) {
if other == nil {
return existing
}
result = new(string)
*result = *other
return result
}
func OverrideWithInt(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 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

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

View File

@@ -3,9 +3,8 @@ package settings
import ( import (
"fmt" "fmt"
"os" "os"
"time"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/govalid/address" "github.com/qdm12/govalid/address"
) )
@@ -34,21 +33,16 @@ 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
uid := os.Getuid() uid := os.Getuid()
err = address.Validate(h.ListeningAddress, address.OptionListening(uid)) _, 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)
} }
return nil return nil
@@ -56,55 +50,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 // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) { func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = gosettings.MergeWithPointer(h.User, other.User) h.User = helpers.MergeWithStringPtr(h.User, other.User)
h.Password = gosettings.MergeWithPointer(h.Password, other.Password) h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
h.ListeningAddress = gosettings.MergeWithString(h.ListeningAddress, other.ListeningAddress) h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = gosettings.MergeWithPointer(h.Enabled, other.Enabled) h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
h.Stealth = gosettings.MergeWithPointer(h.Stealth, other.Stealth) h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
h.Log = gosettings.MergeWithPointer(h.Log, other.Log) h.Log = helpers.MergeWithBool(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.MergeWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.MergeWithNumber(h.ReadTimeout, other.ReadTimeout)
} }
// 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.OverrideWithString(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.OverrideWithNumber(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithNumber(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.DefaultString(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.DefaultNumber(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 3 * time.Second
h.ReadTimeout = gosettings.DefaultNumber(h.ReadTimeout, defaultReadTimeout)
} }
func (h HTTPProxy) String() string { func (h HTTPProxy) String() string {
@@ -113,18 +97,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
} }

View File

@@ -1,16 +1,16 @@
package settings package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/golibs/logging"
"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 nil in the internal state. // It cannot be nil in the internal state.
Level *log.Level Level *logging.Level
} }
func (l Log) validate() (err error) { func (l Log) validate() (err error) {
@@ -19,25 +19,25 @@ func (l Log) validate() (err error) {
func (l *Log) copy() (copied Log) { func (l *Log) copy() (copied Log) {
return Log{ return Log{
Level: gosettings.CopyPointer(l.Level), Level: helpers.CopyLogLevelPtr(l.Level),
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (l *Log) mergeWith(other Log) { func (l *Log) mergeWith(other Log) {
l.Level = gosettings.MergeWithPointer(l.Level, other.Level) 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.OverrideWithPointer(l.Level, other.Level) l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
} }
func (l *Log) setDefaults() { func (l *Log) setDefaults() {
l.Level = gosettings.DefaultPointer(l.Level, log.LevelInfo) l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo)
} }
func (l Log) String() string { func (l Log) String() string {

View File

@@ -1,36 +0,0 @@
package settings
import (
"net/netip"
"inet.af/netaddr"
)
func netipAddressToNetaddrIP(address netip.Addr) (ip netaddr.IP) {
if address.Is4() {
return netaddr.IPFrom4(address.As4())
}
return netaddr.IPFrom16(address.As16())
}
func netipAddressesToNetaddrIPs(addresses []netip.Addr) (ips []netaddr.IP) {
ips = make([]netaddr.IP, len(addresses))
for i := range addresses {
ips[i] = netipAddressToNetaddrIP(addresses[i])
}
return ips
}
func netipPrefixToNetaddrIPPrefix(prefix netip.Prefix) (ipPrefix netaddr.IPPrefix) {
netaddrIP := netipAddressToNetaddrIP(prefix.Addr())
bits := prefix.Bits()
return netaddr.IPPrefixFrom(netaddrIP, uint8(bits))
}
func netipPrefixesToNetaddrIPPrefixes(prefixes []netip.Prefix) (ipPrefixes []netaddr.IPPrefix) {
ipPrefixes = make([]netaddr.IPPrefix, len(prefixes))
for i := range ipPrefixes {
ipPrefixes[i] = netipPrefixToNetaddrIPPrefix(prefixes[i])
}
return ipPrefixes
}

View File

@@ -1,42 +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,138 +1,146 @@
package settings package settings
import ( import (
"encoding/base64"
"fmt" "fmt"
"regexp" "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/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 // Root is true if OpenVPN is to be run as root,
// to use. It cannot be empty in the internal state. // and false otherwise. It cannot be nil in the
// It defaults to 'root'. // internal state.
ProcessUser string `json:"process_user"` Root *bool
// ProcUser is the OpenVPN process OS username
// to use. It cannot be nil in the internal state.
// This is set and injected at runtime.
// TODO only use ProcUser and not Root field.
ProcUser 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) // Validate ConfFile
if err != nil { if isCustom {
return fmt.Errorf("custom configuration file: %w", err) if *o.ConfFile == "" {
return fmt.Errorf("%w: no file path specified", ErrOpenVPNConfigFile)
}
err := helpers.FileExists(*o.ConfFile)
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
}
} }
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert) // Check client certificate
if err != nil { switch vpnProvider {
return fmt.Errorf("client certificate: %w", err) case
constants.Cyberghost,
constants.VPNUnlimited:
if *o.ClientCrt == "" {
return ErrOpenVPNClientCertMissing
}
}
if *o.ClientCrt != "" {
_, err = parse.ExtractCert([]byte(*o.ClientCrt))
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNClientCertNotValid, err)
}
} }
err = validateOpenVPNClientKey(vpnProvider, *o.Key) // Check client key
if err != nil { switch vpnProvider {
return fmt.Errorf("client key: %w", err) case
} constants.Cyberghost,
constants.VPNUnlimited,
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey) constants.Wevpn:
if err != nil { if *o.ClientKey == "" {
return fmt.Errorf("encrypted key: %w", err) return ErrOpenVPNClientKeyMissing
} }
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" { if *o.ClientKey != "" {
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty) _, err = parse.ExtractPrivateKey([]byte(*o.ClientKey))
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNClientKeyNotValid, err)
}
} }
// Validate MSSFix
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",
@@ -144,6 +152,7 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName) ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
} }
// Validate Verbosity
if *o.Verbosity < 0 || *o.Verbosity > 6 { if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("%w: %d can only be between 0 and 5", return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity) ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
@@ -152,183 +161,93 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
return nil return nil
} }
func validateOpenVPNConfigFilepath(isCustom bool,
confFile string) (err error) {
if !isCustom {
return nil
}
if confFile == "" {
return fmt.Errorf("%w", ErrFilepathMissing)
}
err = validate.FileExists(confFile)
if err != nil {
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
}
func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) {
switch vpnProvider {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
}
if clientCert == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(clientCert)
if err != nil {
return err
}
return nil
}
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
}
if clientKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(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 {
return err
}
return nil
}
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, Root: helpers.CopyBoolPtr(o.Root),
ProcessUser: o.ProcessUser, ProcUser: o.ProcUser,
Verbosity: gosettings.CopyPointer(o.Verbosity), Verbosity: helpers.CopyIntPtr(o.Verbosity),
Flags: gosettings.CopySlice(o.Flags), Flags: helpers.CopyStringSlice(o.Flags),
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) { func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = gosettings.MergeWithString(o.Version, other.Version) o.Version = helpers.MergeWithString(o.Version, other.Version)
o.User = gosettings.MergeWithPointer(o.User, other.User) o.User = helpers.MergeWithString(o.User, other.User)
o.Password = gosettings.MergeWithPointer(o.Password, other.Password) o.Password = helpers.MergeWithString(o.Password, other.Password)
o.ConfFile = gosettings.MergeWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = gosettings.MergeWithSlice(o.Ciphers, other.Ciphers) o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
o.Auth = gosettings.MergeWithPointer(o.Auth, other.Auth) o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.Cert = gosettings.MergeWithPointer(o.Cert, other.Cert) o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.Key = gosettings.MergeWithPointer(o.Key, other.Key) o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.EncryptedKey = gosettings.MergeWithPointer(o.EncryptedKey, other.EncryptedKey) o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.KeyPassphrase = gosettings.MergeWithPointer(o.KeyPassphrase, other.KeyPassphrase) o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.PIAEncPreset = gosettings.MergeWithPointer(o.PIAEncPreset, other.PIAEncPreset) o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
o.MSSFix = gosettings.MergeWithPointer(o.MSSFix, other.MSSFix) o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
o.Interface = gosettings.MergeWithString(o.Interface, other.Interface) o.Root = helpers.MergeWithBool(o.Root, other.Root)
o.ProcessUser = gosettings.MergeWithString(o.ProcessUser, other.ProcessUser) o.ProcUser = helpers.MergeWithString(o.ProcUser, other.ProcUser)
o.Verbosity = gosettings.MergeWithPointer(o.Verbosity, other.Verbosity) o.Verbosity = helpers.MergeWithInt(o.Verbosity, other.Verbosity)
o.Flags = gosettings.MergeWithSlice(o.Flags, other.Flags) 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.OverrideWithString(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.OverrideWithString(o.Interface, other.Interface) o.Root = helpers.OverrideWithBool(o.Root, other.Root)
o.ProcessUser = gosettings.OverrideWithString(o.ProcessUser, other.ProcessUser) o.ProcUser = helpers.OverrideWithString(o.ProcUser, other.ProcUser)
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity) o.Verbosity = helpers.OverrideWithInt(o.Verbosity, other.Verbosity)
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags) o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
} }
func (o *OpenVPN) setDefaults(vpnProvider string) { func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = gosettings.DefaultString(o.Version, openvpn.Openvpn25) 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.DefaultString(o.Interface, "tun0") o.IPv6 = helpers.DefaultBool(o.IPv6, false)
o.ProcessUser = gosettings.DefaultString(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.Root = helpers.DefaultBool(o.Root, true)
o.ProcUser = helpers.DefaultString(o.ProcUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
} }
func (o OpenVPN) String() string { func (o OpenVPN) String() string {
@@ -338,8 +257,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)
@@ -353,23 +272,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)
} }
@@ -378,7 +294,14 @@ func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node.Appendf("Network interface: %s", o.Interface) node.Appendf("Network interface: %s", o.Interface)
} }
node.Appendf("Run OpenVPN as: %s", o.ProcessUser) processUser := "root"
if !*o.Root {
processUser = "some non root user" // TODO
if o.ProcUser != "" {
processUser = o.ProcUser
}
}
node.Appendf("Run OpenVPN as: %s", processUser)
node.Appendf("Verbosity level: %d", *o.Verbosity) node.Appendf("Verbosity level: %d", *o.Verbosity)

View File

@@ -1,44 +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 {
testCase := testCase
t.Run(testCase.s, func(t *testing.T) {
t.Parallel()
match := ivpnAccountID.MatchString(testCase.s)
assert.Equal(t, testCase.match, match)
})
}
}

View File

@@ -4,10 +4,7 @@ import (
"fmt" "fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "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/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -16,37 +13,35 @@ 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
// TCP is true if the OpenVPN protocol is TCP, // TCP is true if the OpenVPN protocol is TCP,
// and false for UDP. // and false for UDP.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
TCP *bool `json:"tcp"` TCP *bool
// 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("%w: %s", ErrOpenVPNConfigFile, err)
} }
} }
// Validate TCP // Validate TCP
if *o.TCP && helpers.IsOneOf(vpnProvider, if *o.TCP && helpers.IsOneOf(vpnProvider,
providers.Ipvanish, constants.Perfectprivacy,
providers.Perfectprivacy, constants.Privado,
providers.Privado, constants.Vyprvpn,
providers.VPNUnlimited,
providers.Vyprvpn,
) { ) {
return fmt.Errorf("%w: for VPN service provider %s", return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider) ErrOpenVPNTCPNotSupported, vpnProvider)
@@ -56,74 +51,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.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.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))
} }
} }
@@ -132,37 +113,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),
TCP: gosettings.CopyPointer(o.TCP), TCP: helpers.CopyBoolPtr(o.TCP),
CustomPort: gosettings.CopyPointer(o.CustomPort), CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset), PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
} }
} }
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) { func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = gosettings.MergeWithPointer(o.ConfFile, other.ConfFile) o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = gosettings.MergeWithPointer(o.TCP, other.TCP) o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
o.CustomPort = gosettings.MergeWithPointer(o.CustomPort, other.CustomPort) o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = gosettings.MergeWithPointer(o.PIAEncPreset, other.PIAEncPreset) 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.TCP = gosettings.OverrideWithPointer(o.TCP, other.TCP) 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.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset) o.PIAEncPreset = helpers.OverrideWithStringPtr(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.TCP = gosettings.DefaultPointer(o.TCP, false) o.TCP = helpers.DefaultBool(o.TCP, false)
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0) o.CustomPort = helpers.DefaultUint16(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 {

View File

@@ -3,10 +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/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -14,83 +14,57 @@ 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 forwading 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
// 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"`
} }
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.PrivateInternetAccess,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
} }
// Validate Filepath // Validate Filepath
if *p.Filepath != "" { // optional if *p.Filepath != "" { // optional
_, err := filepath.Abs(*p.Filepath) _, err := filepath.Abs(*p.Filepath)
if err != nil { if err != nil {
return fmt.Errorf("filepath is not valid: %w", err) return fmt.Errorf("%w: %s", ErrPortForwardingFilepathNotValid, err)
} }
} }
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),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
} }
} }
func (p *PortForwarding) mergeWith(other PortForwarding) { func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = gosettings.MergeWithPointer(p.Enabled, other.Enabled) p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
p.Provider = gosettings.MergeWithPointer(p.Provider, other.Provider) p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
p.Filepath = gosettings.MergeWithPointer(p.Filepath, other.Filepath)
p.ListeningPort = gosettings.MergeWithPointer(p.ListeningPort, other.ListeningPort)
} }
func (p *PortForwarding) OverrideWith(other PortForwarding) { func (p *PortForwarding) overrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider) p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
} }
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.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
} }
func (p PortForwarding) String() string { func (p PortForwarding) String() string {
@@ -103,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 == "" {

View File

@@ -3,10 +3,9 @@ package settings
import ( import (
"fmt" "fmt"
"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/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -14,44 +13,42 @@ import (
type Provider struct { type Provider struct {
// Name is the VPN service provider name. // Name is the VPN service provider name.
// It cannot be nil 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, storage Storage) (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.Ivpn, constants.Mullvad,
providers.Mullvad, constants.Windscribe,
providers.Nordvpn,
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, storage) 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 settings validation failed: %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 settings validation failed: %w", err)
} }
return nil return nil
@@ -59,26 +56,26 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
func (p *Provider) copy() (copied Provider) { func (p *Provider) copy() (copied Provider) {
return Provider{ return Provider{
Name: gosettings.CopyPointer(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) { func (p *Provider) mergeWith(other Provider) {
p.Name = gosettings.MergeWithPointer(p.Name, other.Name) p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
p.ServerSelection.mergeWith(other.ServerSelection) p.ServerSelection.mergeWith(other.ServerSelection)
p.PortForwarding.mergeWith(other.PortForwarding) p.PortForwarding.mergeWith(other.PortForwarding)
} }
func (p *Provider) overrideWith(other Provider) { func (p *Provider) overrideWith(other Provider) {
p.Name = gosettings.OverrideWithPointer(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.DefaultPointer(p.Name, providers.PrivateInternetAccess) p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess)
p.ServerSelection.setDefaults(*p.Name) p.ServerSelection.setDefaults(*p.Name)
p.PortForwarding.setDefaults() p.PortForwarding.setDefaults()
} }

View File

@@ -5,8 +5,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/qdm12/gluetun/internal/publicip/api" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -22,28 +21,6 @@ type PublicIP struct {
// to write to a file. It cannot be nil for the // to write to a file. It cannot be nil for the
// internal state // internal state
IPFilepath *string IPFilepath *string
// API is the API name to use to fetch public IP information.
// It can be ipinfo or ip2location. It defaults to ipinfo.
API string
// APIToken is the token to use for the IP data service
// such as ipinfo.io. It can be the empty string to
// indicate not to use a token. It cannot be nil for the
// internal state.
APIToken *string
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// 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) {
@@ -56,47 +33,34 @@ func (p PublicIP) validate() (err error) {
if *p.IPFilepath != "" { // optional if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath) _, err := filepath.Abs(*p.IPFilepath)
if err != nil { if err != nil {
return fmt.Errorf("filepath is not valid: %w", err) return fmt.Errorf("%w: %s", ErrPublicIPFilepathNotValid, err)
} }
} }
_, err = api.ParseProvider(p.API)
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{
Period: gosettings.CopyPointer(p.Period), Period: helpers.CopyDurationPtr(p.Period),
IPFilepath: gosettings.CopyPointer(p.IPFilepath), IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
API: p.API,
APIToken: gosettings.CopyPointer(p.APIToken),
} }
} }
func (p *PublicIP) mergeWith(other PublicIP) { func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = gosettings.MergeWithPointer(p.Period, other.Period) p.Period = helpers.MergeWithDuration(p.Period, other.Period)
p.IPFilepath = gosettings.MergeWithPointer(p.IPFilepath, other.IPFilepath) p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
p.API = gosettings.MergeWithString(p.API, other.API)
p.APIToken = gosettings.MergeWithPointer(p.APIToken, other.APIToken)
} }
func (p *PublicIP) overrideWith(other PublicIP) { func (p *PublicIP) overrideWith(other PublicIP) {
p.Period = gosettings.OverrideWithPointer(p.Period, other.Period) 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.API = gosettings.OverrideWithString(p.API, other.API)
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
} }
func (p *PublicIP) setDefaults() { func (p *PublicIP) setDefaults() {
const defaultPeriod = 12 * time.Hour const defaultPeriod = 12 * time.Hour
p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod) p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip") p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
p.API = gosettings.DefaultString(p.API, "ipinfo")
p.APIToken = gosettings.DefaultPointer(p.APIToken, "")
} }
func (p PublicIP) String() string { func (p PublicIP) String() string {
@@ -121,11 +85,5 @@ func (p PublicIP) toLinesNode() (node *gotree.Node) {
node.Appendf("IP file path: %s", *p.IPFilepath) node.Appendf("IP file path: %s", *p.IPFilepath)
} }
node.Appendf("Public IP data API: %s", p.API)
if *p.APIToken != "" {
node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken))
}
return node return node
} }

View File

@@ -2,40 +2,30 @@ package settings
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"strconv"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// ControlServer contains settings to customize the control server operation. // ControlServer contains settings to customize the control server operation.
type ControlServer struct { type ControlServer struct {
// Address is the listening address to use. // Port is the listening port to use.
// It can be set to 0 to bind to a random port.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
Address *string // TODO change to address
Port *uint16
// 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
} }
func (c ControlServer) validate() (err error) { func (c ControlServer) validate() (err error) {
_, portStr, err := net.SplitHostPort(*c.Address)
if err != nil {
return fmt.Errorf("listening address is not valid: %w", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("listening port it not valid: %w", err)
}
uid := os.Getuid() uid := os.Getuid()
const maxPrivilegedPort = 1023 const maxPrivilegedPort uint16 = 1023
if uid != 0 && port != 0 && port <= maxPrivilegedPort { if uid != 0 && *c.Port <= maxPrivilegedPort {
return fmt.Errorf("%w: %d when running with user ID %d", return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, port, uid) ErrControlServerPrivilegedPort, *c.Port, uid)
} }
return nil return nil
@@ -43,29 +33,30 @@ func (c ControlServer) validate() (err error) {
func (c *ControlServer) copy() (copied ControlServer) { func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{ return ControlServer{
Address: gosettings.CopyPointer(c.Address), Port: helpers.CopyUint16Ptr(c.Port),
Log: gosettings.CopyPointer(c.Log), Log: helpers.CopyBoolPtr(c.Log),
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) { func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = gosettings.MergeWithPointer(c.Address, other.Address) c.Port = helpers.MergeWithUint16(c.Port, other.Port)
c.Log = gosettings.MergeWithPointer(c.Log, other.Log) 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.Port = helpers.MergeWithUint16(c.Port, other.Port)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log) c.Log = helpers.MergeWithBool(c.Log, other.Log)
} }
func (c *ControlServer) setDefaults() { func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000") const defaultPort = 8000
c.Log = gosettings.DefaultPointer(c.Log, true) c.Port = helpers.DefaultUint16(c.Port, defaultPort)
c.Log = helpers.DefaultBool(c.Log, true)
} }
func (c ControlServer) String() string { func (c ControlServer) String() string {
@@ -74,7 +65,7 @@ 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 port: %d", *c.Port)
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log)) node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
return node return node
} }

View File

@@ -1,18 +1,13 @@
package settings package settings
import ( import (
"errors"
"fmt" "fmt"
"net/netip" "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/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"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/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -20,215 +15,232 @@ 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
// TargetIP is the server endpoint IP address to use. // TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked // It will override any IP address from the picked
// built-in server. It cannot be the empty value in the internal // built-in server. It cannot be nil in the internal
// state, and can be set to the unspecified address to indicate // state, and can be set to an empty net.IP{} to indicate
// there is not target IP address to use. // there is not target IP address to use.
TargetIP netip.Addr `json:"target_ip"` TargetIP net.IP
// Countries is the list of countries to filter VPN servers with. // Counties is the list of countries to filter VPN servers with.
Countries []string `json:"countries"` Countries []string
// Categories is the list of categories to filter VPN servers with.
Categories []string `json:"categories"`
// 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 only VPN provider owned servers
// 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 only free VPN servers
// be filtered. This is used with ProtonVPN and VPN Unlimited. // should be filtered. This is used with ProtonVPN.
FreeOnly *bool `json:"free_only"` FreeOnly *bool
// PremiumOnly is true if VPN servers that are not premium should // FreeOnly is true if only free VPN servers
// be filtered. This is used with VPN Secure. // should be filtered. This is used with ProtonVPN.
// TODO extend to providers using FreeOnly. StreamOnly *bool
PremiumOnly *bool `json:"premium_only"` // MultiHopOnly is true if only multihop VPN servers
// StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with VPNUnlimited.
StreamOnly *bool `json:"stream_only"`
// 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.
PortForwardOnly *bool `json:"port_forward_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 (
ErrOwnedOnlyNotSupported = errors.New("owned 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")
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")
)
func (ss *ServerSelection) validate(vpnServiceProvider string, func (ss *ServerSelection) validate(vpnServiceProvider string,
storage Storage) (err error) { allServers models.AllServers) (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, storage) var countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices []string
switch vpnServiceProvider {
case constants.Custom:
case constants.Cyberghost:
servers := allServers.GetCyberghost()
countryChoices = constants.CyberghostCountryChoices(servers)
hostnameChoices = constants.CyberghostHostnameChoices(servers)
case constants.Expressvpn:
servers := allServers.GetExpressvpn()
countryChoices = constants.ExpressvpnCountriesChoices(servers)
cityChoices = constants.ExpressvpnCityChoices(servers)
hostnameChoices = constants.ExpressvpnHostnameChoices(servers)
case constants.Fastestvpn:
servers := allServers.GetFastestvpn()
countryChoices = constants.FastestvpnCountriesChoices(servers)
hostnameChoices = constants.FastestvpnHostnameChoices(servers)
case constants.HideMyAss:
servers := allServers.GetHideMyAss()
countryChoices = constants.HideMyAssCountryChoices(servers)
regionChoices = constants.HideMyAssRegionChoices(servers)
cityChoices = constants.HideMyAssCityChoices(servers)
hostnameChoices = constants.HideMyAssHostnameChoices(servers)
case constants.Ipvanish:
servers := allServers.GetIpvanish()
countryChoices = constants.IpvanishCountryChoices(servers)
cityChoices = constants.IpvanishCityChoices(servers)
hostnameChoices = constants.IpvanishHostnameChoices(servers)
case constants.Ivpn:
servers := allServers.GetIvpn()
countryChoices = constants.IvpnCountryChoices(servers)
cityChoices = constants.IvpnCityChoices(servers)
ispChoices = constants.IvpnISPChoices(servers)
hostnameChoices = constants.IvpnHostnameChoices(servers)
case constants.Mullvad:
servers := allServers.GetMullvad()
countryChoices = constants.MullvadCountryChoices(servers)
cityChoices = constants.MullvadCityChoices(servers)
ispChoices = constants.MullvadISPChoices(servers)
hostnameChoices = constants.MullvadHostnameChoices(servers)
case constants.Nordvpn:
servers := allServers.GetNordvpn()
regionChoices = constants.NordvpnRegionChoices(servers)
hostnameChoices = constants.NordvpnHostnameChoices(servers)
case constants.Perfectprivacy:
servers := allServers.GetPerfectprivacy()
cityChoices = constants.PerfectprivacyCityChoices(servers)
case constants.Privado:
servers := allServers.GetPrivado()
countryChoices = constants.PrivadoCountryChoices(servers)
regionChoices = constants.PrivadoRegionChoices(servers)
cityChoices = constants.PrivadoCityChoices(servers)
hostnameChoices = constants.PrivadoHostnameChoices(servers)
case constants.PrivateInternetAccess:
servers := allServers.GetPia()
regionChoices = constants.PIAGeoChoices(servers)
hostnameChoices = constants.PIAHostnameChoices(servers)
nameChoices = constants.PIANameChoices(servers)
case constants.Privatevpn:
servers := allServers.GetPrivatevpn()
countryChoices = constants.PrivatevpnCountryChoices(servers)
cityChoices = constants.PrivatevpnCityChoices(servers)
hostnameChoices = constants.PrivatevpnHostnameChoices(servers)
case constants.Protonvpn:
servers := allServers.GetProtonvpn()
countryChoices = constants.ProtonvpnCountryChoices(servers)
regionChoices = constants.ProtonvpnRegionChoices(servers)
cityChoices = constants.ProtonvpnCityChoices(servers)
nameChoices = constants.ProtonvpnNameChoices(servers)
hostnameChoices = constants.ProtonvpnHostnameChoices(servers)
case constants.Purevpn:
servers := allServers.GetPurevpn()
countryChoices = constants.PurevpnCountryChoices(servers)
regionChoices = constants.PurevpnRegionChoices(servers)
cityChoices = constants.PurevpnCityChoices(servers)
hostnameChoices = constants.PurevpnHostnameChoices(servers)
case constants.Surfshark:
servers := allServers.GetSurfshark()
countryChoices = constants.SurfsharkCountryChoices(servers)
cityChoices = constants.SurfsharkCityChoices(servers)
hostnameChoices = constants.SurfsharkHostnameChoices(servers)
regionChoices = constants.SurfsharkRegionChoices(servers)
// TODO v4 remove
regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...)
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
// Retro compatibility
// TODO remove in v4
*ss = surfsharkRetroRegion(*ss)
case constants.Torguard:
servers := allServers.GetTorguard()
countryChoices = constants.TorguardCountryChoices(servers)
cityChoices = constants.TorguardCityChoices(servers)
hostnameChoices = constants.TorguardHostnameChoices(servers)
case constants.VPNUnlimited:
servers := allServers.GetVPNUnlimited()
countryChoices = constants.VPNUnlimitedCountryChoices(servers)
cityChoices = constants.VPNUnlimitedCityChoices(servers)
hostnameChoices = constants.VPNUnlimitedHostnameChoices(servers)
case constants.Vyprvpn:
servers := allServers.GetVyprvpn()
regionChoices = constants.VyprvpnRegionChoices(servers)
case constants.Wevpn:
servers := allServers.GetWevpn()
cityChoices = constants.WevpnCityChoices(servers)
hostnameChoices = constants.WevpnHostnameChoices(servers)
case constants.Windscribe:
servers := allServers.GetWindscribe()
regionChoices = constants.WindscribeRegionChoices(servers)
cityChoices = constants.WindscribeCityChoices(servers)
hostnameChoices = constants.WindscribeHostnameChoices(servers)
default:
return fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
}
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices)
if err != nil { if err != nil {
return err // already wrapped error return err // already wrapped error
} }
// Retro-compatibility if ss.VPN == constants.OpenVPN {
switch vpnServiceProvider {
case providers.Nordvpn:
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
case providers.Surfshark:
*ss = surfsharkRetroRegion(*ss)
}
err = validateServerFilters(*ss, filterChoices)
if err != nil {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
if *ss.OwnedOnly &&
vpnServiceProvider != providers.Mullvad {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOwnedOnlyNotSupported, vpnServiceProvider)
}
if *ss.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrFreeOnlyNotSupported, vpnServiceProvider)
}
if *ss.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.VPNSecure,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrPremiumOnlyNotSupported, vpnServiceProvider)
}
if *ss.FreeOnly && *ss.PremiumOnly {
return fmt.Errorf("%w", ErrFreePremiumBothSet)
}
if *ss.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}
if *ss.MultiHopOnly &&
vpnServiceProvider != providers.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}
if *ss.PortForwardOnly &&
vpnServiceProvider != providers.PrivateInternetAccess {
// ProtonVPN also supports port forwarding, but on all their servers, so these
// don't have the port forwarding boolean field. As a consequence, we only allow
// the use of PortForwardOnly for Private Internet Access.
return fmt.Errorf("%w: for VPN service provider %s",
ErrPortForwardOnlyNotSupported, vpnServiceProvider)
}
if ss.VPN == vpn.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 validation failed: %w", err)
} }
} else { } else {
err = ss.Wireguard.validate(vpnServiceProvider) err = ss.Wireguard.validate(vpnServiceProvider)
if err != nil { if err != nil {
return fmt.Errorf("Wireguard server selection settings: %w", err) return fmt.Errorf("Wireguard server selection settings validation failed: %w", err)
} }
} }
return nil return nil
} }
func getLocationFilterChoices(vpnServiceProvider string, // validateServerFilters validates filters against the choices given as arguments.
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices, // Set an argument to nil to pass the check for a particular filter.
err error) { func validateServerFilters(settings ServerSelection,
filterChoices = storage.GetFilterChoices(vpnServiceProvider) countryChoices, regionChoices, cityChoices, ispChoices,
nameChoices, hostnameChoices []string) (err error) {
if vpnServiceProvider == providers.Surfshark { if countryChoices != nil {
// // Retro compatibility if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
// TODO v4 remove return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions)
if err != nil {
// Only return error comparing with newer regions, we don't want to confuse the user
// with the retro regions in the error message.
err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
} }
return filterChoices, nil if regionChoices != nil {
} if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
// validateServerFilters validates filters against the choices given as arguments. }
// Set an argument to nil to pass the check for a particular filter.
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices) (err error) {
err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries)
if err != nil {
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions) if cityChoices != nil {
if err != nil { if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
return fmt.Errorf("%w: %w", ErrRegionNotValid, err) return fmt.Errorf("%w: %s", ErrCityNotValid, err)
}
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities) if ispChoices != nil {
if err != nil { if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
return fmt.Errorf("%w: %w", ErrCityNotValid, err) return fmt.Errorf("%w: %s", ErrISPNotValid, err)
}
} }
err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs) if hostnameChoices != nil {
if err != nil { if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
return fmt.Errorf("%w: %w", ErrISPNotValid, err) return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
}
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames) if nameChoices != nil {
if err != nil { if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil {
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err) return fmt.Errorf("%w: %s", ErrNameNotValid, err)
} }
err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names)
if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
}
err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
} }
return nil return nil
@@ -236,79 +248,68 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
func (ss *ServerSelection) copy() (copied ServerSelection) { func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{ return ServerSelection{
VPN: ss.VPN, VPN: ss.VPN,
TargetIP: ss.TargetIP, TargetIP: helpers.CopyIP(ss.TargetIP),
Countries: gosettings.CopySlice(ss.Countries), Countries: helpers.CopyStringSlice(ss.Countries),
Categories: gosettings.CopySlice(ss.Categories), Regions: helpers.CopyStringSlice(ss.Regions),
Regions: gosettings.CopySlice(ss.Regions), Cities: helpers.CopyStringSlice(ss.Cities),
Cities: gosettings.CopySlice(ss.Cities), ISPs: helpers.CopyStringSlice(ss.ISPs),
ISPs: gosettings.CopySlice(ss.ISPs), Hostnames: helpers.CopyStringSlice(ss.Hostnames),
Hostnames: gosettings.CopySlice(ss.Hostnames), Names: helpers.CopyStringSlice(ss.Names),
Names: gosettings.CopySlice(ss.Names), Numbers: helpers.CopyUint16Slice(ss.Numbers),
Numbers: gosettings.CopySlice(ss.Numbers), OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
OwnedOnly: gosettings.CopyPointer(ss.OwnedOnly), FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
FreeOnly: gosettings.CopyPointer(ss.FreeOnly), StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly), MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
StreamOnly: gosettings.CopyPointer(ss.StreamOnly), OpenVPN: ss.OpenVPN.copy(),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly), Wireguard: ss.Wireguard.copy(),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Wireguard: ss.Wireguard.copy(),
} }
} }
func (ss *ServerSelection) mergeWith(other ServerSelection) { func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = gosettings.MergeWithString(ss.VPN, other.VPN) ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = gosettings.MergeWithValidator(ss.TargetIP, other.TargetIP) ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.MergeWithSlice(ss.Countries, other.Countries) ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
ss.Categories = gosettings.MergeWithSlice(ss.Categories, other.Categories) ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
ss.Regions = gosettings.MergeWithSlice(ss.Regions, other.Regions) ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
ss.Cities = gosettings.MergeWithSlice(ss.Cities, other.Cities) ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
ss.ISPs = gosettings.MergeWithSlice(ss.ISPs, other.ISPs) ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
ss.Hostnames = gosettings.MergeWithSlice(ss.Hostnames, other.Hostnames) ss.Names = helpers.MergeStringSlices(ss.Names, other.Names)
ss.Names = gosettings.MergeWithSlice(ss.Names, other.Names) ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
ss.Numbers = gosettings.MergeWithSlice(ss.Numbers, other.Numbers) ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.OwnedOnly = gosettings.MergeWithPointer(ss.OwnedOnly, other.OwnedOnly) ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
ss.FreeOnly = gosettings.MergeWithPointer(ss.FreeOnly, other.FreeOnly) ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
ss.PremiumOnly = gosettings.MergeWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.StreamOnly = gosettings.MergeWithPointer(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = gosettings.MergeWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.MergeWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
ss.OpenVPN.mergeWith(other.OpenVPN) ss.OpenVPN.mergeWith(other.OpenVPN)
ss.Wireguard.mergeWith(other.Wireguard) ss.Wireguard.mergeWith(other.Wireguard)
} }
func (ss *ServerSelection) overrideWith(other ServerSelection) { func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = gosettings.OverrideWithString(ss.VPN, other.VPN) ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP) ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries) ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories) ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions) ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
ss.Cities = gosettings.OverrideWithSlice(ss.Cities, other.Cities) ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
ss.ISPs = gosettings.OverrideWithSlice(ss.ISPs, other.ISPs) ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
ss.Hostnames = gosettings.OverrideWithSlice(ss.Hostnames, other.Hostnames) ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names)
ss.Names = gosettings.OverrideWithSlice(ss.Names, other.Names) ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
ss.Numbers = gosettings.OverrideWithSlice(ss.Numbers, other.Numbers) ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.OwnedOnly = gosettings.OverrideWithPointer(ss.OwnedOnly, other.OwnedOnly) ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly) ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly)
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) { func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.VPN = gosettings.DefaultString(ss.VPN, vpn.OpenVPN) ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN)
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified()) ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false) ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false) ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false) ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false) ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false)
ss.OpenVPN.setDefaults(vpnProvider) ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults() ss.Wireguard.setDefaults()
} }
@@ -320,7 +321,7 @@ 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 !ss.TargetIP.IsUnspecified() { if len(ss.TargetIP) > 0 {
node.Appendf("Target IP address: %s", ss.TargetIP) node.Appendf("Target IP address: %s", ss.TargetIP)
} }
@@ -328,10 +329,6 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
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, ", "))
} }
@@ -367,10 +364,6 @@ 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")
} }
@@ -379,7 +372,7 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Multi-hop only servers: yes") node.Appendf("Multi-hop only servers: yes")
} }
if ss.VPN == vpn.OpenVPN { if ss.VPN == constants.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode()) node.AppendNode(ss.OpenVPN.toLinesNode())
} else { } else {
node.AppendNode(ss.Wireguard.toLinesNode()) node.AppendNode(ss.Wireguard.toLinesNode())

View File

@@ -3,11 +3,7 @@ package settings
import ( import (
"fmt" "fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -24,17 +20,12 @@ type Settings struct {
Updater Updater Updater Updater
Version Version Version Version
VPN VPN VPN VPN
Pprof pprof.Settings
}
type Storage interface {
GetFilterChoices(provider string) models.FilterChoices
} }
// Validate validates all the settings and returns an error // Validate validates all the settings and returns an error
// if one of them is not valid. // if one of them is not valid.
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) { func (s *Settings) Validate(allServers models.AllServers) (err error) {
nameToValidation := map[string]func() error{ nameToValidation := map[string]func() error{
"control server": s.ControlServer.validate, "control server": s.ControlServer.validate,
"dns": s.DNS.validate, "dns": s.DNS.validate,
@@ -47,16 +38,15 @@ func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
"system": s.System.validate, "system": s.System.validate,
"updater": s.Updater.Validate, "updater": s.Updater.Validate,
"version": s.Version.validate, "version": s.Version.validate,
// Pprof validation done in pprof constructor
"VPN": func() error { "VPN": func() error {
return s.VPN.Validate(storage, ipv6Supported) return s.VPN.validate(allServers)
}, },
} }
for name, validation := range nameToValidation { for name, validation := range nameToValidation {
err = validation() err = validation()
if err != nil { if err != nil {
return fmt.Errorf("%s settings: %w", name, err) return fmt.Errorf("failed validating %s settings: %w", name, err)
} }
} }
@@ -76,8 +66,7 @@ func (s *Settings) copy() (copied Settings) {
System: s.System.copy(), System: s.System.copy(),
Updater: s.Updater.copy(), Updater: s.Updater.copy(),
Version: s.Version.copy(), Version: s.Version.copy(),
VPN: s.VPN.Copy(), VPN: s.VPN.copy(),
Pprof: s.Pprof.Copy(),
} }
} }
@@ -94,11 +83,10 @@ func (s *Settings) MergeWith(other Settings) {
s.Updater.mergeWith(other.Updater) s.Updater.mergeWith(other.Updater)
s.Version.mergeWith(other.Version) s.Version.mergeWith(other.Version)
s.VPN.mergeWith(other.VPN) s.VPN.mergeWith(other.VPN)
s.Pprof.MergeWith(other.Pprof)
} }
func (s *Settings) OverrideWith(other Settings, func (s *Settings) OverrideWith(other Settings,
storage Storage, ipv6Supported bool) (err error) { allServers models.AllServers) (err error) {
patchedSettings := s.copy() patchedSettings := s.copy()
patchedSettings.ControlServer.overrideWith(other.ControlServer) patchedSettings.ControlServer.overrideWith(other.ControlServer)
patchedSettings.DNS.overrideWith(other.DNS) patchedSettings.DNS.overrideWith(other.DNS)
@@ -111,9 +99,8 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.System.overrideWith(other.System) patchedSettings.System.overrideWith(other.System)
patchedSettings.Updater.overrideWith(other.Updater) patchedSettings.Updater.overrideWith(other.Updater)
patchedSettings.Version.overrideWith(other.Version) patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.OverrideWith(other.VPN) patchedSettings.VPN.overrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof) err = patchedSettings.Validate(allServers)
err = patchedSettings.Validate(storage, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
@@ -131,10 +118,9 @@ func (s *Settings) SetDefaults() {
s.PublicIP.setDefaults() s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults() s.Shadowsocks.setDefaults()
s.System.setDefaults() s.System.setDefaults()
s.Updater.SetDefaults()
s.Version.setDefaults() s.Version.setDefaults()
s.VPN.setDefaults() s.VPN.setDefaults()
s.Updater.SetDefaults(*s.VPN.Provider.Name)
s.Pprof.SetDefaults()
} }
func (s Settings) String() string { func (s Settings) String() string {
@@ -156,28 +142,6 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.PublicIP.toLinesNode()) node.AppendNode(s.PublicIP.toLinesNode())
node.AppendNode(s.Updater.toLinesNode()) node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode()) node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode())
return node return node
} }
func (s Settings) Warnings() (warnings []string) {
if *s.VPN.Provider.Name == providers.HideMyAss {
warnings = append(warnings, "HideMyAss dropped support for Linux OpenVPN "+
" so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.")
}
if helpers.IsOneOf(*s.VPN.Provider.Name, providers.SlickVPN) &&
s.VPN.Type == vpn.OpenVPN {
warnings = append(warnings, "OpenVPN 2.5 uses OpenSSL 3 "+
"which prohibits the usage of weak security in today's standards. "+
*s.VPN.Provider.Name+" uses weak security which is out "+
"of Gluetun's control so the only workaround is to allow such weaknesses "+
`using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+
"You might want to reach to your provider so they upgrade their certificates. "+
"Once this is done, you will have to let the Gluetun maintainers know "+
"by creating an issue, attaching the new certificate and we will update Gluetun.")
}
return warnings
}

View File

@@ -34,12 +34,13 @@ func Test_Settings_String(t *testing.T) {
| ├── User: [not set] | ├── User: [not set]
| ├── Password: [not set] | ├── Password: [not set]
| ├── Private Internet Access encryption preset: strong | ├── Private Internet Access encryption preset: strong
| ├── Tunnel IPv6: no
| ├── Network interface: tun0 | ├── Network interface: tun0
| ├── Run OpenVPN as: root | ├── Run OpenVPN as: root
| └── Verbosity level: 1 | └── Verbosity level: 1
├── DNS settings: ├── DNS settings:
| ├── Keep existing nameserver(s): no
| ├── DNS server address to use: 127.0.0.1 | ├── DNS server address to use: 127.0.0.1
| ├── Keep existing nameserver(s): no
| └── DNS over TLS settings: | └── DNS over TLS settings:
| ├── Enabled: yes | ├── Enabled: yes
| ├── Update period: every 24h0m0s | ├── Update period: every 24h0m0s
@@ -65,10 +66,7 @@ func Test_Settings_String(t *testing.T) {
| └── Log level: INFO | └── Log level: INFO
├── Health settings: ├── Health settings:
| ├── Server listening address: 127.0.0.1:9999 | ├── Server listening address: 127.0.0.1:9999
| ├── Target address: cloudflare.com:443 | ├── Target address: github.com:443
| ├── Duration to wait after success: 5s
| ├── Read header timeout: 100ms
| ├── Read timeout: 500ms
| └── VPN wait durations: | └── VPN wait durations:
| ├── Initial duration: 6s | ├── Initial duration: 6s
| └── Additional duration: 5s | └── Additional duration: 5s
@@ -77,15 +75,14 @@ func Test_Settings_String(t *testing.T) {
├── HTTP proxy settings: ├── HTTP proxy settings:
| └── Enabled: no | └── Enabled: no
├── Control server settings: ├── Control server settings:
| ├── Listening address: :8000 | ├── Listening port: 8000
| └── Logging: yes | └── Logging: yes
├── OS Alpine settings: ├── OS Alpine settings:
| ├── Process UID: 1000 | ├── Process UID: 1000
| └── Process GID: 1000 | └── Process GID: 1000
├── Public IP settings: ├── Public IP settings:
| ├── Fetching: every 12h0m0s | ├── Fetching: every 12h0m0s
| ── IP file path: /tmp/gluetun/ip | ── IP file path: /tmp/gluetun/ip
| └── Public IP data API: ipinfo
└── Version settings: └── Version settings:
└── Enabled: yes`, └── Enabled: yes`,
}, },

View File

@@ -1,7 +1,7 @@
package settings package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"github.com/qdm12/ss-server/pkg/tcpudp" "github.com/qdm12/ss-server/pkg/tcpudp"
) )
@@ -21,7 +21,7 @@ func (s Shadowsocks) validate() (err error) {
func (s *Shadowsocks) copy() (copied Shadowsocks) { func (s *Shadowsocks) copy() (copied Shadowsocks) {
return Shadowsocks{ return Shadowsocks{
Enabled: gosettings.CopyPointer(s.Enabled), Enabled: helpers.CopyBoolPtr(s.Enabled),
Settings: s.Settings.Copy(), Settings: s.Settings.Copy(),
} }
} }
@@ -29,20 +29,20 @@ func (s *Shadowsocks) copy() (copied Shadowsocks) {
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (s *Shadowsocks) mergeWith(other Shadowsocks) { func (s *Shadowsocks) mergeWith(other Shadowsocks) {
s.Enabled = gosettings.MergeWithPointer(s.Enabled, other.Enabled) s.Enabled = helpers.MergeWithBool(s.Enabled, other.Enabled)
s.Settings = s.Settings.MergeWith(other.Settings) s.Settings.MergeWith(other.Settings)
} }
// overrideWith overrides fields of the receiver // overrideWith overrides fields of the receiver
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (s *Shadowsocks) overrideWith(other Shadowsocks) { func (s *Shadowsocks) overrideWith(other Shadowsocks) {
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled) s.Enabled = helpers.OverrideWithBool(s.Enabled, other.Enabled)
s.Settings.OverrideWith(other.Settings) s.Settings.OverrideWith(other.Settings)
} }
func (s *Shadowsocks) setDefaults() { func (s *Shadowsocks) setDefaults() {
s.Enabled = gosettings.DefaultPointer(s.Enabled, false) s.Enabled = helpers.DefaultBool(s.Enabled, false)
s.Settings.SetDefaults() s.Settings.SetDefaults()
} }
@@ -53,16 +53,16 @@ func (s Shadowsocks) String() string {
func (s Shadowsocks) toLinesNode() (node *gotree.Node) { func (s Shadowsocks) toLinesNode() (node *gotree.Node) {
node = gotree.New("Shadowsocks server settings:") node = gotree.New("Shadowsocks server settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(s.Enabled)) node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(s.Enabled))
if !*s.Enabled { if !*s.Enabled {
return node return node
} }
// TODO have ToLinesNode in qdm12/ss-server // TODO have ToLinesNode in qdm12/ss-server
node.Appendf("Listening address: %s", *s.Address) node.Appendf("Listening address: %s", s.Address)
node.Appendf("Cipher: %s", s.CipherName) node.Appendf("Cipher: %s", s.CipherName)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password)) node.Appendf("Password: %s", helpers.ObfuscatePassword(*s.Password))
node.Appendf("Log addresses: %s", gosettings.BoolToYesNo(s.LogAddresses)) node.Appendf("Log addresses: %s", helpers.BoolPtrToYesNo(s.LogAddresses))
return node return node
} }

View File

@@ -3,14 +3,15 @@ package settings
import ( import (
"strings" "strings"
"github.com/qdm12/gluetun/internal/provider/surfshark/servers" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
) )
func surfsharkRetroRegion(selection ServerSelection) ( func surfsharkRetroRegion(selection ServerSelection) (
updatedSelection ServerSelection) { updatedSelection ServerSelection) {
locationData := servers.LocationData() locationData := constants.SurfsharkLocationData()
retroToLocation := make(map[string]servers.ServerLocation, len(locationData)) retroToLocation := make(map[string]models.SurfsharkLocationData, len(locationData))
for _, data := range locationData { for _, data := range locationData {
if data.RetroLoc == "" { if data.RetroLoc == "" {
continue continue

View File

@@ -1,14 +1,14 @@
package settings package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
// System contains settings to configure system related elements. // System contains settings to configure system related elements.
type System struct { type System struct {
PUID *uint32 PUID *uint16
PGID *uint32 PGID *uint16
Timezone string Timezone string
} }
@@ -19,28 +19,28 @@ func (s System) validate() (err error) {
func (s *System) copy() (copied System) { func (s *System) copy() (copied System) {
return System{ return System{
PUID: gosettings.CopyPointer(s.PUID), PUID: helpers.CopyUint16Ptr(s.PUID),
PGID: gosettings.CopyPointer(s.PGID), PGID: helpers.CopyUint16Ptr(s.PGID),
Timezone: s.Timezone, Timezone: s.Timezone,
} }
} }
func (s *System) mergeWith(other System) { func (s *System) mergeWith(other System) {
s.PUID = gosettings.MergeWithPointer(s.PUID, other.PUID) s.PUID = helpers.MergeWithUint16(s.PUID, other.PUID)
s.PGID = gosettings.MergeWithPointer(s.PGID, other.PGID) s.PGID = helpers.MergeWithUint16(s.PGID, other.PGID)
s.Timezone = gosettings.MergeWithString(s.Timezone, other.Timezone) s.Timezone = helpers.MergeWithString(s.Timezone, other.Timezone)
} }
func (s *System) overrideWith(other System) { func (s *System) overrideWith(other System) {
s.PUID = gosettings.OverrideWithPointer(s.PUID, other.PUID) s.PUID = helpers.OverrideWithUint16(s.PUID, other.PUID)
s.PGID = gosettings.OverrideWithPointer(s.PGID, other.PGID) s.PGID = helpers.OverrideWithUint16(s.PGID, other.PGID)
s.Timezone = gosettings.OverrideWithString(s.Timezone, other.Timezone) s.Timezone = helpers.OverrideWithString(s.Timezone, other.Timezone)
} }
func (s *System) setDefaults() { func (s *System) setDefaults() {
const defaultID = 1000 const defaultID = 1000
s.PUID = gosettings.DefaultPointer(s.PUID, defaultID) s.PUID = helpers.DefaultUint16(s.PUID, defaultID)
s.PGID = gosettings.DefaultPointer(s.PGID, defaultID) s.PGID = helpers.DefaultUint16(s.PGID, defaultID)
} }
func (s System) String() string { func (s System) String() string {

View File

@@ -3,24 +3,25 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net"
"github.com/qdm12/dns/pkg/provider" "github.com/qdm12/dns/pkg/provider"
"github.com/qdm12/dns/pkg/unbound" "github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"inet.af/netaddr"
) )
// Unbound is settings for the Unbound program. // Unbound is settings for the Unbound program.
type Unbound struct { type Unbound struct {
Providers []string `json:"providers"` Providers []string
Caching *bool `json:"caching"` Caching *bool
IPv6 *bool `json:"ipv6"` IPv6 *bool
VerbosityLevel *uint8 `json:"verbosity_level"` VerbosityLevel *uint8
VerbosityDetailsLevel *uint8 `json:"verbosity_details_level"` VerbosityDetailsLevel *uint8
ValidationLogLevel *uint8 `json:"validation_log_level"` ValidationLogLevel *uint8
Username string `json:"username"` Username string
Allowed []netip.Prefix `json:"allowed"` Allowed []netaddr.IPPrefix
} }
func (u *Unbound) setDefaults() { func (u *Unbound) setDefaults() {
@@ -30,26 +31,26 @@ func (u *Unbound) setDefaults() {
} }
} }
u.Caching = gosettings.DefaultPointer(u.Caching, true) u.Caching = helpers.DefaultBool(u.Caching, true)
u.IPv6 = gosettings.DefaultPointer(u.IPv6, false) u.IPv6 = helpers.DefaultBool(u.IPv6, false)
const defaultVerbosityLevel = 1 const defaultVerbosityLevel = 1
u.VerbosityLevel = gosettings.DefaultPointer(u.VerbosityLevel, defaultVerbosityLevel) u.VerbosityLevel = helpers.DefaultUint8(u.VerbosityLevel, defaultVerbosityLevel)
const defaultVerbosityDetailsLevel = 0 const defaultVerbosityDetailsLevel = 0
u.VerbosityDetailsLevel = gosettings.DefaultPointer(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel) u.VerbosityDetailsLevel = helpers.DefaultUint8(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
const defaultValidationLogLevel = 0 const defaultValidationLogLevel = 0
u.ValidationLogLevel = gosettings.DefaultPointer(u.ValidationLogLevel, defaultValidationLogLevel) u.ValidationLogLevel = helpers.DefaultUint8(u.ValidationLogLevel, defaultValidationLogLevel)
if u.Allowed == nil { if u.Allowed == nil {
u.Allowed = []netip.Prefix{ u.Allowed = []netaddr.IPPrefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0), netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0), netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
} }
} }
u.Username = gosettings.DefaultString(u.Username, "root") u.Username = helpers.DefaultString(u.Username, "root")
} }
var ( var (
@@ -94,37 +95,37 @@ func (u Unbound) validate() (err error) {
func (u Unbound) copy() (copied Unbound) { func (u Unbound) copy() (copied Unbound) {
return Unbound{ return Unbound{
Providers: gosettings.CopySlice(u.Providers), Providers: helpers.CopyStringSlice(u.Providers),
Caching: gosettings.CopyPointer(u.Caching), Caching: helpers.CopyBoolPtr(u.Caching),
IPv6: gosettings.CopyPointer(u.IPv6), IPv6: helpers.CopyBoolPtr(u.IPv6),
VerbosityLevel: gosettings.CopyPointer(u.VerbosityLevel), VerbosityLevel: helpers.CopyUint8Ptr(u.VerbosityLevel),
VerbosityDetailsLevel: gosettings.CopyPointer(u.VerbosityDetailsLevel), VerbosityDetailsLevel: helpers.CopyUint8Ptr(u.VerbosityDetailsLevel),
ValidationLogLevel: gosettings.CopyPointer(u.ValidationLogLevel), ValidationLogLevel: helpers.CopyUint8Ptr(u.ValidationLogLevel),
Username: u.Username, Username: u.Username,
Allowed: gosettings.CopySlice(u.Allowed), Allowed: helpers.CopyIPPrefixSlice(u.Allowed),
} }
} }
func (u *Unbound) mergeWith(other Unbound) { func (u *Unbound) mergeWith(other Unbound) {
u.Providers = gosettings.MergeWithSlice(u.Providers, other.Providers) u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
u.Caching = gosettings.MergeWithPointer(u.Caching, other.Caching) u.Caching = helpers.MergeWithBool(u.Caching, other.Caching)
u.IPv6 = gosettings.MergeWithPointer(u.IPv6, other.IPv6) u.IPv6 = helpers.MergeWithBool(u.IPv6, other.IPv6)
u.VerbosityLevel = gosettings.MergeWithPointer(u.VerbosityLevel, other.VerbosityLevel) u.VerbosityLevel = helpers.MergeWithUint8(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = gosettings.MergeWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel) u.VerbosityDetailsLevel = helpers.MergeWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = gosettings.MergeWithPointer(u.ValidationLogLevel, other.ValidationLogLevel) u.ValidationLogLevel = helpers.MergeWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = gosettings.MergeWithString(u.Username, other.Username) u.Username = helpers.MergeWithString(u.Username, other.Username)
u.Allowed = gosettings.MergeWithSlice(u.Allowed, other.Allowed) u.Allowed = helpers.MergeIPPrefixesSlices(u.Allowed, other.Allowed)
} }
func (u *Unbound) overrideWith(other Unbound) { func (u *Unbound) overrideWith(other Unbound) {
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers) u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
u.Caching = gosettings.OverrideWithPointer(u.Caching, other.Caching) u.Caching = helpers.OverrideWithBool(u.Caching, other.Caching)
u.IPv6 = gosettings.OverrideWithPointer(u.IPv6, other.IPv6) u.IPv6 = helpers.OverrideWithBool(u.IPv6, other.IPv6)
u.VerbosityLevel = gosettings.OverrideWithPointer(u.VerbosityLevel, other.VerbosityLevel) u.VerbosityLevel = helpers.OverrideWithUint8(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = gosettings.OverrideWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel) u.VerbosityDetailsLevel = helpers.OverrideWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = gosettings.OverrideWithPointer(u.ValidationLogLevel, other.ValidationLogLevel) u.ValidationLogLevel = helpers.OverrideWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = gosettings.OverrideWithString(u.Username, other.Username) u.Username = helpers.OverrideWithString(u.Username, other.Username)
u.Allowed = gosettings.OverrideWithSlice(u.Allowed, other.Allowed) u.Allowed = helpers.OverrideWithIPPrefixesSlice(u.Allowed, other.Allowed)
} }
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) { func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
@@ -148,30 +149,20 @@ func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
VerbosityDetailsLevel: *u.VerbosityDetailsLevel, VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
ValidationLogLevel: *u.ValidationLogLevel, ValidationLogLevel: *u.ValidationLogLevel,
AccessControl: unbound.AccessControlSettings{ AccessControl: unbound.AccessControlSettings{
Allowed: netipPrefixesToNetaddrIPPrefixes(u.Allowed), Allowed: u.Allowed,
}, },
Username: u.Username, Username: u.Username,
}, nil }, nil
} }
var ( func (u Unbound) GetFirstPlaintextIPv4() (ipv4 net.IP, err error) {
ErrConvertingNetip = errors.New("converting net.IP to netip.Addr failed")
)
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 netip.Addr, err error) {
s := u.Providers[0] s := u.Providers[0]
provider, err := provider.Parse(s) provider, err := provider.Parse(s)
if err != nil { if err != nil {
return ipv4, err return nil, err
} }
ip := provider.DNS().IPv4[0] return provider.DNS().IPv4[0], nil
ipv4, ok := netip.AddrFromSlice(ip)
if !ok {
return ipv4, fmt.Errorf("%w: for ip %s (%#v)",
ErrConvertingNetip, ip, ip)
}
return ipv4.Unmap(), nil
} }
func (u Unbound) String() string { func (u Unbound) String() string {
@@ -186,8 +177,8 @@ func (u Unbound) toLinesNode() (node *gotree.Node) {
authServers.Appendf(provider) authServers.Appendf(provider)
} }
node.Appendf("Caching: %s", gosettings.BoolToYesNo(u.Caching)) node.Appendf("Caching: %s", helpers.BoolPtrToYesNo(u.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(u.IPv6)) node.Appendf("IPv6: %s", helpers.BoolPtrToYesNo(u.IPv6))
node.Appendf("Verbosity level: %d", *u.VerbosityLevel) node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel) node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
node.Appendf("Validation log level: %d", *u.ValidationLogLevel) node.Appendf("Validation log level: %d", *u.ValidationLogLevel)

View File

@@ -2,11 +2,11 @@ package settings
import ( import (
"encoding/json" "encoding/json"
"net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"inet.af/netaddr"
) )
func Test_Unbound_JSON(t *testing.T) { func Test_Unbound_JSON(t *testing.T) {
@@ -20,18 +20,18 @@ func Test_Unbound_JSON(t *testing.T) {
VerbosityDetailsLevel: nil, VerbosityDetailsLevel: nil,
ValidationLogLevel: uint8Ptr(0), ValidationLogLevel: uint8Ptr(0),
Username: "user", Username: "user",
Allowed: []netip.Prefix{ Allowed: []netaddr.IPPrefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0), netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0), netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
}, },
} }
b, err := json.Marshal(settings) b, err := json.Marshal(settings)
require.NoError(t, err) require.NoError(t, err)
const expected = `{"providers":["cloudflare"],"caching":true,"ipv6":false,` + const expected = `{"Providers":["cloudflare"],"Caching":true,"IPv6":false,` +
`"verbosity_level":1,"verbosity_details_level":null,"validation_log_level":0,` + `"VerbosityLevel":1,"VerbosityDetailsLevel":null,"ValidationLogLevel":0,` +
`"username":"user","allowed":["0.0.0.0/0","::/0"]}` `"Username":"user","Allowed":["0.0.0.0/0","::/0"]}`
assert.Equal(t, expected, string(b)) assert.Equal(t, expected, string(b))

View File

@@ -2,12 +2,12 @@ package settings
import ( import (
"fmt" "fmt"
"net"
"strings" "strings"
"time" "time"
"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/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -21,15 +21,16 @@ type Updater struct {
Period *time.Duration Period *time.Duration
// DNSAddress is the DNS server address to use // DNSAddress is the DNS server address to use
// to resolve VPN server hostnames to IP addresses. // to resolve VPN server hostnames to IP addresses.
// It cannot be the empty string in the internal state. // It cannot be nil in the internal state.
DNSAddress string DNSAddress net.IP
// MinRatio is the minimum ratio of servers to
// find per provider, compared to the total current
// number of servers. It defaults to 0.8.
MinRatio float64
// Providers is the list of VPN service providers // Providers is the list of VPN service providers
// to update server information for. // to update server information for.
Providers []string Providers []string
// CLI is to precise the updater is running in CLI
// mode. This is set automatically and cannot be set
// by settings sources. It cannot be nil in the
// internal state.
CLI *bool
} }
func (u Updater) Validate() (err error) { func (u Updater) Validate() (err error) {
@@ -39,16 +40,21 @@ func (u Updater) Validate() (err error) {
ErrUpdaterPeriodTooSmall, *u.Period, minPeriod) ErrUpdaterPeriodTooSmall, *u.Period, minPeriod)
} }
if u.MinRatio <= 0 || u.MinRatio > 1 { for i, provider := range u.Providers {
return fmt.Errorf("%w: %.2f must be between 0+ and 1", valid := false
ErrMinRatioNotValid, u.MinRatio) for _, validProvider := range constants.AllProviders() {
} if validProvider == constants.Custom {
continue
}
validProviders := providers.All() if provider == validProvider {
for _, provider := range u.Providers { valid = true
err = validate.IsOneOf(provider, validProviders...) break
if err != nil { }
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err) }
if !valid {
return fmt.Errorf("%w: %s at index %d",
ErrVPNProviderNameNotValid, provider, i)
} }
} }
@@ -57,44 +63,36 @@ func (u Updater) Validate() (err error) {
func (u *Updater) copy() (copied Updater) { func (u *Updater) copy() (copied Updater) {
return Updater{ return Updater{
Period: gosettings.CopyPointer(u.Period), Period: helpers.CopyDurationPtr(u.Period),
DNSAddress: u.DNSAddress, DNSAddress: helpers.CopyIP(u.DNSAddress),
MinRatio: u.MinRatio, Providers: helpers.CopyStringSlice(u.Providers),
Providers: gosettings.CopySlice(u.Providers), CLI: u.CLI,
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (u *Updater) mergeWith(other Updater) { func (u *Updater) mergeWith(other Updater) {
u.Period = gosettings.MergeWithPointer(u.Period, other.Period) u.Period = helpers.MergeWithDuration(u.Period, other.Period)
u.DNSAddress = gosettings.MergeWithString(u.DNSAddress, other.DNSAddress) u.DNSAddress = helpers.MergeWithIP(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.MergeWithNumber(u.MinRatio, other.MinRatio) u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
u.Providers = gosettings.MergeWithSlice(u.Providers, other.Providers) u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
} }
// 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 (u *Updater) overrideWith(other Updater) { func (u *Updater) overrideWith(other Updater) {
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period) u.Period = helpers.OverrideWithDuration(u.Period, other.Period)
u.DNSAddress = gosettings.OverrideWithString(u.DNSAddress, other.DNSAddress) u.DNSAddress = helpers.OverrideWithIP(u.DNSAddress, other.DNSAddress)
u.MinRatio = gosettings.OverrideWithNumber(u.MinRatio, other.MinRatio) u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers) u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
} }
func (u *Updater) SetDefaults(vpnProvider string) { func (u *Updater) SetDefaults() {
u.Period = gosettings.DefaultPointer(u.Period, 0) u.Period = helpers.DefaultDuration(u.Period, 0)
u.DNSAddress = gosettings.DefaultString(u.DNSAddress, "1.1.1.1:53") u.DNSAddress = helpers.DefaultIP(u.DNSAddress, net.IPv4(1, 1, 1, 1))
u.CLI = helpers.DefaultBool(u.CLI, false)
if u.MinRatio == 0 {
const defaultMinRatio = 0.8
u.MinRatio = defaultMinRatio
}
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
u.Providers = []string{vpnProvider}
}
} }
func (u Updater) String() string { func (u Updater) String() string {
@@ -102,15 +100,18 @@ func (u Updater) String() string {
} }
func (u Updater) toLinesNode() (node *gotree.Node) { func (u Updater) toLinesNode() (node *gotree.Node) {
if *u.Period == 0 || len(u.Providers) == 0 { if *u.Period == 0 {
return nil return nil
} }
node = gotree.New("Server data updater settings:") node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period) node.Appendf("Update period: %s", *u.Period)
node.Appendf("DNS address: %s", u.DNSAddress) node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", ")) node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
if *u.CLI {
node.Appendf("CLI mode: enabled")
}
return node return node
} }

View File

@@ -1,151 +0,0 @@
package validation
import (
"sort"
"github.com/qdm12/gluetun/internal/models"
)
func sortedInsert(ss []string, s string) []string {
i := sort.SearchStrings(ss, s)
ss = append(ss, "")
copy(ss[i+1:], ss[i:])
ss[i] = s
return ss
}
func ExtractCountries(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.Country
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}
func ExtractCategories(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
categories := server.Categories
if len(categories) == 0 {
continue
}
for _, value := range categories {
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
}
return values
}
func ExtractRegions(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.Region
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}
func ExtractCities(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.City
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}
func ExtractISPs(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.ISP
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}
func ExtractServerNames(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.ServerName
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}
func ExtractHostnames(servers []models.Server) (values []string) {
seen := make(map[string]struct{}, len(servers))
values = make([]string, 0, len(servers))
for _, server := range servers {
value := server.Hostname
if value == "" {
continue
}
_, alreadySeen := seen[value]
if alreadySeen {
continue
}
seen[value] = struct{}{}
values = sortedInsert(values, value)
}
return values
}

View File

@@ -1,24 +0,0 @@
package validation
import (
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
)
// TODO remove in v4.
func SurfsharkRetroLocChoices() (choices []string) {
locationData := servers.LocationData()
choices = make([]string, 0, len(locationData))
seen := make(map[string]struct{}, len(locationData))
for _, data := range locationData {
if data.RetroLoc == "" {
continue
}
if _, ok := seen[data.RetroLoc]; ok {
continue
}
seen[data.RetroLoc] = struct{}{}
choices = sortedInsert(choices, data.RetroLoc)
}
return choices
}

View File

@@ -1,7 +1,7 @@
package settings package settings
import ( import (
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -19,25 +19,25 @@ func (v Version) validate() (err error) {
func (v *Version) copy() (copied Version) { func (v *Version) copy() (copied Version) {
return Version{ return Version{
Enabled: gosettings.CopyPointer(v.Enabled), Enabled: helpers.CopyBoolPtr(v.Enabled),
} }
} }
// mergeWith merges the other settings into any // mergeWith merges the other settings into any
// unset field of the receiver settings object. // unset field of the receiver settings object.
func (v *Version) mergeWith(other Version) { func (v *Version) mergeWith(other Version) {
v.Enabled = gosettings.MergeWithPointer(v.Enabled, other.Enabled) v.Enabled = helpers.MergeWithBool(v.Enabled, other.Enabled)
} }
// 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 (v *Version) overrideWith(other Version) { func (v *Version) overrideWith(other Version) {
v.Enabled = gosettings.OverrideWithPointer(v.Enabled, other.Enabled) v.Enabled = helpers.OverrideWithBool(v.Enabled, other.Enabled)
} }
func (v *Version) setDefaults() { func (v *Version) setDefaults() {
v.Enabled = gosettings.DefaultPointer(v.Enabled, true) v.Enabled = helpers.DefaultBool(v.Enabled, true)
} }
func (v Version) String() string { func (v Version) String() string {
@@ -47,7 +47,7 @@ func (v Version) String() string {
func (v Version) toLinesNode() (node *gotree.Node) { func (v Version) toLinesNode() (node *gotree.Node) {
node = gotree.New("Version settings:") node = gotree.New("Version settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(v.Enabled)) node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(v.Enabled))
return node return node
} }

View File

@@ -2,10 +2,11 @@ package settings
import ( import (
"fmt" "fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants/vpn" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gosettings" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gosettings/validate" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
@@ -13,41 +14,42 @@ type VPN struct {
// Type is the VPN type and can only be // Type is the VPN type and can only be
// 'openvpn' or 'wireguard'. It cannot be the // 'openvpn' or 'wireguard'. It cannot be the
// empty string in the internal state. // empty string in the internal state.
Type string `json:"type"` Type string
Provider Provider `json:"provider"` Provider Provider
OpenVPN OpenVPN `json:"openvpn"` OpenVPN OpenVPN
Wireguard Wireguard `json:"wireguard"` Wireguard Wireguard
} }
// TODO v4 remove pointer for receiver (because of Surfshark). // TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) Validate(storage Storage, ipv6Supported bool) (err error) { func (v *VPN) validate(allServers models.AllServers) (err error) {
// Validate Type // Validate Type
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard} validVPNTypes := []string{constants.OpenVPN, constants.Wireguard}
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil { if !helpers.IsOneOf(v.Type, validVPNTypes...) {
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err) return fmt.Errorf("%w: %q and can only be one of %s",
ErrVPNTypeNotValid, v.Type, strings.Join(validVPNTypes, ", "))
} }
err = v.Provider.validate(v.Type, storage) err = v.Provider.validate(v.Type, allServers)
if err != nil { if err != nil {
return fmt.Errorf("provider settings: %w", err) return fmt.Errorf("provider settings validation failed: %w", err)
} }
if v.Type == vpn.OpenVPN { if v.Type == constants.OpenVPN {
err := v.OpenVPN.validate(*v.Provider.Name) err := v.OpenVPN.validate(*v.Provider.Name)
if err != nil { if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err) return fmt.Errorf("OpenVPN settings validation failed: %w", err)
} }
} else { } else {
err := v.Wireguard.validate(*v.Provider.Name, ipv6Supported) err := v.Wireguard.validate(*v.Provider.Name)
if err != nil { if err != nil {
return fmt.Errorf("Wireguard settings: %w", err) return fmt.Errorf("Wireguard settings validation failed: %w", err)
} }
} }
return nil return nil
} }
func (v *VPN) Copy() (copied VPN) { func (v *VPN) copy() (copied VPN) {
return VPN{ return VPN{
Type: v.Type, Type: v.Type,
Provider: v.Provider.copy(), Provider: v.Provider.copy(),
@@ -57,24 +59,24 @@ func (v *VPN) Copy() (copied VPN) {
} }
func (v *VPN) mergeWith(other VPN) { func (v *VPN) mergeWith(other VPN) {
v.Type = gosettings.MergeWithString(v.Type, other.Type) v.Type = helpers.MergeWithString(v.Type, other.Type)
v.Provider.mergeWith(other.Provider) v.Provider.mergeWith(other.Provider)
v.OpenVPN.mergeWith(other.OpenVPN) v.OpenVPN.mergeWith(other.OpenVPN)
v.Wireguard.mergeWith(other.Wireguard) v.Wireguard.mergeWith(other.Wireguard)
} }
func (v *VPN) OverrideWith(other VPN) { func (v *VPN) overrideWith(other VPN) {
v.Type = gosettings.OverrideWithString(v.Type, other.Type) v.Type = helpers.OverrideWithString(v.Type, other.Type)
v.Provider.overrideWith(other.Provider) v.Provider.overrideWith(other.Provider)
v.OpenVPN.overrideWith(other.OpenVPN) v.OpenVPN.overrideWith(other.OpenVPN)
v.Wireguard.overrideWith(other.Wireguard) v.Wireguard.overrideWith(other.Wireguard)
} }
func (v *VPN) setDefaults() { func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultString(v.Type, vpn.OpenVPN) v.Type = helpers.DefaultString(v.Type, constants.OpenVPN)
v.Provider.setDefaults() v.Provider.setDefaults()
v.OpenVPN.setDefaults(*v.Provider.Name) v.OpenVPN.setDefaults(*v.Provider.Name)
v.Wireguard.setDefaults(*v.Provider.Name) v.Wireguard.setDefaults()
} }
func (v VPN) String() string { func (v VPN) String() string {
@@ -86,7 +88,7 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
node.AppendNode(v.Provider.toLinesNode()) node.AppendNode(v.Provider.toLinesNode())
if v.Type == vpn.OpenVPN { if v.Type == constants.OpenVPN {
node.AppendNode(v.OpenVPN.toLinesNode()) node.AppendNode(v.OpenVPN.toLinesNode())
} else { } else {
node.AppendNode(v.Wireguard.toLinesNode()) node.AppendNode(v.Wireguard.toLinesNode())

View File

@@ -2,13 +2,11 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip" "net"
"regexp" "regexp"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
@@ -17,48 +15,30 @@ import (
type Wireguard struct { type Wireguard struct {
// PrivateKey is the Wireguard client peer private key. // PrivateKey is the Wireguard client peer private key.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
PrivateKey *string `json:"private_key"` PrivateKey *string
// PreSharedKey is the Wireguard pre-shared key. // PreSharedKey is the Wireguard pre-shared key.
// It can be the empty string to indicate there // It can be the empty string to indicate there
// is no pre-shared key. // is no pre-shared key.
// It cannot be nil in the internal state. // It cannot be nil in the internal state.
PreSharedKey *string `json:"pre_shared_key"` PreSharedKey *string
// Addresses are the Wireguard interface addresses. // Addresses are the Wireguard interface addresses.
Addresses []netip.Prefix `json:"addresses"` Addresses []net.IPNet
// AllowedIPs are the Wireguard allowed IPs.
// If left unset, they default to "0.0.0.0/0"
// and, if IPv6 is supported, "::0".
AllowedIPs []netip.Prefix `json:"allowed_ips"`
// Interface is the name of the Wireguard interface // Interface is the name of the Wireguard interface
// to create. It cannot be the empty string in the // to create. It cannot be the empty string in the
// internal state. // internal state.
Interface string `json:"interface"` Interface string
// Maximum Transmission Unit (MTU) of the Wireguard interface.
// It cannot be zero in the internal state, and defaults to
// 1400. Note it is not the wireguard-go MTU default of 1420
// because this impacts bandwidth a lot on some VPN providers,
// see https://github.com/qdm12/gluetun/issues/1650.
MTU uint16 `json:"mtu"`
// Implementation is the Wireguard implementation to use.
// It can be "auto", "userspace" or "kernelspace".
// It defaults to "auto" and cannot be the empty string
// in the internal state.
Implementation string `json:"implementation"`
} }
var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
// Validate validates Wireguard settings. // Validate validates Wireguard settings.
// It should only be ran if the VPN type chosen is Wireguard. // It should only be ran if the VPN type chosen is Wireguard.
func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error) { func (w Wireguard) validate(vpnProvider string) (err error) {
if !helpers.IsOneOf(vpnProvider, if !helpers.IsOneOf(vpnProvider,
providers.Airvpn, constants.Custom,
providers.Custom, constants.Ivpn,
providers.Ivpn, constants.Mullvad,
providers.Mullvad, constants.Windscribe,
providers.Nordvpn,
providers.Surfshark,
providers.Windscribe,
) { ) {
// do not validate for VPN provider not supporting Wireguard // do not validate for VPN provider not supporting Wireguard
return nil return nil
@@ -66,58 +46,29 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
// Validate PrivateKey // Validate PrivateKey
if *w.PrivateKey == "" { if *w.PrivateKey == "" {
return fmt.Errorf("%w", ErrWireguardPrivateKeyNotSet) return ErrWireguardPrivateKeyNotSet
} }
_, err = wgtypes.ParseKey(*w.PrivateKey) _, err = wgtypes.ParseKey(*w.PrivateKey)
if err != nil { if err != nil {
err = fmt.Errorf("private key is not valid: %w", err) return fmt.Errorf("%w: %s", ErrWireguardPrivateKeyNotValid, err)
if vpnProvider == providers.Nordvpn &&
err.Error() == "wgtypes: incorrect key size: 48" {
err = fmt.Errorf("%w - you might be using your access token instead of the Wireguard private key", err)
}
return err
}
if vpnProvider == providers.Airvpn {
if *w.PreSharedKey == "" {
return fmt.Errorf("%w", ErrWireguardPreSharedKeyNotSet)
}
} }
// Validate PreSharedKey // Validate PreSharedKey
if *w.PreSharedKey != "" { // Note: this is optional if *w.PreSharedKey != "" { // Note: this is optional
_, err = wgtypes.ParseKey(*w.PreSharedKey) _, err = wgtypes.ParseKey(*w.PreSharedKey)
if err != nil { if err != nil {
return fmt.Errorf("pre-shared key is not valid: %w", err) return fmt.Errorf("%w: %s", ErrWireguardPreSharedKeyNotValid, err)
} }
} }
// Validate Addresses // Validate Addresses
if len(w.Addresses) == 0 { if len(w.Addresses) == 0 {
return fmt.Errorf("%w", ErrWireguardInterfaceAddressNotSet) return ErrWireguardInterfaceAddressNotSet
} }
for i, ipNet := range w.Addresses { for i, ipNet := range w.Addresses {
if !ipNet.IsValid() { if ipNet.IP == nil || ipNet.Mask == nil {
return fmt.Errorf("%w: for address at index %d", return fmt.Errorf("%w: for address at index %d: %s",
ErrWireguardInterfaceAddressNotSet, i) ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
}
if !ipv6Supported && ipNet.Addr().Is6() {
return fmt.Errorf("%w: address %s",
ErrWireguardInterfaceAddressIPv6, ipNet.String())
}
}
// Validate AllowedIPs
// WARNING: do not check for IPv6 networks in the allowed IPs,
// the wireguard code will take care to ignore it.
if len(w.AllowedIPs) == 0 {
return fmt.Errorf("%w", ErrWireguardAllowedIPsNotSet)
}
for i, allowedIP := range w.AllowedIPs {
if !allowedIP.IsValid() {
return fmt.Errorf("%w: for allowed ip %d of %d",
ErrWireguardAllowedIPNotSet, i+1, len(w.AllowedIPs))
} }
} }
@@ -127,63 +78,36 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName) ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
} }
validImplementations := []string{"auto", "userspace", "kernelspace"}
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err)
}
return nil return nil
} }
func (w *Wireguard) copy() (copied Wireguard) { func (w *Wireguard) copy() (copied Wireguard) {
return Wireguard{ return Wireguard{
PrivateKey: gosettings.CopyPointer(w.PrivateKey), PrivateKey: helpers.CopyStringPtr(w.PrivateKey),
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey), PreSharedKey: helpers.CopyStringPtr(w.PreSharedKey),
Addresses: gosettings.CopySlice(w.Addresses), Addresses: helpers.CopyIPNetSlice(w.Addresses),
AllowedIPs: gosettings.CopySlice(w.AllowedIPs), Interface: w.Interface,
Interface: w.Interface,
MTU: w.MTU,
Implementation: w.Implementation,
} }
} }
func (w *Wireguard) mergeWith(other Wireguard) { func (w *Wireguard) mergeWith(other Wireguard) {
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey) w.PrivateKey = helpers.MergeWithStringPtr(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey) w.PreSharedKey = helpers.MergeWithStringPtr(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.MergeWithSlice(w.Addresses, other.Addresses) w.Addresses = helpers.MergeIPNetsSlices(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.MergeWithSlice(w.AllowedIPs, other.AllowedIPs) w.Interface = helpers.MergeWithString(w.Interface, other.Interface)
w.Interface = gosettings.MergeWithString(w.Interface, other.Interface)
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
} }
func (w *Wireguard) overrideWith(other Wireguard) { func (w *Wireguard) overrideWith(other Wireguard) {
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey) w.PrivateKey = helpers.OverrideWithStringPtr(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey) w.PreSharedKey = helpers.OverrideWithStringPtr(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses) w.Addresses = helpers.OverrideWithIPNetsSlice(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.OverrideWithSlice(w.AllowedIPs, other.AllowedIPs) w.Interface = helpers.OverrideWithString(w.Interface, other.Interface)
w.Interface = gosettings.OverrideWithString(w.Interface, other.Interface)
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU)
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
} }
func (w *Wireguard) setDefaults(vpnProvider string) { func (w *Wireguard) setDefaults() {
w.PrivateKey = gosettings.DefaultPointer(w.PrivateKey, "") w.PrivateKey = helpers.DefaultStringPtr(w.PrivateKey, "")
w.PreSharedKey = gosettings.DefaultPointer(w.PreSharedKey, "") w.PreSharedKey = helpers.DefaultStringPtr(w.PreSharedKey, "")
if vpnProvider == providers.Nordvpn { w.Interface = helpers.DefaultString(w.Interface, "wg0")
defaultNordVPNAddress := netip.AddrFrom4([4]byte{10, 5, 0, 2})
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
}
defaultAllowedIPs := []netip.Prefix{
netip.PrefixFrom(netip.IPv4Unspecified(), 0),
netip.PrefixFrom(netip.IPv6Unspecified(), 0),
}
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
w.Interface = gosettings.DefaultString(w.Interface, "wg0")
const defaultMTU = 1400
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU)
w.Implementation = gosettings.DefaultString(w.Implementation, "auto")
} }
func (w Wireguard) String() string { func (w Wireguard) String() string {
@@ -194,12 +118,12 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
node = gotree.New("Wireguard settings:") node = gotree.New("Wireguard settings:")
if *w.PrivateKey != "" { if *w.PrivateKey != "" {
s := gosettings.ObfuscateKey(*w.PrivateKey) s := helpers.ObfuscateWireguardKey(*w.PrivateKey)
node.Appendf("Private key: %s", s) node.Appendf("Private key: %s", s)
} }
if *w.PreSharedKey != "" { if *w.PreSharedKey != "" {
s := gosettings.ObfuscateKey(*w.PreSharedKey) s := helpers.ObfuscateWireguardKey(*w.PreSharedKey)
node.Appendf("Pre-shared key: %s", s) node.Appendf("Pre-shared key: %s", s)
} }
@@ -208,17 +132,7 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
addressesNode.Appendf(address.String()) addressesNode.Appendf(address.String())
} }
allowedIPsNode := node.Appendf("Allowed IPs:") node.Appendf("Network interface: %s", w.Interface)
for _, allowedIP := range w.AllowedIPs {
allowedIPsNode.Appendf(allowedIP.String())
}
interfaceNode := node.Appendf("Network interface: %s", w.Interface)
interfaceNode.Appendf("MTU: %d", w.MTU)
if w.Implementation != "auto" {
node.Appendf("Implementation: %s", w.Implementation)
}
return node return node
} }

View File

@@ -2,11 +2,10 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip" "net"
"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/validate"
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
@@ -16,20 +15,20 @@ type WireguardSelection struct {
// It is only used with VPN providers generating Wireguard // It is only used with VPN providers generating Wireguard
// configurations specific to each server and user. // configurations specific to each server and user.
// To indicate it should not be used, it should be set // To indicate it should not be used, it should be set
// to netip.IPv4Unspecified(). It can never be the zero value // to the empty net.IP{} slice. It can never be nil
// in the internal state. // in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"` EndpointIP net.IP
// EndpointPort is a the server port to use for the VPN server. // EndpointPort is a the server port to use for the VPN server.
// It is optional for VPN providers IVPN, Mullvad, Surfshark // It is optional for VPN providers IVPN, Mullvad
// and Windscribe, and compulsory for the others. // and Windscribe, and compulsory for the others.
// When optional, it can be set to 0 to indicate not use // When optional, it can be set to 0 to indicate not use
// a custom endpoint port. It cannot be nil in the internal // a custom endpoint port. It cannot be nil in the internal
// state. // state.
EndpointPort *uint16 `json:"endpoint_port"` EndpointPort *uint16
// PublicKey is the server public key. // PublicKey is the server public key.
// It is only used with VPN providers generating Wireguard // It is only used with VPN providers generating Wireguard
// configurations specific to each server and user. // configurations specific to each server and user.
PublicKey string `json:"public_key"` PublicKey string
} }
// Validate validates WireguardSelection settings. // Validate validates WireguardSelection settings.
@@ -37,12 +36,10 @@ type WireguardSelection struct {
func (w WireguardSelection) validate(vpnProvider string) (err error) { func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointIP // Validate EndpointIP
switch vpnProvider { switch vpnProvider {
case providers.Airvpn, providers.Ivpn, providers.Mullvad, case constants.Ivpn, constants.Mullvad, constants.Windscribe: // endpoint IP addresses are baked in
providers.Nordvpn, providers.Surfshark, providers.Windscribe: case constants.Custom:
// endpoint IP addresses are baked in if len(w.EndpointIP) == 0 {
case providers.Custom: return ErrWireguardEndpointIPNotSet
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
return fmt.Errorf("%w", ErrWireguardEndpointIPNotSet)
} }
default: // Providers not supporting Wireguard default: // Providers not supporting Wireguard
} }
@@ -50,50 +47,41 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointPort // Validate EndpointPort
switch vpnProvider { switch vpnProvider {
// EndpointPort is required // EndpointPort is required
case providers.Custom: case constants.Custom:
if *w.EndpointPort == 0 { if *w.EndpointPort == 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet) return ErrWireguardEndpointPortNotSet
} }
// EndpointPort cannot be set case constants.Ivpn, constants.Mullvad, constants.Windscribe:
case providers.Surfshark, providers.Nordvpn:
if *w.EndpointPort != 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
}
case providers.Airvpn, providers.Ivpn, providers.Mullvad, providers.Windscribe:
// EndpointPort is optional and can be 0 // EndpointPort is optional and can be 0
if *w.EndpointPort == 0 { if *w.EndpointPort == 0 {
break // no custom endpoint port set break // no custom endpoint port set
} }
if vpnProvider == providers.Mullvad { if vpnProvider == constants.Mullvad {
break // no restriction on custom endpoint port value break // no restriction on custom endpoint port value
} }
var allowed []uint16 var allowed []uint16
switch vpnProvider { switch vpnProvider {
case providers.Airvpn: case constants.Ivpn:
allowed = []uint16{1637, 47107}
case providers.Ivpn:
allowed = []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237} allowed = []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237}
case providers.Windscribe: case constants.Windscribe:
allowed = []uint16{53, 80, 123, 443, 1194, 65142} allowed = []uint16{53, 80, 123, 443, 1194, 65142}
} }
err = validate.IsOneOf(*w.EndpointPort, allowed...) if helpers.Uint16IsOneOf(*w.EndpointPort, allowed) {
if err == nil {
break break
} }
return fmt.Errorf("%w: for VPN service provider %s: %w", return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrWireguardEndpointPortNotAllowed, vpnProvider, err) ErrWireguardEndpointPortNotAllowed, w.EndpointPort, vpnProvider,
helpers.PortChoicesOrString(allowed))
default: // Providers not supporting Wireguard default: // Providers not supporting Wireguard
} }
// Validate PublicKey // Validate PublicKey
switch vpnProvider { switch vpnProvider {
case providers.Ivpn, providers.Mullvad, case constants.Ivpn, constants.Mullvad, constants.Windscribe: // public keys are baked in
providers.Surfshark, providers.Windscribe: case constants.Custom:
// public keys are baked in
case providers.Custom:
if w.PublicKey == "" { if w.PublicKey == "" {
return fmt.Errorf("%w", ErrWireguardPublicKeyNotSet) return ErrWireguardPublicKeyNotSet
} }
default: // Providers not supporting Wireguard default: // Providers not supporting Wireguard
} }
@@ -110,27 +98,27 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
func (w *WireguardSelection) copy() (copied WireguardSelection) { func (w *WireguardSelection) copy() (copied WireguardSelection) {
return WireguardSelection{ return WireguardSelection{
EndpointIP: w.EndpointIP, EndpointIP: helpers.CopyIP(w.EndpointIP),
EndpointPort: gosettings.CopyPointer(w.EndpointPort), EndpointPort: helpers.CopyUint16Ptr(w.EndpointPort),
PublicKey: w.PublicKey, PublicKey: w.PublicKey,
} }
} }
func (w *WireguardSelection) mergeWith(other WireguardSelection) { func (w *WireguardSelection) mergeWith(other WireguardSelection) {
w.EndpointIP = gosettings.MergeWithValidator(w.EndpointIP, other.EndpointIP) w.EndpointIP = helpers.MergeWithIP(w.EndpointIP, other.EndpointIP)
w.EndpointPort = gosettings.MergeWithPointer(w.EndpointPort, other.EndpointPort) w.EndpointPort = helpers.MergeWithUint16(w.EndpointPort, other.EndpointPort)
w.PublicKey = gosettings.MergeWithString(w.PublicKey, other.PublicKey) w.PublicKey = helpers.MergeWithString(w.PublicKey, other.PublicKey)
} }
func (w *WireguardSelection) overrideWith(other WireguardSelection) { func (w *WireguardSelection) overrideWith(other WireguardSelection) {
w.EndpointIP = gosettings.OverrideWithValidator(w.EndpointIP, other.EndpointIP) w.EndpointIP = helpers.OverrideWithIP(w.EndpointIP, other.EndpointIP)
w.EndpointPort = gosettings.OverrideWithPointer(w.EndpointPort, other.EndpointPort) w.EndpointPort = helpers.OverrideWithUint16(w.EndpointPort, other.EndpointPort)
w.PublicKey = gosettings.OverrideWithString(w.PublicKey, other.PublicKey) w.PublicKey = helpers.OverrideWithString(w.PublicKey, other.PublicKey)
} }
func (w *WireguardSelection) setDefaults() { func (w *WireguardSelection) setDefaults() {
w.EndpointIP = gosettings.DefaultValidator(w.EndpointIP, netip.IPv4Unspecified()) w.EndpointIP = helpers.DefaultIP(w.EndpointIP, net.IP{})
w.EndpointPort = gosettings.DefaultPointer(w.EndpointPort, 0) w.EndpointPort = helpers.DefaultUint16(w.EndpointPort, 0)
} }
func (w WireguardSelection) String() string { func (w WireguardSelection) String() string {
@@ -140,7 +128,7 @@ func (w WireguardSelection) String() string {
func (w WireguardSelection) toLinesNode() (node *gotree.Node) { func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Wireguard selection settings:") node = gotree.New("Wireguard selection settings:")
if !w.EndpointIP.IsUnspecified() { if len(w.EndpointIP) > 0 {
node.Appendf("Endpoint IP address: %s", w.EndpointIP) node.Appendf("Endpoint IP address: %s", w.EndpointIP)
} }

View File

@@ -2,51 +2,47 @@ package env
import ( import (
"fmt" "fmt"
"net/netip" "net"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (s *Source) readDNS() (dns settings.DNS, err error) { func (r *Reader) readDNS() (dns settings.DNS, err error) {
dns.ServerAddress, err = s.readDNSServerAddress() dns.ServerAddress, err = r.readDNSServerAddress()
if err != nil { if err != nil {
return dns, err return dns, err
} }
dns.KeepNameserver, err = s.env.BoolPtr("DNS_KEEP_NAMESERVER") dns.KeepNameserver, err = envToBoolPtr("DNS_KEEP_NAMESERVER")
if err != nil { if err != nil {
return dns, err return dns, fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err)
} }
dns.DoT, err = s.readDoT() dns.DoT, err = r.readDoT()
if err != nil { if err != nil {
return dns, fmt.Errorf("DoT settings: %w", err) return dns, fmt.Errorf("cannot read DoT settings: %w", err)
} }
return dns, nil return dns, nil
} }
func (s *Source) readDNSServerAddress() (address netip.Addr, err error) { func (r *Reader) readDNSServerAddress() (address net.IP, err error) {
const currentKey = "DNS_ADDRESS" s := os.Getenv("DNS_PLAINTEXT_ADDRESS")
key := firstKeySet(s.env, "DNS_PLAINTEXT_ADDRESS", currentKey) if s == "" {
switch key { return nil, nil
case "":
return address, nil
case currentKey:
default: // Retro-compatibility
s.handleDeprecatedKey(key, currentKey)
} }
address, err = s.env.NetipAddr(key) address = net.ParseIP(s)
if err != nil { if address == nil {
return address, err return nil, fmt.Errorf("environment variable DNS_PLAINTEXT_ADDRESS: %w: %s", ErrIPAddressParse, s)
} }
// TODO remove in v4 // TODO remove in v4
if address.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 { if !address.Equal(net.IPv4(127, 0, 0, 1)) { //nolint:gomnd
s.warner.Warn(key + " is set to " + address.String() + r.warner.Warn("DNS_PLAINTEXT_ADDRESS is set to " + s +
" so the DNS over TLS (DoT) server will not be used." + " so the DNS over TLS (DoT) server will not be used." +
" The default value changed to 127.0.0.1 so it uses the internal DoT serves." + " The default value changed to 127.0.0.1 so it uses the internal DoT server." +
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" + " If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" +
" corresponding to the first DoT provider chosen is used.") " corresponding to the first DoT provider chosen is used.")
} }

View File

@@ -3,62 +3,79 @@ package env
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env" "inet.af/netaddr"
) )
func (s *Source) readDNSBlacklist() (blacklist settings.DNSBlacklist, err error) { func (r *Reader) readDNSBlacklist() (blacklist settings.DNSBlacklist, err error) {
blacklist.BlockMalicious, err = s.env.BoolPtr("BLOCK_MALICIOUS") blacklist.BlockMalicious, err = envToBoolPtr("BLOCK_MALICIOUS")
if err != nil { if err != nil {
return blacklist, err return blacklist, fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err)
} }
blacklist.BlockSurveillance, err = s.env.BoolPtr("BLOCK_SURVEILLANCE", blacklist.BlockSurveillance, err = r.readBlockSurveillance()
env.RetroKeys("BLOCK_NSA"))
if err != nil { if err != nil {
return blacklist, err return blacklist, fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err)
} }
blacklist.BlockAds, err = s.env.BoolPtr("BLOCK_ADS") blacklist.BlockAds, err = envToBoolPtr("BLOCK_ADS")
if err != nil { if err != nil {
return blacklist, err return blacklist, fmt.Errorf("environment variable BLOCK_ADS: %w", err)
} }
blacklist.AddBlockedIPs, blacklist.AddBlockedIPPrefixes, blacklist.AddBlockedIPs, blacklist.AddBlockedIPPrefixes,
err = s.readDoTPrivateAddresses() // TODO v4 split in 2 err = readDoTPrivateAddresses() // TODO v4 split in 2
if err != nil { if err != nil {
return blacklist, err return blacklist, err
} }
blacklist.AllowedHosts = s.env.CSV("UNBLOCK") // TODO v4 change name blacklist.AllowedHosts = envToCSV("UNBLOCK") // TODO v4 change name
return blacklist, nil return blacklist, nil
} }
func (r *Reader) readBlockSurveillance() (blocked *bool, err error) {
blocked, err = envToBoolPtr("BLOCK_SURVEILLANCE")
if err != nil {
return nil, fmt.Errorf("environment variable BLOCK_SURVEILLANCE: %w", err)
} else if blocked != nil {
return blocked, nil
}
blocked, err = envToBoolPtr("BLOCK_NSA")
if err != nil {
return nil, fmt.Errorf("environment variable BLOCK_NSA: %w", err)
} else if blocked != nil {
r.onRetroActive("BLOCK_NSA", "BLOCK_SURVEILLANCE")
return blocked, nil
}
return nil, nil //nolint:nilnil
}
var ( var (
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range") ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
) )
func (s *Source) readDoTPrivateAddresses() (ips []netip.Addr, func readDoTPrivateAddresses() (ips []netaddr.IP,
ipPrefixes []netip.Prefix, err error) { ipPrefixes []netaddr.IPPrefix, err error) {
privateAddresses := s.env.CSV("DOT_PRIVATE_ADDRESS") privateAddresses := envToCSV("DOT_PRIVATE_ADDRESS")
if len(privateAddresses) == 0 { if len(privateAddresses) == 0 {
return nil, nil, nil return nil, nil, nil
} }
ips = make([]netip.Addr, 0, len(privateAddresses)) ips = make([]netaddr.IP, 0, len(privateAddresses))
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses)) ipPrefixes = make([]netaddr.IPPrefix, 0, len(privateAddresses))
for _, privateAddress := range privateAddresses { for _, privateAddress := range privateAddresses {
ip, err := netip.ParseAddr(privateAddress) ip, err := netaddr.ParseIP(privateAddress)
if err == nil { if err == nil {
ips = append(ips, ip) ips = append(ips, ip)
continue continue
} }
ipPrefix, err := netip.ParsePrefix(privateAddress) ipPrefix, err := netaddr.ParseIPPrefix(privateAddress)
if err == nil { if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix) ipPrefixes = append(ipPrefixes, ipPrefix)
continue continue

View File

@@ -1,26 +1,28 @@
package env package env
import ( import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (s *Source) readDoT() (dot settings.DoT, err error) { func (r *Reader) readDoT() (dot settings.DoT, err error) {
dot.Enabled, err = s.env.BoolPtr("DOT") dot.Enabled, err = envToBoolPtr("DOT")
if err != nil {
return dot, fmt.Errorf("environment variable DOT: %w", err)
}
dot.UpdatePeriod, err = envToDurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return dot, fmt.Errorf("environment variable DNS_UPDATE_PERIOD: %w", err)
}
dot.Unbound, err = readUnbound()
if err != nil { if err != nil {
return dot, err return dot, err
} }
dot.UpdatePeriod, err = s.env.DurationPtr("DNS_UPDATE_PERIOD") dot.Blacklist, err = r.readDNSBlacklist()
if err != nil {
return dot, err
}
dot.Unbound, err = s.readUnbound()
if err != nil {
return dot, err
}
dot.Blacklist, err = s.readDNSBlacklist()
if err != nil { if err != nil {
return dot, err return dot, err
} }

View File

@@ -1,36 +1,96 @@
package env package env
import ( import (
"errors"
"fmt"
"net"
"strconv"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
) )
func (s *Source) readFirewall() (firewall settings.Firewall, err error) { func (r *Reader) readFirewall() (firewall settings.Firewall, err error) {
firewall.VPNInputPorts, err = s.env.CSVUint16("FIREWALL_VPN_INPUT_PORTS") vpnInputPortStrings := envToCSV("FIREWALL_VPN_INPUT_PORTS")
firewall.VPNInputPorts, err = stringsToPorts(vpnInputPortStrings)
if err != nil { if err != nil {
return firewall, err return firewall, fmt.Errorf("environment variable FIREWALL_VPN_INPUT_PORTS: %w", err)
} }
firewall.InputPorts, err = s.env.CSVUint16("FIREWALL_INPUT_PORTS") inputPortStrings := envToCSV("FIREWALL_INPUT_PORTS")
firewall.InputPorts, err = stringsToPorts(inputPortStrings)
if err != nil { if err != nil {
return firewall, err return firewall, fmt.Errorf("environment variable FIREWALL_INPUT_PORTS: %w", err)
} }
firewall.OutboundSubnets, err = s.env.CSVNetipPrefixes("FIREWALL_OUTBOUND_SUBNETS", outboundSubnetsKey := "FIREWALL_OUTBOUND_SUBNETS"
env.RetroKeys("EXTRA_SUBNETS")) outboundSubnetStrings := envToCSV(outboundSubnetsKey)
if len(outboundSubnetStrings) == 0 {
// Retro-compatibility
outboundSubnetStrings = envToCSV("EXTRA_SUBNETS")
if len(outboundSubnetStrings) > 0 {
outboundSubnetsKey = "EXTRA_SUBNETS"
r.onRetroActive("EXTRA_SUBNETS", "FIREWALL_OUTBOUND_SUBNETS")
}
}
firewall.OutboundSubnets, err = stringsToIPNets(outboundSubnetStrings)
if err != nil { if err != nil {
return firewall, err return firewall, fmt.Errorf("environment variable %s: %w", outboundSubnetsKey, err)
} }
firewall.Enabled, err = s.env.BoolPtr("FIREWALL") firewall.Enabled, err = envToBoolPtr("FIREWALL")
if err != nil { if err != nil {
return firewall, err return firewall, fmt.Errorf("environment variable FIREWALL: %w", err)
} }
firewall.Debug, err = s.env.BoolPtr("FIREWALL_DEBUG") firewall.Debug, err = envToBoolPtr("FIREWALL_DEBUG")
if err != nil { if err != nil {
return firewall, err return firewall, fmt.Errorf("environment variable FIREWALL_DEBUG: %w", err)
} }
return firewall, nil return firewall, nil
} }
var (
ErrPortParsing = errors.New("cannot parse port")
ErrPortValue = errors.New("port value is not valid")
)
func stringsToPorts(ss []string) (ports []uint16, err error) {
if len(ss) == 0 {
return nil, nil
}
ports = make([]uint16, len(ss))
for i, s := range ss {
port, err := strconv.Atoi(s)
if err != nil {
return nil, fmt.Errorf("%w: %s: %s",
ErrPortParsing, s, err)
} else if port < 1 || port > 65535 {
return nil, fmt.Errorf("%w: must be between 1 and 65535: %d",
ErrPortValue, port)
}
ports[i] = uint16(port)
}
return ports, nil
}
var (
ErrIPNetParsing = errors.New("cannot parse IP network")
)
func stringsToIPNets(ss []string) (ipNets []net.IPNet, err error) {
if len(ss) == 0 {
return nil, nil
}
ipNets = make([]net.IPNet, len(ss))
for i, s := range ss {
ip, ipNet, err := net.ParseCIDR(s)
if err != nil {
return nil, fmt.Errorf("%w: %s: %s",
ErrIPNetParsing, s, err)
}
ipNet.IP = ip
ipNets[i] = *ipNet
}
return ipNets, nil
}

View File

@@ -1,35 +1,55 @@
package env package env
import ( import (
"fmt"
"os"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
) )
func (s *Source) ReadHealth() (health settings.Health, err error) { func (r *Reader) ReadHealth() (health settings.Health, err error) {
health.ServerAddress = s.env.String("HEALTH_SERVER_ADDRESS") health.ServerAddress = os.Getenv("HEALTH_SERVER_ADDRESS")
health.TargetAddress = s.env.String("HEALTH_TARGET_ADDRESS", health.TargetAddress = os.Getenv("HEALTH_ADDRESS_TO_PING")
env.RetroKeys("HEALTH_ADDRESS_TO_PING")) if health.TargetAddress == "" {
health.TargetAddress = os.Getenv("HEALTH_TARGET_ADDRESS")
successWaitPtr, err := s.env.DurationPtr("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return health, err
} else if successWaitPtr != nil {
health.SuccessWait = *successWaitPtr
} }
health.VPN.Initial, err = s.env.DurationPtr( health.VPN.Initial, err = r.readDurationWithRetro(
"HEALTH_VPN_DURATION_INITIAL", "HEALTH_VPN_DURATION_INITIAL",
env.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL")) "HEALTH_OPENVPN_DURATION_INITIAL")
if err != nil { if err != nil {
return health, err return health, err
} }
health.VPN.Addition, err = s.env.DurationPtr( health.VPN.Initial, err = r.readDurationWithRetro(
"HEALTH_VPN_DURATION_ADDITION", "HEALTH_VPN_DURATION_ADDITION",
env.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION")) "HEALTH_OPENVPN_DURATION_ADDITION")
if err != nil { if err != nil {
return health, err return health, err
} }
return health, nil return health, nil
} }
func (r *Reader) readDurationWithRetro(envKey, retroEnvKey string) (d *time.Duration, err error) {
s := os.Getenv(envKey)
if s == "" {
s = os.Getenv(retroEnvKey)
if s == "" {
return nil, nil //nolint:nilnil
}
r.onRetroActive(envKey, retroEnvKey)
envKey = retroEnvKey
}
d = new(time.Duration)
*d, err = time.ParseDuration(s)
if err != nil {
return nil, fmt.Errorf(
"environment variable %s: %w",
envKey, err)
}
return d, nil
}

View File

@@ -1,33 +1,134 @@
package env package env
import ( import (
"encoding/base64"
"errors"
"fmt" "fmt"
"os" "os"
"strconv"
"strings"
"time"
"github.com/qdm12/gosettings/sources/env" "github.com/qdm12/govalid/binary"
"github.com/qdm12/govalid/integer"
) )
func envToCSV(envKey string) (values []string) {
csv := os.Getenv(envKey)
if csv == "" {
return nil
}
return lowerAndSplit(csv)
}
func envToStringPtr(envKey string) (stringPtr *string) {
s := os.Getenv(envKey)
if s == "" {
return nil
}
return &s
}
func envToBoolPtr(envKey string) (boolPtr *bool, err error) {
s := os.Getenv(envKey)
if s == "" {
return nil, nil //nolint:nilnil
}
value, err := binary.Validate(s)
if err != nil {
return nil, err
}
return &value, nil
}
func envToIntPtr(envKey string) (intPtr *int, err error) {
s := os.Getenv(envKey)
if s == "" {
return nil, nil //nolint:nilnil
}
value, err := strconv.Atoi(s)
if err != nil {
return nil, err
}
return &value, nil
}
func envToUint8Ptr(envKey string) (uint8Ptr *uint8, err error) {
s := os.Getenv(envKey)
if s == "" {
return nil, nil //nolint:nilnil
}
const min, max = 0, 255
value, err := integer.Validate(s, integer.OptionRange(min, max))
if err != nil {
return nil, err
}
uint8Ptr = new(uint8)
*uint8Ptr = uint8(value)
return uint8Ptr, nil
}
func envToUint16Ptr(envKey string) (uint16Ptr *uint16, err error) {
s := os.Getenv(envKey)
if s == "" {
return nil, nil //nolint:nilnil
}
const min, max = 0, 65535
value, err := integer.Validate(s, integer.OptionRange(min, max))
if err != nil {
return nil, err
}
uint16Ptr = new(uint16)
*uint16Ptr = uint16(value)
return uint16Ptr, nil
}
func envToDurationPtr(envKey string) (durationPtr *time.Duration, err error) {
s := os.Getenv(envKey)
if s == "" {
return nil, nil //nolint:nilnil
}
durationPtr = new(time.Duration)
*durationPtr, err = time.ParseDuration(s)
if err != nil {
return nil, err
}
return durationPtr, nil
}
func lowerAndSplit(csv string) (values []string) {
csv = strings.ToLower(csv)
return strings.Split(csv, ",")
}
var ErrDecodeBase64 = errors.New("cannot decode base64 string")
func decodeBase64(b64String string) (decoded string, err error) {
b, err := base64.StdEncoding.DecodeString(b64String)
if err != nil {
return "", fmt.Errorf("%w: %s: %s",
ErrDecodeBase64, b64String, err)
}
return string(b), nil
}
func unsetEnvKeys(envKeys []string, err error) (newErr error) { func unsetEnvKeys(envKeys []string, err error) (newErr error) {
newErr = err newErr = err
for _, envKey := range envKeys { for _, envKey := range envKeys {
unsetErr := os.Unsetenv(envKey) unsetErr := os.Unsetenv(envKey)
if unsetErr != nil && newErr == nil { if unsetErr != nil && newErr == nil {
newErr = fmt.Errorf("unsetting environment variable %s: %w", envKey, unsetErr) newErr = fmt.Errorf("cannot unset environment variable %s: %w", envKey, unsetErr)
} }
} }
return newErr return newErr
} }
func ptrTo[T any](value T) *T { func stringPtr(s string) *string { return &s }
return &value func uint16Ptr(n uint16) *uint16 { return &n }
} func boolPtr(b bool) *bool { return &b }
func firstKeySet(e env.Env, keys ...string) (firstKeySet string) {
for _, key := range keys {
value := e.Get(key)
if value != nil {
return key
}
}
return ""
}

View File

@@ -0,0 +1,22 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setTestEnv is used to set environment variables in
// parallel tests.
func setTestEnv(t *testing.T, key, value string) {
t.Helper()
existing := os.Getenv(key)
err := os.Setenv(key, value) //nolint:tenv
t.Cleanup(func() {
err = os.Setenv(key, existing)
assert.NoError(t, err)
})
require.NoError(t, err)
}

View File

@@ -2,37 +2,29 @@ package env
import ( import (
"fmt" "fmt"
"os"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
"github.com/qdm12/govalid/binary" "github.com/qdm12/govalid/binary"
) )
func (s *Source) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) { func (r *Reader) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) {
httpProxy.User = s.env.Get("HTTPPROXY_USER", httpProxy.User = r.readHTTProxyUser()
env.RetroKeys("PROXY_USER", "TINYPROXY_USER"), httpProxy.Password = r.readHTTProxyPassword()
env.ForceLowercase(false)) httpProxy.ListeningAddress = r.readHTTProxyListeningAddress()
httpProxy.Password = s.env.Get("HTTPPROXY_PASSWORD", httpProxy.Enabled, err = r.readHTTProxyEnabled()
env.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
env.ForceLowercase(false))
httpProxy.ListeningAddress, err = s.readHTTProxyListeningAddress()
if err != nil { if err != nil {
return httpProxy, err return httpProxy, err
} }
httpProxy.Enabled, err = s.env.BoolPtr("HTTPPROXY", env.RetroKeys("PROXY", "TINYPROXY")) httpProxy.Stealth, err = envToBoolPtr("HTTPPROXY_STEALTH")
if err != nil { if err != nil {
return httpProxy, err return httpProxy, fmt.Errorf("environment variable HTTPPROXY_STEALTH: %w", err)
} }
httpProxy.Stealth, err = s.env.BoolPtr("HTTPPROXY_STEALTH") httpProxy.Log, err = r.readHTTProxyLog()
if err != nil {
return httpProxy, err
}
httpProxy.Log, err = s.readHTTProxyLog()
if err != nil { if err != nil {
return httpProxy, err return httpProxy, err
} }
@@ -40,45 +32,139 @@ func (s *Source) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) {
return httpProxy, nil return httpProxy, nil
} }
func (s *Source) readHTTProxyListeningAddress() (listeningAddress string, err error) { func (r *Reader) readHTTProxyUser() (user *string) {
const currentKey = "HTTPPROXY_LISTENING_ADDRESS" s := os.Getenv("HTTPPROXY_USER")
key := firstKeySet(s.env, "HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT", if s != "" {
currentKey) return &s
switch key {
case "":
return "", nil
case currentKey:
return s.env.String(key), nil
} }
// Retro-compatible keys using a port only // Retro-compatibility
s.handleDeprecatedKey(key, currentKey) s = os.Getenv("TINYPROXY_USER")
port, err := s.env.Uint16Ptr(key) if s != "" {
if err != nil { r.onRetroActive("TINYPROXY_USER", "HTTPPROXY_USER")
return "", err return &s
} }
return fmt.Sprintf(":%d", *port), nil
// Retro-compatibility
s = os.Getenv("PROXY_USER")
if s != "" {
r.onRetroActive("PROXY_USER", "HTTPPROXY_USER")
return &s
}
return nil
} }
func (s *Source) readHTTProxyLog() (enabled *bool, err error) { func (r *Reader) readHTTProxyPassword() (user *string) {
const currentKey = "HTTPPROXY_LOG" s := os.Getenv("HTTPPROXY_PASSWORD")
key := firstKeySet(s.env, "PROXY_LOG", "TINYPROXY_LOG", "HTTPPROXY_LOG") if s != "" {
switch key { return &s
case "":
return nil, nil //nolint:nilnil
case currentKey:
return s.env.BoolPtr(key)
} }
// Retro-compatible keys using different boolean verbs // Retro-compatibility
s.handleDeprecatedKey(key, currentKey) s = os.Getenv("TINYPROXY_PASSWORD")
value := s.env.String(key) if s != "" {
r.onRetroActive("TINYPROXY_PASSWORD", "HTTPPROXY_PASSWORD")
return &s
}
// Retro-compatibility
s = os.Getenv("PROXY_PASSWORD")
if s != "" {
r.onRetroActive("PROXY_PASSWORD", "HTTPPROXY_PASSWORD")
return &s
}
return nil
}
func (r *Reader) readHTTProxyListeningAddress() (listeningAddress string) {
// Retro-compatibility
retroKeys := []string{"PROXY_PORT", "TINYPROXY_PORT", "HTTPPROXY_PORT"}
for _, retroKey := range retroKeys {
s := os.Getenv(retroKey)
if s != "" {
r.onRetroActive(retroKey, "HTTPPROXY_LISTENING_ADDRESS")
return ":" + s
}
}
return os.Getenv("HTTPPROXY_LISTENING_ADDRESS")
}
func (r *Reader) readHTTProxyEnabled() (enabled *bool, err error) {
s := strings.ToLower(os.Getenv("HTTPPROXY"))
if s != "" {
enabled = new(bool)
*enabled, err = binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable HTTPPROXY: %w", err)
}
return enabled, nil
}
// Retro-compatibility
s = strings.ToLower(os.Getenv("TINYPROXY"))
if s != "" {
r.onRetroActive("TINYPROXY", "HTTPPROXY")
enabled = new(bool)
*enabled, err = binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable TINYPROXY: %w", err)
}
return enabled, nil
}
// Retro-compatibility
s = strings.ToLower(os.Getenv("PROXY"))
if s != "" {
r.onRetroActive("PROXY", "HTTPPROXY")
enabled = new(bool)
*enabled, err = binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable PROXY: %w", err)
}
return enabled, nil
}
return nil, nil //nolint:nilnil
}
func (r *Reader) readHTTProxyLog() (enabled *bool, err error) {
s := strings.ToLower(os.Getenv("HTTPPROXY_LOG"))
if s != "" {
enabled = new(bool)
*enabled, err = binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable HTTPPROXY_LOG: %w", err)
}
return enabled, nil
}
// Retro-compatibility
retroOption := binary.OptionEnabled("on", "info", "connect", "notice") retroOption := binary.OptionEnabled("on", "info", "connect", "notice")
s = strings.ToLower(os.Getenv("TINYPROXY_LOG"))
enabled, err = binary.Validate(value, retroOption) if s != "" {
if err != nil { r.onRetroActive("TINYPROXY_LOG", "HTTPPROXY_LOG")
return nil, fmt.Errorf("environment variable %s: %w", key, err) enabled = new(bool)
*enabled, err = binary.Validate(s, retroOption)
if err != nil {
return nil, fmt.Errorf("environment variable TINYPROXY_LOG: %w", err)
}
return enabled, nil
} }
return enabled, nil // Retro-compatibility
s = strings.ToLower(os.Getenv("PROXY_LOG_LEVEL"))
if s != "" {
r.onRetroActive("PROXY_LOG_LEVEL", "HTTPPROXY_LOG")
enabled = new(bool)
*enabled, err = binary.Validate(s, retroOption)
if err != nil {
return nil, fmt.Errorf("environment variable PROXY_LOG_LEVEL: %w", err)
}
return enabled, nil
}
return nil, nil //nolint:nilnil
} }

View File

@@ -3,14 +3,15 @@ package env
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/log" "github.com/qdm12/golibs/logging"
) )
func (s *Source) readLog() (log settings.Log, err error) { func readLog() (log settings.Log, err error) {
log.Level, err = s.readLogLevel() log.Level, err = readLogLevel()
if err != nil { if err != nil {
return log, err return log, err
} }
@@ -18,14 +19,14 @@ func (s *Source) readLog() (log settings.Log, err error) {
return log, nil return log, nil
} }
func (s *Source) readLogLevel() (level *log.Level, err error) { func readLogLevel() (level *logging.Level, err error) {
value := s.env.String("LOG_LEVEL") s := os.Getenv("LOG_LEVEL")
if value == "" { if s == "" {
return nil, nil //nolint:nilnil return nil, nil //nolint:nilnil
} }
level = new(log.Level) level = new(logging.Level)
*level, err = parseLogLevel(value) *level, err = parseLogLevel(s)
if err != nil { if err != nil {
return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err) return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
} }
@@ -35,19 +36,19 @@ func (s *Source) readLogLevel() (level *log.Level, err error) {
var ErrLogLevelUnknown = errors.New("log level is unknown") var ErrLogLevelUnknown = errors.New("log level is unknown")
func parseLogLevel(s string) (level log.Level, err error) { func parseLogLevel(s string) (level logging.Level, err error) {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "debug": case "debug":
return log.LevelDebug, nil return logging.LevelDebug, nil
case "info": case "info":
return log.LevelInfo, nil return logging.LevelInfo, nil
case "warning": case "warning":
return log.LevelWarn, nil return logging.LevelWarn, nil
case "error": case "error":
return log.LevelError, nil return logging.LevelError, nil
default: default:
return level, fmt.Errorf( return level, fmt.Errorf(
"%w: %q is not valid and can be one of debug, info, warning or error", "%w: %s: can be one of: debug, info, warning or error",
ErrLogLevelUnknown, s) ErrLogLevelUnknown, s)
} }
} }

View File

@@ -1,77 +1,130 @@
package env package env
import ( import (
"fmt"
"os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
) )
func (s *Source) readOpenVPN() ( func (r *Reader) readOpenVPN() (
openVPN settings.OpenVPN, err error) { openVPN settings.OpenVPN, err error) {
defer func() { defer func() {
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT", err = unsetEnvKeys([]string{"OPENVPN_CLIENTKEY", "OPENVPN_CLIENTCRT"}, err)
"OPENVPN_KEY_PASSPHRASE", "OPENVPN_ENCRYPTED_KEY"}, err)
}() }()
openVPN.Version = s.env.String("OPENVPN_VERSION") openVPN.Version = os.Getenv("OPENVPN_VERSION")
openVPN.User = s.env.Get("OPENVPN_USER", openVPN.User = r.readOpenVPNUser()
env.RetroKeys("USER"), env.ForceLowercase(false)) openVPN.Password = r.readOpenVPNPassword()
openVPN.Password = s.env.Get("OPENVPN_PASSWORD", confFile := os.Getenv("OPENVPN_CUSTOM_CONFIG")
env.RetroKeys("PASSWORD"), env.ForceLowercase(false)) if confFile != "" {
openVPN.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false)) openVPN.ConfFile = &confFile
openVPN.Ciphers = s.env.CSV("OPENVPN_CIPHERS", env.RetroKeys("OPENVPN_CIPHER"))
openVPN.Auth = s.env.Get("OPENVPN_AUTH")
openVPN.Cert = s.env.Get("OPENVPN_CERT", env.ForceLowercase(false))
openVPN.Key = s.env.Get("OPENVPN_KEY", env.ForceLowercase(false))
openVPN.EncryptedKey = s.env.Get("OPENVPN_ENCRYPTED_KEY", env.ForceLowercase(false))
openVPN.KeyPassphrase = s.env.Get("OPENVPN_KEY_PASSPHRASE", env.ForceLowercase(false))
openVPN.PIAEncPreset = s.readPIAEncryptionPreset()
openVPN.MSSFix, err = s.env.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return openVPN, err
} }
openVPN.Interface = s.env.String("VPN_INTERFACE", openVPN.Ciphers = envToCSV("OPENVPN_CIPHER")
env.RetroKeys("OPENVPN_INTERFACE"), env.ForceLowercase(false)) auth := os.Getenv("OPENVPN_AUTH")
if auth != "" {
openVPN.ProcessUser, err = s.readOpenVPNProcessUser() openVPN.Auth = &auth
if err != nil {
return openVPN, err
} }
openVPN.Verbosity, err = s.env.IntPtr("OPENVPN_VERBOSITY") openVPN.ClientCrt, err = readBase64OrNil("OPENVPN_CLIENTCRT")
if err != nil { if err != nil {
return openVPN, err return openVPN, fmt.Errorf("environment variable OPENVPN_CLIENTCRT: %w", err)
} }
flagsPtr := s.env.Get("OPENVPN_FLAGS", env.ForceLowercase(false)) openVPN.ClientKey, err = readBase64OrNil("OPENVPN_CLIENTKEY")
if flagsPtr != nil { if err != nil {
openVPN.Flags = strings.Fields(*flagsPtr) return openVPN, fmt.Errorf("environment variable OPENVPN_CLIENTKEY: %w", err)
}
openVPN.PIAEncPreset = r.readPIAEncryptionPreset()
openVPN.IPv6, err = envToBoolPtr("OPENVPN_IPV6")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_IPV6: %w", err)
}
openVPN.MSSFix, err = envToUint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_MSSFIX: %w", err)
}
openVPN.Interface = os.Getenv("OPENVPN_INTERFACE")
openVPN.Root, err = envToBoolPtr("OPENVPN_ROOT")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_ROOT: %w", err)
}
// TODO ProcUser once Root is deprecated.
openVPN.Verbosity, err = envToIntPtr("OPENVPN_VERBOSITY")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_VERBOSITY: %w", err)
}
flagsStr := os.Getenv("OPENVPN_FLAGS")
if flagsStr != "" {
openVPN.Flags = strings.Fields(flagsStr)
} }
return openVPN, nil return openVPN, nil
} }
func (s *Source) readPIAEncryptionPreset() (presetPtr *string) { func (r *Reader) readOpenVPNUser() (user string) {
return s.env.Get( user = os.Getenv("OPENVPN_USER")
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET", if user == "" {
env.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION")) // Retro-compatibility
user = os.Getenv("USER")
if user != "" {
r.onRetroActive("USER", "OPENVPN_USER")
}
}
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
return strings.ReplaceAll(user, " ", "")
} }
func (s *Source) readOpenVPNProcessUser() (processUser string, err error) { func (r *Reader) readOpenVPNPassword() (password string) {
value, err := s.env.BoolPtr("OPENVPN_ROOT") // Retro-compatibility password = os.Getenv("OPENVPN_PASSWORD")
if err != nil { if password != "" {
return "", err return password
} else if value != nil {
if *value {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
} }
return s.env.String("OPENVPN_PROCESS_USER"), nil // Retro-compatibility
password = os.Getenv("PASSWORD")
if password != "" {
r.onRetroActive("PASSWORD", "OPENVPN_PASSWORD")
}
return password
}
func readBase64OrNil(envKey string) (valueOrNil *string, err error) {
value := os.Getenv(envKey)
if value == "" {
return nil, nil //nolint:nilnil
}
decoded, err := decodeBase64(value)
if err != nil {
return nil, err
}
return &decoded, nil
}
func (r *Reader) readPIAEncryptionPreset() (presetPtr *string) {
preset := strings.ToLower(os.Getenv("PIA_ENCRYPTION"))
if preset != "" {
return &preset
}
// Retro-compatibility
preset = strings.ToLower(os.Getenv("ENCRYPTION"))
if preset != "" {
r.onRetroActive("ENCRYPTION", "PIA_ENCRYPTION")
return &preset
}
return nil
} }

View File

@@ -3,54 +3,81 @@ package env
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"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/gosettings/sources/env" "github.com/qdm12/govalid/port"
) )
func (s *Source) readOpenVPNSelection() ( func (r *Reader) readOpenVPNSelection() (
selection settings.OpenVPNSelection, err error) { selection settings.OpenVPNSelection, err error) {
selection.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false)) confFile := os.Getenv("OPENVPN_CUSTOM_CONFIG")
if confFile != "" {
selection.ConfFile = &confFile
}
selection.TCP, err = s.readOpenVPNProtocol() selection.TCP, err = r.readOpenVPNProtocol()
if err != nil { if err != nil {
return selection, err return selection, err
} }
selection.CustomPort, err = s.env.Uint16Ptr("VPN_ENDPOINT_PORT", selection.CustomPort, err = r.readOpenVPNCustomPort()
env.RetroKeys("PORT", "OPENVPN_PORT"))
if err != nil { if err != nil {
return selection, err return selection, err
} }
selection.PIAEncPreset = s.readPIAEncryptionPreset() selection.PIAEncPreset = r.readPIAEncryptionPreset()
return selection, nil return selection, nil
} }
var ErrOpenVPNProtocolNotValid = errors.New("OpenVPN protocol is not valid") var ErrOpenVPNProtocolNotValid = errors.New("OpenVPN protocol is not valid")
func (s *Source) readOpenVPNProtocol() (tcp *bool, err error) { func (r *Reader) readOpenVPNProtocol() (tcp *bool, err error) {
const currentKey = "OPENVPN_PROTOCOL" envKey := "OPENVPN_PROTOCOL"
envKey := firstKeySet(s.env, "PROTOCOL", currentKey) protocol := strings.ToLower(os.Getenv("OPENVPN_PROTOCOL"))
switch envKey { if protocol == "" {
case "": // Retro-compatibility
return nil, nil //nolint:nilnil protocol = strings.ToLower(os.Getenv("PROTOCOL"))
case currentKey: if protocol != "" {
default: // Retro compatibility envKey = "PROTOCOL"
s.handleDeprecatedKey(envKey, currentKey) r.onRetroActive("PROTOCOL", "OPENVPN_PROTOCOL")
}
} }
protocol := s.env.String(envKey) switch protocol {
switch strings.ToLower(protocol) { case "":
return nil, nil //nolint:nilnil
case constants.UDP: case constants.UDP:
return ptrTo(false), nil return boolPtr(false), nil
case constants.TCP: case constants.TCP:
return ptrTo(true), nil return boolPtr(true), nil
default: default:
return nil, fmt.Errorf("environment variable %s: %w: %s", return nil, fmt.Errorf("environment variable %s: %w: %s",
envKey, ErrOpenVPNProtocolNotValid, protocol) envKey, ErrOpenVPNProtocolNotValid, protocol)
} }
} }
func (r *Reader) readOpenVPNCustomPort() (customPort *uint16, err error) {
key := "OPENVPN_PORT"
s := os.Getenv(key)
if s == "" {
// Retro-compatibility
key = "PORT"
s = os.Getenv(key)
if s == "" {
return nil, nil //nolint:nilnil
}
r.onRetroActive("PORT", "OPENVPN_PORT")
}
customPort = new(uint16)
*customPort, err = port.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable %s: %w", key, err)
}
return customPort, nil
}

View File

@@ -1,34 +1,19 @@
package env package env
import ( import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
) )
func (s *Source) readPortForward() ( func readPortForward() (
portForwarding settings.PortForwarding, err error) { portForwarding settings.PortForwarding, err error) {
portForwarding.Enabled, err = s.env.BoolPtr("VPN_PORT_FORWARDING", portForwarding.Enabled, err = envToBoolPtr("PORT_FORWARDING")
env.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil { if err != nil {
return portForwarding, err return portForwarding, fmt.Errorf("environment variable PORT_FORWARDING: %w", err)
} }
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER") portForwarding.Filepath = envToStringPtr("PORT_FORWARDING_STATUS_FILE")
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
env.ForceLowercase(false),
env.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
portForwarding.ListeningPort, err = s.env.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return portForwarding, err
}
return portForwarding, nil return portForwarding, nil
} }

View File

@@ -1,26 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/pprof"
)
func (s *Source) readPprof() (settings pprof.Settings, err error) {
settings.Enabled, err = s.env.BoolPtr("PPROF_ENABLED")
if err != nil {
return settings, err
}
settings.BlockProfileRate, err = s.env.IntPtr("PPROF_BLOCK_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.MutexProfileRate, err = s.env.IntPtr("PPROF_MUTEX_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.HTTPServer.Address = s.env.String("PPROF_HTTP_SERVER_ADDRESS")
return settings, nil
}

View File

@@ -2,49 +2,43 @@ package env
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings/sources/env"
) )
func (s *Source) readProvider(vpnType string) (provider settings.Provider, err error) { func (r *Reader) readProvider(vpnType string) (provider settings.Provider, err error) {
provider.Name = s.readVPNServiceProvider(vpnType) provider.Name = readVPNServiceProvider(vpnType)
var providerName string var providerName string
if provider.Name != nil { if provider.Name != nil {
providerName = *provider.Name providerName = *provider.Name
} }
provider.ServerSelection, err = s.readServerSelection(providerName, vpnType) provider.ServerSelection, err = r.readServerSelection(providerName, vpnType)
if err != nil { if err != nil {
return provider, fmt.Errorf("server selection: %w", err) return provider, fmt.Errorf("cannot read server selection settings: %w", err)
} }
provider.PortForwarding, err = s.readPortForward() provider.PortForwarding, err = readPortForward()
if err != nil { if err != nil {
return provider, fmt.Errorf("port forwarding: %w", err) return provider, fmt.Errorf("cannot read port forwarding settings: %w", err)
} }
return provider, nil return provider, nil
} }
func (s *Source) readVPNServiceProvider(vpnType string) (vpnProviderPtr *string) { func readVPNServiceProvider(vpnType string) (vpnProviderPtr *string) {
valuePtr := s.env.Get("VPN_SERVICE_PROVIDER", env.RetroKeys("VPNSP")) s := strings.ToLower(os.Getenv("VPNSP"))
if valuePtr == nil { switch {
if vpnType != vpn.Wireguard && s.env.Get("OPENVPN_CUSTOM_CONFIG") != nil { case vpnType != constants.Wireguard &&
// retro compatibility os.Getenv("OPENVPN_CUSTOM_CONFIG") != "": // retro compatibility
return ptrTo(providers.Custom) return stringPtr(constants.Custom)
} case s == "":
return nil return nil
case s == "pia": // retro compatibility
return stringPtr(constants.PrivateInternetAccess)
} }
return stringPtr(s)
value := *valuePtr
value = strings.ToLower(value)
if value == "pia" { // retro compatibility
return ptrTo(providers.PrivateInternetAccess)
}
return ptrTo(value)
} }

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