Compare commits

..

1 Commits

Author SHA1 Message Date
Quentin McGaw (desktop)
b4ec59f9bd Fix IPVanish TLS verification 2021-07-02 03:21:33 +00:00
797 changed files with 28918 additions and 156987 deletions

View File

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

View File

@@ -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,27 @@ 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
- ~/.docker:/root/.docker:z
# SSH directory for Linux, OSX and WSL
- ~/.ssh:/root/.ssh
- ~/.ssh:/root/.ssh:z
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
#- ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history
- ~/.zsh_history:/root/.zsh_history:z
# Git config
- ~/.gitconfig:/root/.gitconfig
- ~/.gitconfig:/root/.gitconfig:z
environment:
- TZ=
cap_add:
# For debugging with dlv
# - SYS_PTRACE
- NET_ADMIN
- SYS_PTRACE
security_opt:
# For debugging with dlv
- seccomp:unconfined

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

@@ -0,0 +1,45 @@
---
name: Bug
about: Report a bug
title: 'Bug: FILL THIS TEXT!'
labels: ":bug: bug"
assignees: qdm12
---
<!---
⚠️ Answer the following or I'll insta-close your issue
-->
**Is this urgent?**: No
**Host OS** (approximate answer is fine too): Ubuntu 18
**CPU arch** or **device name**: amd64
**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 in example tokens)**
```log
PASTE YOUR LOGS
IN THERE
```
<!---
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
-->

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

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

@@ -0,0 +1,67 @@
---
name: Help
about: Ask for help
title: 'Help: FILL THIS TEXT!'
labels: ":pray: help wanted"
assignees:
---
<!---
⚠️ If this about a Docker configuration problem or another service:
Start a discussion at https://github.com/qdm12/gluetun/discussions/new
OR I WILL INSTA-CLOSE YOUR ISSUE.
-->
<!---
⚠️ Answer the following or I'll insta-close your issue
-->
**Is this urgent?**: No
**Host OS** (approximate answer is fine too): Ubuntu 18
**CPU arch** or **device name**: amd64
**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 in example tokens)**
```log
PASTE YOUR LOGS
IN THERE
```
**What are you using to run your container?**: Docker Compose
<!---
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
-->
Please also share your configuration file:
```yml
your .yml
content
in here
```
or
```sh
# your docker
# run command
# in here
```

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

20
.github/labels.yml vendored
View File

@@ -14,14 +14,6 @@
color: "795548"
description: ""
# Priority
- name: "🚨 Urgent"
color: "d5232f"
description: ""
- name: "💤 Low priority"
color: "4285f4"
description: ""
# VPN providers
- name: ":cloud: Cyberghost"
color: "cfe8d4"
@@ -35,9 +27,6 @@
- name: ":cloud: IVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ExpressVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: FastestVPN"
color: "cfe8d4"
description: ""
@@ -47,9 +36,6 @@
- name: ":cloud: NordVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Perfect Privacy"
color: "cfe8d4"
description: ""
- name: ":cloud: PIA"
color: "cfe8d4"
description: ""
@@ -76,9 +62,6 @@
- name: ":cloud: Vyprvpn"
color: "cfe8d4"
description: ""
- name: ":cloud: WeVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Windscribe"
color: "cfe8d4"
description: ""
@@ -87,9 +70,6 @@
- name: "Openvpn"
color: "ffc7ea"
description: ""
- name: "Wireguard"
color: "ffc7ea"
description: ""
- name: "Unbound (DNS over TLS)"
color: "ffc7ea"
description: ""

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

@@ -0,0 +1,105 @@
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.3.4
- 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
- name: Code security analysis
uses: snyk/actions/golang@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Build final image
run: docker build -t final-image .
- name: Image security analysis
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: final-image
publish:
needs: [verify]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- 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/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/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,linux/ppc64le
fi
- name: Build and push final image
uses: docker/build-push-action@v2.4.0
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 }}
ALLTARGETPLATFORMS=${{ steps.vars.outputs.platforms }}
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.3.4
- 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.3.4
- 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.3.4
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error

View File

@@ -1,4 +1,6 @@
linters-settings:
maligned:
suggest-new: true
misspell:
locale: US
@@ -7,50 +9,34 @@ issues:
- path: _test\.go
linters:
- dupl
- maligned
- goerr113
- containedctx
- path: "internal\\/server\\/.+\\.go"
- path: internal/server/
linters:
- dupl
- path: "internal\\/configuration\\/settings\\/.+\\.go"
- path: internal/configuration/
linters:
- dupl
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
linters:
- gomnd
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
- text: "mnd: Magic number: 0400*"
linters:
- gomnd
- linters:
- lll
source: "^//go:generate .+$"
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
- ireturn
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
@@ -63,37 +49,33 @@ linters:
- goheader
- goimports
- gomnd
- gomoddirectives
- goprintffuncname
- gosec
- grouper
- ifshort
- gosimple
- govet
- importas
- ireturn
- ineffassign
- lll
- maintidx
- makezero
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- tenv
- staticcheck
- structcheck
- thelper
- tparallel
- 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,63 @@
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.13
ARG GO_VERSION=1.16
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 qmcgaw/xcputranslate:v0.6.0 AS xcputranslate
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
RUN apk --update add git g++
ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint
ARG GOLANGCI_LINT_VERSION=v1.41.1
RUN go get github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}
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
ENV CGO_ENABLED=1
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
FROM --platform=${BUILDPLATFORM} base AS lint
FROM --platform=$BUILDPLATFORM base AS lint
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
ARG TARGETPLATFORM
ARG ALLTARGETPLATFORMS=${TARGETPLATFORM}
ARG VERSION=unknown
ARG CREATED="an unknown date"
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
RUN xcputranslate sleep -targetplatform ${TARGETPLATFORM} -buildtime=10s -order=${ALLTARGETPLATFORMS}
RUN GOARCH="$(xcputranslate translate -field arch -targetplatform ${TARGETPLATFORM})" \
GOARM="$(xcputranslate translate -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,67 +65,49 @@ 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 \
OPENVPN_USER= \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
ENV VPNSP=pia \
VERSION_INFORMATION=on \
PROTOCOL=udp \
OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_PROCESS_USER= \
OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
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:
TZ= \
PUID= \
PGID= \
PUBLICIP_FILE="/tmp/gluetun/ip" \
# VPN provider settings
OPENVPN_USER= \
OPENVPN_PASSWORD= \
USER_SECRETFILE=/run/secrets/openvpn_user \
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
REGION= \
COUNTRY= \
CITY= \
PORT= \
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 \
# Private Internet Access only:
PIA_ENCRYPTION=strong \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_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:
# NordVPN and ProtonVPN only:
SERVER_NAME= \
# 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 \
@@ -132,12 +122,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 +141,20 @@ 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 && \
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /entrypoint healthcheck
RUN apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-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 && \
deluser openvpn && \
deluser unbound && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
COPY --from=build /tmp/gobuild/entrypoint /entrypoint

160
README.md
View File

@@ -1,125 +1,125 @@
# Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN sercice providers
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN and Windscribe 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**:
[![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.13 for a small Docker image of 54MB
- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** 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)
- [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 ppc64le 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- 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. Ensure your `tun` kernel module is setup:
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
```sh
sudo modprobe tun
# or, if you don't have modprobe, with
sudo insmod /lib/modules/tun.ko
```
[🐛 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+)
1. Extra steps:
- [For Synology users](https://github.com/qdm12/gluetun/wiki/Synology-setup)
- [For 32 bit Operating systems (**Rasberry Pis**)](https://github.com/qdm12/gluetun/wiki/32-bit-setup)
1. Launch the container with:
Here's a docker-compose.yml for the laziest:
```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
```
```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=
```
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
```bash
docker-compose up -d
```
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.
- [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.
- Use [Docker secrets](https://github.com/qdm12/gluetun/wiki/Docker-secrets) to read your credentials instead of environment variables
## 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

@@ -4,104 +4,90 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
nativeos "os"
"os/signal"
"strconv"
"strings"
"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/configuration"
"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/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/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/golibs/command"
"github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/gluetun/internal/updater"
versionpkg "github.com/qdm12/gluetun/internal/version"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user"
"github.com/qdm12/golibs/params"
"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/updated/pkg/dnscrypto"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
created = "an unknown date"
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
)
var (
errSetupRouting = errors.New("cannot setup routing")
errCreateUser = errors.New("cannot create user")
)
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, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, nativeos.Interrupt)
ctx, cancel := context.WithCancel(ctx)
logger := log.New(log.SetLevel(log.LevelInfo))
logger := logging.NewParent(logging.Settings{})
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)
}()
select {
case signal := <-signalCh:
fmt.Println("")
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
cancel()
case <-ctx.Done():
stop()
logger.Warn("Caught OS signal, shutting down")
case err := <-errorCh:
stop()
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
nativeos.Exit(0)
}
logger.Error(err.Error())
logger.Error(err)
cancel()
}
@@ -115,149 +101,69 @@ 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
//nolint:gocognit,gocyclo
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 {
args []string, logger logging.ParentLogger, 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(ctx)
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, logger)
case "update":
return cli.Update(ctx, args[2:], logger)
case "format-servers":
return cli.FormatServers(args[2:])
return cli.Update(ctx, args[2:], os, logger)
default:
return fmt.Errorf("%w: %s", errCommandUnknown, 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)
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.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
os, unix)
dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
dnsConf := unbound.NewConfigurator(nil, os.OpenFile, dnsCrypto,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
routingConf := routing.NewRouting(
logger.NewChild(logging.Settings{Prefix: "routing: "}))
firewallConf := firewall.NewConfigurator(
logger.NewChild(logging.Settings{Prefix: "firewall: "}),
routingConf, os.OpenFile)
err = printVersions(ctx, logger, []printVersionElement{
fmt.Println(gluetunLogging.Splash(buildInfo))
if 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)
}},
})
if err != nil {
{name: "IPtables", getVersion: firewallConf.Version},
}); err != nil {
return err
}
var allSettings configuration.Settings
err := allSettings.Read(params.NewEnv(), os,
logger.NewChild(logging.Settings{Prefix: "configuration: "}))
if err != nil {
return err
}
logger.Info(allSettings.String())
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
@@ -267,33 +173,66 @@ 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.NewChild(logging.Settings{Prefix: "storage: "}),
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 fmt.Errorf("%w: %s", errCreateUser, 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
allSettings.DNS.Unbound.Username = 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
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
defaultIP, err := routingConf.DefaultIP()
if err != nil {
return err
}
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localNetworks, 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 fmt.Errorf("%w: %s", errSetupRouting, 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("cannot teardown routing: " + err.Error())
}
}()
@@ -304,26 +243,41 @@ 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{})
defer close(tunnelReadyCh)
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
}
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
if err != nil {
return err
}
} // TODO move inside firewall?
healthy := make(chan bool)
// Shutdown settings
const totalShutdownTimeout = 3 * time.Second
const defaultShutdownTimeout = 400 * time.Millisecond
defaultShutdownOnSuccess := func(goRoutineName string) {
logger.Info(goRoutineName + ": terminated ✔️")
@@ -331,134 +285,117 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
defaultShutdownOnFailure := func(goRoutineName string, err error) {
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
}
defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess)}
defaultGoRoutineSettings := goshutdown.GoRoutineSettings{Timeout: defaultShutdownTimeout}
defaultGroupSettings := goshutdown.GroupSettings{
Timeout: defaultShutdownTimeout,
OnFailure: defaultShutdownOnFailure,
OnSuccess: defaultShutdownOnSuccess,
}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupSettings)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupSettings)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupSettings)
pprofReady := make(chan struct{})
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers,
ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, tunnelReadyCh, healthy)
openvpnHandler, openvpnCtx, openvpnDone := goshutdown.NewGoRoutineHandler(
"openvpn", goshutdown.GoRoutineSettings{Timeout: time.Second})
// wait for restartOpenvpn
go openvpnLooper.Run(openvpnCtx, openvpnDone)
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)
updaterLooper := updater.NewLooper(allSettings.Updater,
allServers, storage, openvpnLooper.SetServers, httpClient,
logger.NewChild(logging.Settings{Prefix: "updater: "}))
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
"updater", defaultGoRoutineSettings)
// 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)
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, httpClient,
unboundLogger, os.OpenFile)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"unbound", defaultGoRoutineSettings)
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler)
httpProxyLooper := httpproxy.NewLoop(
logger.New(log.SetComponent("http proxy")),
publicIPLooper := publicip.NewLooper(httpClient,
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
allSettings.PublicIP, puid, pgid, os)
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
"public IP", defaultGoRoutineSettings)
go publicIPLooper.Run(pubIPCtx, pubIPDone)
otherGroupHandler.Add(pubIPHandler)
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
"public IP", defaultGoRoutineSettings)
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
httpProxyLooper := httpproxy.NewLooper(
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
"http proxy", defaultGoRoutineSettings)
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
logger.New(log.SetComponent("shadowsocks")))
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks,
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
"shadowsocks proxy", defaultGoRoutineSettings)
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
eventsRoutingHandler, eventsRoutingCtx, eventsRoutingDone := goshutdown.NewGoRoutineHandler(
"events routing", defaultGoRoutineSettings)
go routeReadyEvents(eventsRoutingCtx, eventsRoutingDone, buildInfo, tunnelReadyCh,
unboundLooper, updaterLooper, publicIPLooper, routingConf, logger, httpClient,
allSettings.VersionInformation, allSettings.OpenVPN.Provider.PortForwarding.Enabled, openvpnLooper.PortForward,
)
controlGroupHandler.Add(eventsRoutingHandler)
controlServerAddress := ":" + strconv.Itoa(int(allSettings.ControlServer.Port))
controlServerLogging := allSettings.ControlServer.Log
httpServer := server.New(controlServerAddress, controlServerLogging,
logger.NewChild(logging.Settings{Prefix: "http server: "}),
buildInfo, openvpnLooper, unboundLooper, updaterLooper, publicIPLooper)
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
"http server", defaultGoRoutineSettings)
go httpServer.Run(httpServerCtx, httpServerDone)
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthcheckServer := healthcheck.NewServer(constants.HealthcheckAddress,
logger.NewChild(logging.Settings{Prefix: "healthcheck: "}))
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
"HTTP health server", defaultGoRoutineSettings)
go healthcheckServer.Run(healthServerCtx, healthy, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
const orderShutdownTimeout = 3 * time.Second
orderSettings := goshutdown.OrderSettings{
Timeout: orderShutdownTimeout,
OnFailure: defaultShutdownOnFailure,
OnSuccess: defaultShutdownOnSuccess,
}
orderHandler := goshutdown.NewOrder("gluetun", orderSettings)
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler)
openvpnHandler, 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
// 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(allSettings.OpenVPN.Provider.PortForwarding.Filepath); err != nil {
logger.Error(err)
}
}
return orderHandler.Shutdown(context.Background())
}
@@ -467,11 +404,7 @@ type printVersionElement struct {
getVersion func(ctx context.Context) (version string, err error)
}
type infoer interface {
Info(s string)
}
func printVersions(ctx context.Context, logger infoer,
func printVersions(ctx context.Context, logger logging.Logger,
elements []printVersionElement) (err error) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
@@ -488,37 +421,76 @@ func printVersions(ctx context.Context, logger infoer,
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 routeReadyEvents(ctx context.Context, done chan<- struct{}, buildInfo models.BuildInformation,
tunnelReadyCh <-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 close(done)
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
}
// for linters only
var restartTickerContext context.Context
var restartTickerCancel context.CancelFunc = func() {}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
unboundTickerDone := make(chan struct{})
close(unboundTickerDone)
updaterTickerDone := make(chan struct{})
close(updaterTickerDone)
first := true
for {
select {
case <-ctx.Done():
restartTickerCancel() // for linters only
<-unboundTickerDone
<-updaterTickerDone
return
case <-tunnelReadyCh: // blocks until openvpn is connected
vpnDestination, err := routing.VPNDestinationIP()
if err != nil {
logger.Warn(err)
} else {
logger.Info("VPN routing IP address: %s", vpnDestination)
}
if unboundLooper.GetSettings().Enabled {
_, _ = unboundLooper.SetStatus(constants.Running)
}
restartTickerCancel() // stop previous restart tickers
<-unboundTickerDone
<-updaterTickerDone
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
// Runs the Public IP getter job once
_, _ = publicIPLooper.SetStatus(constants.Running)
if !versionInformation {
break
}
if first {
first = false
message, err := versionpkg.GetMessage(ctx, buildInfo, httpClient)
if err != nil {
logger.Error("cannot get version information: " + err.Error())
} else {
logger.Info(message)
}
}
unboundTickerDone = make(chan struct{})
updaterTickerDone = make(chan struct{})
go unboundLooper.RunRestartTicker(restartTickerContext, unboundTickerDone)
go updaterLooper.RunRestartTicker(restartTickerContext, updaterTickerDone)
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)
}
}
}
}

24
docker-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
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
environment:
# More variables are available, see the readme table
- OPENVPN_USER=
- OPENVPN_PASSWORD=
- VPNSP=private internet access
# Timezone for accurate logs times
- TZ=
restart: always

48
go.mod
View File

@@ -1,45 +1,17 @@
module github.com/qdm12/gluetun
go 1.17
go 1.16
require (
github.com/breml/rootcerts v0.2.3
github.com/fatih/color v1.13.0
github.com/fatih/color v1.12.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/dns v1.8.0
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb
github.com/qdm12/goshutdown v0.1.0
github.com/qdm12/ss-server v0.2.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/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.0
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
inet.af/netaddr v0.0.0-20210511181906-37180328850c
)

142
go.sum
View File

@@ -4,8 +4,6 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/g
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.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=
@@ -13,9 +11,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -36,28 +33,11 @@ github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3K
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -67,30 +47,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.8 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.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/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@@ -101,24 +63,14 @@ github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMg
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/dns v1.8.0 h1:GZ40kptmfDHOMNxBKWSA4zrbNyGm41BA57zv2MaDtCI=
github.com/qdm12/dns v1.8.0/go.mod h1:P2mm63NDYZdx2NAd5CVLM0FBnNdi1ZgVjsRSnX+96vg=
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb h1:5WkOssTWl6Tv2H7VFb2jwB08A7BxxNCebkkpvz1PzrY=
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/goshutdown v0.1.0 h1:lmwnygdXtnr2pa6VqfR/bm8077/BnBef1+7CP96B7Sw=
github.com/qdm12/goshutdown v0.1.0/go.mod h1:/LP3MWLqI+wGH/ijfaUG+RHzBbKXIiVKnrg5vXOCf6Q=
github.com/qdm12/ss-server v0.2.0 h1:+togLzeeLAJ68MD1JqOWvYi9rl9t/fx1Qh7wKzZhY1g=
github.com/qdm12/ss-server v0.2.0/go.mod h1:+1bWO1EfWNvsGM5Cuep6vneChK2OHniqtAsED9Fh1y0=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -130,14 +82,12 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 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=
@@ -145,87 +95,50 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
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=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-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 h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
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/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 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
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-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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.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-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -234,14 +147,7 @@ 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/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=
@@ -251,9 +157,7 @@ gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQb
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 h1:rzDy/tC8LjEdN94+i0Bu22tTo/qE9cvhKyfD0HMU0NU=
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=

View File

@@ -1,21 +1,26 @@
// Package alpine defines a configurator to interact with the Alpine operating system.
package alpine
import (
"os/user"
"context"
"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)
Version(ctx context.Context) (version 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

@@ -13,9 +13,9 @@ var (
)
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
func (c *configurator) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)
u, err := a.lookupID(UIDStr)
u, err := c.osUser.LookupID(UIDStr)
_, unknownUID := err.(user.UnknownUserIdError)
if err != nil && !unknownUID {
return "", err
@@ -28,7 +28,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
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
@@ -39,7 +39,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
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
}

View File

@@ -7,8 +7,8 @@ import (
"strings"
)
func (a *Alpine) Version(ctx context.Context) (version string, err error) {
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
func (c *configurator) Version(ctx context.Context) (version string, err error) {
file, err := c.openFile("/etc/alpine-release", os.O_RDONLY, 0)
if err != nil {
return "", err
}

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,22 @@
// Package cli defines an interface CLI to run command line operations.
package cli
type CLI struct {
repoServersPath string
import (
"context"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type CLI interface {
ClientKey(args []string, openFile os.OpenFileFunc) error
HealthCheck(ctx context.Context) error
OpenvpnConfig(os os.OS, logger logging.Logger) error
Update(ctx context.Context, args []string, os os.OS, logger logging.Logger) error
}
func New() *CLI {
return &CLI{
repoServersPath: "./internal/storage/servers.json",
}
type cli struct{}
func New() CLI {
return &cli{}
}

View File

@@ -4,19 +4,19 @@ import (
"flag"
"fmt"
"io"
"os"
"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", 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
}

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,36 @@
package cli
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"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/storage"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/params"
)
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, logger logging.Logger) error {
var allSettings configuration.Settings
err := allSettings.Read(params.NewEnv(), os, logger)
if err != nil {
return err
}
allSettings, err := source.Read()
allServers, err := storage.New(logger, os, constants.ServersData).
SyncServers(constants.GetAllServers())
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)
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.OpenVPNConfig(connection, allSettings.VPN.OpenVPN)
lines := providerConf.BuildConf(connection, "nonroortuser", allSettings.OpenVPN)
fmt.Println(strings.Join(lines, "\n"))
return nil
}

View File

@@ -6,95 +6,69 @@ import (
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration"
"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/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")
ErrNoFileOrStdoutFlag = errors.New("at least one of -file or -stdout must be specified")
ErrSyncServers = errors.New("cannot sync hardcoded and persisted servers")
ErrUpdateServerInformation = errors.New("cannot update server information")
ErrWriteToFile = errors.New("cannot write updated information to file")
)
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(ctx context.Context, args []string, os os.OS, logger logging.Logger) error {
options := configuration.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.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", "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(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers")
flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers")
flagSet.BoolVar(&options.Ipvanish, "ipvanish", false, "Update IpVanish servers")
flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN 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.Privatevpn, "privatevpn", false, "Update Private VPN servers")
flagSet.BoolVar(&options.Protonvpn, "protonvpn", false, "Update Protonvpn servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers")
flagSet.BoolVar(&options.VPNUnlimited, "vpnunlimited", false, "Update VPN Unlimited 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()
if err != nil {
return fmt.Errorf("options validation failed: %w", 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 ErrNoFileOrStdoutFlag
}
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("%w: %s", ErrSyncServers, 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 fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
}
if flushToFile {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
}
}

View File

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

View File

@@ -0,0 +1,3 @@
// Package configuration reads initial settings from environment variables
// and secret files.
package configuration

View File

@@ -0,0 +1,6 @@
package configuration
const (
lastIndent = "|--"
indent = " "
)

View File

@@ -0,0 +1,70 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) cyberghostLines() (lines []string) {
lines = append(lines, lastIndent+"Server group: "+settings.ServerSelection.Group)
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if settings.ExtraConfigOptions.ClientKey != "" {
lines = append(lines, lastIndent+"Client key is set")
}
if settings.ExtraConfigOptions.ClientCertificate != "" {
lines = append(lines, lastIndent+"Client certificate is set")
}
return lines
}
func (settings *Provider) readCyberghost(r reader) (err error) {
settings.Name = constants.Cyberghost
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ExtraConfigOptions.ClientKey, err = readClientKey(r)
if err != nil {
return err
}
settings.ExtraConfigOptions.ClientCertificate, err = readClientCertificate(r)
if err != nil {
return err
}
settings.ServerSelection.Group, err = r.env.Inside("CYBERGHOST_GROUP",
constants.CyberghostGroupChoices(), params.Default("Premium UDP Europe"))
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.CyberghostRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.CyberghostHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,117 @@
package configuration
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/golibs/params"
)
// DNS contains settings to configure Unbound for DNS over TLS operation.
type DNS struct { //nolint:maligned
Enabled bool
PlaintextAddress net.IP
KeepNameserver bool
UpdatePeriod time.Duration
Unbound unbound.Settings
BlacklistBuild blacklist.BuilderSettings
}
func (settings *DNS) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *DNS) lines() (lines []string) {
lines = append(lines, lastIndent+"DNS:")
if settings.PlaintextAddress != nil {
lines = append(lines, indent+lastIndent+"Plaintext address: "+settings.PlaintextAddress.String())
}
if settings.KeepNameserver {
lines = append(lines, indent+lastIndent+"Keep nameserver (disabled blocking): yes")
}
if !settings.Enabled {
return lines
}
lines = append(lines, indent+lastIndent+"DNS over TLS:")
lines = append(lines, indent+indent+lastIndent+"Unbound:")
for _, line := range settings.Unbound.Lines() {
lines = append(lines, indent+indent+indent+line)
}
lines = append(lines, indent+indent+lastIndent+"Blacklist:")
for _, line := range settings.BlacklistBuild.Lines(indent, lastIndent) {
lines = append(lines, indent+indent+indent+line)
}
if settings.UpdatePeriod > 0 {
lines = append(lines, indent+indent+lastIndent+"Update: every "+settings.UpdatePeriod.String())
}
return lines
}
var (
ErrUnboundSettings = errors.New("failed getting Unbound settings")
ErrBlacklistSettings = errors.New("failed getting DNS blacklist settings")
)
func (settings *DNS) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("DOT", params.Default("on"))
if err != nil {
return err
}
// Plain DNS settings
if err := settings.readDNSPlaintext(r.env); err != nil {
return err
}
settings.KeepNameserver, err = r.env.OnOff("DNS_KEEP_NAMESERVER", params.Default("off"))
if err != nil {
return err
}
// DNS over TLS external settings
if err := settings.readBlacklistBuilding(r); err != nil {
return fmt.Errorf("%w: %s", ErrBlacklistSettings, err)
}
settings.UpdatePeriod, err = r.env.Duration("DNS_UPDATE_PERIOD", params.Default("24h"))
if err != nil {
return err
}
// Unbound settings
if err := settings.readUnbound(r); err != nil {
return fmt.Errorf("%w: %s", ErrUnboundSettings, err)
}
return nil
}
var (
ErrDNSAddressNotAnIP = errors.New("DNS plaintext address is not an IP address")
)
func (settings *DNS) readDNSPlaintext(env params.Env) error {
s, err := env.Get("DNS_PLAINTEXT_ADDRESS", params.Default("1.1.1.1"))
if err != nil {
return err
}
settings.PlaintextAddress = net.ParseIP(s)
if settings.PlaintextAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddressNotAnIP, s)
}
return nil
}

View File

@@ -0,0 +1,76 @@
package configuration
import (
"net"
"testing"
"time"
"github.com/qdm12/dns/pkg/blacklist"
"github.com/qdm12/dns/pkg/provider"
"github.com/qdm12/dns/pkg/unbound"
"github.com/stretchr/testify/assert"
)
func Test_DNS_Lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings DNS
lines []string
}{
"disabled DOT": {
settings: DNS{
PlaintextAddress: net.IP{1, 1, 1, 1},
},
lines: []string{
"|--DNS:",
" |--Plaintext address: 1.1.1.1",
},
},
"enabled DOT": {
settings: DNS{
Enabled: true,
KeepNameserver: true,
Unbound: unbound.Settings{
Providers: []provider.Provider{
provider.Cloudflare(),
},
},
BlacklistBuild: blacklist.BuilderSettings{
BlockMalicious: true,
BlockAds: true,
BlockSurveillance: true,
},
UpdatePeriod: time.Hour,
},
lines: []string{
"|--DNS:",
" |--Keep nameserver (disabled blocking): yes",
" |--DNS over TLS:",
" |--Unbound:",
" |--DNS over TLS providers:",
" |--Cloudflare",
" |--Listening port: 0",
" |--Access control:",
" |--Allowed:",
" |--Caching: disabled",
" |--IPv4 resolution: disabled",
" |--IPv6 resolution: disabled",
" |--Verbosity level: 0/5",
" |--Verbosity details level: 0/4",
" |--Validation log level: 0/2",
" |--Username: ",
" |--Blacklist:",
" |--Blocked categories: malicious, surveillance, ads",
" |--Update: every 1h0m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}

View File

@@ -0,0 +1,87 @@
package configuration
import (
"errors"
"fmt"
"github.com/qdm12/golibs/params"
"inet.af/netaddr"
)
func (settings *DNS) readBlacklistBuilding(r reader) (err error) {
settings.BlacklistBuild.BlockMalicious, err = r.env.OnOff("BLOCK_MALICIOUS", params.Default("on"))
if err != nil {
return err
}
settings.BlacklistBuild.BlockSurveillance, err = r.env.OnOff("BLOCK_SURVEILLANCE", params.Default("on"),
params.RetroKeys([]string{"BLOCK_NSA"}, r.onRetroActive))
if err != nil {
return err
}
settings.BlacklistBuild.BlockAds, err = r.env.OnOff("BLOCK_ADS", params.Default("off"))
if err != nil {
return err
}
if err := settings.readPrivateAddresses(r.env); err != nil {
return err
}
return settings.readBlacklistUnblockedHostnames(r)
}
var (
ErrInvalidPrivateAddress = errors.New("private address is not a valid IP or CIDR range")
)
func (settings *DNS) readPrivateAddresses(env params.Env) (err error) {
privateAddresses, err := env.CSV("DOT_PRIVATE_ADDRESS")
if err != nil {
return err
} else if len(privateAddresses) == 0 {
return nil
}
ips := make([]netaddr.IP, 0, len(privateAddresses))
ipPrefixes := make([]netaddr.IPPrefix, 0, len(privateAddresses))
for _, address := range privateAddresses {
ip, err := netaddr.ParseIP(address)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netaddr.ParseIPPrefix(address)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return fmt.Errorf("%w: %s", ErrInvalidPrivateAddress, address)
}
settings.BlacklistBuild.AddBlockedIPs = append(settings.BlacklistBuild.AddBlockedIPs, ips...)
settings.BlacklistBuild.AddBlockedIPPrefixes = append(settings.BlacklistBuild.AddBlockedIPPrefixes, ipPrefixes...)
return nil
}
func (settings *DNS) readBlacklistUnblockedHostnames(r reader) (err error) {
hostnames, err := r.env.CSV("UNBLOCK")
if err != nil {
return err
} else if len(hostnames) == 0 {
return nil
}
for _, hostname := range hostnames {
if !r.regex.MatchHostname(hostname) {
return fmt.Errorf("%w: %s", ErrInvalidHostname, hostname)
}
}
settings.BlacklistBuild.AllowedHosts = append(settings.BlacklistBuild.AllowedHosts, hostnames...)
return nil
}

View File

@@ -0,0 +1,43 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) fastestvpnLines() (lines []string) {
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
return lines
}
func (settings *Provider) readFastestvpn(r reader) (err error) {
settings.Name = constants.Fastestvpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.FastestvpnHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.FastestvpnCountriesChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,89 @@
package configuration
import (
"net"
"strings"
"github.com/qdm12/golibs/params"
)
// Firewall contains settings to customize the firewall operation.
type Firewall struct {
VPNInputPorts []uint16
InputPorts []uint16
OutboundSubnets []net.IPNet
Enabled bool
Debug bool
}
func (settings *Firewall) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Firewall) lines() (lines []string) {
if !settings.Enabled {
lines = append(lines, lastIndent+"Firewall: disabled ⚠️")
return lines
}
lines = append(lines, lastIndent+"Firewall:")
if settings.Debug {
lines = append(lines, indent+lastIndent+"Debug: on")
}
if len(settings.VPNInputPorts) > 0 {
lines = append(lines, indent+lastIndent+"VPN input ports: "+
strings.Join(uint16sToStrings(settings.VPNInputPorts), ", "))
}
if len(settings.InputPorts) > 0 {
lines = append(lines, indent+lastIndent+"Input ports: "+
strings.Join(uint16sToStrings(settings.InputPorts), ", "))
}
if len(settings.OutboundSubnets) > 0 {
lines = append(lines, indent+lastIndent+"Outbound subnets: "+
strings.Join(ipNetsToStrings(settings.OutboundSubnets), ", "))
}
return lines
}
func (settings *Firewall) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("FIREWALL", params.Default("on"))
if err != nil {
return err
}
settings.Debug, err = r.env.OnOff("FIREWALL_DEBUG", params.Default("off"))
if err != nil {
return err
}
if err := settings.readVPNInputPorts(r.env); err != nil {
return err
}
if err := settings.readInputPorts(r.env); err != nil {
return err
}
return settings.readOutboundSubnets(r)
}
func (settings *Firewall) readVPNInputPorts(env params.Env) (err error) {
settings.VPNInputPorts, err = readCSVPorts(env, "FIREWALL_VPN_INPUT_PORTS")
return err
}
func (settings *Firewall) readInputPorts(env params.Env) (err error) {
settings.InputPorts, err = readCSVPorts(env, "FIREWALL_INPUT_PORTS")
return err
}
func (settings *Firewall) readOutboundSubnets(r reader) (err error) {
retroOption := params.RetroKeys([]string{"EXTRA_SUBNETS"}, r.onRetroActive)
settings.OutboundSubnets, err = readCSVIPNets(r.env, "FIREWALL_OUTBOUND_SUBNETS", retroOption)
return err
}

View File

@@ -0,0 +1,61 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) hideMyAssLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readHideMyAss(r reader) (err error) {
settings.Name = constants.HideMyAss
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.HideMyAssCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.HideMyAssCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.HideMyAssCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.HideMyAssHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,105 @@
package configuration
import (
"strconv"
"strings"
"github.com/qdm12/golibs/params"
)
// HTTPProxy contains settings to configure the HTTP proxy.
type HTTPProxy struct {
User string
Password string
Port uint16
Enabled bool
Stealth bool
Log bool
}
func (settings *HTTPProxy) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *HTTPProxy) lines() (lines []string) {
if !settings.Enabled {
return nil
}
lines = append(lines, lastIndent+"HTTP proxy:")
lines = append(lines, indent+lastIndent+"Port: "+strconv.Itoa(int(settings.Port)))
if settings.User != "" {
lines = append(lines, indent+lastIndent+"Authentication: enabled")
}
if settings.Log {
lines = append(lines, indent+lastIndent+"Log: enabled")
}
if settings.Stealth {
lines = append(lines, indent+lastIndent+"Stealth: enabled")
}
return lines
}
func (settings *HTTPProxy) read(r reader) (err error) {
settings.Enabled, err = r.env.OnOff("HTTPPROXY", params.Default("off"),
params.RetroKeys([]string{"TINYPROXY", "PROXY"}, r.onRetroActive))
if err != nil {
return err
}
settings.User, err = r.getFromEnvOrSecretFile("HTTPPROXY_USER", false, // compulsory
[]string{"TINYPROXY_USER", "PROXY_USER"})
if err != nil {
return err
}
settings.Password, err = r.getFromEnvOrSecretFile("HTTPPROXY_PASSWORD", false,
[]string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"})
if err != nil {
return err
}
settings.Stealth, err = r.env.OnOff("HTTPPROXY_STEALTH", params.Default("off"))
if err != nil {
return err
}
if err := settings.readLog(r); err != nil {
return err
}
var warning string
settings.Port, warning, err = r.env.ListeningPort("HTTPPROXY_PORT", params.Default("8888"),
params.RetroKeys([]string{"TINYPROXY_PORT", "PROXY_PORT"}, r.onRetroActive))
if len(warning) > 0 {
r.logger.Warn(warning)
}
if err != nil {
return err
}
return nil
}
func (settings *HTTPProxy) readLog(r reader) error {
s, err := r.env.Get("HTTPPROXY_LOG",
params.RetroKeys([]string{"PROXY_LOG_LEVEL", "TINYPROXY_LOG"}, r.onRetroActive))
if err != nil {
return err
}
switch strings.ToLower(s) {
case "on":
settings.Log = true
// Retro compatibility
case "info", "connect", "notice":
settings.Log = true
}
return nil
}

View File

@@ -0,0 +1,52 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) ipvanishLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readIpvanish(r reader) (err error) {
settings.Name = constants.Ipvanish
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IpvanishCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IpvanishCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,192 @@
package configuration
import (
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Provider_ipvanishLines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Provider
lines []string
}{
"empty settings": {},
"full settings": {
settings: Provider{
ServerSelection: ServerSelection{
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
lines: []string{
"|--Countries: A, B",
"|--Cities: C, D",
"|--Hostnames: E, F",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.ipvanishLines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_Provider_readIpvanish(t *testing.T) {
t.Parallel()
var errDummy = errors.New("dummy test error")
type singleStringCall struct {
call bool
value string
err error
}
type sliceStringCall struct {
call bool
values []string
err error
}
testCases := map[string]struct {
protocol singleStringCall
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
hostnames sliceStringCall
settings Provider
err error
}{
"protocol error": {
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"target IP error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"countries error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"cities error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"hostnames error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"default settings": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
settings: Provider{
Name: constants.Ipvanish,
},
},
"set settings": {
protocol: singleStringCall{call: true, value: constants.TCP},
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
TCP: true,
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockEnv(ctrl)
if testCase.protocol.call {
env.EXPECT().Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
if testCase.targetIP.call {
env.EXPECT().Get("OPENVPN_TARGET_IP").
Return(testCase.targetIP.value, testCase.targetIP.err)
}
if testCase.countries.call {
env.EXPECT().CSVInside("COUNTRY", constants.IpvanishCountryChoices()).
Return(testCase.countries.values, testCase.countries.err)
}
if testCase.cities.call {
env.EXPECT().CSVInside("CITY", constants.IpvanishCityChoices()).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices()).
Return(testCase.hostnames.values, testCase.hostnames.err)
}
r := reader{env: env}
var settings Provider
err := settings.readIpvanish(r)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.settings, settings)
})
}
}

View File

@@ -0,0 +1,52 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) ivpnLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readIvpn(r reader) (err error) {
settings.Name = constants.Ivpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IvpnCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IvpnCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,192 @@
package configuration
import (
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Provider_ivpnLines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Provider
lines []string
}{
"empty settings": {},
"full settings": {
settings: Provider{
ServerSelection: ServerSelection{
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
lines: []string{
"|--Countries: A, B",
"|--Cities: C, D",
"|--Hostnames: E, F",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.ivpnLines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_Provider_readIvpn(t *testing.T) {
t.Parallel()
var errDummy = errors.New("dummy test error")
type singleStringCall struct {
call bool
value string
err error
}
type sliceStringCall struct {
call bool
values []string
err error
}
testCases := map[string]struct {
protocol singleStringCall
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
hostnames sliceStringCall
settings Provider
err error
}{
"protocol error": {
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errDummy,
},
"target IP error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errDummy,
},
"countries error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errDummy,
},
"cities error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errDummy,
},
"hostnames error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ivpn,
},
err: errDummy,
},
"default settings": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
settings: Provider{
Name: constants.Ivpn,
},
},
"set settings": {
protocol: singleStringCall{call: true, value: constants.TCP},
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
settings: Provider{
Name: constants.Ivpn,
ServerSelection: ServerSelection{
TCP: true,
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockEnv(ctrl)
if testCase.protocol.call {
env.EXPECT().Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
if testCase.targetIP.call {
env.EXPECT().Get("OPENVPN_TARGET_IP").
Return(testCase.targetIP.value, testCase.targetIP.err)
}
if testCase.countries.call {
env.EXPECT().CSVInside("COUNTRY", constants.IvpnCountryChoices()).
Return(testCase.countries.values, testCase.countries.err)
}
if testCase.cities.call {
env.EXPECT().CSVInside("CITY", constants.IvpnCityChoices()).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices()).
Return(testCase.hostnames.values, testCase.hostnames.err)
}
r := reader{env: env}
var settings Provider
err := settings.readIvpn(r)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.settings, settings)
})
}
}

View File

@@ -0,0 +1,55 @@
package configuration
import (
"encoding/pem"
"errors"
"strings"
"github.com/qdm12/gluetun/internal/constants"
)
func readClientKey(r reader) (clientKey string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", constants.ClientKey)
if err != nil {
return "", err
}
return extractClientKey(b)
}
var errDecodePEMBlockClientKey = errors.New("cannot decode PEM block from client key")
func extractClientKey(b []byte) (key string, err error) {
pemBlock, _ := pem.Decode(b)
if pemBlock == nil {
return "", errDecodePEMBlockClientKey
}
parsedBytes := pem.EncodeToMemory(pemBlock)
s := string(parsedBytes)
s = strings.ReplaceAll(s, "\n", "")
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
return s, nil
}
func readClientCertificate(r reader) (clientCertificate string, err error) {
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", constants.ClientCertificate)
if err != nil {
return "", err
}
return extractClientCertificate(b)
}
var errDecodePEMBlockClientCert = errors.New("cannot decode PEM block from client certificate")
func extractClientCertificate(b []byte) (certificate string, err error) {
pemBlock, _ := pem.Decode(b)
if pemBlock == nil {
return "", errDecodePEMBlockClientCert
}
parsedBytes := pem.EncodeToMemory(pemBlock)
s := string(parsedBytes)
s = strings.ReplaceAll(s, "\n", "")
s = strings.TrimPrefix(s, "-----BEGIN CERTIFICATE-----")
s = strings.TrimSuffix(s, "-----END CERTIFICATE-----")
return s, nil
}

View File

@@ -0,0 +1,174 @@
package configuration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_extractClientKey(t *testing.T) {
t.Parallel()
const validPEM = `
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxX
ywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL
2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlG
AB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+Hx
Ymy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVo
rV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/
lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChU
i2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ez
L19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4L
O66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdr
pimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3
bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyM
yqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqS
xrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+
iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y
0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBW
IsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPu
K5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9Fkrp
B6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hE
rVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1N
KvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hn
kIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C1
12Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgK
VwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+s
qsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jb
kh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhz
nQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sG
mw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4
TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMe
xFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0
SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oe
o8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscP
yFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99
OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYy
zNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQi
S5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5
///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4Y
JLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3
z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSX
yQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9Ulw
khtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzR
nULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR
7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn
2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopu
AnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh
8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4Kxdi
JKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggyn
q00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT
+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZ
zZjrL52saevO25cigVl+hxcnY8DTpbk=
-----END PRIVATE KEY-----
`
const validKeyString = "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxXywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlGAB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+HxYmy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVorV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChUi2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ezL19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4LO66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdrpimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyMyqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqSxrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBWIsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPuK5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9FkrpB6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hErVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1NKvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hnkIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C112Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgKVwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+sqsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jbkh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhznQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sGmw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMexFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oeo8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscPyFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYyzNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQiS5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4YJLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSXyQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9UlwkhtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzRnULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopuAnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4KxdiJKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggynq00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZzZjrL52saevO25cigVl+hxcnY8DTpbk=" //nolint:lll
testCases := map[string]struct {
b []byte
key string
err error
}{
"no input": {
err: errDecodePEMBlockClientKey,
},
"bad input": {
b: []byte{1, 2, 3},
err: errDecodePEMBlockClientKey,
},
"valid key": {
b: []byte(validPEM),
key: validKeyString,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
key, err := extractClientKey(testCase.b)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.key, key)
})
}
}
func Test_extractClientCertificate(t *testing.T) {
t.Parallel()
const validPEM = `
-----BEGIN CERTIFICATE-----
MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJS
TzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4x
GzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5m
b0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlow
fTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3li
ZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8G
CSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJT
NZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E
7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPh
EzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXm
PFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0D
Jt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAz
KI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ
2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8
SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVH
euH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOi
O2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMC
AwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIB
DQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCG
SAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0G
A1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYT
AlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5B
LjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJp
bmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEA
ystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmP
Pb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDao
hQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6
lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+v
r54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORh
obElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNF
bcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0Uyb
aobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQ
rRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMud
xXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1Tp
iOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=
-----END CERTIFICATE-----
`
const validCertificateString = "MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJSTzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4xGzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlowfTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3liZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJTNZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPhEzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXmPFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0DJt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAzKI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVHeuH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOiO2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMCAwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIBDQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCGSAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0GA1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYTAlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5BLjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJpbmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEAystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmPPb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDaohQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+vr54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORhobElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNFbcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0UybaobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQrRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMudxXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1TpiOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=" //nolint:lll
testCases := map[string]struct {
b []byte
certificate string
err error
}{
"no input": {
err: errDecodePEMBlockClientCert,
},
"bad input": {
b: []byte{1, 2, 3},
err: errDecodePEMBlockClientCert,
},
"valid key": {
b: []byte(validPEM),
certificate: validCertificateString,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
certificate, err := extractClientCertificate(testCase.b)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.certificate, certificate)
})
}
}

View File

@@ -0,0 +1,22 @@
package configuration
import (
"net"
"strconv"
)
func uint16sToStrings(uint16s []uint16) (strings []string) {
strings = make([]string, len(uint16s))
for i := range uint16s {
strings[i] = strconv.Itoa(int(uint16s[i]))
}
return strings
}
func ipNetsToStrings(ipNets []net.IPNet) (strings []string) {
strings = make([]string, len(ipNets))
for i := range ipNets {
strings[i] = ipNets[i].String()
}
return strings
}

View File

@@ -0,0 +1,88 @@
package configuration
import (
"strconv"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) mullvadLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if len(settings.ServerSelection.ISPs) > 0 {
lines = append(lines, lastIndent+"ISPs: "+commaJoin(settings.ServerSelection.ISPs))
}
if settings.ServerSelection.CustomPort > 0 {
lines = append(lines, lastIndent+"Custom port: "+strconv.Itoa(int(settings.ServerSelection.CustomPort)))
}
if settings.ExtraConfigOptions.OpenVPNIPv6 {
lines = append(lines, lastIndent+"IPv6: enabled")
}
return lines
}
func (settings *Provider) readMullvad(r reader) (err error) {
settings.Name = constants.Mullvad
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.MullvadCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.MullvadCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.MullvadHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.MullvadISPChoices())
if err != nil {
return err
}
settings.ServerSelection.CustomPort, err = readCustomPort(r.env, settings.ServerSelection.TCP,
[]uint16{80, 443, 1401}, []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400})
if err != nil {
return err
}
settings.ServerSelection.Owned, err = r.env.YesNo("OWNED", params.Default("no"))
if err != nil {
return err
}
settings.ExtraConfigOptions.OpenVPNIPv6, err = r.env.OnOff("OPENVPN_IPV6", params.Default("off"))
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,91 @@
package configuration
import (
"fmt"
"strconv"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) nordvpnLines() (lines []string) {
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if len(settings.ServerSelection.Names) > 0 {
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Hostnames))
}
if numbersUint16 := settings.ServerSelection.Numbers; len(numbersUint16) > 0 {
numbersString := make([]string, len(numbersUint16))
for i, numberUint16 := range numbersUint16 {
numbersString[i] = strconv.Itoa(int(numberUint16))
}
lines = append(lines, lastIndent+"Numbers: "+commaJoin(numbersString))
}
return lines
}
func (settings *Provider) readNordvpn(r reader) (err error) {
settings.Name = constants.Nordvpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.NordvpnRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.NordvpnHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.NordvpnHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.Numbers, err = readNordVPNServerNumbers(r.env)
if err != nil {
return err
}
return nil
}
func readNordVPNServerNumbers(env params.Env) (numbers []uint16, err error) {
const possiblePortsCount = 65537
possibilities := make([]string, possiblePortsCount)
for i := range possibilities {
possibilities[i] = fmt.Sprintf("%d", i)
}
possibilities[65536] = ""
values, err := env.CSVInside("SERVER_NUMBER", possibilities)
if err != nil {
return nil, err
}
numbers = make([]uint16, len(values))
for i := range values {
n, err := strconv.Atoi(values[i])
if err != nil {
return nil, err
}
numbers[i] = uint16(n)
}
return numbers, nil
}

View File

@@ -0,0 +1,190 @@
package configuration
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
// OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct {
User string `json:"user"`
Password string `json:"password"`
Verbosity int `json:"verbosity"`
MSSFix uint16 `json:"mssfix"`
Root bool `json:"run_as_root"`
Cipher string `json:"cipher"`
Auth string `json:"auth"`
Provider Provider `json:"provider"`
Config string `json:"custom_config"`
Version string `json:"version"`
}
func (settings *OpenVPN) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *OpenVPN) lines() (lines []string) {
lines = append(lines, lastIndent+"OpenVPN:")
lines = append(lines, indent+lastIndent+"Version: "+settings.Version)
lines = append(lines, indent+lastIndent+"Verbosity level: "+strconv.Itoa(settings.Verbosity))
if settings.Root {
lines = append(lines, indent+lastIndent+"Run as root: enabled")
}
if len(settings.Cipher) > 0 {
lines = append(lines, indent+lastIndent+"Custom cipher: "+settings.Cipher)
}
if len(settings.Auth) > 0 {
lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth)
}
if len(settings.Config) > 0 {
lines = append(lines, indent+lastIndent+"Custom configuration: "+settings.Config)
}
if settings.Provider.Name == "" {
lines = append(lines, indent+lastIndent+"Provider: custom configuration")
} else {
lines = append(lines, indent+lastIndent+"Provider:")
for _, line := range settings.Provider.lines() {
lines = append(lines, indent+indent+line)
}
}
return lines
}
var (
ErrInvalidVPNProvider = errors.New("invalid VPN provider")
)
func (settings *OpenVPN) read(r reader) (err error) {
vpnsp, err := r.env.Inside("VPNSP", []string{
"cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn",
"privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"},
params.Default("private internet access"))
if err != nil {
return err
}
if vpnsp == "pia" { // retro compatibility
vpnsp = "private internet access"
}
settings.Provider.Name = vpnsp
settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue())
if err != nil {
return err
}
customConfig := settings.Config != ""
if customConfig {
settings.Provider.Name = ""
}
credentialsRequired := !customConfig && settings.Provider.Name != constants.VPNUnlimited
settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"})
if err != nil {
return err
}
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
settings.User = strings.ReplaceAll(settings.User, " ", "")
if settings.Provider.Name == constants.Mullvad {
settings.Password = "m"
} else {
settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", credentialsRequired, []string{"PASSWORD"})
if err != nil {
return err
}
}
settings.Version, err = r.env.Inside("OPENVPN_VERSION",
[]string{constants.Openvpn24, constants.Openvpn25}, params.Default(constants.Openvpn25))
if err != nil {
return err
}
settings.Verbosity, err = r.env.IntRange("OPENVPN_VERBOSITY", 0, 6, params.Default("1")) //nolint:gomnd
if err != nil {
return err
}
settings.Root, err = r.env.YesNo("OPENVPN_ROOT", params.Default("yes"))
if err != nil {
return err
}
settings.Cipher, err = r.env.Get("OPENVPN_CIPHER")
if err != nil {
return err
}
settings.Auth, err = r.env.Get("OPENVPN_AUTH")
if err != nil {
return err
}
const maxMSSFix = 10000
mssFix, err := r.env.IntRange("OPENVPN_MSSFIX", 0, maxMSSFix, params.Default("0"))
if err != nil {
return err
}
settings.MSSFix = uint16(mssFix)
return settings.readProvider(r)
}
func (settings *OpenVPN) readProvider(r reader) error {
var readProvider func(r reader) error
switch settings.Provider.Name {
case "": // custom config
readProvider = func(r reader) error { return nil }
case constants.Cyberghost:
readProvider = settings.Provider.readCyberghost
case constants.Fastestvpn:
readProvider = settings.Provider.readFastestvpn
case constants.HideMyAss:
readProvider = settings.Provider.readHideMyAss
case constants.Ipvanish:
readProvider = settings.Provider.readIpvanish
case constants.Ivpn:
readProvider = settings.Provider.readIvpn
case constants.Mullvad:
readProvider = settings.Provider.readMullvad
case constants.Nordvpn:
readProvider = settings.Provider.readNordvpn
case constants.Privado:
readProvider = settings.Provider.readPrivado
case constants.PrivateInternetAccess:
readProvider = settings.Provider.readPrivateInternetAccess
case constants.Privatevpn:
readProvider = settings.Provider.readPrivatevpn
case constants.Protonvpn:
readProvider = settings.Provider.readProtonvpn
case constants.Purevpn:
readProvider = settings.Provider.readPurevpn
case constants.Surfshark:
readProvider = settings.Provider.readSurfshark
case constants.Torguard:
readProvider = settings.Provider.readTorguard
case constants.VPNUnlimited:
readProvider = settings.Provider.readVPNUnlimited
case constants.Vyprvpn:
readProvider = settings.Provider.readVyprvpn
case constants.Windscribe:
readProvider = settings.Provider.readWindscribe
default:
return fmt.Errorf("%w: %s", ErrInvalidVPNProvider, settings.Provider.Name)
}
return readProvider(r)
}

View File

@@ -0,0 +1,63 @@
package configuration
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_OpenVPN_JSON(t *testing.T) {
t.Parallel()
in := OpenVPN{
Root: true,
Provider: Provider{
Name: "name",
},
}
data, err := json.MarshalIndent(in, "", " ")
require.NoError(t, err)
assert.Equal(t, `{
"user": "",
"password": "",
"verbosity": 0,
"mssfix": 0,
"run_as_root": true,
"cipher": "",
"auth": "",
"provider": {
"name": "name",
"server_selection": {
"tcp": false,
"regions": null,
"group": "",
"countries": null,
"cities": null,
"hostnames": null,
"names": null,
"isps": null,
"owned": false,
"custom_port": 0,
"numbers": null,
"encryption_preset": "",
"free_only": false,
"stream_only": false
},
"extra_config": {
"encryption_preset": "",
"openvpn_ipv6": false
},
"port_forwarding": {
"enabled": false,
"filepath": ""
}
},
"custom_config": "",
"version": ""
}`, string(data))
var out OpenVPN
err = json.Unmarshal(data, &out)
require.NoError(t, err)
assert.Equal(t, in, out)
}

View File

@@ -0,0 +1,61 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) privadoLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readPrivado(r reader) (err error) {
settings.Name = constants.Privado
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivadoCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PrivadoRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivadoCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivadoHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,97 @@
package configuration
import (
"strconv"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) privateinternetaccessLines() (lines []string) {
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if len(settings.ServerSelection.Names) > 0 {
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Names))
}
lines = append(lines, lastIndent+"Encryption preset: "+settings.ServerSelection.EncryptionPreset)
if settings.ServerSelection.CustomPort > 0 {
lines = append(lines, lastIndent+"Custom port: "+strconv.Itoa(int(settings.ServerSelection.CustomPort)))
}
if settings.PortForwarding.Enabled {
lines = append(lines, lastIndent+"Port forwarding:")
for _, line := range settings.PortForwarding.lines() {
lines = append(lines, indent+line)
}
}
return lines
}
func (settings *Provider) readPrivateInternetAccess(r reader) (err error) {
settings.Name = constants.PrivateInternetAccess
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
encryptionPreset, err := r.env.Inside("PIA_ENCRYPTION",
[]string{constants.PIAEncryptionPresetNone, constants.PIAEncryptionPresetNormal, constants.PIAEncryptionPresetStrong},
params.RetroKeys([]string{"ENCRYPTION"}, r.onRetroActive),
params.Default(constants.PIACertificateStrong),
)
if err != nil {
return err
}
settings.ServerSelection.EncryptionPreset = encryptionPreset
settings.ExtraConfigOptions.EncryptionPreset = encryptionPreset
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PIAGeoChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PIAHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_NAME", constants.PIANameChoices())
if err != nil {
return err
}
settings.ServerSelection.CustomPort, err = readPortOrZero(r.env, "PORT")
if err != nil {
return err
}
settings.PortForwarding.Enabled, err = r.env.OnOff("PORT_FORWARDING", params.Default("off"))
if err != nil {
return err
}
if settings.PortForwarding.Enabled {
settings.PortForwarding.Filepath, err = r.env.Path("PORT_FORWARDING_STATUS_FILE",
params.Default("/tmp/gluetun/forwarded_port"), params.CaseSensitiveValue())
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) privatevpnLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readPrivatevpn(r reader) (err error) {
settings.Name = constants.Privatevpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivatevpnCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivatevpnCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivatevpnHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,85 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
func (settings *Provider) protonvpnLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Names) > 0 {
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Names))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
if settings.ServerSelection.FreeOnly {
lines = append(lines, lastIndent+"Free only: yes")
}
return lines
}
func (settings *Provider) readProtonvpn(r reader) (err error) {
settings.Name = constants.Protonvpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.CustomPort, err = readPortOrZero(r.env, "PORT")
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ProtonvpnCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.ProtonvpnRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ProtonvpnCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.ProtonvpnNameChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.ProtonvpnHostnameChoices())
if err != nil {
return err
}
settings.ServerSelection.FreeOnly, err = r.env.YesNo("FREE_ONLY", params.Default("no"))
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,125 @@
package configuration
import (
"fmt"
"net"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params"
)
// Provider contains settings specific to a VPN provider.
type Provider struct {
Name string `json:"name"`
ServerSelection ServerSelection `json:"server_selection"`
ExtraConfigOptions ExtraConfigOptions `json:"extra_config"`
PortForwarding PortForwarding `json:"port_forwarding"`
}
func (settings *Provider) lines() (lines []string) {
lines = append(lines, lastIndent+strings.Title(settings.Name)+" settings:")
selection := settings.ServerSelection
lines = append(lines, indent+lastIndent+"Network protocol: "+protoToString(selection.TCP))
if selection.TargetIP != nil {
lines = append(lines, indent+lastIndent+"Target IP address: "+selection.TargetIP.String())
}
var providerLines []string
switch strings.ToLower(settings.Name) {
case "cyberghost":
providerLines = settings.cyberghostLines()
case "fastestvpn":
providerLines = settings.fastestvpnLines()
case "hidemyass":
providerLines = settings.hideMyAssLines()
case "ipvanish":
providerLines = settings.ipvanishLines()
case "ivpn":
providerLines = settings.ivpnLines()
case "mullvad":
providerLines = settings.mullvadLines()
case "nordvpn":
providerLines = settings.nordvpnLines()
case "privado":
providerLines = settings.privadoLines()
case "privatevpn":
providerLines = settings.privatevpnLines()
case "private internet access":
providerLines = settings.privateinternetaccessLines()
case "protonvpn":
providerLines = settings.protonvpnLines()
case "purevpn":
providerLines = settings.purevpnLines()
case "surfshark":
providerLines = settings.surfsharkLines()
case "torguard":
providerLines = settings.torguardLines()
case strings.ToLower(constants.VPNUnlimited):
providerLines = settings.vpnUnlimitedLines()
case "vyprvpn":
providerLines = settings.vyprvpnLines()
case "windscribe":
providerLines = settings.windscribeLines()
default:
panic(`Missing lines method for provider "` +
settings.Name + `"! Please create a Github issue.`)
}
for _, line := range providerLines {
lines = append(lines, indent+line)
}
return lines
}
func commaJoin(slice []string) string {
return strings.Join(slice, ", ")
}
func readProtocol(env params.Env) (tcp bool, err error) {
protocol, err := env.Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, params.Default(constants.UDP))
if err != nil {
return false, err
}
return protocol == constants.TCP, nil
}
func protoToString(tcp bool) string {
if tcp {
return constants.TCP
}
return constants.UDP
}
func readTargetIP(env params.Env) (targetIP net.IP, err error) {
return readIP(env, "OPENVPN_TARGET_IP")
}
func readCustomPort(env params.Env, tcp bool,
allowedTCP, allowedUDP []uint16) (port uint16, err error) {
port, err = readPortOrZero(env, "PORT")
if err != nil {
return 0, err
} else if port == 0 {
return 0, nil
}
if tcp {
for i := range allowedTCP {
if allowedTCP[i] == port {
return port, nil
}
}
return 0, fmt.Errorf("%w: port %d for TCP protocol", ErrInvalidPort, port)
}
for i := range allowedUDP {
if allowedUDP[i] == port {
return port, nil
}
}
return 0, fmt.Errorf("%w: port %d for UDP protocol", ErrInvalidPort, port)
}

View File

@@ -0,0 +1,382 @@
package configuration
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errDummy = errors.New("dummy")
func Test_Provider_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Provider
lines []string
}{
"cyberghost": {
settings: Provider{
Name: constants.Cyberghost,
ServerSelection: ServerSelection{
Group: "group",
Regions: []string{"a", "El country"},
},
ExtraConfigOptions: ExtraConfigOptions{
ClientKey: "a",
ClientCertificate: "a",
},
},
lines: []string{
"|--Cyberghost settings:",
" |--Network protocol: udp",
" |--Server group: group",
" |--Regions: a, El country",
" |--Client key is set",
" |--Client certificate is set",
},
},
"fastestvpn": {
settings: Provider{
Name: constants.Fastestvpn,
ServerSelection: ServerSelection{
Hostnames: []string{"a", "b"},
Countries: []string{"c", "d"},
},
},
lines: []string{
"|--Fastestvpn settings:",
" |--Network protocol: udp",
" |--Hostnames: a, b",
" |--Countries: c, d",
},
},
"hidemyass": {
settings: Provider{
Name: constants.HideMyAss,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Hidemyass settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
},
},
"ipvanish": {
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Ipvanish settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
},
},
"ivpn": {
settings: Provider{
Name: constants.Ivpn,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Ivpn settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
},
},
"mullvad": {
settings: Provider{
Name: constants.Mullvad,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
ISPs: []string{"e", "f"},
CustomPort: 1,
},
ExtraConfigOptions: ExtraConfigOptions{
OpenVPNIPv6: true,
},
},
lines: []string{
"|--Mullvad settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--ISPs: e, f",
" |--Custom port: 1",
" |--IPv6: enabled",
},
},
"nordvpn": {
settings: Provider{
Name: constants.Nordvpn,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
Numbers: []uint16{1, 2},
},
},
lines: []string{
"|--Nordvpn settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
" |--Numbers: 1, 2",
},
},
"privado": {
settings: Provider{
Name: constants.Privado,
ServerSelection: ServerSelection{
Hostnames: []string{"a", "b"},
},
},
lines: []string{
"|--Privado settings:",
" |--Network protocol: udp",
" |--Hostnames: a, b",
},
},
"privatevpn": {
settings: Provider{
Name: constants.Privatevpn,
ServerSelection: ServerSelection{
Hostnames: []string{"a", "b"},
Countries: []string{"c", "d"},
Cities: []string{"e", "f"},
},
},
lines: []string{
"|--Privatevpn settings:",
" |--Network protocol: udp",
" |--Countries: c, d",
" |--Cities: e, f",
" |--Hostnames: a, b",
},
},
"protonvpn": {
settings: Provider{
Name: constants.Protonvpn,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Regions: []string{"c", "d"},
Cities: []string{"e", "f"},
Names: []string{"g", "h"},
Hostnames: []string{"i", "j"},
},
},
lines: []string{
"|--Protonvpn settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Regions: c, d",
" |--Cities: e, f",
" |--Names: g, h",
" |--Hostnames: i, j",
},
},
"private internet access": {
settings: Provider{
Name: constants.PrivateInternetAccess,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
EncryptionPreset: constants.PIAEncryptionPresetStrong,
CustomPort: 1,
},
PortForwarding: PortForwarding{
Enabled: true,
Filepath: string("/here"),
},
},
lines: []string{
"|--Private Internet Access settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
" |--Encryption preset: strong",
" |--Custom port: 1",
" |--Port forwarding:",
" |--File path: /here",
},
},
"purevpn": {
settings: Provider{
Name: constants.Purevpn,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
Countries: []string{"c", "d"},
Cities: []string{"e", "f"},
},
},
lines: []string{
"|--Purevpn settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
" |--Countries: c, d",
" |--Cities: e, f",
},
},
"surfshark": {
settings: Provider{
Name: constants.Surfshark,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
},
},
lines: []string{
"|--Surfshark settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
},
},
"torguard": {
settings: Provider{
Name: constants.Torguard,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e"},
},
},
lines: []string{
"|--Torguard settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e",
},
},
constants.VPNUnlimited: {
settings: Provider{
Name: constants.VPNUnlimited,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
FreeOnly: true,
StreamOnly: true,
},
ExtraConfigOptions: ExtraConfigOptions{
ClientKey: "a",
},
},
lines: []string{
"|--Vpn Unlimited settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--Free servers only",
" |--Stream servers only",
" |--Client key is set",
},
},
"vyprvpn": {
settings: Provider{
Name: constants.Vyprvpn,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
},
},
lines: []string{
"|--Vyprvpn settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
},
},
"windscribe": {
settings: Provider{
Name: constants.Windscribe,
ServerSelection: ServerSelection{
Regions: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
CustomPort: 1,
},
},
lines: []string{
"|--Windscribe settings:",
" |--Network protocol: udp",
" |--Regions: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
" |--Custom port: 1",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_readProtocol(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
mockStr string
mockErr error
tcp bool
err error
}{
"error": {
mockErr: errDummy,
err: errDummy,
},
"success": {
mockStr: "tcp",
tcp: true,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockEnv(ctrl)
env.EXPECT().
Inside("PROTOCOL", []string{"tcp", "udp"}, gomock.Any()).
Return(testCase.mockStr, testCase.mockErr)
tcp, err := readProtocol(env)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.tcp, tcp)
})
}
}

View File

@@ -0,0 +1,46 @@
package configuration
import (
"strings"
"time"
"github.com/qdm12/golibs/params"
)
type PublicIP struct {
Period time.Duration `json:"period"`
IPFilepath string `json:"ip_filepath"`
}
func (settings *PublicIP) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *PublicIP) lines() (lines []string) {
if settings.Period == 0 {
lines = append(lines, lastIndent+"Public IP getter: disabled")
return lines
}
lines = append(lines, lastIndent+"Public IP getter:")
lines = append(lines, indent+lastIndent+"Fetch period: "+settings.Period.String())
lines = append(lines, indent+lastIndent+"IP file: "+settings.IPFilepath)
return lines
}
func (settings *PublicIP) read(r reader) (err error) {
settings.Period, err = r.env.Duration("PUBLICIP_PERIOD", params.Default("12h"))
if err != nil {
return err
}
settings.IPFilepath, err = r.env.Path("PUBLICIP_FILE", params.CaseSensitiveValue(),
params.Default("/tmp/gluetun/ip"),
params.RetroKeys([]string{"IP_STATUS_FILE"}, r.onRetroActive))
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,61 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) purevpnLines() (lines []string) {
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readPurevpn(r reader) (err error) {
settings.Name = constants.Purevpn
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PurevpnRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PurevpnCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PurevpnCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PurevpnHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,129 @@
package configuration
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification"
)
type reader struct {
env params.Env
logger logging.Logger
regex verification.Regex
os os.OS
}
func newReader(env params.Env, os os.OS, logger logging.Logger) reader {
return reader{
env: env,
logger: logger,
regex: verification.NewRegex(),
os: os,
}
}
func (r *reader) onRetroActive(oldKey, newKey string) {
r.logger.Warn(
"You are using the old environment variable %s, please consider changing it to %s",
oldKey, newKey,
)
}
var (
ErrInvalidPort = errors.New("invalid port")
)
func readCSVPorts(env params.Env, key string) (ports []uint16, err error) {
s, err := env.Get(key)
if err != nil {
return nil, err
} else if len(s) == 0 {
return nil, nil
}
portsStr := strings.Split(s, ",")
ports = make([]uint16, len(portsStr))
for i, portStr := range portsStr {
portInt, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("%w: %q from environment variable %s: %s",
ErrInvalidPort, portStr, key, err)
} else if portInt <= 0 || portInt > 65535 {
return nil, fmt.Errorf("%w: %d from environment variable %s: must be between 1 and 65535",
ErrInvalidPort, portInt, key)
}
ports[i] = uint16(portInt)
}
return ports, nil
}
var (
ErrInvalidIPNet = errors.New("invalid IP network")
)
func readCSVIPNets(env params.Env, key string, options ...params.OptionSetter) (
ipNets []net.IPNet, err error) {
s, err := env.Get(key, options...)
if err != nil {
return nil, err
} else if s == "" {
return nil, nil
}
ipNetsStr := strings.Split(s, ",")
ipNets = make([]net.IPNet, len(ipNetsStr))
for i, ipNetStr := range ipNetsStr {
_, ipNet, err := net.ParseCIDR(ipNetStr)
if err != nil {
return nil, fmt.Errorf("%w: %q from environment variable %s: %s",
ErrInvalidIPNet, ipNetStr, key, err)
} else if ipNet == nil {
return nil, fmt.Errorf("%w: %q from environment variable %s: subnet is nil",
ErrInvalidIPNet, ipNetStr, key)
}
ipNets[i] = *ipNet
}
return ipNets, nil
}
var (
ErrInvalidIP = errors.New("invalid IP address")
)
func readIP(env params.Env, key string) (ip net.IP, err error) {
s, err := env.Get(key)
if len(s) == 0 {
return nil, nil
} else if err != nil {
return nil, err
}
ip = net.ParseIP(s)
if ip == nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, s)
}
return ip, nil
}
func readPortOrZero(env params.Env, key string) (port uint16, err error) {
s, err := env.Get(key)
if err != nil {
return 0, err
}
if len(s) == 0 || s == "0" {
return 0, nil
}
return env.Port(key)
}

View File

@@ -0,0 +1,109 @@
package configuration
import (
"errors"
"fmt"
"io"
"strings"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/params"
)
var (
ErrGetSecretFilepath = errors.New("cannot get secret file path from env")
ErrReadSecretFile = errors.New("cannot read secret file")
ErrSecretFileIsEmpty = errors.New("secret file is empty")
ErrReadNonSecretFile = errors.New("cannot read non secret file")
ErrFilesDoNotExist = errors.New("files do not exist")
)
func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKeys []string) (value string, err error) {
envOptions := []params.OptionSetter{
params.Compulsory(), // to fallback on file reading
params.CaseSensitiveValue(),
params.Unset(),
params.RetroKeys(retroKeys, r.onRetroActive),
}
value, envErr := r.env.Get(envKey, envOptions...)
if envErr == nil {
return value, nil
}
defaultSecretFile := "/run/secrets/" + strings.ToLower(envKey)
filepath, err := r.env.Get(envKey+"_SECRETFILE",
params.CaseSensitiveValue(),
params.Default(defaultSecretFile),
)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrGetSecretFilepath, err)
}
file, fileErr := r.os.OpenFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(fileErr) {
if compulsory {
return "", envErr
}
return "", nil
} else if fileErr != nil {
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, fileErr)
}
b, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err)
}
value = string(b)
value = strings.TrimSuffix(value, "\n")
if compulsory && len(value) == 0 {
return "", ErrSecretFileIsEmpty
}
return value, nil
}
// Tries to read from the secret file then the non secret file.
func (r *reader) getFromFileOrSecretFile(secretName, filepath string) (
b []byte, err error) {
defaultSecretFile := "/run/secrets/" + strings.ToLower(secretName)
secretFilepath, err := r.env.Get(strings.ToUpper(secretName)+"_SECRETFILE",
params.CaseSensitiveValue(),
params.Default(defaultSecretFile),
)
if err != nil {
return b, fmt.Errorf("%w: %s", ErrGetSecretFilepath, err)
}
b, err = readFromFile(r.os.OpenFile, secretFilepath)
if err != nil && !os.IsNotExist(err) {
return b, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
} else if err == nil {
return b, nil
}
// Secret file does not exist, try the non secret file
b, err = readFromFile(r.os.OpenFile, filepath)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
} else if err == nil {
return b, nil
}
return nil, fmt.Errorf("%w: %s and %s", ErrFilesDoNotExist, secretFilepath, filepath)
}
func readFromFile(openFile os.OpenFileFunc, filepath string) (b []byte, err error) {
file, err := openFile(filepath, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
b, err = io.ReadAll(file)
if err != nil {
_ = file.Close()
return nil, err
}
if err := file.Close(); err != nil {
return nil, err
}
return b, nil
}

View File

@@ -0,0 +1,63 @@
package configuration
import (
"net"
)
type ServerSelection struct { //nolint:maligned
// Common
TCP bool `json:"tcp"` // UDP if TCP is false
TargetIP net.IP `json:"target_ip,omitempty"`
// TODO comments
// Cyberghost, PIA, Protonvpn, Surfshark, Windscribe, Vyprvpn, NordVPN
Regions []string `json:"regions"`
// Cyberghost
Group string `json:"group"`
// Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited
Countries []string `json:"countries"`
// HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe
Cities []string `json:"cities"`
// Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited
Hostnames []string `json:"hostnames"`
Names []string `json:"names"` // Protonvpn
// Mullvad
ISPs []string `json:"isps"`
Owned bool `json:"owned"`
// Mullvad, Windscribe, PIA
CustomPort uint16 `json:"custom_port"`
// NordVPN
Numbers []uint16 `json:"numbers"`
// PIA
EncryptionPreset string `json:"encryption_preset"`
// ProtonVPN
FreeOnly bool `json:"free_only"`
// VPNUnlimited
StreamOnly bool `json:"stream_only"`
}
type ExtraConfigOptions struct {
ClientCertificate string `json:"-"` // Cyberghost
ClientKey string `json:"-"` // Cyberghost, VPNUnlimited
EncryptionPreset string `json:"encryption_preset"` // PIA
OpenVPNIPv6 bool `json:"openvpn_ipv6"` // Mullvad
}
// PortForwarding contains settings for port forwarding.
type PortForwarding struct {
Enabled bool `json:"enabled"`
Filepath string `json:"filepath"`
}
func (p *PortForwarding) lines() (lines []string) {
return []string{
lastIndent + "File path: " + p.Filepath,
}
}

View File

@@ -0,0 +1,49 @@
package configuration
import (
"strconv"
"strings"
"github.com/qdm12/golibs/params"
)
// ControlServer contains settings to customize the control server operation.
type ControlServer struct {
Port uint16
Log bool
}
func (settings *ControlServer) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *ControlServer) lines() (lines []string) {
lines = append(lines, lastIndent+"HTTP control server:")
lines = append(lines, indent+lastIndent+"Listening port: "+strconv.Itoa(int(settings.Port)))
if settings.Log {
lines = append(lines, indent+lastIndent+"Logging: enabled")
}
return lines
}
func (settings *ControlServer) read(r reader) (err error) {
settings.Log, err = r.env.OnOff("HTTP_CONTROL_SERVER_LOG", params.Default("on"))
if err != nil {
return err
}
var warning string
settings.Port, warning, err = r.env.ListeningPort(
"HTTP_CONTROL_SERVER_PORT", params.Default("8000"))
if len(warning) > 0 {
r.logger.Warn(warning)
}
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,111 @@
package configuration
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/params"
)
// Settings contains all settings for the program to run.
type Settings struct {
OpenVPN OpenVPN
System System
DNS DNS
Firewall Firewall
HTTPProxy HTTPProxy
ShadowSocks ShadowSocks
Updater Updater
PublicIP PublicIP
VersionInformation bool
ControlServer ControlServer
}
func (settings *Settings) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Settings) lines() (lines []string) {
lines = append(lines, "Settings summary below:")
lines = append(lines, settings.OpenVPN.lines()...)
lines = append(lines, settings.DNS.lines()...)
lines = append(lines, settings.Firewall.lines()...)
lines = append(lines, settings.System.lines()...)
lines = append(lines, settings.HTTPProxy.lines()...)
lines = append(lines, settings.ShadowSocks.lines()...)
lines = append(lines, settings.ControlServer.lines()...)
lines = append(lines, settings.Updater.lines()...)
lines = append(lines, settings.PublicIP.lines()...)
if settings.VersionInformation {
lines = append(lines, lastIndent+"Github version information: enabled")
}
return lines
}
var (
ErrOpenvpn = errors.New("cannot read Openvpn settings")
ErrSystem = errors.New("cannot read System settings")
ErrDNS = errors.New("cannot read DNS settings")
ErrFirewall = errors.New("cannot read firewall settings")
ErrHTTPProxy = errors.New("cannot read HTTP proxy settings")
ErrShadowsocks = errors.New("cannot read Shadowsocks settings")
ErrControlServer = errors.New("cannot read control server settings")
ErrUpdater = errors.New("cannot read Updater settings")
ErrPublicIP = errors.New("cannot read Public IP getter settings")
)
// Read obtains all configuration options for the program and returns an error as soon
// as an error is encountered reading them.
func (settings *Settings) Read(env params.Env, os os.OS, logger logging.Logger) (err error) {
r := newReader(env, os, logger)
settings.VersionInformation, err = r.env.OnOff("VERSION_INFORMATION", params.Default("on"))
if err != nil {
return err
}
if err := settings.OpenVPN.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrOpenvpn, err)
}
if err := settings.System.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrSystem, err)
}
if err := settings.DNS.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrDNS, err)
}
if err := settings.Firewall.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrFirewall, err)
}
if err := settings.HTTPProxy.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrHTTPProxy, err)
}
if err := settings.ShadowSocks.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrShadowsocks, err)
}
if err := settings.ControlServer.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrControlServer, err)
}
if err := settings.Updater.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrUpdater, err)
}
if ip := settings.DNS.PlaintextAddress; ip != nil {
settings.Updater.DNSAddress = ip.String()
}
if err := settings.PublicIP.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrPublicIP, err)
}
return nil
}

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,350 +0,0 @@
package settings
import (
"encoding/base64"
"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 = base64.StdEncoding.DecodeString(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 = base64.StdEncoding.DecodeString(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
}

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