Compare commits

..

2 Commits

Author SHA1 Message Date
Quentin McGaw
29379ae35d Fix: Empty connections for NordVPN and Windscribe 2021-01-31 18:56:38 +00:00
Quentin McGaw
c74ec9a38b Revert back to Alpine 3.12 for 32 bit systems 2021-01-30 23:15:54 +00:00
709 changed files with 18217 additions and 161829 deletions

View File

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

View File

@@ -19,7 +19,6 @@ It works on Linux, Windows and OSX.
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. 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

View File

@@ -8,7 +8,7 @@
"vscode"
],
"shutdownAction": "stopCompose",
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
"postCreateCommand": "go mod download",
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",
@@ -25,7 +25,6 @@
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"IBM.output-colorizer", // Colorize your output/test logs
"mohsen1.prettify-json", // Prettify JSON data
"github.copilot",
],
"settings": {
"files.eol": "\n",

View File

@@ -3,29 +3,22 @@ version: "3.7"
services:
vscode:
build: .
devices:
- /dev/net/tun:/dev/net/tun
image: godevcontainer
volumes:
- ../:/workspace
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# Docker configuration
- ~/.docker:/root/.docker
# SSH directory for Linux, OSX and WSL
# SSH directory
- ~/.ssh:/home/vscode/.ssh
- ~/.ssh:/root/.ssh
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
#- ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history
# Git config
- ~/.gitconfig:/home/districter/.gitconfig
- ~/.gitconfig:/root/.gitconfig
environment:
- TZ=
cap_add:
# For debugging with dlv
# - SYS_PTRACE
- NET_ADMIN
- SYS_PTRACE
security_opt:
# For debugging with dlv
- seccomp:unconfined

37
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Bug
about: Report a bug
title: 'Bug: FILL THIS TEXT!'
labels: ":bug: bug"
assignees: qdm12
---
**Host OS** (approximate answer is fine too): Ubuntu 18
**Is this urgent?**: No
**What VPN provider are you using**:
**What are you using to run your container?**: Docker Compose
**What is the version of the program** (See the line at the top of your logs)
```
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
```
**What's the problem** 🤔
That feature doesn't work
**Share your logs...**
...*careful to remove i.e. token information with PIA port forwarding*
```log
PASTE YOUR LOGS
IN THERE
```

View File

@@ -1,107 +0,0 @@
name: Bug
description: Report a bug
title: "Bug: "
labels: [":bug: bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: urgent
attributes:
label: Is this urgent?
description: |
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/Docker-image-tags) if that can help.
options:
- "No"
- "Yes"
- type: input
id: host-os
attributes:
label: Host OS
description: What is your host OS?
placeholder: "Debian Buster"
- type: dropdown
id: cpu-arch
attributes:
label: CPU arch
description: You can find it on Linux with `uname -m`.
options:
- x86_64
- aarch64
- armv7l
- "386"
- s390x
- ppc64le
- type: dropdown
id: vpn-service-provider
attributes:
label: VPN service provider
options:
- Custom
- Cyberghost
- ExpressVPN
- FastestVPN
- HideMyAss
- IPVanish
- IVPN
- Mullvad
- NordVPN
- Privado
- Private Internet Access
- PrivateVPN
- ProtonVPN
- PureVPN
- Surfshark
- TorGuard
- VPNUnlimited
- VyprVPN
- WeVPN
- Windscribe
validations:
required: true
- type: dropdown
id: docker
attributes:
label: What are you using to run the container
options:
- docker run
- docker-compose
- Portainer
- Kubernetes
- Podman
- Other
validations:
required: true
- type: input
id: version
attributes:
label: What is the version of Gluetun
description: |
Copy paste the version line at the top of your logs.
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
validations:
required: true
- type: textarea
id: problem
attributes:
label: "What's the problem 🤔"
placeholder: "That feature does not work..."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Share your logs
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: plain text
validations:
required: true
- type: textarea
id: config
attributes:
label: Share your configuration
description: Share your configuration such as `docker-compose.yml`. Ensure to remove credentials.
render: yml

View File

@@ -1,7 +0,0 @@
contact_links:
- name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new
about: Please create a Github discussion.
- name: Unraid template issue
url: https://github.com/qdm12/gluetun/discussions/550
about: Please read the relevant Github discussion.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest a feature to add to this project
title: 'Feature request: FILL THIS TEXT!'
labels: ":bulb: feature request"
assignees: qdm12
---
**What's the feature?** 🧐
- Support this new feature because that and that
**Optional extra information** 🚀
- I tried `docker run something` and it doesn't work
- That [url](https://github.com/qdm12/gluetun) is interesting

View File

@@ -1,19 +0,0 @@
name: Feature request
description: Suggest a feature to add to Gluetun
title: "Feature request: "
labels: [":bulb: feature request"]
body:
- type: textarea
id: description
attributes:
label: "What's the feature 🧐"
placeholder: "Make the tunnel resistant to earth quakes"
validations:
required: true
- type: textarea
id: extra
attributes:
label: "Extra information and references"
placeholder: |
- I tried `docker run something` and it doesn't work
- That [url](https://github.com/qdm12/gluetun) is interesting

53
.github/ISSUE_TEMPLATE/help.md vendored Normal file
View File

@@ -0,0 +1,53 @@
---
name: Help
about: Ask for help
title: 'Help: FILL THIS TEXT!'
labels: ":pray: help wanted"
assignees:
---
**Host OS** (approximate answer is fine too): Ubuntu 18
**Is this urgent?**: No
**What VPN provider are you using**:
**What is the version of the program** (See the line at the top of your logs)
```
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
```
**What's the problem** 🤔
That feature doesn't work
**Share your logs...**
...*careful to remove i.e. token information with PIA port forwarding*
```log
PASTE YOUR LOGS
IN THERE
```
**What are you using to run your container?**: Docker Compose
Please also share your configuration file:
```yml
your .yml
content
in here
```
or
```sh
# your docker
# run command
# in here
```

View File

@@ -1,17 +0,0 @@
---
name: Support a VPN provider
about: Suggest a VPN provider to be supported
title: 'VPN provider support: NAME OF THE PROVIDER'
labels: ":bulb: New provider"
---
One of the following is required:
- Publicly accessible URL to a zip file containing the Openvpn configuration files
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file.
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.

View File

@@ -1,18 +0,0 @@
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

43
.github/labels.yml vendored
View File

@@ -14,71 +14,31 @@
color: "795548"
description: ""
# Priority
- name: "🚨 Urgent"
color: "d5232f"
description: ""
- name: "💤 Low priority"
color: "4285f4"
description: ""
# VPN providers
- name: ":cloud: Cyberghost"
color: "cfe8d4"
description: ""
- name: ":cloud: HideMyAss"
color: "cfe8d4"
description: ""
- name: ":cloud: IPVanish"
color: "cfe8d4"
description: ""
- name: ":cloud: IVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ExpressVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: FastestVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Mullvad"
color: "cfe8d4"
description: ""
- name: ":cloud: NordVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Perfect Privacy"
color: "cfe8d4"
description: ""
- name: ":cloud: PIA"
color: "cfe8d4"
description: ""
- name: ":cloud: Privado"
color: "cfe8d4"
description: ""
- name: ":cloud: PrivateVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ProtonVPN"
color: "cfe8d4"
- name: ":cloud: PureVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Surfshark"
color: "cfe8d4"
description: ""
- name: ":cloud: Torguard"
color: "cfe8d4"
description: ""
- name: ":cloud: VPNUnlimited"
color: "cfe8d4"
description: ""
- name: ":cloud: Vyprvpn"
color: "cfe8d4"
description: ""
- name: ":cloud: WeVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Windscribe"
color: "cfe8d4"
description: ""
@@ -87,9 +47,6 @@
- name: "Openvpn"
color: "ffc7ea"
description: ""
- name: "Wireguard"
color: "ffc7ea"
description: ""
- name: "Unbound (DNS over TLS)"
color: "ffc7ea"
description: ""

100
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: CI
on:
push:
paths:
- .github/workflows/build.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2
- name: Linting
run: docker build --target lint .
- name: Go mod tidy check
run: docker build --target tidy .
- 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 \
go test \
-race \
-coverpkg=./... \
-coverprofile=coverage.txt \
-covermode=atomic \
./...
# We run this here to use the caching of the previous steps
- if: github.event_name == 'push'
name: Build final image
run: docker build .
publish:
needs: [verify]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set variables
id: vars
env:
EVENT_NAME: ${{ github.event_name }}
run: |
BRANCH=${GITHUB_REF#refs/heads/}
TAG=${GITHUB_REF#refs/tags/}
echo ::set-output name=commit::$(git rev-parse --short HEAD)
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ "$TAG" != "$GITHUB_REF" ]; then
echo ::set-output name=version::$TAG
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le
elif [ "$BRANCH" = "master" ]; then
echo ::set-output name=version::latest
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le
else
echo ::set-output name=version::$BRANCH
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7
fi
- name: Build and push final image
uses: docker/build-push-action@v2
with:
platforms: ${{ steps.vars.outputs.platforms }}
build-args: |
BUILD_DATE=${{ steps.vars.outputs.build_date }}
COMMIT=${{ steps.vars.outputs.commit }}
VERSION=${{ steps.vars.outputs.version }}
tags: |
qmcgaw/gluetun:${{ steps.vars.outputs.version }}
qmcgaw/private-internet-access:${{ steps.vars.outputs.version }}
push: true
- if: github.event_name == 'push' && github.event.ref == 'refs/heads/master'
name: Microbadger hook
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/gluetun/l-keGI7p4IhX4QuIDMFYKhsZ1L0=
continue-on-error: true

View File

@@ -1,37 +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:
branches:
- master
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

@@ -1,135 +0,0 @@
name: CI
on:
release:
types:
- published
push:
branches:
- master
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
branches:
- master
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v3
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
- 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 .
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v3
- uses: github/codeql-action/init@v2
with:
languages: go
- uses: github/codeql-action/autobuild@v2
- uses: github/codeql-action/analyze@v2
publish:
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, codeql]
permissions:
actions: read
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
qmcgaw/gluetun
qmcgaw/private-internet-access
tags: |
type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
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) }}
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Short commit
id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- name: Build and push final image
uses: docker/build-push-action@v3.0.0
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
labels: ${{ steps.meta.outputs.labels }}
build-args: |
CREATED=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
COMMIT=${{ steps.shortcommit.outputs.value }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
tags: ${{ steps.meta.outputs.tags }}
push: true

View File

@@ -1,22 +1,18 @@
name: Docker Hub description
on:
push:
branches:
- master
branches: [master]
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
docker-hub-description:
if: github.repository == 'qdm12/gluetun'
dockerHubDescription:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v3
- uses: peter-evans/dockerhub-description@v3
- name: Checkout
uses: actions/checkout@v2
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}

View File

@@ -9,7 +9,7 @@ jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: crazy-max/ghaction-github-labeler@v4
- uses: actions/checkout@v2
- uses: crazy-max/ghaction-github-labeler@v3
with:
yaml-file: .github/labels.yml

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
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error

1
.gitignore vendored
View File

@@ -1 +0,0 @@
scratch.txt

View File

@@ -1,4 +1,6 @@
linters-settings:
maligned:
suggest-new: true
misspell:
locale: US
@@ -7,50 +9,22 @@ issues:
- path: _test\.go
linters:
- dupl
- goerr113
- containedctx
- path: "internal\\/server\\/.+\\.go"
- maligned
- path: internal/unix/constants\.go
linters:
- dupl
- path: "internal\\/configuration\\/settings\\/.+\\.go"
linters:
- dupl
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
linters:
- gomnd
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
linters:
- gomnd
- linters:
- lll
source: "^//go:generate .+$"
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
- ireturn
- golint
text: don't use ALL_CAPS in Go names; use CamelCase
linters:
disable-all: true
enable:
# - cyclop
# - errorlint
# - varnamelen
# - wrapcheck
- asciicheck
- bidichk
- bodyclose
- containedctx
- decorder
- deadcode
- dogsled
- dupl
- durationcheck
- errchkjson
- errname
- execinquery
- errcheck
- exhaustive
- exportloopref
- forcetypeassert
- gci
- gochecknoglobals
- gochecknoinits
@@ -59,41 +33,34 @@ linters:
- gocritic
- gocyclo
- godot
- goerr113
- goheader
- goimports
- golint
- gomnd
- gomoddirectives
- goprintffuncname
- gosec
- grouper
- ifshort
- importas
- ireturn
- gosimple
- govet
- ineffassign
- interfacer
- lll
- maintidx
- makezero
- maligned
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- predeclared
- promlinter
- revive
- rowserrcheck
- scopelint
- sqlclosecheck
- tenv
- thelper
- tparallel
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- wastedassign
- unused
- varcheck
- whitespace
run:

25
.vscode/launch.json vendored
View File

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

View File

@@ -1,55 +1,60 @@
ARG ALPINE_VERSION=3.16
ARG GO_ALPINE_VERSION=3.16
ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.46.2
ARG ALPINE_VERSION=3.12
ARG GO_VERSION=1.15
ARG BUILDPLATFORM=linux/amd64
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} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
RUN apk --update add git g++
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
RUN apk --update add git
ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint
WORKDIR /tmp/gobuild
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
FROM --platform=${BUILDPLATFORM} base AS test
FROM --platform=$BUILDPLATFORM base AS test
# Note on the go race detector:
# - we set CGO_ENABLED=1 to have it enabled
# - we installed g++ to support the race detector
# - we install g++ to support the race detector
ENV CGO_ENABLED=1
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
RUN apk --update --no-cache add g++
FROM --platform=${BUILDPLATFORM} base AS lint
FROM --platform=$BUILDPLATFORM base AS lint
ARG GOLANGCI_LINT_VERSION=v1.35.2
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_VERSION}
COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m
FROM --platform=${BUILDPLATFORM} base AS build
FROM --platform=$BUILDPLATFORM base AS tidy
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git add -A && git commit -m ci && \
sed -i '/\/\/ indirect/d' go.mod && \
go mod tidy && \
git diff --exit-code -- go.mod
FROM --platform=$BUILDPLATFORM base AS build
COPY --from=qmcgaw/xcputranslate:v0.4.0 /xcputranslate /usr/local/bin/xcputranslate
ARG TARGETPLATFORM
ARG VERSION=unknown
ARG CREATED="an unknown date"
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
RUN GOARCH="$(xcputranslate translate -field arch -targetplatform ${TARGETPLATFORM})" \
GOARM="$(xcputranslate translate -field arm -targetplatform ${TARGETPLATFORM})" \
RUN GOARCH="$(xcputranslate -field arch -targetplatform ${TARGETPLATFORM})" \
GOARM="$(xcputranslate -field arm -targetplatform ${TARGETPLATFORM})" \
go build -trimpath -ldflags="-s -w \
-X 'main.version=$VERSION' \
-X 'main.created=$CREATED' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o entrypoint cmd/gluetun/main.go
FROM alpine:${ALPINE_VERSION}
ARG VERSION=unknown
ARG CREATED="an unknown date"
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
LABEL \
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
org.opencontainers.image.created=$CREATED \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
@@ -57,71 +62,51 @@ LABEL \
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.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 \
VPN_TYPE=openvpn \
# Common VPN options
VPN_ENDPOINT_IP= \
VPN_ENDPOINT_PORT= \
VPN_INTERFACE=tun0 \
# OpenVPN
OPENVPN_PROTOCOL=udp \
ENV VPNSP=pia \
VERSION_INFORMATION=on \
PROTOCOL=udp \
OPENVPN_VERBOSITY=1 \
OPENVPN_ROOT=no \
OPENVPN_TARGET_IP= \
OPENVPN_IPV6=off \
TZ= \
PUID= \
PGID= \
PUBLICIP_FILE="/tmp/gluetun/ip" \
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only
OPENVPN_USER= \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_PROCESS_USER= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \
# Wireguard
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ADDRESSES= \
# VPN server filtering
SERVER_REGIONS= \
SERVER_COUNTRIES= \
SERVER_CITIES= \
SERVER_HOSTNAMES= \
# # Mullvad only:
USER_SECRETFILE=/run/secrets/openvpn_user \
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
REGION= \
# PIA only
PIA_ENCRYPTION=strong \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# Mullvad and PureVPN only
COUNTRY= \
# Mullvad, PureVPN, Windscribe only
CITY= \
# Windscribe only
SERVER_HOSTNAME= \
# Mullvad only
ISP= \
OWNED_ONLY=no \
# # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING=off \
PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# # Cyberghost only:
OWNED=no \
# Mullvad and Windscribe only
PORT= \
# Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # Nordvpn only:
# NordVPN only
SERVER_NUMBER= \
# # PIA only:
SERVER_NAMES= \
# # ProtonVPN only:
FREE_ONLY= \
# # Surfshark only:
MULTIHOP_ONLY= \
# Firewall
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# Logging
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# Openvpn
OPENVPN_CIPHER= \
OPENVPN_AUTH= \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:0:0/96 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
@@ -132,12 +117,18 @@ ENV VPN_SERVICE_PROVIDER=pia \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
DNS_KEEP_NAMESERVER=off \
# Firewall
FIREWALL=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# HTTP proxy
HTTPPROXY= \
HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_PORT=8888 \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
@@ -145,43 +136,19 @@ ENV VPN_SERVICE_PROVIDER=pia \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
SHADOWSOCKS_LISTENING_ADDRESS=":8388" \
SHADOWSOCKS_PORT=8388 \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
# Server data updater
UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \
# Pprof
PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \
PPROF_MUTEX_PROFILE_RATE=0 \
PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID= \
PGID=
ENTRYPOINT ["/gluetun-entrypoint"]
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \
UPDATER_PERIOD=0
ENTRYPOINT ["/entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l apk-tools && \
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.4 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
# Fix vulnerability issue
apk add --no-cache --update busybox && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /entrypoint healthcheck
RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* && \
deluser openvpn && \
deluser unbound && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
# TODO remove once SAN is added to PIA servers certificates, see https://github.com/pia-foss/manual-connections/issues/10
ENV GODEBUG=x509ignoreCN=0
COPY --from=build /tmp/gobuild/entrypoint /entrypoint

153
README.md
View File

@@ -1,125 +1,116 @@
# Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN sercice providers
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
**ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`*
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
[![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)
[![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)
[![Size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
[![Docker stars qmcgaw/gluetun](https://img.shields.io/docker/stars/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker stars qmcgaw/private-internet-access](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/private-internet-access)
[![Docker Pulls](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)
![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 release date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
![GitHub Release Date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits/master)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub 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)
[![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)
![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)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
## Quick links
- [Setup](#Setup)
- [Features](#Features)
- Problem?
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- Problem or suggestion?
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Suggestion?
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- **Want to add a VPN provider?** check [Development](https://github.com/qdm12/gluetun/wiki/Development) and [Add a provider](https://github.com/qdm12/gluetun/wiki/Add-a-provider)
- Video:
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- [Substack Console interview](https://console.substack.com/p/console-72)
## Features
- Based on Alpine 3.16 for a small Docker image of 29MB
- 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
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- For **Mullvad**, **Ivpn** and **Windscribe**
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- Based on Alpine 3.12 for a small Docker image of 52MB
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn**, **NordVPN**, **PureVPN** and **Privado** servers
- Supports Openvpn only for now
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp`
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
- [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 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even s390x as well as ppc64le 🎆
- VPN server side port forwarding for Private Internet Access and Vyprvpn
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Unbound subprogram drops root privileges once launched
- Subprograms all drop root privileges once launched
- Subprograms output streams are all merged together
- Can work as a Kubernetes sidecar container, thanks @rorph
## Setup
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
1. On some devices you may need to setup your tunnel kernel module on your host with `insmod /lib/modules/tun.ko` or `modprobe tun`
- [Synology users Wiki page](https://github.com/qdm12/gluetun/wiki/Synology-setup)
1. Launch the container with:
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \
-e VPNSP="private internet access" -e REGION="CA Montreal" \
-e OPENVPN_USER=js89ds7 -e OPENVPN_PASSWORD=8fd9s239G \
-v /yourpath:/gluetun \
qmcgaw/gluetun
```
[🐛 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+)
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
Here's a docker-compose.yml for the laziest:
```bash
echo "your openvpn username" > openvpn_user
echo "your openvpn password" > openvpn_password
docker-compose up -d
```
```yml
version: "3"
services:
gluetun:
image: qmcgaw/gluetun
# container_name: gluetun
# 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
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
volumes:
- /yourpath:/gluetun
environment:
# See https://github.com/qdm12/gluetun/wiki
- VPN_SERVICE_PROVIDER=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
- OPENVPN_USER=
- OPENVPN_PASSWORD=
# Wireguard:
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# - WIREGUARD_ADDRESSES=10.64.222.21/32
# Timezone for accurate log times
- TZ=
```
You should probably check the many [environment variables](https://github.com/qdm12/gluetun/wiki/Environment-variables) available to adapt the container to your needs.
## Further setup
The following points are all optional but should give you insights on all the possibilities with this container.
- Use [Docker secrets](https://github.com/qdm12/gluetun/wiki/Docker-secrets) to read your credentials instead of environment variables
- [Test your setup](https://github.com/qdm12/gluetun/wiki/Test-your-setup)
- [How to connect other containers and devices to Gluetun](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [VPN server side port forwarding](https://github.com/qdm12/gluetun/wiki/Port-forwarding)
- [HTTP control server](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server) to automate things, restart Openvpn etc.
- Update the image with `docker pull qmcgaw/gluetun:latest`. See this [Wiki document](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) for Docker tags available.
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/master/LICENSE)
## Metadata
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub 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)
![GitHub stars](https://img.shields.io/github/stars/qdm12/gluetun?style=social)
![GitHub watchers](https://img.shields.io/github/watchers/qdm12/gluetun?style=social)
![Contributors](https://img.shields.io/github/contributors/qdm12/gluetun)
![GitHub forks](https://img.shields.io/github/forks/qdm12/gluetun?style=social)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
[![dockeri.co](https://dockeri.co/image/qmcgaw/gluetun)](https://hub.docker.com/r/qmcgaw/gluetun)
![Docker Layers for latest](https://img.shields.io/microbadger/layers/qmcgaw/gluetun/latest?label=Docker%20image%20layers)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)

View File

@@ -2,109 +2,98 @@ package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
nativeos "os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gluetun/internal/httpproxy"
gluetunLogging "github.com/qdm12/gluetun/internal/logging"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop"
"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/unix"
"github.com/qdm12/gluetun/internal/updater"
versionpkg "github.com/qdm12/gluetun/internal/version"
"github.com/qdm12/golibs/command"
"github.com/qdm12/goshutdown"
"github.com/qdm12/goshutdown/goroutine"
"github.com/qdm12/goshutdown/group"
"github.com/qdm12/goshutdown/order"
"github.com/qdm12/gosplash"
"github.com/qdm12/log"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user"
"github.com/qdm12/updated/pkg/dnscrypto"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
created = "an unknown date"
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
)
func main() {
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
Created: created,
Version: version,
Commit: commit,
BuildDate: buildDate,
}
background := context.Background()
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(background)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
logger := log.New(log.SetLevel(log.LevelInfo))
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
fmt.Println(err)
nativeos.Exit(1)
}
args := os.Args
tun := tun.New()
netLinker := netlink.New()
args := nativeos.Args
os := os.New()
osUser := user.New()
unix := unix.New()
cli := cli.New()
cmder := command.NewCmder()
envReader := env.New(logger)
filesReader := files.New()
secretsReader := secrets.New()
muxReader := mux.New(envReader, filesReader, secretsReader)
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli)
errorCh <- _main(ctx, buildInfo, args, logger, os, osUser, unix, cli)
}()
signalsCh := make(chan nativeos.Signal, 1)
signal.Notify(signalsCh,
syscall.SIGINT,
syscall.SIGTERM,
nativeos.Interrupt,
)
select {
case signal := <-signalCh:
fmt.Println("")
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
cancel()
case signal := <-signalsCh:
logger.Warn("Caught OS signal %s, shutting down", signal)
case err := <-errorCh:
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
nativeos.Exit(0)
}
logger.Error(err.Error())
cancel()
logger.Error(err)
}
cancel()
const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod)
select {
@@ -115,149 +104,61 @@ func main() {
logger.Info("Shutdown successful")
case <-timer.C:
logger.Warn("Shutdown timed out")
case signal := <-signalCh:
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
}
os.Exit(1)
nativeos.Exit(1)
}
var (
errCommandUnknown = errors.New("command is unknown")
)
//nolint:gocognit,gocyclo,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, source sources.Source,
tun Tun, netLinker netLinker, cmder command.RunStarter,
cli clier) error {
//nolint:gocognit,gocyclo
func _main(background context.Context, buildInfo models.BuildInformation,
args []string, logger logging.Logger, os os.OS, osUser user.OSUser, unix unix.Unix,
cli cli.CLI) error {
if len(args) > 1 { // cli operation
switch args[1] {
case "healthcheck":
return cli.HealthCheck(ctx, source, logger)
return cli.HealthCheck(background)
case "clientkey":
return cli.ClientKey(args[2:])
return cli.ClientKey(args[2:], os.OpenFile)
case "openvpnconfig":
return cli.OpenvpnConfig(logger, source)
return cli.OpenvpnConfig(os)
case "update":
return cli.Update(ctx, args[2:], logger)
case "format-servers":
return cli.FormatServers(args[2:])
return cli.Update(args[2:], os)
default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
return fmt.Errorf("command %q is unknown", args[1])
}
}
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
if err != nil {
return err
}
splashSettings := gosplash.Settings{
User: "qdm12",
Repository: "gluetun",
Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Created,
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
GithubSponsor: "qdm12",
}
for _, line := range gosplash.MakeLines(splashSettings) {
fmt.Println(line)
}
allSettings, err := source.Read()
if err != nil {
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))
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
storageLogger := logger.New(log.SetComponent("storage"))
storage, err := storage.New(storageLogger, constants.ServersData)
if err != nil {
return err
}
err = allSettings.Validate(storage)
if err != nil {
return err
}
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil {
return fmt.Errorf("cannot create Pprof server: %w", err)
}
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
ctx, cancel := context.WithCancel(background)
defer cancel()
const clientTimeout = 15 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
// Create configurators
alpineConf := alpine.New()
ovpnConf := openvpn.New(
logger.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
alpineConf := alpine.NewConfigurator(os.OpenFile, osUser)
ovpnConf := openvpn.NewConfigurator(logger, os, unix)
dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
dnsConf := unbound.NewConfigurator(logger, os.OpenFile, dnsCrypto,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
routingConf := routing.NewRouting(logger)
firewallConf := firewall.NewConfigurator(logger, routingConf, os.OpenFile)
streamMerger := command.NewStreamMerger()
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "Unbound", getVersion: dnsConf.Version},
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
return firewall.Version(ctx, cmder)
}},
paramsReader := params.NewReader(logger, os)
fmt.Println(gluetunLogging.Splash(buildInfo))
printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){
"OpenVPN": ovpnConf.Version,
"Unbound": dnsConf.Version,
"IPtables": firewallConf.Version,
})
allSettings, warnings, err := settings.GetAllSettings(paramsReader)
for _, warning := range warnings {
logger.Warn(warning)
}
if err != nil {
return err
}
logger.Info(allSettings.String())
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
@@ -267,33 +168,58 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err
}
// TODO run this in a loop or in openvpn to reload from file without restarting
storage := storage.New(logger, os, constants.ServersData)
allServers, err := storage.SyncServers(constants.GetAllServers())
if err != nil {
return err
}
// Should never change
puid, pgid := allSettings.System.PUID, allSettings.System.PGID
const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil {
return fmt.Errorf("cannot create user: %w", err)
return err
}
if nonRootUsername != defaultUsername {
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
logger.Info("using existing username %s corresponding to user id %d", nonRootUsername, puid)
}
// set it for Unbound
// TODO remove this when migrating to qdm12/dns v2
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err
}
if allSettings.Firewall.Debug {
firewallConf.SetDebug()
routingConf.SetDebug()
}
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil {
return err
}
localSubnet, err := routingConf.LocalSubnet()
if err != nil {
return err
}
defaultIP, err := routingConf.DefaultIP()
if err != nil {
return err
}
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet, defaultIP)
if err := routingConf.Setup(); err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
}
return fmt.Errorf("cannot setup routing: %w", err)
return err
}
defer func() {
routingLogger.Info("routing cleanup...")
routingConf.SetVerbose(false)
if err := routingConf.TearDown(); err != nil {
routingLogger.Error("cannot teardown routing: " + err.Error())
logger.Error(err)
}
}()
@@ -304,221 +230,218 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err
}
const tunDevice = "/dev/net/tun"
if err := tun.Check(tunDevice); err != nil {
logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice)
if err := ovpnConf.CheckTUN(); err != nil {
logger.Warn(err)
err = ovpnConf.CreateTUN()
if err != nil {
return err
}
}
tunnelReadyCh := make(chan struct{})
dnsReadyCh := make(chan struct{})
defer close(tunnelReadyCh)
defer close(dnsReadyCh)
if allSettings.Firewall.Enabled {
err := firewallConf.SetEnabled(ctx, true) // disabled by default
if err != nil {
return err
}
}
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
if err != nil {
return err
}
}
for _, port := range allSettings.Firewall.InputPorts {
for _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
if err != nil {
return err
}
}
} // TODO move inside firewall?
// Shutdown settings
const totalShutdownTimeout = 3 * time.Second
const defaultShutdownTimeout = 400 * time.Millisecond
defaultShutdownOnSuccess := func(goRoutineName string) {
logger.Info(goRoutineName + ": terminated ✔️")
}
defaultShutdownOnFailure := func(goRoutineName string, err error) {
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
}
defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess)}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
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,
httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
"port forwarding", goroutine.OptionTimeout(time.Second))
go portForwardLooper.Run(portForwardCtx, portForwardDone)
unboundLogger := logger.New(log.SetComponent("dns over tls"))
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler)
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler)
ipFetcher := ipinfo.New(httpClient)
publicIPLooper := publicip.NewLoop(ipFetcher,
logger.New(log.SetComponent("ip getter")),
allSettings.PublicIP, puid, pgid)
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.Run(pubIPCtx, pubIPDone)
otherGroupHandler.Add(pubIPHandler)
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone)
updaterLooper := updater.NewLoop(allSettings.Updater,
providers, storage, httpClient, updaterLogger)
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(updaterCtx, updaterDone)
tickersGroupHandler.Add(updaterHandler)
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop(
logger.New(log.SetComponent("http proxy")),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
logger.New(log.SetComponent("shadowsocks")))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
if err != nil {
return fmt.Errorf("cannot setup control server: %w", err)
}
httpServerReady := make(chan struct{})
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
<-httpServerReady
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call
// until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
<-ctx.Done()
return orderHandler.Shutdown(context.Background())
}
type printVersionElement struct {
name string
getVersion func(ctx context.Context) (version string, err error)
}
type infoer interface {
Info(s string)
}
func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement) (err error) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for _, element := range elements {
version, err := element.getVersion(ctx)
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
if err != nil {
return err
}
logger.Info(element.name + " version: " + version)
} // TODO move inside firewall?
wg := &sync.WaitGroup{}
wg.Add(1)
go collectStreamLines(ctx, wg, streamMerger, logger, tunnelReadyCh)
openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers,
ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, streamMerger, cancel)
wg.Add(1)
// wait for restartOpenvpn
go openvpnLooper.Run(ctx, wg)
updaterLooper := updater.NewLooper(allSettings.Updater,
allServers, storage, openvpnLooper.SetServers, httpClient, logger)
wg.Add(1)
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(ctx, wg)
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, httpClient,
logger, streamMerger, nonRootUsername, puid, pgid)
wg.Add(1)
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(ctx, wg, dnsReadyCh)
publicIPLooper := publicip.NewLooper(
httpClient, logger, allSettings.PublicIP, puid, pgid, os)
wg.Add(1)
go publicIPLooper.Run(ctx, wg)
wg.Add(1)
go publicIPLooper.RunRestartTicker(ctx, wg)
httpProxyLooper := httpproxy.NewLooper(logger, allSettings.HTTPProxy)
wg.Add(1)
go httpProxyLooper.Run(ctx, wg)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks, logger)
wg.Add(1)
go shadowsocksLooper.Run(ctx, wg)
wg.Add(1)
go routeReadyEvents(ctx, wg, buildInfo, tunnelReadyCh, dnsReadyCh,
unboundLooper, updaterLooper, publicIPLooper, routingConf, logger, httpClient,
allSettings.VersionInformation, allSettings.OpenVPN.Provider.PortForwarding.Enabled, openvpnLooper.PortForward,
)
controlServerAddress := fmt.Sprintf("0.0.0.0:%d", allSettings.ControlServer.Port)
controlServerLogging := allSettings.ControlServer.Log
httpServer := server.New(controlServerAddress, controlServerLogging,
logger, buildInfo, openvpnLooper, unboundLooper, updaterLooper, publicIPLooper)
wg.Add(1)
go httpServer.Run(ctx, wg)
healthcheckServer := healthcheck.NewServer(
constants.HealthcheckAddress, logger)
wg.Add(1)
go healthcheckServer.Run(ctx, wg)
// Start openvpn for the first time in a blocking call
// until openvpn is launched
_, _ = openvpnLooper.SetStatus(constants.Running) // TODO option to disable with variable
<-ctx.Done()
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
logger.Info("Clearing forwarded port status file %s", allSettings.OpenVPN.Provider.PortForwarding.Filepath)
if err := os.Remove(string(allSettings.OpenVPN.Provider.PortForwarding.Filepath)); err != nil {
logger.Error(err)
}
}
wg.Wait()
return nil
}
type netLinker interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrAdd(link netlink.Link, addr *netlink.Addr) error
IsWireguardSupported() (ok bool, err error)
RouteList(link netlink.Link, family int) (
routes []netlink.Route, err error)
RouteAdd(route *netlink.Route) error
RouteDel(route *netlink.Route) error
RouteReplace(route *netlink.Route) error
RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule *netlink.Rule) error
RuleDel(rule *netlink.Rule) error
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) (err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (err error)
LinkSetDown(link netlink.Link) (err error)
func printVersions(ctx context.Context, logger logging.Logger,
versionFunctions map[string]func(ctx context.Context) (string, error)) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for name, f := range versionFunctions {
version, err := f(ctx)
if err != nil {
logger.Error(err)
} else {
logger.Info("%s version: %s", name, version)
}
}
}
type clier interface {
ClientKey(args []string) error
FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, source sources.Source) error
HealthCheck(ctx context.Context, source sources.Source, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
func collectStreamLines(ctx context.Context, wg *sync.WaitGroup,
streamMerger command.StreamMerger,
logger logging.Logger, tunnelReadyCh chan<- struct{}) {
defer wg.Done()
// Blocking line merging paramsReader for openvpn and unbound
logger.Info("Launching standard output merger")
streamMerger.CollectLines(ctx, func(line string) {
line, level := gluetunLogging.PostProcessLine(line)
if line == "" {
return
}
switch level {
case logging.DebugLevel:
logger.Debug(line)
case logging.InfoLevel:
logger.Info(line)
case logging.WarnLevel:
logger.Warn(line)
case logging.ErrorLevel:
logger.Error(line)
}
switch {
case strings.Contains(line, "Initialization Sequence Completed"):
tunnelReadyCh <- struct{}{}
case strings.Contains(line, "TLS Error: TLS key negotiation failed to occur within 60 seconds (check your network connectivity)"): //nolint:lll
logger.Warn("This means that either...")
logger.Warn("1. The VPN server IP address you are trying to connect to is no longer valid, see https://github.com/qdm12/gluetun/wiki/Update-servers-information") //nolint:lll
logger.Warn("2. The VPN server crashed, try changing region")
logger.Warn("3. Your Internet connection is not working, ensure it works")
logger.Warn("Feel free to create an issue at https://github.com/qdm12/gluetun/issues/new/choose")
}
}, func(err error) {
logger.Warn(err)
})
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, buildInfo models.BuildInformation,
tunnelReadyCh, dnsReadyCh <-chan struct{},
unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper,
routing routing.Routing, logger logging.Logger, httpClient *http.Client,
versionInformation, portForwardingEnabled bool, startPortForward func(vpnGateway net.IP)) {
defer wg.Done()
tickerWg := &sync.WaitGroup{}
// for linters only
var restartTickerContext context.Context
var restartTickerCancel context.CancelFunc = func() {}
for {
select {
case <-ctx.Done():
restartTickerCancel() // for linters only
tickerWg.Wait()
return
case <-tunnelReadyCh: // blocks until openvpn is connected
if unboundLooper.GetSettings().Enabled {
_, _ = unboundLooper.SetStatus(constants.Running)
}
restartTickerCancel() // stop previous restart tickers
tickerWg.Wait()
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
//nolint:gomnd
tickerWg.Add(2)
go unboundLooper.RunRestartTicker(restartTickerContext, tickerWg)
go updaterLooper.RunRestartTicker(restartTickerContext, tickerWg)
vpnDestination, err := routing.VPNDestinationIP()
if err != nil {
logger.Warn(err)
} else {
logger.Info("VPN routing IP address: %s", vpnDestination)
}
if portForwardingEnabled {
// vpnGateway required only for PIA
vpnGateway, err := routing.VPNLocalGatewayIP()
if err != nil {
logger.Error(err)
}
logger.Info("VPN gateway IP address: %s", vpnGateway)
startPortForward(vpnGateway)
}
case <-dnsReadyCh:
// Runs the Public IP getter job once
_, _ = publicIPLooper.SetStatus(constants.Running)
if !versionInformation {
break
}
message, err := versionpkg.GetMessage(ctx, buildInfo, httpClient)
if err != nil {
logger.Error(err)
break
}
logger.Info(message)
}
}
}

31
docker-compose.yml Normal file
View File

@@ -0,0 +1,31 @@
version: "3.7"
services:
gluetun:
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
network_mode: bridge
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
- 8000:8000/tcp # Built-in HTTP control server
# command:
volumes:
- /yourpath:/gluetun
secrets:
- openvpn_user
- openvpn_password
environment:
# More variables are available, see the readme table
- VPNSP=private internet access
# Timezone for accurate logs times
- TZ=
restart: always
secrets:
openvpn_user:
file: ./openvpn_user
openvpn_password:
file: ./openvpn_password

51
go.mod
View File

@@ -1,45 +1,16 @@
module github.com/qdm12/gluetun
go 1.17
go 1.15
require (
github.com/breml/rootcerts v0.2.3
github.com/fatih/color v1.13.0
github.com/golang/mock v1.6.0
github.com/qdm12/dns v1.11.0
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/gotree v0.2.0
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/updated v0.0.0-20210603204757-205acfe6937e
github.com/stretchr/testify v1.7.2
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/text v0.3.7
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mdlayher/genetlink v1.0.0 // indirect
github.com/mdlayher/netlink v1.4.0 // indirect
github.com/miekg/dns v1.1.40 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/fatih/color v1.10.0
github.com/golang/mock v1.4.4
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/qdm12/dns v1.4.0-rc4
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09
github.com/qdm12/ss-server v0.1.0
github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a
github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.0
golang.org/x/sys v0.0.0-20201223074533-0d417f636930
)

257
go.sum
View File

@@ -1,259 +1,192 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.3 h1:1vkYjKOiHVSyuz9Ue4AOrViEvUm8gk8phTg0vbcuU0A=
github.com/breml/rootcerts v0.2.3/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
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/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
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/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
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/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
github.com/qdm12/dns v1.4.0-rc4 h1:pCcFMqismbktPQX7yrtdmJZq30Y05JBfWRTXY1ZLVFw=
github.com/qdm12/dns v1.4.0-rc4/go.mod h1:JhUKBhuDRYBUQ2XwW/jbeWx/qS0sSJjIFjGTCFGP5I8=
github.com/qdm12/golibs v0.0.0-20201227203847-2fd99ffdfdba/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/golibs v0.0.0-20210102020307-17bc97def973 h1:5YeJALmDjvg2wSi6XB8MpQQekbT/eBnwGahJrh01HHQ=
github.com/qdm12/golibs v0.0.0-20210102020307-17bc97def973/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09 h1:zP+ZRwV3GldgTWFgKNBQ2zoFA8mIczb+fvTvrX8LZRo=
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/ss-server v0.1.0 h1:WV9MkHCDEWRwe4WpnYFeR/zcZAxYoTbfntLDnw9AQ50=
github.com/qdm12/ss-server v0.1.0/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a h1:gkyP+gMEeBgMgyRYGrVNcoy6cL1065IvXsyfB6xboIc=
github.com/qdm12/updated v0.0.0-20210102005021-dd457d77f94a/go.mod h1:bbJGxEYCnsA8WU4vBcXYU6mOoHyzdP458FIKP4mfLJM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w=
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
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-20190510104115-cbcb75029529/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-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-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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-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.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,21 +1,22 @@
package alpine
import (
"os/user"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user"
)
type Alpine struct {
alpineReleasePath string
passwdPath string
lookupID func(uid string) (*user.User, error)
lookup func(username string) (*user.User, error)
type Configurator interface {
CreateUser(username string, uid int) (createdUsername string, err error)
}
func New() *Alpine {
return &Alpine{
alpineReleasePath: "/etc/alpine-release",
passwdPath: "/etc/passwd",
lookupID: user.LookupId,
lookup: user.Lookup,
type configurator struct {
openFile os.OpenFileFunc
osUser user.OSUser
}
func NewConfigurator(openFile os.OpenFileFunc, osUser user.OSUser) Configurator {
return &configurator{
openFile: openFile,
osUser: osUser,
}
}

View File

@@ -1,47 +1,35 @@
package alpine
import (
"errors"
"fmt"
"os"
"os/user"
"strconv"
)
var (
ErrUserAlreadyExists = errors.New("user already exists")
)
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)
u, err := a.lookupID(UIDStr)
func (c *configurator) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := fmt.Sprintf("%d", uid)
u, err := c.osUser.LookupID(UIDStr)
_, unknownUID := err.(user.UnknownUserIdError)
if err != nil && !unknownUID {
return "", err
}
if u != nil {
return "", fmt.Errorf("cannot create user: %w", err)
} else if u != nil {
if u.Username == username {
return "", nil
}
return u.Username, nil
}
u, err = a.lookup(username)
u, err = c.osUser.Lookup(username)
_, unknownUsername := err.(user.UnknownUserError)
if err != nil && !unknownUsername {
return "", err
return "", fmt.Errorf("cannot create user: %w", err)
} else if u != nil {
return "", fmt.Errorf("cannot create user: user with name %s already exists for ID %s instead of %d",
username, u.Uid, uid)
}
if u != nil {
return "", fmt.Errorf("%w: with name %s for ID %s instead of %d",
ErrUserAlreadyExists, username, u.Uid, uid)
}
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
file, err := c.openFile("/etc/passwd", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return "", err
return "", fmt.Errorf("cannot create user: %w", err)
}
s := fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid)
_, err = file.WriteString(s)
@@ -49,6 +37,5 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
_ = file.Close()
return "", err
}
return username, file.Close()
}

View File

@@ -1,27 +0,0 @@
package alpine
import (
"context"
"io"
"os"
"strings"
)
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
if err != nil {
return "", err
}
b, err := io.ReadAll(file)
if err != nil {
return "", err
}
if err := file.Close(); err != nil {
return "", err
}
version = strings.ReplaceAll(string(b), "\n", "")
return version, nil
}

View File

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

View File

@@ -1,11 +1,20 @@
package cli
type CLI struct {
repoServersPath string
import (
"context"
"github.com/qdm12/golibs/os"
)
type CLI interface {
ClientKey(args []string, openFile os.OpenFileFunc) error
HealthCheck(ctx context.Context) error
OpenvpnConfig(os os.OS) error
Update(args []string, os os.OS) error
}
func New() *CLI {
return &CLI{
repoServersPath: "./internal/storage/servers.json",
}
type cli struct{}
func New() CLI {
return &cli{}
}

View File

@@ -3,24 +3,24 @@ package cli
import (
"flag"
"fmt"
"io"
"os"
"io/ioutil"
"strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/os"
)
func (c *CLI) ClientKey(args []string) error {
func (c *cli) ClientKey(args []string, openFile os.OpenFileFunc) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", files.OpenVPNClientKeyPath, "file path to the client.key file")
filepath := flagSet.String("path", string(constants.ClientKey), "file path to the client.key file")
if err := flagSet.Parse(args); err != nil {
return err
}
file, err := os.OpenFile(*filepath, os.O_RDONLY, 0)
file, err := openFile(*filepath, os.O_RDONLY, 0)
if err != nil {
return err
}
data, err := io.ReadAll(file)
data, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err

View File

@@ -1,99 +0,0 @@
package cli
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/storage"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
ErrFormatNotRecognized = errors.New("format is not recognized")
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
)
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser) {
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 {
var format, output string
allProviders := providers.All()
providersToFormat := make(map[string]*bool, len(allProviders))
for _, provider := range allProviders {
providersToFormat[provider] = new(bool)
}
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
titleCaser := cases.Title(language.English)
for _, provider := range allProviders {
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
}
if err := flagSet.Parse(args); err != nil {
return err
}
if format != "markdown" {
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
}
// Verify only one provider is set to be formatted.
var providers []string
for provider, formatPtr := range providersToFormat {
if *formatPtr {
providers = append(providers, provider)
}
}
switch len(providers) {
case 0:
return ErrProviderUnspecified
case 1:
default:
return fmt.Errorf("%w: %d specified: %s",
ErrMultipleProvidersToFormat, len(providers),
strings.Join(providers, ", "))
}
providerToFormat := providers[0]
logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("cannot create servers storage: %w", err)
}
formatted := storage.FormatToMarkdown(providerToFormat)
output = filepath.Clean(output)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return fmt.Errorf("cannot open output file: %w", err)
}
_, err = fmt.Fprint(file, formatted)
if err != nil {
_ = file.Close()
return fmt.Errorf("cannot write to output file: %w", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("cannot close output file: %w", err)
}
return nil
}

View File

@@ -2,37 +2,19 @@ package cli
import (
"context"
"net"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/healthcheck"
)
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
// Extract the health server port from the configuration.
config, err := source.ReadHealth()
if err != nil {
return err
}
err = config.Validate()
if err != nil {
return err
}
_, port, err := net.SplitHostPort(config.ServerAddress)
if err != nil {
return err
}
func (c *cli) HealthCheck(ctx context.Context) error {
const timeout = 10 * time.Second
httpClient := &http.Client{Timeout: timeout}
client := healthcheck.NewClient(httpClient)
healthchecker := healthcheck.NewChecker(httpClient)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "http://127.0.0.1:" + port
return client.Check(ctx, url)
const url = "http://" + constants.HealthcheckAddress
return healthchecker.Check(ctx, url)
}

View File

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

View File

@@ -1,74 +1,40 @@
package cli
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type OpenvpnConfigLogger interface {
Info(s string)
Warn(s string)
}
type Unzipper interface {
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][]net.IP, warnings []string, err error)
}
type IPFetcher interface {
FetchMultiInfo(ctx context.Context, ips []net.IP) (data []ipinfo.Response, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
storage, err := storage.New(logger, constants.ServersData)
func (c *cli) OpenvpnConfig(os os.OS) error {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return err
}
allSettings, err := source.Read()
paramsReader := params.NewReader(logger, os)
allSettings, _, err := settings.GetAllSettings(paramsReader)
if err != nil {
return err
}
if err = allSettings.Validate(storage); err != nil {
return 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)
allServers, err := storage.New(logger, os, constants.ServersData).
SyncServers(constants.GetAllServers())
if err != nil {
return err
}
lines := providerConf.OpenVPNConfig(connection, allSettings.VPN.OpenVPN)
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetOpenVPNConnection(allSettings.OpenVPN.Provider.ServerSelection)
if err != nil {
return err
}
lines := providerConf.BuildConf(connection, "nonroortuser", allSettings.OpenVPN)
fmt.Println(strings.Join(lines, "\n"))
return nil
}

View File

@@ -2,99 +2,61 @@ package cli
import (
"context"
"errors"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/ipinfo"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNoProviderSpecified = errors.New("no provider was specified")
)
type UpdaterLogger interface {
Info(s string)
Warn(s string)
Error(s string)
}
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool
var csvProviders string
func (c *cli) Update(args []string, os os.OS) error {
options := settings.Updater{CLI: true}
var flushToFile bool
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
"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")
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.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
flagSet.BoolVar(&flushToFile, "file", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&options.Stdout, "stdout", false, "Write results to console to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "1.1.1.1", "DNS resolver address to use")
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
if err := flagSet.Parse(args); err != nil {
return err
}
if !endUserMode && !maintainerMode {
return ErrModeUnspecified
}
if updateAll {
options.Providers = providers.All()
} else {
if csvProviders == "" {
return ErrNoProviderSpecified
}
options.Providers = strings.Split(csvProviders, ",")
}
options.SetDefaults(options.Providers[0])
err := options.Validate()
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return fmt.Errorf("options validation failed: %w", err)
return err
}
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("cannot create servers storage: %w", err)
if !flushToFile && !options.Stdout {
return fmt.Errorf("at least one of -file or -stdout must be specified")
}
ctx := context.Background()
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
ipFetcher := ipinfo.New(httpClient)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
storage := storage.New(logger, os, constants.ServersData)
currentServers, err := storage.SyncServers(constants.GetAllServers())
if err != nil {
return fmt.Errorf("cannot update server information: %w", err)
return fmt.Errorf("cannot update servers: %w", err)
}
if maintainerMode {
err := storage.FlushToFile(c.repoServersPath)
if err != nil {
return fmt.Errorf("cannot write servers data to embedded JSON file: %w", err)
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return err
}
if flushToFile {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
}

View File

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

View File

@@ -1,82 +0,0 @@
package settings
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// DNS contains settings to configure DNS.
type DNS struct {
// ServerAddress is the DNS server to use inside
// the Go program and for the system.
// It defaults to '127.0.0.1' to be used with the
// DoT server. It cannot be nil in the internal
// state.
ServerAddress net.IP
// KeepNameserver is true if the Docker DNS server
// found in /etc/resolv.conf should be kept.
// Note settings this to true will go around the
// DoT server blocking.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
// DOT contains settings to configure the DoT
// server.
DoT DoT
}
func (d DNS) validate() (err error) {
err = d.DoT.validate()
if err != nil {
return fmt.Errorf("failed validating DoT settings: %w", err)
}
return nil
}
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerAddress: helpers.CopyIP(d.ServerAddress),
KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
DoT: d.DoT.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DNS) mergeWith(other DNS) {
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.mergeWith(other.DoT)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DNS) overrideWith(other DNS) {
d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
d.DoT.setDefaults()
}
func (d DNS) String() string {
return d.toLinesNode().String()
}
func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:")
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())
return node
}

View File

@@ -1,138 +0,0 @@
package settings
import (
"errors"
"fmt"
"regexp"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"inet.af/netaddr"
)
// DNSBlacklist is settings for the DNS blacklist building.
type DNSBlacklist struct {
BlockMalicious *bool
BlockAds *bool
BlockSurveillance *bool
AllowedHosts []string
AddBlockedHosts []string
AddBlockedIPs []netaddr.IP
AddBlockedIPPrefixes []netaddr.IPPrefix
}
func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true)
b.BlockAds = helpers.DefaultBool(b.BlockAds, false)
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 (
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
)
func (b DNSBlacklist) validate() (err error) {
for _, host := range b.AllowedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host)
}
}
for _, host := range b.AddBlockedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
}
}
return nil
}
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{
BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious),
BlockAds: helpers.CopyBoolPtr(b.BlockAds),
BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance),
AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts),
AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts),
AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes),
}
}
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
return blacklist.BuilderSettings{
BlockMalicious: *b.BlockMalicious,
BlockAds: *b.BlockAds,
BlockSurveillance: *b.BlockSurveillance,
AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}, nil
}
func (b DNSBlacklist) String() string {
return b.toLinesNode().String()
}
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS filtering settings:")
node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Appendf("Allowed hosts:")
for _, host := range b.AllowedHosts {
allowedHostsNode.Appendf(host)
}
}
if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Appendf("Blocked hosts:")
for _, host := range b.AddBlockedHosts {
blockedHostsNode.Appendf(host)
}
}
if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Appendf("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Appendf(ip.String())
}
}
if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Appendf(ipNetwork.String())
}
}
return node
}

View File

@@ -1,113 +0,0 @@
package settings
import (
"errors"
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// DoT contains settings to configure the DoT server.
type DoT struct {
// Enabled is true if the DoT server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
Enabled *bool
// UpdatePeriod is the period to update DNS block
// lists and cryptographic files for DNSSEC validation.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Unbound contains settings to configure Unbound.
Unbound Unbound
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
}
var (
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
err = d.Unbound.validate()
if err != nil {
return err
}
err = d.Blacklist.validate()
if err != nil {
return err
}
return nil
}
func (d *DoT) copy() (copied DoT) {
return DoT{
Enabled: helpers.CopyBoolPtr(d.Enabled),
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
Unbound: d.Unbound.copy(),
Blacklist: d.Blacklist.copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (d *DoT) mergeWith(other DoT) {
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.mergeWith(other.Unbound)
d.Blacklist.mergeWith(other.Blacklist)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
d.Unbound.overrideWith(other.Unbound)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = helpers.DefaultBool(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
d.Unbound.setDefaults()
d.Blacklist.setDefaults()
}
func (d DoT) String() string {
return d.toLinesNode().String()
}
func (d DoT) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS over TLS settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled"
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
node.AppendNode(d.Unbound.toLinesNode())
node.AppendNode(d.Blacklist.toLinesNode())
return node
}

View File

@@ -1,45 +0,0 @@
package settings
import "errors"
var (
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
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")
ErrHostnameNotValid = errors.New("the hostname 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")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
ErrOpenVPNUserIsEmpty = errors.New("user is empty")
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
)

View File

@@ -1,117 +0,0 @@
package settings
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// Firewall contains settings to customize the firewall operation.
type Firewall struct {
VPNInputPorts []uint16
InputPorts []uint16
OutboundSubnets []net.IPNet
Enabled *bool
Debug *bool
}
func (f Firewall) validate() (err error) {
if hasZeroPort(f.VPNInputPorts) {
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
}
if hasZeroPort(f.InputPorts) {
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
}
return nil
}
func hasZeroPort(ports []uint16) (has bool) {
for _, port := range ports {
if port == 0 {
return true
}
}
return false
}
func (f *Firewall) copy() (copied Firewall) {
return Firewall{
VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts),
InputPorts: helpers.CopyUint16Slice(f.InputPorts),
OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets),
Enabled: helpers.CopyBoolPtr(f.Enabled),
Debug: helpers.CopyBoolPtr(f.Debug),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
// It merges values of slices together, even if they
// are set in the receiver settings.
func (f *Firewall) mergeWith(other Firewall) {
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (f *Firewall) overrideWith(other Firewall) {
f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled)
f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug)
}
func (f *Firewall) setDefaults() {
f.Enabled = helpers.DefaultBool(f.Enabled, true)
f.Debug = helpers.DefaultBool(f.Debug, false)
}
func (f Firewall) String() string {
return f.toLinesNode().String()
}
func (f Firewall) toLinesNode() (node *gotree.Node) {
node = gotree.New("Firewall settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled))
if !*f.Enabled {
return node
}
if *f.Debug {
node.Appendf("Debug mode: on")
}
if len(f.VPNInputPorts) > 0 {
vpnInputPortsNode := node.Appendf("VPN input ports:")
for _, port := range f.VPNInputPorts {
vpnInputPortsNode.Appendf("%d", port)
}
}
if len(f.InputPorts) > 0 {
inputPortsNode := node.Appendf("Input ports:")
for _, port := range f.InputPorts {
inputPortsNode.Appendf("%d", port)
}
}
if len(f.OutboundSubnets) > 0 {
outboundSubnets := node.Appendf("Outbound subnets:")
for _, subnet := range f.OutboundSubnets {
outboundSubnets.Appendf("%s", subnet)
}
}
return node
}

View File

@@ -1,82 +0,0 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
)
// Health contains settings for the healthcheck and health server.
type Health struct {
// ServerAddress is the listening address
// for the health check server.
// It cannot be the empty string in the internal state.
ServerAddress string
// TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state.
TargetAddress string
VPN HealthyWait
}
func (h Health) Validate() (err error) {
uid := os.Getuid()
_, err = address.Validate(h.ServerAddress,
address.OptionListening(uid))
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
}
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
}
return nil
}
func (h *Health) copy() (copied Health) {
return Health{
ServerAddress: h.ServerAddress,
TargetAddress: h.TargetAddress,
VPN: h.VPN.copy(),
}
}
// MergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *Health) MergeWith(other Health) {
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddress = helpers.MergeWithString(h.TargetAddress, other.TargetAddress)
h.VPN.mergeWith(other.VPN)
}
// OverrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *Health) OverrideWith(other Health) {
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
h.TargetAddress = helpers.OverrideWithString(h.TargetAddress, other.TargetAddress)
h.VPN.overrideWith(other.VPN)
}
func (h *Health) SetDefaults() {
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
h.TargetAddress = helpers.DefaultString(h.TargetAddress, "cloudflare.com:443")
h.VPN.setDefaults()
}
func (h Health) String() string {
return h.toLinesNode().String()
}
func (h Health) toLinesNode() (node *gotree.Node) {
node = gotree.New("Health settings:")
node.Appendf("Server listening address: %s", h.ServerAddress)
node.Appendf("Target address: %s", h.TargetAddress)
node.AppendNode(h.VPN.toLinesNode("VPN"))
return node
}

View File

@@ -1,66 +0,0 @@
package settings
import (
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
// It cannot be nil in the internal state.
Initial *time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
// It cannot be nil in the internal state.
Addition *time.Duration
}
func (h HealthyWait) validate() (err error) {
return nil
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: helpers.CopyDurationPtr(h.Initial),
Addition: helpers.CopyDurationPtr(h.Addition),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HealthyWait) mergeWith(other HealthyWait) {
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HealthyWait) overrideWith(other HealthyWait) {
h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
}
func (h HealthyWait) String() string {
return h.toLinesNode("Health").String()
}
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
node = gotree.New(kind + " wait durations:")
node.Appendf("Initial duration: %s", *h.Initial)
node.Appendf("Additional duration: %s", *h.Addition)
return node
}

View File

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

View File

@@ -1,199 +0,0 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/log"
"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 CopyUint32Ptr(original *uint32) (copied *uint32) {
if original == nil {
return nil
}
copied = new(uint32)
*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 *log.Level) (copied *log.Level) {
if original == nil {
return nil
}
copied = new(log.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

@@ -1,102 +0,0 @@
package helpers
import (
"net"
"time"
"github.com/qdm12/log"
)
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 DefaultUint32(existing *uint32, defaultValue uint32) (
result *uint32) {
if existing != nil {
return existing
}
result = new(uint32)
*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 *log.Level,
defaultValue log.Level) (result *log.Level) {
if existing != nil {
return existing
}
result = new(log.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

@@ -1,31 +0,0 @@
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

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

View File

@@ -1,29 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,164 +0,0 @@
package helpers
import (
"net"
"net/http"
"time"
"github.com/qdm12/log"
"inet.af/netaddr"
)
func OverrideWithBool(existing, other *bool) (result *bool) {
if other == nil {
return existing
}
result = new(bool)
*result = *other
return result
}
func OverrideWithString(existing, other string) (result string) {
if other == "" {
return existing
}
return other
}
func OverrideWithInt(existing, other int) (result int) {
if other == 0 {
return existing
}
return other
}
func OverrideWithFloat64(existing, other float64) (result float64) {
if other == 0 {
return existing
}
return other
}
func OverrideWithStringPtr(existing, other *string) (result *string) {
if other == nil {
return existing
}
result = new(string)
*result = *other
return result
}
func OverrideWithIntPtr(existing, other *int) (result *int) {
if other == nil {
return existing
}
result = new(int)
*result = *other
return result
}
func OverrideWithUint8(existing, other *uint8) (result *uint8) {
if other == nil {
return existing
}
result = new(uint8)
*result = *other
return result
}
func OverrideWithUint16(existing, other *uint16) (result *uint16) {
if other == nil {
return existing
}
result = new(uint16)
*result = *other
return result
}
func OverrideWithUint32(existing, other *uint32) (result *uint32) {
if other == nil {
return existing
}
result = new(uint32)
*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 *log.Level) (result *log.Level) {
if other == nil {
return existing
}
result = new(log.Level)
*result = *other
return result
}
func OverrideWithHTTPHandler(existing, other http.Handler) (result http.Handler) {
if other != nil {
return other
}
return existing
}
func OverrideWithStringSlice(existing, other []string) (result []string) {
if other == nil {
return existing
}
result = make([]string, len(other))
copy(result, other)
return result
}
func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) {
if other == nil {
return existing
}
result = make([]uint16, len(other))
copy(result, other)
return result
}
func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) {
if other == nil {
return existing
}
result = make([]net.IPNet, len(other))
copy(result, other)
return result
}
func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) {
if other == nil {
return existing
}
result = make([]netaddr.IP, len(other))
copy(result, other)
return result
}
func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
if other == nil {
return existing
}
result = make([]netaddr.IPPrefix, len(other))
copy(result, other)
return result
}

View File

@@ -1,11 +0,0 @@
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,15 +0,0 @@
package helpers
func BoolPtrToYesNo(b *bool) string {
if *b {
return "yes"
}
return "no"
}
func TCPPtrToString(tcp *bool) string {
if *tcp {
return "TCP"
}
return "UDP"
}

View File

@@ -1,4 +0,0 @@
package settings
func boolPtr(b bool) *bool { return &b }
func uint8Ptr(n uint8) *uint8 { return &n }

View File

@@ -1,111 +0,0 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/govalid/address"
)
// HTTPProxy contains settings to configure the HTTP proxy.
type HTTPProxy struct {
// User is the username to use for the HTTP proxy.
// It cannot be nil in the internal state.
User *string
// Password is the password to use for the HTTP proxy.
// It cannot be nil in the internal state.
Password *string
// ListeningAddress is the listening address
// of the HTTP proxy server.
// It cannot be the empty string in the internal state.
ListeningAddress string
// Enabled is true if the HTTP proxy server should run,
// and false otherwise. It cannot be nil in the
// internal state.
Enabled *bool
// Stealth is true if the HTTP proxy server should hide
// each request has been proxied to the destination.
// It cannot be nil in the internal state.
Stealth *bool
// Log is true if the HTTP proxy server should log
// each request/response. It cannot be nil in the
// internal state.
Log *bool
}
func (h HTTPProxy) validate() (err error) {
// Do not validate user and password
uid := os.Getuid()
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
}
return nil
}
func (h *HTTPProxy) copy() (copied HTTPProxy) {
return HTTPProxy{
User: helpers.CopyStringPtr(h.User),
Password: helpers.CopyStringPtr(h.Password),
ListeningAddress: h.ListeningAddress,
Enabled: helpers.CopyBoolPtr(h.Enabled),
Stealth: helpers.CopyBoolPtr(h.Stealth),
Log: helpers.CopyBoolPtr(h.Log),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
h.User = helpers.MergeWithStringPtr(h.User, other.User)
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
h.Log = helpers.MergeWithBool(h.Log, other.Log)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
h.User = helpers.OverrideWithStringPtr(h.User, other.User)
h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password)
h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress)
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
h.Log = helpers.OverrideWithBool(h.Log, other.Log)
}
func (h *HTTPProxy) setDefaults() {
h.User = helpers.DefaultStringPtr(h.User, "")
h.Password = helpers.DefaultStringPtr(h.Password, "")
h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, ":8888")
h.Enabled = helpers.DefaultBool(h.Enabled, false)
h.Stealth = helpers.DefaultBool(h.Stealth, false)
h.Log = helpers.DefaultBool(h.Log, false)
}
func (h HTTPProxy) String() string {
return h.toLinesNode().String()
}
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
node = gotree.New("HTTP proxy settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled))
if !*h.Enabled {
return node
}
node.Appendf("Listening address: %s", h.ListeningAddress)
node.Appendf("User: %s", *h.User)
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
return node
}

View File

@@ -1,51 +0,0 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
)
// Log contains settings to configure the logger.
type Log struct {
// Level is the log level of the logger.
// It cannot be nil in the internal state.
Level *log.Level
}
func (l Log) validate() (err error) {
return nil
}
func (l *Log) copy() (copied Log) {
return Log{
Level: helpers.CopyLogLevelPtr(l.Level),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (l *Log) mergeWith(other Log) {
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (l *Log) overrideWith(other Log) {
l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
}
func (l *Log) setDefaults() {
l.Level = helpers.DefaultLogLevel(l.Level, log.LevelInfo)
}
func (l Log) String() string {
return l.toLinesNode().String()
}
func (l Log) toLinesNode() (node *gotree.Node) {
node = gotree.New("Log settings:")
node.Appendf("Log level: %s", l.Level.String())
return node
}

View File

@@ -1,349 +0,0 @@
package settings
import (
"fmt"
"regexp"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gotree"
)
// OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct {
// Version is the OpenVPN version to run.
// It can only be "2.4" or "2.5".
Version string
// User is the OpenVPN authentication username.
// It cannot be an empty string in the internal state
// if OpenVPN is used.
User string
// Password is the OpenVPN authentication password.
// It cannot be an empty string in the internal state
// if OpenVPN is used.
Password string
// ConfFile is a custom OpenVPN configuration file path.
// It can be set to the empty string for it to be ignored.
// It cannot be nil in the internal state.
ConfFile *string
// Ciphers is a list of ciphers to use for OpenVPN,
// different from the ones specified by the VPN
// service provider configuration files.
Ciphers []string
// Auth is an auth algorithm to use in OpenVPN instead
// of the one specified by the VPN service provider.
// It cannot be nil in the internal state.
// It is ignored if it is set to the empty string.
Auth *string
// ClientCrt is the OpenVPN client certificate.
// This is notably used by Cyberghost.
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
ClientCrt *string
// ClientKey is the OpenVPN client key.
// This is used by Cyberghost and VPN Unlimited.
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
ClientKey *string
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
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 option for OpenVPN. It is ignored if set to 0.
// It cannot be nil in the internal state.
MSSFix *uint16
// Interface is the OpenVPN device interface name.
// It cannot be an empty string in the internal state.
Interface string
// ProcessUser is the OpenVPN process OS username
// to use. It cannot be empty in the internal state.
// It defaults to 'root'.
ProcessUser string
// Verbosity is the OpenVPN verbosity level from 0 to 6.
// It cannot be nil in the internal state.
Verbosity *int
// Flags is a slice of additional flags to be passed
// to the OpenVPN program.
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) {
// Validate version
validVersions := []string{openvpn.Openvpn24, openvpn.Openvpn25}
if !helpers.IsOneOf(o.Version, validVersions...) {
return fmt.Errorf("%w: %q can only be one of %s",
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
}
isCustom := vpnProvider == providers.Custom
if !isCustom && o.User == "" {
return ErrOpenVPNUserIsEmpty
}
passwordRequired := !isCustom &&
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(o.User))
if passwordRequired && o.Password == "" {
return ErrOpenVPNPasswordIsEmpty
}
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
if err != nil {
return fmt.Errorf("custom configuration file: %w", err)
}
err = validateOpenVPNClientCertificate(vpnProvider, *o.ClientCrt)
if err != nil {
return fmt.Errorf("client certificate: %w", err)
}
err = validateOpenVPNClientKey(vpnProvider, *o.ClientKey)
if err != nil {
return fmt.Errorf("client key: %w", err)
}
const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
}
if !regexpInterfaceName.MatchString(o.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
}
if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
}
return nil
}
func validateOpenVPNConfigFilepath(isCustom bool,
confFile string) (err error) {
if !isCustom {
return nil
}
if confFile == "" {
return ErrFilepathMissing
}
err = helpers.FileExists(confFile)
if err != nil {
return err
}
extractor := extract.New()
_, _, err = extractor.Data(confFile)
if err != nil {
return fmt.Errorf("failed extracting information from custom configuration file: %w", err)
}
return nil
}
func validateOpenVPNClientCertificate(vpnProvider,
clientCert string) (err error) {
switch vpnProvider {
case
providers.Cyberghost,
providers.VPNUnlimited:
if clientCert == "" {
return ErrMissingValue
}
}
if clientCert == "" {
return nil
}
_, err = extract.PEM([]byte(clientCert))
if err != nil {
return err
}
return nil
}
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider {
case
providers.Cyberghost,
providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" {
return ErrMissingValue
}
}
if clientKey == "" {
return nil
}
_, err = extract.PEM([]byte(clientKey))
if err != nil {
return err
}
return nil
}
func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{
Version: o.Version,
User: o.User,
Password: o.Password,
ConfFile: helpers.CopyStringPtr(o.ConfFile),
Ciphers: helpers.CopyStringSlice(o.Ciphers),
Auth: helpers.CopyStringPtr(o.Auth),
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
ClientKey: helpers.CopyStringPtr(o.ClientKey),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
IPv6: helpers.CopyBoolPtr(o.IPv6),
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
Interface: o.Interface,
ProcessUser: o.ProcessUser,
Verbosity: helpers.CopyIntPtr(o.Verbosity),
Flags: helpers.CopyStringSlice(o.Flags),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (o *OpenVPN) mergeWith(other OpenVPN) {
o.Version = helpers.MergeWithString(o.Version, other.Version)
o.User = helpers.MergeWithString(o.User, other.User)
o.Password = helpers.MergeWithString(o.Password, other.Password)
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.MergeWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.MergeWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (o *OpenVPN) overrideWith(other OpenVPN) {
o.Version = helpers.OverrideWithString(o.Version, other.Version)
o.User = helpers.OverrideWithString(o.User, other.User)
o.Password = helpers.OverrideWithString(o.Password, other.Password)
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
o.Interface = helpers.OverrideWithString(o.Interface, other.Interface)
o.ProcessUser = helpers.OverrideWithString(o.ProcessUser, other.ProcessUser)
o.Verbosity = helpers.OverrideWithIntPtr(o.Verbosity, other.Verbosity)
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
}
func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = helpers.DefaultString(o.Version, openvpn.Openvpn25)
if vpnProvider == providers.Mullvad {
o.Password = "m"
}
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
o.IPv6 = helpers.DefaultBool(o.IPv6, false)
o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0)
o.Interface = helpers.DefaultString(o.Interface, "tun0")
o.ProcessUser = helpers.DefaultString(o.ProcessUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
}
func (o OpenVPN) String() string {
return o.toLinesNode().String()
}
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN settings:")
node.Appendf("OpenVPN version: %s", o.Version)
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile)
}
if len(o.Ciphers) > 0 {
node.Appendf("Ciphers: %s", o.Ciphers)
}
if *o.Auth != "" {
node.Appendf("Auth: %s", *o.Auth)
}
if *o.ClientCrt != "" {
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
}
if *o.ClientKey != "" {
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
}
if *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 {
node.Appendf("MSS Fix: %d", *o.MSSFix)
}
if o.Interface != "" {
node.Appendf("Network interface: %s", o.Interface)
}
node.Appendf("Run OpenVPN as: %s", o.ProcessUser)
node.Appendf("Verbosity level: %d", *o.Verbosity)
if len(o.Flags) > 0 {
node.Appendf("Flags: %s", o.Flags)
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (o OpenVPN) WithDefaults(provider string) OpenVPN {
o.setDefaults(provider)
return o
}

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

@@ -1,178 +0,0 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gotree"
)
type OpenVPNSelection struct {
// ConfFile is the custom configuration file path.
// It can be set to an empty string to indicate to
// NOT use a custom configuration file.
// It cannot be nil in the internal state.
ConfFile *string
// TCP is true if the OpenVPN protocol is TCP,
// and false for UDP.
// It cannot be nil in the internal state.
TCP *bool
// CustomPort is the OpenVPN server endpoint port.
// It can be set to 0 to indicate no custom port should
// be used. It cannot be nil in the internal state.
CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
PIAEncPreset *string
}
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate ConfFile
if confFile := *o.ConfFile; confFile != "" {
err := helpers.FileExists(confFile)
if err != nil {
return fmt.Errorf("configuration file: %w", err)
}
}
// Validate TCP
if *o.TCP && helpers.IsOneOf(vpnProvider,
providers.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.VPNUnlimited,
providers.Vyprvpn,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider)
}
// Validate CustomPort
if *o.CustomPort != 0 {
switch vpnProvider {
// no restriction on port
case providers.Cyberghost, providers.HideMyAss,
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNUnlimited,
providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
case providers.Ivpn:
allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050}
case providers.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case providers.PrivateInternetAccess:
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.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
}
if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) {
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
helpers.PortChoicesOrString(allowedTCP))
} else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) {
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
helpers.PortChoicesOrString(allowedUDP))
}
}
}
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) {
return fmt.Errorf("%w: %s; valid presets are %s",
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
helpers.ChoicesOrString(validEncryptionPresets))
}
}
return nil
}
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{
ConfFile: helpers.CopyStringPtr(o.ConfFile),
TCP: helpers.CopyBoolPtr(o.TCP),
CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
}
}
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP)
o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort)
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
o.TCP = helpers.DefaultBool(o.TCP, false)
o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0)
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
}
func (o OpenVPNSelection) String() string {
return o.toLinesNode().String()
}
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("OpenVPN server selection settings:")
node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP))
if *o.CustomPort != 0 {
node.Appendf("Custom port: %d", *o.CustomPort)
}
if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
}
if *o.ConfFile != "" {
node.Appendf("Custom configuration file: %s", *o.ConfFile)
}
return node
}

View File

@@ -1,89 +0,0 @@
package settings
import (
"fmt"
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gotree"
)
// PortForwarding contains settings for port forwarding.
type PortForwarding struct {
// Enabled is true if port forwarding should be activated.
// It cannot be nil for the internal state.
Enabled *bool
// Filepath is the port forwarding status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
// internal state
Filepath *string
}
func (p PortForwarding) validate(vpnProvider string) (err error) {
if !*p.Enabled {
return nil
}
// Validate Enabled
validProviders := []string{providers.PrivateInternetAccess}
if !helpers.IsOneOf(vpnProvider, validProviders...) {
return fmt.Errorf("%w: for provider %s, it is only available for %s",
ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", "))
}
// Validate Filepath
if *p.Filepath != "" { // optional
_, err := filepath.Abs(*p.Filepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
return nil
}
func (p *PortForwarding) copy() (copied PortForwarding) {
return PortForwarding{
Enabled: helpers.CopyBoolPtr(p.Enabled),
Filepath: helpers.CopyStringPtr(p.Filepath),
}
}
func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
}
func (p *PortForwarding) overrideWith(other PortForwarding) {
p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
}
func (p *PortForwarding) setDefaults() {
p.Enabled = helpers.DefaultBool(p.Enabled, false)
p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port")
}
func (p PortForwarding) String() string {
return p.toLinesNode().String()
}
func (p PortForwarding) toLinesNode() (node *gotree.Node) {
if !*p.Enabled {
return nil
}
node = gotree.New("Automatic port forwarding settings:")
node.Appendf("Enabled: yes")
filepath := *p.Filepath
if filepath == "" {
filepath = "[not set]"
}
node.Appendf("Forwarded port file path: %s", filepath)
return node
}

View File

@@ -1,19 +0,0 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PortForwarding_String(t *testing.T) {
t.Parallel()
settings := PortForwarding{
Enabled: boolPtr(false),
}
s := settings.String()
assert.Empty(t, s)
}

View File

@@ -1,93 +0,0 @@
package settings
import (
"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/gotree"
)
// Provider contains settings specific to a VPN provider.
type Provider struct {
// Name is the VPN service provider name.
// It cannot be nil in the internal state.
Name *string
// ServerSelection is the settings to
// select the VPN server.
ServerSelection ServerSelection
// PortForwarding is the settings about port forwarding.
PortForwarding PortForwarding
}
// TODO v4 remove pointer for receiver (because of Surfshark).
func (p *Provider) validate(vpnType string, storage Storage) (err error) {
// Validate Name
var validNames []string
if vpnType == vpn.OpenVPN {
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard
validNames = []string{
providers.Custom,
providers.Ivpn,
providers.Mullvad,
providers.Windscribe,
}
}
if !helpers.IsOneOf(*p.Name, validNames...) {
return fmt.Errorf("%w: %q can only be one of %s",
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
}
err = p.ServerSelection.validate(*p.Name, storage)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.validate(*p.Name)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func (p *Provider) copy() (copied Provider) {
return Provider{
Name: helpers.CopyStringPtr(p.Name),
ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.copy(),
}
}
func (p *Provider) mergeWith(other Provider) {
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
p.ServerSelection.mergeWith(other.ServerSelection)
p.PortForwarding.mergeWith(other.PortForwarding)
}
func (p *Provider) overrideWith(other Provider) {
p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.overrideWith(other.PortForwarding)
}
func (p *Provider) setDefaults() {
p.Name = helpers.DefaultStringPtr(p.Name, providers.PrivateInternetAccess)
p.ServerSelection.setDefaults(*p.Name)
p.PortForwarding.setDefaults()
}
func (p Provider) String() string {
return p.toLinesNode().String()
}
func (p Provider) toLinesNode() (node *gotree.Node) {
node = gotree.New("VPN provider settings:")
node.Appendf("Name: %s", *p.Name)
node.AppendNode(p.ServerSelection.toLinesNode())
node.AppendNode(p.PortForwarding.toLinesNode())
return node
}

View File

@@ -1,89 +0,0 @@
package settings
import (
"fmt"
"path/filepath"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// PublicIP contains settings for port forwarding.
type PublicIP struct {
// Period is the period to get the public IP address.
// It can be set to 0 to disable periodic checking.
// It cannot be nil for the internal state.
// TODO change to value and add enabled field
Period *time.Duration
// IPFilepath is the public IP address status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
// internal state
IPFilepath *string
}
func (p PublicIP) validate() (err error) {
const minPeriod = 5 * time.Second
if *p.Period < minPeriod {
return fmt.Errorf("%w: %s must be at least %s",
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
}
if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
return nil
}
func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{
Period: helpers.CopyDurationPtr(p.Period),
IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
}
}
func (p *PublicIP) mergeWith(other PublicIP) {
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) overrideWith(other PublicIP) {
p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
}
func (p *PublicIP) setDefaults() {
const defaultPeriod = 12 * time.Hour
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
}
func (p PublicIP) String() string {
return p.toLinesNode().String()
}
func (p PublicIP) toLinesNode() (node *gotree.Node) {
node = gotree.New("Public IP settings:")
if *p.Period == 0 {
node.Appendf("Enabled: no")
return node
}
updatePeriod := "disabled"
if *p.Period > 0 {
updatePeriod = "every " + p.Period.String()
}
node.Appendf("Fetching: %s", updatePeriod)
if *p.IPFilepath != "" {
node.Appendf("IP file path: %s", *p.IPFilepath)
}
return node
}

View File

@@ -1,80 +0,0 @@
package settings
import (
"fmt"
"net"
"os"
"strconv"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// ControlServer contains settings to customize the control server operation.
type ControlServer struct {
// Address is the listening address to use.
// It cannot be nil in the internal state.
Address *string
// Log can be true or false to enable logging on requests.
// It cannot be nil in the internal state.
Log *bool
}
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()
const maxPrivilegedPort = 1023
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, port, uid)
}
return nil
}
func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{
Address: helpers.CopyStringPtr(c.Address),
Log: helpers.CopyBoolPtr(c.Log),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (c *ControlServer) mergeWith(other ControlServer) {
c.Address = helpers.MergeWithStringPtr(c.Address, other.Address)
c.Log = helpers.MergeWithBool(c.Log, other.Log)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = helpers.OverrideWithStringPtr(c.Address, other.Address)
c.Log = helpers.OverrideWithBool(c.Log, other.Log)
}
func (c *ControlServer) setDefaults() {
c.Address = helpers.DefaultStringPtr(c.Address, ":8000")
c.Log = helpers.DefaultBool(c.Log, true)
}
func (c ControlServer) String() string {
return c.toLinesNode().String()
}
func (c ControlServer) toLinesNode() (node *gotree.Node) {
node = gotree.New("Control server settings:")
node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
return node
}

View File

@@ -1,324 +0,0 @@
package settings
import (
"errors"
"fmt"
"net"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/configuration/settings/validation"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gotree"
)
type ServerSelection struct { //nolint:maligned
// VPN is the VPN type which can be 'openvpn'
// or 'wireguard'. It cannot be the empty string
// in the internal state.
VPN string
// TargetIP is the server endpoint IP address to use.
// It will override any IP address from the picked
// built-in server. It cannot be nil in the internal
// state, and can be set to an empty net.IP{} to indicate
// there is not target IP address to use.
TargetIP net.IP
// Counties is the list of countries to filter VPN servers with.
Countries []string
// Regions is the list of regions to filter VPN servers with.
Regions []string
// Cities is the list of cities to filter VPN servers with.
Cities []string
// ISPs is the list of ISP names to filter VPN servers with.
ISPs []string
// Names is the list of server names to filter VPN servers with.
Names []string
// Numbers is the list of server numbers to filter VPN servers with.
Numbers []uint16
// Hostnames is the list of hostnames to filter VPN servers with.
Hostnames []string
// OwnedOnly is true if VPN provider servers that are not owned
// should be filtered. This is used with Mullvad.
OwnedOnly *bool
// FreeOnly is true if VPN servers that are not free should
// be filtered. This is used with ProtonVPN and VPN Unlimited.
FreeOnly *bool
// StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with VPNUnlimited.
StreamOnly *bool
// MultiHopOnly is true if VPN servers that are not multihop
// should be filtered. This is used with Surfshark.
MultiHopOnly *bool
// OpenVPN contains settings to select OpenVPN servers
// and the final connection.
OpenVPN OpenVPNSelection
// Wireguard contains settings to select Wireguard servers
// and the final connection.
Wireguard WireguardSelection
}
var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
)
func (ss *ServerSelection) validate(vpnServiceProvider string,
storage Storage) (err error) {
switch ss.VPN {
case vpn.OpenVPN, vpn.Wireguard:
default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
}
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
if err != nil {
return err // already wrapped error
}
err = validateServerFilters(*ss, filterChoices)
if err != nil {
if errors.Is(err, helpers.ErrNoChoice) {
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}
return err // already wrapped error
}
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.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.VPN == vpn.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("OpenVPN server selection settings: %w", err)
}
} else {
err = ss.Wireguard.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("Wireguard server selection settings: %w", err)
}
}
return nil
}
func getLocationFilterChoices(vpnServiceProvider string,
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
err error) {
filterChoices = storage.GetFilterChoices(vpnServiceProvider)
if vpnServiceProvider == providers.Surfshark {
// // Retro compatibility
// TODO v4 remove
filterChoices.Regions = append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...)
if err := helpers.AreAllOneOf(ss.Regions, filterChoices.Regions); err != nil {
return models.FilterChoices{}, fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
*ss = surfsharkRetroRegion(*ss)
}
return filterChoices, nil
}
// 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) {
if err := helpers.AreAllOneOf(settings.Countries, filterChoices.Countries); err != nil {
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Regions, filterChoices.Regions); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Cities, filterChoices.Cities); err != nil {
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
}
if err := helpers.AreAllOneOf(settings.ISPs, filterChoices.ISPs); err != nil {
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Hostnames, filterChoices.Hostnames); err != nil {
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
}
if err := helpers.AreAllOneOf(settings.Names, filterChoices.Names); err != nil {
return fmt.Errorf("%w: %s", ErrNameNotValid, err)
}
return nil
}
func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{
VPN: ss.VPN,
TargetIP: helpers.CopyIP(ss.TargetIP),
Countries: helpers.CopyStringSlice(ss.Countries),
Regions: helpers.CopyStringSlice(ss.Regions),
Cities: helpers.CopyStringSlice(ss.Cities),
ISPs: helpers.CopyStringSlice(ss.ISPs),
Hostnames: helpers.CopyStringSlice(ss.Hostnames),
Names: helpers.CopyStringSlice(ss.Names),
Numbers: helpers.CopyUint16Slice(ss.Numbers),
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Wireguard: ss.Wireguard.copy(),
}
}
func (ss *ServerSelection) mergeWith(other ServerSelection) {
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
ss.Names = helpers.MergeStringSlices(ss.Names, other.Names)
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.mergeWith(other.OpenVPN)
ss.Wireguard.mergeWith(other.Wireguard)
}
func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
ss.Names = helpers.OverrideWithStringSlice(ss.Names, other.Names)
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
ss.OpenVPN.overrideWith(other.OpenVPN)
ss.Wireguard.overrideWith(other.Wireguard)
}
func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.VPN = helpers.DefaultString(ss.VPN, vpn.OpenVPN)
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
ss.OpenVPN.setDefaults(vpnProvider)
ss.Wireguard.setDefaults()
}
func (ss ServerSelection) String() string {
return ss.toLinesNode().String()
}
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Server selection settings:")
node.Appendf("VPN type: %s", ss.VPN)
if len(ss.TargetIP) > 0 {
node.Appendf("Target IP address: %s", ss.TargetIP)
}
if len(ss.Countries) > 0 {
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
}
if len(ss.Regions) > 0 {
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
}
if len(ss.Cities) > 0 {
node.Appendf("Cities: %s", strings.Join(ss.Cities, ", "))
}
if len(ss.ISPs) > 0 {
node.Appendf("ISPs: %s", strings.Join(ss.ISPs, ", "))
}
if len(ss.Names) > 0 {
node.Appendf("Server names: %s", strings.Join(ss.Names, ", "))
}
if len(ss.Numbers) > 0 {
numbersNode := node.Appendf("Server numbers:")
for _, number := range ss.Numbers {
numbersNode.Appendf("%d", number)
}
}
if len(ss.Hostnames) > 0 {
node.Appendf("Hostnames: %s", strings.Join(ss.Hostnames, ", "))
}
if *ss.OwnedOnly {
node.Appendf("Owned only servers: yes")
}
if *ss.FreeOnly {
node.Appendf("Free only servers: yes")
}
if *ss.StreamOnly {
node.Appendf("Stream only servers: yes")
}
if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes")
}
if ss.VPN == vpn.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode())
} else {
node.AppendNode(ss.Wireguard.toLinesNode())
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
ss.setDefaults(provider)
return ss
}

View File

@@ -1,159 +0,0 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gotree"
)
type Settings struct {
ControlServer ControlServer
DNS DNS
Firewall Firewall
Health Health
HTTPProxy HTTPProxy
Log Log
PublicIP PublicIP
Shadowsocks Shadowsocks
System System
Updater Updater
Version Version
VPN VPN
Pprof pprof.Settings
}
type Storage interface {
GetFilterChoices(provider string) models.FilterChoices
}
// Validate validates all the settings and returns an error
// if one of them is not valid.
// TODO v4 remove pointer for receiver (because of Surfshark).
func (s *Settings) Validate(storage Storage) (err error) {
nameToValidation := map[string]func() error{
"control server": s.ControlServer.validate,
"dns": s.DNS.validate,
"firewall": s.Firewall.validate,
"health": s.Health.Validate,
"http proxy": s.HTTPProxy.validate,
"log": s.Log.validate,
"public ip check": s.PublicIP.validate,
"shadowsocks": s.Shadowsocks.validate,
"system": s.System.validate,
"updater": s.Updater.Validate,
"version": s.Version.validate,
// Pprof validation done in pprof constructor
"VPN": func() error {
return s.VPN.validate(storage)
},
}
for name, validation := range nameToValidation {
err = validation()
if err != nil {
return fmt.Errorf("%s settings: %w", name, err)
}
}
return nil
}
func (s *Settings) copy() (copied Settings) {
return Settings{
ControlServer: s.ControlServer.copy(),
DNS: s.DNS.Copy(),
Firewall: s.Firewall.copy(),
Health: s.Health.copy(),
HTTPProxy: s.HTTPProxy.copy(),
Log: s.Log.copy(),
PublicIP: s.PublicIP.copy(),
Shadowsocks: s.Shadowsocks.copy(),
System: s.System.copy(),
Updater: s.Updater.copy(),
Version: s.Version.copy(),
VPN: s.VPN.copy(),
Pprof: s.Pprof.Copy(),
}
}
func (s *Settings) MergeWith(other Settings) {
s.ControlServer.mergeWith(other.ControlServer)
s.DNS.mergeWith(other.DNS)
s.Firewall.mergeWith(other.Firewall)
s.Health.MergeWith(other.Health)
s.HTTPProxy.mergeWith(other.HTTPProxy)
s.Log.mergeWith(other.Log)
s.PublicIP.mergeWith(other.PublicIP)
s.Shadowsocks.mergeWith(other.Shadowsocks)
s.System.mergeWith(other.System)
s.Updater.mergeWith(other.Updater)
s.Version.mergeWith(other.Version)
s.VPN.mergeWith(other.VPN)
s.Pprof.MergeWith(other.Pprof)
}
func (s *Settings) OverrideWith(other Settings,
storage Storage) (err error) {
patchedSettings := s.copy()
patchedSettings.ControlServer.overrideWith(other.ControlServer)
patchedSettings.DNS.overrideWith(other.DNS)
patchedSettings.Firewall.overrideWith(other.Firewall)
patchedSettings.Health.OverrideWith(other.Health)
patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy)
patchedSettings.Log.overrideWith(other.Log)
patchedSettings.PublicIP.overrideWith(other.PublicIP)
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
patchedSettings.System.overrideWith(other.System)
patchedSettings.Updater.overrideWith(other.Updater)
patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.overrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof)
err = patchedSettings.Validate(storage)
if err != nil {
return err
}
*s = patchedSettings
return nil
}
func (s *Settings) SetDefaults() {
s.ControlServer.setDefaults()
s.DNS.setDefaults()
s.Firewall.setDefaults()
s.Health.SetDefaults()
s.HTTPProxy.setDefaults()
s.Log.setDefaults()
s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults()
s.System.setDefaults()
s.Version.setDefaults()
s.VPN.setDefaults()
s.Updater.SetDefaults(*s.VPN.Provider.Name)
s.Pprof.SetDefaults()
}
func (s Settings) String() string {
return s.toLinesNode().String()
}
func (s Settings) toLinesNode() (node *gotree.Node) {
node = gotree.New("Settings summary:")
node.AppendNode(s.VPN.toLinesNode())
node.AppendNode(s.DNS.toLinesNode())
node.AppendNode(s.Firewall.toLinesNode())
node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode())
node.AppendNode(s.ControlServer.toLinesNode())
node.AppendNode(s.System.toLinesNode())
node.AppendNode(s.PublicIP.toLinesNode())
node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode())
return node
}

View File

@@ -1,101 +0,0 @@
package settings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Settings_String(t *testing.T) {
t.Parallel()
withDefaults := func(s Settings) Settings {
s.SetDefaults()
return s
}
testCases := map[string]struct {
settings Settings
s string
}{
"default settings": {
settings: withDefaults(Settings{}),
s: `Settings summary:
├── VPN settings:
| ├── VPN provider settings:
| | ├── Name: private internet access
| | └── Server selection settings:
| | ├── VPN type: openvpn
| | └── OpenVPN server selection settings:
| | ├── Protocol: UDP
| | └── Private Internet Access encryption preset: strong
| └── OpenVPN settings:
| ├── OpenVPN version: 2.5
| ├── User: [not set]
| ├── Password: [not set]
| ├── Private Internet Access encryption preset: strong
| ├── Tunnel IPv6: no
| ├── Network interface: tun0
| ├── Run OpenVPN as: root
| └── Verbosity level: 1
├── DNS settings:
| ├── DNS server address to use: 127.0.0.1
| ├── Keep existing nameserver(s): no
| └── DNS over TLS settings:
| ├── Enabled: yes
| ├── Update period: every 24h0m0s
| ├── Unbound settings:
| | ├── Authoritative servers:
| | | └── Cloudflare
| | ├── Caching: yes
| | ├── IPv6: no
| | ├── Verbosity level: 1
| | ├── Verbosity details level: 0
| | ├── Validation log level: 0
| | ├── System user: root
| | └── Allowed networks:
| | ├── 0.0.0.0/0
| | └── ::/0
| └── DNS filtering settings:
| ├── Block malicious: yes
| ├── Block ads: no
| └── Block surveillance: yes
├── Firewall settings:
| └── Enabled: yes
├── Log settings:
| └── Log level: INFO
├── Health settings:
| ├── Server listening address: 127.0.0.1:9999
| ├── Target address: cloudflare.com:443
| └── VPN wait durations:
| ├── Initial duration: 6s
| └── Additional duration: 5s
├── Shadowsocks server settings:
| └── Enabled: no
├── HTTP proxy settings:
| └── Enabled: no
├── Control server settings:
| ├── Listening address: :8000
| └── Logging: yes
├── OS Alpine settings:
| ├── Process UID: 1000
| └── Process GID: 1000
├── Public IP settings:
| ├── Fetching: every 12h0m0s
| └── IP file path: /tmp/gluetun/ip
└── Version settings:
└── Enabled: yes`,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
s := testCase.settings.String()
assert.Equal(t, testCase.s, s)
})
}
}

View File

@@ -1,68 +0,0 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"github.com/qdm12/ss-server/pkg/tcpudp"
)
// Shadowsocks contains settings to configure the Shadowsocks server.
type Shadowsocks struct {
// Enabled is true if the server should be running.
// It defaults to false, and cannot be nil in the internal state.
Enabled *bool
// Settings are settings for the TCP+UDP server.
tcpudp.Settings
}
func (s Shadowsocks) validate() (err error) {
return s.Settings.Validate()
}
func (s *Shadowsocks) copy() (copied Shadowsocks) {
return Shadowsocks{
Enabled: helpers.CopyBoolPtr(s.Enabled),
Settings: s.Settings.Copy(),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (s *Shadowsocks) mergeWith(other Shadowsocks) {
s.Enabled = helpers.MergeWithBool(s.Enabled, other.Enabled)
s.Settings.MergeWith(other.Settings)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (s *Shadowsocks) overrideWith(other Shadowsocks) {
s.Enabled = helpers.OverrideWithBool(s.Enabled, other.Enabled)
s.Settings.OverrideWith(other.Settings)
}
func (s *Shadowsocks) setDefaults() {
s.Enabled = helpers.DefaultBool(s.Enabled, false)
s.Settings.SetDefaults()
}
func (s Shadowsocks) String() string {
return s.toLinesNode().String()
}
func (s Shadowsocks) toLinesNode() (node *gotree.Node) {
node = gotree.New("Shadowsocks server settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(s.Enabled))
if !*s.Enabled {
return node
}
// TODO have ToLinesNode in qdm12/ss-server
node.Appendf("Listening address: %s", s.Address)
node.Appendf("Cipher: %s", s.CipherName)
node.Appendf("Password: %s", helpers.ObfuscatePassword(*s.Password))
node.Appendf("Log addresses: %s", helpers.BoolPtrToYesNo(s.LogAddresses))
return node
}

View File

@@ -1,55 +0,0 @@
package settings
import (
"strings"
"github.com/qdm12/gluetun/internal/provider/surfshark/servers"
)
func surfsharkRetroRegion(selection ServerSelection) (
updatedSelection ServerSelection) {
locationData := servers.LocationData()
retroToLocation := make(map[string]servers.ServerLocation, len(locationData))
for _, data := range locationData {
if data.RetroLoc == "" {
continue
}
retroToLocation[strings.ToLower(data.RetroLoc)] = data
}
for i, region := range selection.Regions {
location, ok := retroToLocation[region]
if !ok {
continue
}
selection.Regions[i] = strings.ToLower(location.Region)
selection.Countries = append(selection.Countries, strings.ToLower(location.Country))
selection.Cities = append(selection.Cities, strings.ToLower(location.City)) // even empty string
selection.Hostnames = append(selection.Hostnames, location.Hostname)
}
selection.Regions = dedupSlice(selection.Regions)
selection.Countries = dedupSlice(selection.Countries)
selection.Cities = dedupSlice(selection.Cities)
selection.Hostnames = dedupSlice(selection.Hostnames)
return selection
}
func dedupSlice(slice []string) (deduped []string) {
if slice == nil {
return nil
}
deduped = make([]string, 0, len(slice))
seen := make(map[string]struct{}, len(slice))
for _, s := range slice {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
deduped = append(deduped, s)
}
}
return deduped
}

View File

@@ -1,61 +0,0 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// System contains settings to configure system related elements.
type System struct {
PUID *uint32
PGID *uint32
Timezone string
}
// Validate validates System settings.
func (s System) validate() (err error) {
return nil
}
func (s *System) copy() (copied System) {
return System{
PUID: helpers.CopyUint32Ptr(s.PUID),
PGID: helpers.CopyUint32Ptr(s.PGID),
Timezone: s.Timezone,
}
}
func (s *System) mergeWith(other System) {
s.PUID = helpers.MergeWithUint32(s.PUID, other.PUID)
s.PGID = helpers.MergeWithUint32(s.PGID, other.PGID)
s.Timezone = helpers.MergeWithString(s.Timezone, other.Timezone)
}
func (s *System) overrideWith(other System) {
s.PUID = helpers.OverrideWithUint32(s.PUID, other.PUID)
s.PGID = helpers.OverrideWithUint32(s.PGID, other.PGID)
s.Timezone = helpers.OverrideWithString(s.Timezone, other.Timezone)
}
func (s *System) setDefaults() {
const defaultID = 1000
s.PUID = helpers.DefaultUint32(s.PUID, defaultID)
s.PGID = helpers.DefaultUint32(s.PGID, defaultID)
}
func (s System) String() string {
return s.toLinesNode().String()
}
func (s System) toLinesNode() (node *gotree.Node) {
node = gotree.New("OS Alpine settings:")
node.Appendf("Process UID: %d", *s.PUID)
node.Appendf("Process GID: %d", *s.PGID)
if s.Timezone != "" {
node.Appendf("Timezone: %s", s.Timezone)
}
return node
}

View File

@@ -1,193 +0,0 @@
package settings
import (
"errors"
"fmt"
"net"
"github.com/qdm12/dns/pkg/provider"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
"inet.af/netaddr"
)
// Unbound is settings for the Unbound program.
type Unbound struct {
Providers []string
Caching *bool
IPv6 *bool
VerbosityLevel *uint8
VerbosityDetailsLevel *uint8
ValidationLogLevel *uint8
Username string
Allowed []netaddr.IPPrefix
}
func (u *Unbound) setDefaults() {
if len(u.Providers) == 0 {
u.Providers = []string{
provider.Cloudflare().String(),
}
}
u.Caching = helpers.DefaultBool(u.Caching, true)
u.IPv6 = helpers.DefaultBool(u.IPv6, false)
const defaultVerbosityLevel = 1
u.VerbosityLevel = helpers.DefaultUint8(u.VerbosityLevel, defaultVerbosityLevel)
const defaultVerbosityDetailsLevel = 0
u.VerbosityDetailsLevel = helpers.DefaultUint8(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
const defaultValidationLogLevel = 0
u.ValidationLogLevel = helpers.DefaultUint8(u.ValidationLogLevel, defaultValidationLogLevel)
if u.Allowed == nil {
u.Allowed = []netaddr.IPPrefix{
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
}
}
u.Username = helpers.DefaultString(u.Username, "root")
}
var (
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
)
func (u Unbound) validate() (err error) {
for _, s := range u.Providers {
_, err := provider.Parse(s)
if err != nil {
return err
}
}
const maxVerbosityLevel = 5
if *u.VerbosityLevel > maxVerbosityLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundVerbosityLevelNotValid,
*u.VerbosityLevel,
maxVerbosityLevel)
}
const maxVerbosityDetailsLevel = 4
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundVerbosityDetailsLevelNotValid,
*u.VerbosityDetailsLevel,
maxVerbosityDetailsLevel)
}
const maxValidationLogLevel = 2
if *u.ValidationLogLevel > maxValidationLogLevel {
return fmt.Errorf("%w: %d must be between 0 and %d",
ErrUnboundValidationLogLevelNotValid,
*u.ValidationLogLevel, maxValidationLogLevel)
}
return nil
}
func (u Unbound) copy() (copied Unbound) {
return Unbound{
Providers: helpers.CopyStringSlice(u.Providers),
Caching: helpers.CopyBoolPtr(u.Caching),
IPv6: helpers.CopyBoolPtr(u.IPv6),
VerbosityLevel: helpers.CopyUint8Ptr(u.VerbosityLevel),
VerbosityDetailsLevel: helpers.CopyUint8Ptr(u.VerbosityDetailsLevel),
ValidationLogLevel: helpers.CopyUint8Ptr(u.ValidationLogLevel),
Username: u.Username,
Allowed: helpers.CopyIPPrefixSlice(u.Allowed),
}
}
func (u *Unbound) mergeWith(other Unbound) {
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
u.Caching = helpers.MergeWithBool(u.Caching, other.Caching)
u.IPv6 = helpers.MergeWithBool(u.IPv6, other.IPv6)
u.VerbosityLevel = helpers.MergeWithUint8(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = helpers.MergeWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = helpers.MergeWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = helpers.MergeWithString(u.Username, other.Username)
u.Allowed = helpers.MergeIPPrefixesSlices(u.Allowed, other.Allowed)
}
func (u *Unbound) overrideWith(other Unbound) {
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
u.Caching = helpers.OverrideWithBool(u.Caching, other.Caching)
u.IPv6 = helpers.OverrideWithBool(u.IPv6, other.IPv6)
u.VerbosityLevel = helpers.OverrideWithUint8(u.VerbosityLevel, other.VerbosityLevel)
u.VerbosityDetailsLevel = helpers.OverrideWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
u.ValidationLogLevel = helpers.OverrideWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
u.Username = helpers.OverrideWithString(u.Username, other.Username)
u.Allowed = helpers.OverrideWithIPPrefixesSlice(u.Allowed, other.Allowed)
}
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
providers := make([]provider.Provider, len(u.Providers))
for i := range providers {
providers[i], err = provider.Parse(u.Providers[i])
if err != nil {
return settings, err
}
}
const port = 53
return unbound.Settings{
ListeningPort: port,
IPv4: true,
Providers: providers,
Caching: *u.Caching,
IPv6: *u.IPv6,
VerbosityLevel: *u.VerbosityLevel,
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
ValidationLogLevel: *u.ValidationLogLevel,
AccessControl: unbound.AccessControlSettings{
Allowed: u.Allowed,
},
Username: u.Username,
}, nil
}
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 net.IP, err error) {
s := u.Providers[0]
provider, err := provider.Parse(s)
if err != nil {
return nil, err
}
return provider.DNS().IPv4[0], nil
}
func (u Unbound) String() string {
return u.toLinesNode().String()
}
func (u Unbound) toLinesNode() (node *gotree.Node) {
node = gotree.New("Unbound settings:")
authServers := node.Appendf("Authoritative servers:")
for _, provider := range u.Providers {
authServers.Appendf(provider)
}
node.Appendf("Caching: %s", helpers.BoolPtrToYesNo(u.Caching))
node.Appendf("IPv6: %s", helpers.BoolPtrToYesNo(u.IPv6))
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
node.Appendf("System user: %s", u.Username)
allowedNetworks := node.Appendf("Allowed networks:")
for _, network := range u.Allowed {
allowedNetworks.Appendf(network.String())
}
return node
}

View File

@@ -1,43 +0,0 @@
package settings
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"inet.af/netaddr"
)
func Test_Unbound_JSON(t *testing.T) {
t.Parallel()
settings := Unbound{
Providers: []string{"cloudflare"},
Caching: boolPtr(true),
IPv6: boolPtr(false),
VerbosityLevel: uint8Ptr(1),
VerbosityDetailsLevel: nil,
ValidationLogLevel: uint8Ptr(0),
Username: "user",
Allowed: []netaddr.IPPrefix{
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
},
}
b, err := json.Marshal(settings)
require.NoError(t, err)
const expected = `{"Providers":["cloudflare"],"Caching":true,"IPv6":false,` +
`"VerbosityLevel":1,"VerbosityDetailsLevel":null,"ValidationLogLevel":0,` +
`"Username":"user","Allowed":["0.0.0.0/0","::/0"]}`
assert.Equal(t, expected, string(b))
var resultSettings Unbound
err = json.Unmarshal(b, &resultSettings)
require.NoError(t, err)
assert.Equal(t, settings, resultSettings)
}

View File

@@ -1,122 +0,0 @@
package settings
import (
"fmt"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gotree"
)
// Updater contains settings to configure the VPN
// server information updater.
type Updater struct {
// Period is the period for which the updater
// should run. It can be set to 0 to disable the
// updater. It cannot be nil in the internal state.
// TODO change to value and add Enabled field.
Period *time.Duration
// DNSAddress is the DNS server address to use
// to resolve VPN server hostnames to IP addresses.
// It cannot be the empty string in the internal state.
DNSAddress string
// 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
// to update server information for.
Providers []string
}
func (u Updater) Validate() (err error) {
const minPeriod = time.Minute
if *u.Period > 0 && *u.Period < minPeriod {
return fmt.Errorf("%w: %d must be larger than %s",
ErrUpdaterPeriodTooSmall, *u.Period, minPeriod)
}
if u.MinRatio <= 0 || u.MinRatio > 1 {
return fmt.Errorf("%w: %.2f must be between 0+ and 1",
ErrMinRatioNotValid, u.MinRatio)
}
validProviders := providers.All()
for _, provider := range u.Providers {
valid := false
for _, validProvider := range validProviders {
if provider == validProvider {
valid = true
break
}
}
if !valid {
return fmt.Errorf("%w: %q can only be one of %s",
ErrVPNProviderNameNotValid, provider, helpers.ChoicesOrString(validProviders))
}
}
return nil
}
func (u *Updater) copy() (copied Updater) {
return Updater{
Period: helpers.CopyDurationPtr(u.Period),
DNSAddress: u.DNSAddress,
MinRatio: u.MinRatio,
Providers: helpers.CopyStringSlice(u.Providers),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (u *Updater) mergeWith(other Updater) {
u.Period = helpers.MergeWithDuration(u.Period, other.Period)
u.DNSAddress = helpers.MergeWithString(u.DNSAddress, other.DNSAddress)
u.MinRatio = helpers.MergeWithFloat64(u.MinRatio, other.MinRatio)
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (u *Updater) overrideWith(other Updater) {
u.Period = helpers.OverrideWithDuration(u.Period, other.Period)
u.DNSAddress = helpers.OverrideWithString(u.DNSAddress, other.DNSAddress)
u.MinRatio = helpers.OverrideWithFloat64(u.MinRatio, other.MinRatio)
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
}
func (u *Updater) SetDefaults(vpnProvider string) {
u.Period = helpers.DefaultDuration(u.Period, 0)
u.DNSAddress = helpers.DefaultString(u.DNSAddress, "1.1.1.1:53")
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 {
return u.toLinesNode().String()
}
func (u Updater) toLinesNode() (node *gotree.Node) {
if *u.Period == 0 || len(u.Providers) == 0 {
return nil
}
node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period)
node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
return node
}

View File

@@ -1,129 +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 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,21 +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 _, ok := seen[data.RetroLoc]; ok {
continue
}
seen[data.RetroLoc] = struct{}{}
choices = sortedInsert(choices, data.RetroLoc)
}
return choices
}

View File

@@ -1,53 +0,0 @@
package settings
import (
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// Version contains settings to configure the version
// information fetcher.
type Version struct {
// Enabled is true if the version information should
// be fetched from Github.
Enabled *bool
}
func (v Version) validate() (err error) {
return nil
}
func (v *Version) copy() (copied Version) {
return Version{
Enabled: helpers.CopyBoolPtr(v.Enabled),
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (v *Version) mergeWith(other Version) {
v.Enabled = helpers.MergeWithBool(v.Enabled, other.Enabled)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (v *Version) overrideWith(other Version) {
v.Enabled = helpers.OverrideWithBool(v.Enabled, other.Enabled)
}
func (v *Version) setDefaults() {
v.Enabled = helpers.DefaultBool(v.Enabled, true)
}
func (v Version) String() string {
return v.toLinesNode().String()
}
func (v Version) toLinesNode() (node *gotree.Node) {
node = gotree.New("Version settings:")
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(v.Enabled))
return node
}

View File

@@ -1,97 +0,0 @@
package settings
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gotree"
)
type VPN struct {
// Type is the VPN type and can only be
// 'openvpn' or 'wireguard'. It cannot be the
// empty string in the internal state.
Type string
Provider Provider
OpenVPN OpenVPN
Wireguard Wireguard
}
// TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) validate(storage Storage) (err error) {
// Validate Type
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
if !helpers.IsOneOf(v.Type, validVPNTypes...) {
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)
if err != nil {
return fmt.Errorf("provider settings: %w", err)
}
if v.Type == vpn.OpenVPN {
err := v.OpenVPN.validate(*v.Provider.Name)
if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err)
}
} else {
err := v.Wireguard.validate(*v.Provider.Name)
if err != nil {
return fmt.Errorf("Wireguard settings: %w", err)
}
}
return nil
}
func (v *VPN) copy() (copied VPN) {
return VPN{
Type: v.Type,
Provider: v.Provider.copy(),
OpenVPN: v.OpenVPN.copy(),
Wireguard: v.Wireguard.copy(),
}
}
func (v *VPN) mergeWith(other VPN) {
v.Type = helpers.MergeWithString(v.Type, other.Type)
v.Provider.mergeWith(other.Provider)
v.OpenVPN.mergeWith(other.OpenVPN)
v.Wireguard.mergeWith(other.Wireguard)
}
func (v *VPN) overrideWith(other VPN) {
v.Type = helpers.OverrideWithString(v.Type, other.Type)
v.Provider.overrideWith(other.Provider)
v.OpenVPN.overrideWith(other.OpenVPN)
v.Wireguard.overrideWith(other.Wireguard)
}
func (v *VPN) setDefaults() {
v.Type = helpers.DefaultString(v.Type, vpn.OpenVPN)
v.Provider.setDefaults()
v.OpenVPN.setDefaults(*v.Provider.Name)
v.Wireguard.setDefaults()
}
func (v VPN) String() string {
return v.toLinesNode().String()
}
func (v VPN) toLinesNode() (node *gotree.Node) {
node = gotree.New("VPN settings:")
node.AppendNode(v.Provider.toLinesNode())
if v.Type == vpn.OpenVPN {
node.AppendNode(v.OpenVPN.toLinesNode())
} else {
node.AppendNode(v.Wireguard.toLinesNode())
}
return node
}

View File

@@ -1,138 +0,0 @@
package settings
import (
"fmt"
"net"
"regexp"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
// Wireguard contains settings to configure the Wireguard client.
type Wireguard struct {
// PrivateKey is the Wireguard client peer private key.
// It cannot be nil in the internal state.
PrivateKey *string
// PreSharedKey is the Wireguard pre-shared key.
// It can be the empty string to indicate there
// is no pre-shared key.
// It cannot be nil in the internal state.
PreSharedKey *string
// Addresses are the Wireguard interface addresses.
Addresses []net.IPNet
// Interface is the name of the Wireguard interface
// to create. It cannot be the empty string in the
// internal state.
Interface string
}
var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
// Validate validates Wireguard settings.
// It should only be ran if the VPN type chosen is Wireguard.
func (w Wireguard) validate(vpnProvider string) (err error) {
if !helpers.IsOneOf(vpnProvider,
providers.Custom,
providers.Ivpn,
providers.Mullvad,
providers.Windscribe,
) {
// do not validate for VPN provider not supporting Wireguard
return nil
}
// Validate PrivateKey
if *w.PrivateKey == "" {
return ErrWireguardPrivateKeyNotSet
}
_, err = wgtypes.ParseKey(*w.PrivateKey)
if err != nil {
return fmt.Errorf("private key is not valid: %w", err)
}
// Validate PreSharedKey
if *w.PreSharedKey != "" { // Note: this is optional
_, err = wgtypes.ParseKey(*w.PreSharedKey)
if err != nil {
return fmt.Errorf("pre-shared key is not valid: %w", err)
}
}
// Validate Addresses
if len(w.Addresses) == 0 {
return ErrWireguardInterfaceAddressNotSet
}
for i, ipNet := range w.Addresses {
if ipNet.IP == nil || ipNet.Mask == nil {
return fmt.Errorf("%w: for address at index %d: %s",
ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
}
}
// Validate interface
if !regexpInterfaceName.MatchString(w.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
}
return nil
}
func (w *Wireguard) copy() (copied Wireguard) {
return Wireguard{
PrivateKey: helpers.CopyStringPtr(w.PrivateKey),
PreSharedKey: helpers.CopyStringPtr(w.PreSharedKey),
Addresses: helpers.CopyIPNetSlice(w.Addresses),
Interface: w.Interface,
}
}
func (w *Wireguard) mergeWith(other Wireguard) {
w.PrivateKey = helpers.MergeWithStringPtr(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = helpers.MergeWithStringPtr(w.PreSharedKey, other.PreSharedKey)
w.Addresses = helpers.MergeIPNetsSlices(w.Addresses, other.Addresses)
w.Interface = helpers.MergeWithString(w.Interface, other.Interface)
}
func (w *Wireguard) overrideWith(other Wireguard) {
w.PrivateKey = helpers.OverrideWithStringPtr(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = helpers.OverrideWithStringPtr(w.PreSharedKey, other.PreSharedKey)
w.Addresses = helpers.OverrideWithIPNetsSlice(w.Addresses, other.Addresses)
w.Interface = helpers.OverrideWithString(w.Interface, other.Interface)
}
func (w *Wireguard) setDefaults() {
w.PrivateKey = helpers.DefaultStringPtr(w.PrivateKey, "")
w.PreSharedKey = helpers.DefaultStringPtr(w.PreSharedKey, "")
w.Interface = helpers.DefaultString(w.Interface, "wg0")
}
func (w Wireguard) String() string {
return w.toLinesNode().String()
}
func (w Wireguard) toLinesNode() (node *gotree.Node) {
node = gotree.New("Wireguard settings:")
if *w.PrivateKey != "" {
s := helpers.ObfuscateWireguardKey(*w.PrivateKey)
node.Appendf("Private key: %s", s)
}
if *w.PreSharedKey != "" {
s := helpers.ObfuscateWireguardKey(*w.PreSharedKey)
node.Appendf("Pre-shared key: %s", s)
}
addressesNode := node.Appendf("Interface addresses:")
for _, address := range w.Addresses {
addressesNode.Appendf(address.String())
}
node.Appendf("Network interface: %s", w.Interface)
return node
}

View File

@@ -1,144 +0,0 @@
package settings
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gotree"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type WireguardSelection struct {
// EndpointIP is the server endpoint IP address.
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
// To indicate it should not be used, it should be set
// to the empty net.IP{} slice. It can never be nil
// in the internal state.
EndpointIP net.IP
// EndpointPort is a the server port to use for the VPN server.
// It is optional for VPN providers IVPN, Mullvad
// and Windscribe, and compulsory for the others.
// When optional, it can be set to 0 to indicate not use
// a custom endpoint port. It cannot be nil in the internal
// state.
EndpointPort *uint16
// PublicKey is the server public key.
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
PublicKey string
}
// Validate validates WireguardSelection settings.
// It should only be ran if the VPN type chosen is Wireguard.
func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointIP
switch vpnProvider {
case providers.Ivpn, providers.Mullvad, providers.Windscribe: // endpoint IP addresses are baked in
case providers.Custom:
if len(w.EndpointIP) == 0 {
return ErrWireguardEndpointIPNotSet
}
default: // Providers not supporting Wireguard
}
// Validate EndpointPort
switch vpnProvider {
// EndpointPort is required
case providers.Custom:
if *w.EndpointPort == 0 {
return ErrWireguardEndpointPortNotSet
}
case providers.Ivpn, providers.Mullvad, providers.Windscribe:
// EndpointPort is optional and can be 0
if *w.EndpointPort == 0 {
break // no custom endpoint port set
}
if vpnProvider == providers.Mullvad {
break // no restriction on custom endpoint port value
}
var allowed []uint16
switch vpnProvider {
case providers.Ivpn:
allowed = []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237}
case providers.Windscribe:
allowed = []uint16{53, 80, 123, 443, 1194, 65142}
}
if helpers.Uint16IsOneOf(*w.EndpointPort, allowed) {
break
}
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
ErrWireguardEndpointPortNotAllowed, w.EndpointPort, vpnProvider,
helpers.PortChoicesOrString(allowed))
default: // Providers not supporting Wireguard
}
// Validate PublicKey
switch vpnProvider {
case providers.Ivpn, providers.Mullvad, providers.Windscribe: // public keys are baked in
case providers.Custom:
if w.PublicKey == "" {
return ErrWireguardPublicKeyNotSet
}
default: // Providers not supporting Wireguard
}
if w.PublicKey != "" {
_, err := wgtypes.ParseKey(w.PublicKey)
if err != nil {
return fmt.Errorf("%w: %s: %s",
ErrWireguardPublicKeyNotValid, w.PublicKey, err)
}
}
return nil
}
func (w *WireguardSelection) copy() (copied WireguardSelection) {
return WireguardSelection{
EndpointIP: helpers.CopyIP(w.EndpointIP),
EndpointPort: helpers.CopyUint16Ptr(w.EndpointPort),
PublicKey: w.PublicKey,
}
}
func (w *WireguardSelection) mergeWith(other WireguardSelection) {
w.EndpointIP = helpers.MergeWithIP(w.EndpointIP, other.EndpointIP)
w.EndpointPort = helpers.MergeWithUint16(w.EndpointPort, other.EndpointPort)
w.PublicKey = helpers.MergeWithString(w.PublicKey, other.PublicKey)
}
func (w *WireguardSelection) overrideWith(other WireguardSelection) {
w.EndpointIP = helpers.OverrideWithIP(w.EndpointIP, other.EndpointIP)
w.EndpointPort = helpers.OverrideWithUint16(w.EndpointPort, other.EndpointPort)
w.PublicKey = helpers.OverrideWithString(w.PublicKey, other.PublicKey)
}
func (w *WireguardSelection) setDefaults() {
w.EndpointIP = helpers.DefaultIP(w.EndpointIP, net.IP{})
w.EndpointPort = helpers.DefaultUint16(w.EndpointPort, 0)
}
func (w WireguardSelection) String() string {
return w.toLinesNode().String()
}
func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
node = gotree.New("Wireguard selection settings:")
if len(w.EndpointIP) > 0 {
node.Appendf("Endpoint IP address: %s", w.EndpointIP)
}
if *w.EndpointPort != 0 {
node.Appendf("Endpoint port: %d", *w.EndpointPort)
}
if w.PublicKey != "" {
node.Appendf("Server public key: %s", w.PublicKey)
}
return node
}

View File

@@ -1,50 +0,0 @@
package env
import (
"fmt"
"net"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readDNS() (dns settings.DNS, err error) {
dns.ServerAddress, err = r.readDNSServerAddress()
if err != nil {
return dns, err
}
dns.KeepNameserver, err = envToBoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return dns, fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err)
}
dns.DoT, err = r.readDoT()
if err != nil {
return dns, fmt.Errorf("DoT settings: %w", err)
}
return dns, nil
}
func (r *Reader) readDNSServerAddress() (address net.IP, err error) {
key, s := r.getEnvWithRetro("DNS_ADDRESS", "DNS_PLAINTEXT_ADDRESS")
if s == "" {
return nil, nil
}
address = net.ParseIP(s)
if address == nil {
return nil, fmt.Errorf("environment variable %s: %w: %s", key, ErrIPAddressParse, s)
}
// TODO remove in v4
if !address.Equal(net.IPv4(127, 0, 0, 1)) { //nolint:gomnd
r.warner.Warn(key + " is set to " + s +
" 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 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.")
}
return address, nil
}

View File

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

View File

@@ -1,31 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readDoT() (dot settings.DoT, err error) {
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 {
return dot, err
}
dot.Blacklist, err = r.readDNSBlacklist()
if err != nil {
return dot, err
}
return dot, nil
}

View File

@@ -1,82 +0,0 @@
package env
import (
"errors"
"fmt"
"net"
"strconv"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readFirewall() (firewall settings.Firewall, err error) {
vpnInputPortStrings := envToCSV("FIREWALL_VPN_INPUT_PORTS")
firewall.VPNInputPorts, err = stringsToPorts(vpnInputPortStrings)
if err != nil {
return firewall, fmt.Errorf("environment variable FIREWALL_VPN_INPUT_PORTS: %w", err)
}
inputPortStrings := envToCSV("FIREWALL_INPUT_PORTS")
firewall.InputPorts, err = stringsToPorts(inputPortStrings)
if err != nil {
return firewall, fmt.Errorf("environment variable FIREWALL_INPUT_PORTS: %w", err)
}
outboundSubnetsKey, _ := r.getEnvWithRetro("FIREWALL_OUTBOUND_SUBNETS", "EXTRA_SUBNETS")
outboundSubnetStrings := envToCSV(outboundSubnetsKey)
firewall.OutboundSubnets, err = stringsToIPNets(outboundSubnetStrings)
if err != nil {
return firewall, fmt.Errorf("environment variable %s: %w", outboundSubnetsKey, err)
}
firewall.Enabled, err = envToBoolPtr("FIREWALL")
if err != nil {
return firewall, fmt.Errorf("environment variable FIREWALL: %w", err)
}
firewall.Debug, err = envToBoolPtr("FIREWALL_DEBUG")
if err != nil {
return firewall, fmt.Errorf("environment variable FIREWALL_DEBUG: %w", err)
}
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
}
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("cannot parse IP network %q: %w", s, err)
}
ipNet.IP = ip
ipNets[i] = *ipNet
}
return ipNets, nil
}

View File

@@ -1,44 +0,0 @@
package env
import (
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) ReadHealth() (health settings.Health, err error) {
health.ServerAddress = getCleanedEnv("HEALTH_SERVER_ADDRESS")
_, health.TargetAddress = r.getEnvWithRetro("HEALTH_TARGET_ADDRESS", "HEALTH_ADDRESS_TO_PING")
health.VPN.Initial, err = r.readDurationWithRetro(
"HEALTH_VPN_DURATION_INITIAL",
"HEALTH_OPENVPN_DURATION_INITIAL")
if err != nil {
return health, err
}
health.VPN.Addition, err = r.readDurationWithRetro(
"HEALTH_VPN_DURATION_ADDITION",
"HEALTH_OPENVPN_DURATION_ADDITION")
if err != nil {
return health, err
}
return health, nil
}
func (r *Reader) readDurationWithRetro(envKey, retroEnvKey string) (d *time.Duration, err error) {
envKey, s := r.getEnvWithRetro(envKey, retroEnvKey)
if s == "" {
return nil, nil //nolint:nilnil
}
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,158 +0,0 @@
package env
import (
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/qdm12/govalid/binary"
"github.com/qdm12/govalid/integer"
)
// getCleanedEnv returns an environment variable value with
// surrounding spaces and trailing new line characters removed.
func getCleanedEnv(envKey string) (value string) {
value = os.Getenv(envKey)
value = strings.TrimSpace(value)
value = strings.TrimSuffix(value, "\r\n")
value = strings.TrimSuffix(value, "\n")
return value
}
func envToCSV(envKey string) (values []string) {
csv := getCleanedEnv(envKey)
if csv == "" {
return nil
}
return lowerAndSplit(csv)
}
func envToInt(envKey string) (n int, err error) {
s := getCleanedEnv(envKey)
if s == "" {
return 0, nil
}
return strconv.Atoi(s)
}
func envToFloat64(envKey string) (f float64, err error) {
s := getCleanedEnv(envKey)
if s == "" {
return 0, nil
}
const bits = 64
return strconv.ParseFloat(s, bits)
}
func envToStringPtr(envKey string) (stringPtr *string) {
s := getCleanedEnv(envKey)
if s == "" {
return nil
}
return &s
}
func envToBoolPtr(envKey string) (boolPtr *bool, err error) {
s := getCleanedEnv(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 := getCleanedEnv(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 := getCleanedEnv(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 := getCleanedEnv(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 := getCleanedEnv(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, ",")
}
func decodeBase64(b64String string) (decoded string, err error) {
b, err := base64.StdEncoding.DecodeString(b64String)
if err != nil {
return "", fmt.Errorf("cannot decode base64 string %q: %w",
b64String, err)
}
return string(b), nil
}
func unsetEnvKeys(envKeys []string, err error) (newErr error) {
newErr = err
for _, envKey := range envKeys {
unsetErr := os.Unsetenv(envKey)
if unsetErr != nil && newErr == nil {
newErr = fmt.Errorf("cannot unset environment variable %s: %w", envKey, unsetErr)
}
}
return newErr
}
func stringPtr(s string) *string { return &s }
func uint32Ptr(n uint32) *uint32 { return &n }
func boolPtr(b bool) *bool { return &b }

View File

@@ -1,22 +0,0 @@
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

@@ -1,91 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/govalid/binary"
)
func (r *Reader) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) {
httpProxy.User = r.readHTTProxyUser()
httpProxy.Password = r.readHTTProxyPassword()
httpProxy.ListeningAddress = r.readHTTProxyListeningAddress()
httpProxy.Enabled, err = r.readHTTProxyEnabled()
if err != nil {
return httpProxy, err
}
httpProxy.Stealth, err = envToBoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return httpProxy, fmt.Errorf("environment variable HTTPPROXY_STEALTH: %w", err)
}
httpProxy.Log, err = r.readHTTProxyLog()
if err != nil {
return httpProxy, err
}
return httpProxy, nil
}
func (r *Reader) readHTTProxyUser() (user *string) {
_, s := r.getEnvWithRetro("HTTPPROXY_USER", "PROXY_USER", "TINYPROXY_USER")
if s != "" {
return &s
}
return nil
}
func (r *Reader) readHTTProxyPassword() (user *string) {
_, s := r.getEnvWithRetro("HTTPPROXY_PASSWORD", "PROXY_PASSWORD", "TINYPROXY_PASSWORD")
if s != "" {
return &s
}
return nil
}
func (r *Reader) readHTTProxyListeningAddress() (listeningAddress string) {
key, value := r.getEnvWithRetro("HTTPPROXY_LISTENING_ADDRESS", "PROXY_PORT", "TINYPROXY_PORT", "HTTPPROXY_PORT")
if key == "HTTPPROXY_LISTENING_ADDRESS" {
return value
}
return ":" + value
}
func (r *Reader) readHTTProxyEnabled() (enabled *bool, err error) {
key, s := r.getEnvWithRetro("HTTPPROXY", "PROXY", "TINYPROXY")
if s == "" {
return nil, nil //nolint:nilnil
}
enabled = new(bool)
*enabled, err = binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable %s: %w", key, err)
}
return enabled, nil
}
func (r *Reader) readHTTProxyLog() (enabled *bool, err error) {
key, s := r.getEnvWithRetro("HTTPPROXY_LOG", "PROXY_LOG_LEVEL", "TINYPROXY_LOG")
if s == "" {
return nil, nil //nolint:nilnil
}
var binaryOptions []binary.Option
if key != "HTTPROXY_LOG" {
retroOption := binary.OptionEnabled("on", "info", "connect", "notice")
binaryOptions = append(binaryOptions, retroOption)
}
enabled = new(bool)
*enabled, err = binary.Validate(s, binaryOptions...)
if err != nil {
return nil, fmt.Errorf("environment variable %s: %w", key, err)
}
return enabled, nil
}

View File

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

View File

@@ -1,129 +0,0 @@
package env
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/govalid/binary"
)
func (r *Reader) readOpenVPN() (
openVPN settings.OpenVPN, err error) {
defer func() {
err = unsetEnvKeys([]string{"OPENVPN_CLIENTKEY", "OPENVPN_CLIENTCRT"}, err)
}()
openVPN.Version = getCleanedEnv("OPENVPN_VERSION")
openVPN.User = r.readOpenVPNUser()
openVPN.Password = r.readOpenVPNPassword()
confFile := getCleanedEnv("OPENVPN_CUSTOM_CONFIG")
if confFile != "" {
openVPN.ConfFile = &confFile
}
ciphersKey, _ := r.getEnvWithRetro("OPENVPN_CIPHERS", "OPENVPN_CIPHER")
openVPN.Ciphers = envToCSV(ciphersKey)
auth := getCleanedEnv("OPENVPN_AUTH")
if auth != "" {
openVPN.Auth = &auth
}
openVPN.ClientCrt, err = readBase64OrNil("OPENVPN_CLIENTCRT")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_CLIENTCRT: %w", err)
}
openVPN.ClientKey, err = readBase64OrNil("OPENVPN_CLIENTKEY")
if err != nil {
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 = r.getEnvWithRetro("VPN_INTERFACE", "OPENVPN_INTERFACE")
openVPN.ProcessUser, err = r.readOpenVPNProcessUser()
if err != nil {
return openVPN, err
}
openVPN.Verbosity, err = envToIntPtr("OPENVPN_VERBOSITY")
if err != nil {
return openVPN, fmt.Errorf("environment variable OPENVPN_VERBOSITY: %w", err)
}
flagsStr := getCleanedEnv("OPENVPN_FLAGS")
if flagsStr != "" {
openVPN.Flags = strings.Fields(flagsStr)
}
return openVPN, nil
}
func (r *Reader) readOpenVPNUser() (user string) {
_, user = r.getEnvWithRetro("OPENVPN_USER", "USER")
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
return strings.ReplaceAll(user, " ", "")
}
func (r *Reader) readOpenVPNPassword() (password string) {
_, password = r.getEnvWithRetro("OPENVPN_PASSWORD", "PASSWORD")
return password
}
func readBase64OrNil(envKey string) (valueOrNil *string, err error) {
value := getCleanedEnv(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 := r.getEnvWithRetro(
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
"PIA_ENCRYPTION", "ENCRYPTION")
if preset != "" {
return &preset
}
return nil
}
func (r *Reader) readOpenVPNProcessUser() (processUser string, err error) {
key, value := r.getEnvWithRetro("OPENVPN_PROCESS_USER", "OPENVPN_ROOT")
if key == "OPENVPN_PROCESS_USER" {
return value, nil
}
// Retro-compatibility
if value == "" {
return "", nil
}
root, err := binary.Validate(value)
if err != nil {
return "", fmt.Errorf("environment variable %s: %w", key, err)
}
if root {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
}

View File

@@ -1,66 +0,0 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/govalid/port"
)
func (r *Reader) readOpenVPNSelection() (
selection settings.OpenVPNSelection, err error) {
confFile := getCleanedEnv("OPENVPN_CUSTOM_CONFIG")
if confFile != "" {
selection.ConfFile = &confFile
}
selection.TCP, err = r.readOpenVPNProtocol()
if err != nil {
return selection, err
}
selection.CustomPort, err = r.readOpenVPNCustomPort()
if err != nil {
return selection, err
}
selection.PIAEncPreset = r.readPIAEncryptionPreset()
return selection, nil
}
var ErrOpenVPNProtocolNotValid = errors.New("OpenVPN protocol is not valid")
func (r *Reader) readOpenVPNProtocol() (tcp *bool, err error) {
envKey, protocol := r.getEnvWithRetro("OPENVPN_PROTOCOL", "PROTOCOL")
switch strings.ToLower(protocol) {
case "":
return nil, nil //nolint:nilnil
case constants.UDP:
return boolPtr(false), nil
case constants.TCP:
return boolPtr(true), nil
default:
return nil, fmt.Errorf("environment variable %s: %w: %s",
envKey, ErrOpenVPNProtocolNotValid, protocol)
}
}
func (r *Reader) readOpenVPNCustomPort() (customPort *uint16, err error) {
key, s := r.getEnvWithRetro("VPN_ENDPOINT_PORT", "PORT", "OPENVPN_PORT")
if s == "" {
return nil, nil //nolint:nilnil
}
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,27 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readPortForward() (
portForwarding settings.PortForwarding, err error) {
key, _ := r.getEnvWithRetro(
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
"PORT_FORWARDING")
portForwarding.Enabled, err = envToBoolPtr(key)
if err != nil {
return portForwarding, fmt.Errorf("environment variable %s: %w", key, err)
}
_, value := r.getEnvWithRetro(
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
"PORT_FORWARDING_STATUS_FILE")
if value != "" {
portForwarding.Filepath = stringPtr(value)
}
return portForwarding, nil
}

View File

@@ -1,28 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/pprof"
)
func readPprof() (settings pprof.Settings, err error) {
settings.Enabled, err = envToBoolPtr("PPROF_ENABLED")
if err != nil {
return settings, fmt.Errorf("environment variable PPROF_ENABLED: %w", err)
}
settings.BlockProfileRate, err = envToInt("PPROF_BLOCK_PROFILE_RATE")
if err != nil {
return settings, fmt.Errorf("environment variable PPROF_BLOCK_PROFILE_RATE: %w", err)
}
settings.MutexProfileRate, err = envToInt("PPROF_MUTEX_PROFILE_RATE")
if err != nil {
return settings, fmt.Errorf("environment variable PPROF_MUTEX_PROFILE_RATE: %w", err)
}
settings.HTTPServer.Address = getCleanedEnv("PPROF_HTTP_SERVER_ADDRESS")
return settings, nil
}

View File

@@ -1,45 +0,0 @@
package env
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
)
func (r *Reader) readProvider(vpnType string) (provider settings.Provider, err error) {
provider.Name = r.readVPNServiceProvider(vpnType)
var providerName string
if provider.Name != nil {
providerName = *provider.Name
}
provider.ServerSelection, err = r.readServerSelection(providerName, vpnType)
if err != nil {
return provider, fmt.Errorf("server selection: %w", err)
}
provider.PortForwarding, err = r.readPortForward()
if err != nil {
return provider, fmt.Errorf("port forwarding: %w", err)
}
return provider, nil
}
func (r *Reader) readVPNServiceProvider(vpnType string) (vpnProviderPtr *string) {
_, s := r.getEnvWithRetro("VPN_SERVICE_PROVIDER", "VPNSP")
s = strings.ToLower(s)
switch {
case vpnType != vpn.Wireguard &&
getCleanedEnv("OPENVPN_CUSTOM_CONFIG") != "": // retro compatibility
return stringPtr(providers.Custom)
case s == "":
return nil
case s == "pia": // retro compatibility
return stringPtr(providers.PrivateInternetAccess)
}
return stringPtr(s)
}

View File

@@ -1,42 +0,0 @@
package env
import (
"fmt"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readPublicIP() (publicIP settings.PublicIP, err error) {
publicIP.Period, err = readPublicIPPeriod()
if err != nil {
return publicIP, err
}
publicIP.IPFilepath = r.readPublicIPFilepath()
return publicIP, nil
}
func readPublicIPPeriod() (period *time.Duration, err error) {
s := getCleanedEnv("PUBLICIP_PERIOD")
if s == "" {
return nil, nil //nolint:nilnil
}
period = new(time.Duration)
*period, err = time.ParseDuration(s)
if err != nil {
return nil, fmt.Errorf("environment variable PUBLICIP_PERIOD: %w", err)
}
return period, nil
}
func (r *Reader) readPublicIPFilepath() (filepath *string) {
_, s := r.getEnvWithRetro("PUBLICIP_FILE", "IP_STATUS_FILE")
if s != "" {
return &s
}
return nil
}

View File

@@ -1,120 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources"
)
var _ sources.Source = (*Reader)(nil)
type Reader struct {
warner Warner
}
type Warner interface {
Warn(s string)
}
func New(warner Warner) *Reader {
return &Reader{
warner: warner,
}
}
func (r *Reader) String() string { return "environment variables" }
func (r *Reader) Read() (settings settings.Settings, err error) {
settings.VPN, err = r.readVPN()
if err != nil {
return settings, err
}
settings.Firewall, err = r.readFirewall()
if err != nil {
return settings, err
}
settings.System, err = r.readSystem()
if err != nil {
return settings, err
}
settings.Health, err = r.ReadHealth()
if err != nil {
return settings, err
}
settings.HTTPProxy, err = r.readHTTPProxy()
if err != nil {
return settings, err
}
settings.Log, err = readLog()
if err != nil {
return settings, err
}
settings.PublicIP, err = r.readPublicIP()
if err != nil {
return settings, err
}
settings.Updater, err = readUpdater()
if err != nil {
return settings, err
}
settings.Version, err = readVersion()
if err != nil {
return settings, err
}
settings.Shadowsocks, err = r.readShadowsocks()
if err != nil {
return settings, err
}
settings.DNS, err = r.readDNS()
if err != nil {
return settings, err
}
settings.ControlServer, err = r.readControlServer()
if err != nil {
return settings, err
}
settings.Pprof, err = readPprof()
if err != nil {
return settings, err
}
return settings, nil
}
func (r *Reader) onRetroActive(oldKey, newKey string) {
r.warner.Warn(
"You are using the old environment variable " + oldKey +
", please consider changing it to " + newKey)
}
// getEnvWithRetro returns the first environment variable
// key and corresponding non empty value from the environment
// variable keys given. It first goes through the retroKeys
// and end on returning the value corresponding to the currentKey.
// Note retroKeys should be in order from oldest to most
// recent retro-compatibility key.
func (r *Reader) getEnvWithRetro(currentKey string,
retroKeys ...string) (key, value string) {
// We check retro-compatibility keys first since
// the current key might be set in the Dockerfile.
for _, key = range retroKeys {
value = getCleanedEnv(key)
if value != "" {
r.onRetroActive(key, currentKey)
return key, value
}
}
return currentKey, getCleanedEnv(currentKey)
}

View File

@@ -1,48 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/govalid/binary"
)
func (r *Reader) readControlServer() (controlServer settings.ControlServer, err error) {
controlServer.Log, err = readControlServerLog()
if err != nil {
return controlServer, err
}
controlServer.Address = r.readControlServerAddress()
return controlServer, nil
}
func readControlServerLog() (enabled *bool, err error) {
s := getCleanedEnv("HTTP_CONTROL_SERVER_LOG")
if s == "" {
return nil, nil //nolint:nilnil
}
log, err := binary.Validate(s)
if err != nil {
return nil, fmt.Errorf("environment variable HTTP_CONTROL_SERVER_LOG: %w", err)
}
return &log, nil
}
func (r *Reader) readControlServerAddress() (address *string) {
key, s := r.getEnvWithRetro("HTTP_CONTROL_SERVER_ADDRESS", "HTTP_CONTROL_SERVER_PORT")
if s == "" {
return nil
}
if key == "HTTP_CONTROL_SERVER_ADDRESS" {
return &s
}
address = new(string)
*address = ":" + s
return address
}

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