Compare commits

..

5 Commits

Author SHA1 Message Date
Quentin McGaw
293d9cf600 Fix SS CPU cycles bad eating habits (#211) 2020-08-19 00:56:48 +00:00
Quentin McGaw
fc1d96087e Fix SS TCP relay error handling 2020-08-16 23:25:44 +00:00
Quentin McGaw
cf1919c27f Update list of ciphers for Shadowsocks 2020-08-16 23:09:48 +00:00
Quentin McGaw
02930b6a95 Shadowsocks in Go, refers to #211 2020-08-16 23:07:15 +00:00
Quentin McGaw
c59447c646 Bump versions and binary build changes
- Go version 1.15
- Golangci-lint 1.30
- Trim path of binary built
2020-08-16 23:06:35 +00:00
705 changed files with 17391 additions and 150267 deletions

View File

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

View File

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

View File

@@ -1,69 +0,0 @@
# Development container
Development container that can be used with VSCode.
It works on Linux, Windows and OSX.
## Requirements
- [VS code](https://code.visualstudio.com/download) installed
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
- [Docker Compose](https://docs.docker.com/compose/install/) installed
- Ensure your host has the following and that they are accessible by Docker:
- `~/.ssh` directory
- `~/.gitconfig` file (can be empty)
## Setup
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
## Customization
### Customize the image
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
```Dockerfile
FROM qmcgaw/godevcontainer
USER root
RUN apk add curl
USER vscode
```
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
To rebuild the image, either:
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
- With a terminal, go to this directory and `docker-compose build`
### Customize VS code settings
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
### Entrypoint script
You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
### Publish a port
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
### Run other services
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
```yml
database:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
```
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
1. In the VS code command palette, rebuild the container.

View File

@@ -1,5 +1,5 @@
{
"name": "gluetun-dev",
"name": "pia-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
@@ -8,74 +8,108 @@
"vscode"
],
"shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"postCreateCommand": "go mod download",
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",
"eamodio.gitlens", // IDE Git information
"IBM.output-colorizer",
"eamodio.gitlens",
"mhutchie.git-graph",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", // Docker integration and linting
"shardulm94.trailing-spaces", // Show trailing spaces
"Gruntfuggly.todo-tree", // Highlights TODO comments
"bierner.emojisense", // Emoji sense for markdown
"stkb.rewrap", // rewrap comments after n characters on one line
"vscode-icons-team.vscode-icons", // Better file extension icons
"github.vscode-pull-request-github", // Github interaction
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"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
"shardulm94.trailing-spaces",
"alefragnani.Bookmarks",
"Gruntfuggly.todo-tree",
"mohsen1.prettify-json",
"quicktype.quicktype",
"spikespaz.vscode-smoothtype",
"stkb.rewrap",
"vscode-icons-team.vscode-icons"
],
"settings": {
// General settings
"files.eol": "\n",
// Docker
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
"editor.codeActionsOnSaveTimeout": 3000,
// Golang general settings
"go.useLanguageServer": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
// Optional: Disable snippets, as they conflict with completion ranking.
"editor.snippetSuggestions": "none"
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true
},
"go.autocompleteUnimportedPackages": true,
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"gopls": {
"completeUnimported": true,
"deepCompletion": true,
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--enable",
"staticcheck",
"--enable",
"bodyclose",
"--enable",
"dogsled",
"--enable",
"gochecknoglobals",
"--enable",
"gochecknoinits",
"--enable",
"gocognit",
"--enable",
"goconst",
"--enable",
"gocritic",
"--enable",
"gocyclo",
"--enable",
"golint",
"--enable",
"gosec",
"--enable",
"interfacer",
"--enable",
"maligned",
"--enable",
"misspell",
"--enable",
"nakedret",
"--enable",
"prealloc",
"--enable",
"scopelint",
"--enable",
"unconvert",
"--enable",
"unparam",
"--enable",
"whitespace"
],
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
"editor.formatOnSave": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
// Golang testing
"go.toolsEnvVars": {
"GOFLAGS": "-tags=",
// "CGO_ENABLED": 1 // for the race detector
"GOFLAGS": "-tags=integration"
},
"gopls.env": {
"GOFLAGS": "-tags="
},
"go.testEnvVars": {
"": ""
"GOFLAGS": "-tags=integration"
},
"go.testEnvVars": {},
"go.testFlags": [
"-v",
// "-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.testTimeout": "600s",
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true
"go.coverOnSingleTest": true
}
}

View File

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

View File

@@ -1,9 +1,11 @@
.devcontainer
.git
.github
.vscode
cmd
!cmd/gluetun
doc
docker-compose.yml
Dockerfile
LICENSE
README.md
title.svg

View File

@@ -7,12 +7,23 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
1. [Fork](https://github.com/qdm12/gluetun/fork) and clone the repository
1. Create a new branch `git checkout -b my-branch-name`
1. Modify the code
1. Ensure the docker build succeeds `docker build .` (you might need `export DOCKER_BUILDKIT=1`)
1. Ensure the docker build succeeds `docker build .`
1. Commit your modifications
1. Push to your fork and [submit a pull request](https://github.com/qdm12/gluetun/compare)
## Resources
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
## Contributors
Thanks for all the contributions, whether small or not so small!
- [@JeordyR](https://github.com/JeordyR) for testing the Mullvad version and opening a [PR with a few fixes](https://github.com/qdm12/gluetun/pull/84/files) 👍
- [@rorph](https://github.com/rorph) for a [PR to pick a random region for PIA](https://github.com/qdm12/gluetun/pull/70) and a [PR to make the container work with kubernetes](https://github.com/qdm12/gluetun/pull/69)
- [@JesterEE](https://github.com/JesterEE) for a [PR to fix silly line endings in block lists back then](https://github.com/qdm12/gluetun/pull/55) 📎
- [@elmerfdz](https://github.com/elmerfdz) for a [PR to add timezone information to have correct log timestampts](https://github.com/qdm12/gluetun/pull/51) 🕙
- [@Juggels](https://github.com/Juggels) for a [PR to write the PIA forwarded port to a file](https://github.com/qdm12/gluetun/pull/43)
- [@gdlx](https://github.com/gdlx) for a [PR to fix and improve PIA port forwarding script](https://github.com/qdm12/gluetun/pull/32)
- [@janaz](https://github.com/janaz) for keeping an eye on [updating things in the Dockerfile](https://github.com/qdm12/gluetun/pull/8)

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

@@ -0,0 +1,55 @@
---
name: Bug
about: Report a bug
title: 'Bug: ...'
labels: ":bug: bug"
assignees: qdm12
---
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent?
- [ ] Yes
- [x] No
2. What VPN service provider are you using?
- [x] PIA
- [ ] Mullvad
- [ ] Windscribe
- [ ] Surfshark
- [ ] Cyberghost
3. What's 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)`
4. What are you using to run the container?
- [ ] Docker run
- [x] Docker Compose
- [ ] Kubernetes
- [ ] Docker stack
- [ ] Docker swarm
- [ ] Podman
- [ ] Other:
5. Extra information
Logs:
```log
```
Configuration file:
```yml
```
Host OS:

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: log
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,14 @@
---
name: Feature request
about: Suggest a feature to add to this project
title: 'Feature request: ...'
labels: ":bulb: feature request"
assignees: qdm12
---
1. What's the feature?
2. Why do you need this feature?
3. Extra information?

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

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

@@ -0,0 +1,55 @@
---
name: Help
about: Ask for help
title: 'Help: ...'
labels: ":pray: help wanted"
assignees:
---
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent?
- [ ] Yes
- [x] No
2. What VPN service provider are you using?
- [x] PIA
- [ ] Mullvad
- [ ] Windscribe
- [ ] Surfshark
- [ ] Cyberghost
3. What's 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)`
4. What are you using to run the container?
- [ ] Docker run
- [x] Docker Compose
- [ ] Kubernetes
- [ ] Docker stack
- [ ] Docker swarm
- [ ] Podman
- [ ] Other:
5. Extra information
Logs:
```log
```
Configuration file:
```yml
```
Host OS:

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: docker
directory: /
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: /
schedule:
interval: "daily"

143
.github/labels.yml vendored
View File

@@ -1,110 +1,51 @@
- name: "Bug :bug:"
- name: ":robot: bot"
color: "69cde9"
description: ""
- name: ":bug: bug"
color: "b60205"
description: ""
- name: "Feature request :bulb:"
color: "0e8a16"
- name: ":game_die: dependencies"
color: "0366d6"
description: ""
- name: "Help wanted :pray:"
color: "4caf50"
description: ""
- name: "Documentation :memo:"
- name: ":memo: documentation"
color: "c5def5"
description: ""
- name: "Needs more info :thinking:"
- name: ":busts_in_silhouette: duplicate"
color: "cccccc"
description: ""
- name: ":sparkles: enhancement"
color: "0054ca"
description: ""
- name: ":bulb: feature request"
color: "0e8a16"
description: ""
- name: ":mega: feedback"
color: "03a9f4"
description: ""
- name: ":rocket: future maybe"
color: "fef2c0"
description: ""
- name: ":hatching_chick: good first issue"
color: "7057ff"
description: ""
- name: ":pray: help wanted"
color: "4caf50"
description: ""
- name: ":hand: hold"
color: "24292f"
description: ""
- name: ":no_entry_sign: invalid"
color: "e6e6e6"
description: ""
- name: ":interrobang: maybe bug"
color: "ff5722"
description: ""
- name: ":thinking: needs more info"
color: "795548"
description: ""
# Priority
- name: "🚨 Urgent"
color: "d5232f"
- name: ":question: question"
color: "3f51b5"
description: ""
- name: "💤 Low priority"
color: "4285f4"
description: ""
# VPN providers
- name: ":cloud: Cyberghost"
color: "cfe8d4"
description: ""
- name: ":cloud: HideMyAss"
color: "cfe8d4"
description: ""
- name: ":cloud: IPVanish"
color: "cfe8d4"
description: ""
- name: ":cloud: IVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ExpressVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: FastestVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Mullvad"
color: "cfe8d4"
description: ""
- name: ":cloud: NordVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Perfect Privacy"
color: "cfe8d4"
description: ""
- name: ":cloud: PIA"
color: "cfe8d4"
description: ""
- name: ":cloud: Privado"
color: "cfe8d4"
description: ""
- name: ":cloud: PrivateVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ProtonVPN"
color: "cfe8d4"
- name: ":cloud: PureVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Surfshark"
color: "cfe8d4"
description: ""
- name: ":cloud: Torguard"
color: "cfe8d4"
description: ""
- name: ":cloud: VPNUnlimited"
color: "cfe8d4"
description: ""
- name: ":cloud: Vyprvpn"
color: "cfe8d4"
description: ""
- name: ":cloud: WeVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: Windscribe"
color: "cfe8d4"
description: ""
# Problem category
- name: "Openvpn"
color: "ffc7ea"
description: ""
- name: "Wireguard"
color: "ffc7ea"
description: ""
- name: "Unbound (DNS over TLS)"
color: "ffc7ea"
description: ""
- name: "Firewall"
color: "ffc7ea"
description: ""
- name: "HTTP proxy"
color: "ffc7ea"
description: ""
- name: "Shadowsocks"
color: "ffc7ea"
description: ""
- name: "Healthcheck server"
color: "ffc7ea"
description: ""
- name: "Control server"
color: "ffc7ea"
- name: ":coffin: wontfix"
color: "ffffff"
description: ""

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

@@ -0,0 +1,34 @@
name: Docker build
on:
pull_request:
branches: [master]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/buildx-release.yml
- .github/workflows/buildx-branch.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/workflows/misspell.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- cmd/ovpnparser
- cmd/resolver
- doc
- .gitignore
- docker-compose.yml
- LICENSE
- README.md
- title.svg
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build image
run: docker build .

50
.github/workflows/buildx-branch.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Buildx branch
on:
push:
branches:
- '*'
- '*/*'
- '!master'
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-release.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/workflows/misspell.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- cmd/ovpnparser
- cmd/resolver
- doc
- .gitignore
- docker-compose.yml
- LICENSE
- README.md
- title.svg
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0

47
.github/workflows/buildx-latest.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Buildx latest
on:
push:
branches: [master]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-branch.yml
- .github/workflows/buildx-release.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/workflows/misspell.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- cmd/ovpnparser
- cmd/resolver
- doc
- .gitignore
- docker-compose.yml
- LICENSE
- README.md
- title.svg
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=latest \
-t qmcgaw/private-internet-access:latest \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0

47
.github/workflows/buildx-release.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Buildx release
on:
release:
types: [published]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-branch.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/workflows/misspell.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- cmd/ovpnparser
- cmd/resolver
- doc
- .gitignore
- docker-compose.yml
- LICENSE
- README.md
- title.svg
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || 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:
# Only run if it's a push event or if it's a PR from this repository, and it is not dependabot.
if: |
github.actor != 'dependabot[bot]' &&
(github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
runs-on: ubuntu-latest
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: 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:
# Only run if it's a push event or if it's a PR from this repository
if: |
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
needs: [verify]
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@v3
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=branch,enable=${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
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@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
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@v2.9.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,37 +0,0 @@
name: Dependabot
on:
pull_request:
branches:
- master
paths:
- .github/workflows/dependabot.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2.4.0
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .

View File

@@ -10,12 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.4.0
uses: actions/checkout@v2
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/gluetun
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
readme-filepath: README.md
uses: peter-evans/dockerhub-description@v2.1.0
env:
DOCKERHUB_USERNAME: qmcgaw
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
DOCKERHUB_REPOSITORY: qmcgaw/private-internet-access

View File

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

View File

@@ -1,15 +1,18 @@
name: labels
on:
push:
branches: [master]
branches: ["master"]
paths:
- .github/labels.yml
- .github/workflows/labels.yml
- '.github/labels.yml'
- '.github/workflows/labels.yml'
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: crazy-max/ghaction-github-labeler@v3
with:
yaml-file: .github/labels.yml
- name: Checkout
uses: actions/checkout@v2
- name: Labeler
if: success()
uses: crazy-max/ghaction-github-labeler@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,8 +8,9 @@ jobs:
misspell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.4.0
- uses: reviewdog/action-misspell@v1
- uses: actions/checkout@v2
- uses: reviewdog/action-misspell@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
locale: "US"
level: error

1
.gitignore vendored
View File

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

View File

@@ -1,100 +1,47 @@
linters-settings:
maligned:
suggest-new: true
misspell:
locale: US
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- maligned
- goerr113
- path: internal/server/
linters:
- dupl
- path: internal/configuration/
linters:
- dupl
- path: internal/constants/
linters:
- dupl
- text: "exported: exported var Err*"
linters:
- revive
- text: "mnd: Magic number: 0644*"
linters:
- gomnd
- text: "mnd: Magic number: 0400*"
linters:
- gomnd
- text: "variable 'mssFix' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
- text: "variable 'auth' is only used in the if-statement*"
path: "openvpnconf.go"
linters:
- ifshort
linters:
disable-all: true
enable:
# - cyclop
# - errorlint
# - ireturn
# - varnamelen
# - wrapcheck
- asciicheck
- bidichk
- bodyclose
- deadcode
- dogsled
- dupl
- durationcheck
- errname
- exhaustive
- exportloopref
- forcetypeassert
- gci
- errcheck
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- goerr113
- goheader
- goimports
- gomnd
- gomoddirectives
- goprintffuncname
- golint
- gosec
- ifshort
- importas
- lll
- makezero
- gosimple
- govet
- ineffassign
- interfacer
- maligned
- misspell
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- prealloc
- predeclared
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- tenv
- thelper
- tparallel
- scopelint
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- wastedassign
- unused
- varcheck
- whitespace
run:
skip-dirs:
- .devcontainer
- .github
- doc
- postgres

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"shardulm94.trailing-spaces",
"ms-azuretools.vscode-docker",
"davidanson.vscode-markdownlint",
"IBM.output-colorizer",
"golang.go"
]
}

91
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,91 @@
{
// General settings
"files.eol": "\n",
// Docker
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
// Golang general settings
"go.useLanguageServer": true,
"go.autocompleteUnimportedPackages": true,
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"gopls": {
"completeUnimported": true,
"deepCompletion": true,
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--enable",
"rowserrcheck",
"--enable",
"bodyclose",
"--enable",
"dogsled",
"--enable",
"dupl",
"--enable",
"gochecknoglobals",
"--enable",
"gochecknoinits",
"--enable",
"gocognit",
"--enable",
"goconst",
"--enable",
"gocritic",
"--enable",
"gocyclo",
"--enable",
"goimports",
"--enable",
"golint",
"--enable",
"gosec",
"--enable",
"interfacer",
"--enable",
"maligned",
"--enable",
"misspell",
"--enable",
"nakedret",
"--enable",
"prealloc",
"--enable",
"scopelint",
"--enable",
"unconvert",
"--enable",
"unparam",
"--enable",
"whitespace"
],
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
"editor.formatOnSave": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
// Golang testing
"go.toolsEnvVars": {
"GOFLAGS": "-tags="
},
"gopls.env": {
"GOFLAGS": "-tags="
},
"go.testEnvVars": {},
"go.testFlags": [
"-v",
// "-race"
],
"go.testTimeout": "600s",
"go.coverOnSingleTestFile": true,
"go.coverOnSingleTest": true
}

View File

@@ -1,189 +1,108 @@
ARG ALPINE_VERSION=3.15
ARG GO_ALPINE_VERSION=3.15
ARG GO_VERSION=1.17
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.43.0
ARG BUILDPLATFORM=linux/amd64
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
RUN apk --update add git g++
ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint
WORKDIR /tmp/gobuild
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
FROM --platform=${BUILDPLATFORM} base AS test
# 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
COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m
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 VERSION=unknown
ARG CREATED="an unknown date"
ARG COMMIT=unknown
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.commit=$COMMIT' \
" -o entrypoint cmd/gluetun/main.go
FROM alpine:${ALPINE_VERSION}
ARG VERSION=unknown
ARG CREATED="an unknown date"
ARG COMMIT=unknown
LABEL \
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
org.opencontainers.image.created=$CREATED \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.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 VPNSP=pia \
VPN_TYPE=openvpn \
# OpenVPN
OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.5 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_CIPHER= \
OPENVPN_AUTH= \
OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \
OPENVPN_INTERFACE=tun0 \
OPENVPN_PORT= \
# Wireguard
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ADDRESS= \
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_INTERFACE=wg0 \
# VPN server filtering
REGION= \
COUNTRY= \
CITY= \
SERVER_HOSTNAME= \
# # Mullvad only:
ISP= \
OWNED=no \
# # Private Internet Access only:
PIA_ENCRYPTION= \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# # Cyberghost only:
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # Nordvpn only:
SERVER_NUMBER= \
# # PIA 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=github.com:443 \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
DOT_CACHING=on \
DOT_IPV6=off \
BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_PLAINTEXT_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy
HTTPPROXY= \
HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
SHADOWSOCKS_LISTENING_ADDRESS=":8388" \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Server data updater
UPDATER_PERIOD=0 \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_PERIOD=12h \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID= \
PGID=
ENTRYPOINT ["/gluetun-entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l apk-tools && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.12-r0 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
# Fix vulnerability issue
apk add --no-cache --update busybox && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
deluser openvpn && \
deluser unbound && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
ARG ALPINE_VERSION=3.12
ARG GO_VERSION=1.15
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
RUN apk --update add git
ENV CGO_ENABLED=0
ARG GOLANGCI_LINT_VERSION=v1.30.0
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
WORKDIR /tmp/gobuild
COPY .golangci.yml .
COPY go.mod go.sum ./
RUN go mod download 2>&1
COPY cmd/gluetun/main.go .
COPY internal/ ./internal/
RUN go test ./...
RUN golangci-lint run --timeout=10m
RUN go build -trimpath -ldflags="-s -w" -o entrypoint main.go
FROM alpine:${ALPINE_VERSION}
ARG VERSION
ARG BUILD_DATE
ARG VCS_REF
ENV VERSION=$VERSION \
BUILD_DATE=$BUILD_DATE \
VCS_REF=$VCS_REF
LABEL \
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \
org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux"
ENV VPNSP=pia \
PROTOCOL=udp \
OPENVPN_VERBOSITY=1 \
OPENVPN_ROOT=no \
OPENVPN_TARGET_IP= \
TZ= \
UID=1000 \
GID=1000 \
IP_STATUS_FILE="/ip" \
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only
USER= \
PASSWORD= \
REGION= \
# PIA only
PIA_ENCRYPTION=strong \
PORT_FORWARDING=off \
PORT_FORWARDING_STATUS_FILE="/forwarded_port" \
# Mullvad and PureVPN only
COUNTRY= \
CITY= \
# Mullvad only
ISP= \
# Mullvad and Windscribe only
PORT= \
# Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \
# NordVPN only
SERVER_NUMBER= \
# Openvpn
OPENVPN_CIPHER= \
OPENVPN_AUTH= \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:0:0/96 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
DOT_CACHING=on \
DOT_IPV6=off \
BLOCK_MALICIOUS=on \
BLOCK_SURVEILLANCE=off \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
DNS_KEEP_NAMESERVER=off \
# Firewall
FIREWALL=on \
EXTRA_SUBNETS= \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_DEBUG=off \
# Tinyproxy
TINYPROXY=off \
TINYPROXY_LOG=Info \
TINYPROXY_PORT=8888 \
TINYPROXY_USER= \
TINYPROXY_PASSWORD= \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
SHADOWSOCKS_PORT=8388 \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305
ENTRYPOINT ["/entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=10m --timeout=10s --start-period=30s --retries=2 CMD /entrypoint healthcheck
RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tinyproxy tzdata && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \
deluser openvpn && \
deluser tinyproxy && \
deluser unbound
COPY --from=builder /tmp/gobuild/entrypoint /entrypoint

449
README.md
View File

@@ -1,127 +1,378 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN,
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN,
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers
using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN and PureVPN VPN servers, using Go, OpenVPN,
iptables, DNS over TLS, ShadowSocks and Tinyproxy*
**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem!
**ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)*
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
[![Build status](https://github.com/qdm12/gluetun/workflows/Buildx%20latest/badge.svg)](https://github.com/qdm12/gluetun/actions?query=workflow%3A%22Buildx+latest%22)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/private-internet-access)
[![Docker Stars](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/private-internet-access)
[![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)
[![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)
![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)
![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 last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![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)
[![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)
- [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)
- [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)
- Video:
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
- [Substack Console interview](https://console.substack.com/p/console-72)
[![Image size](https://images.microbadger.com/badges/image/qmcgaw/private-internet-access.svg)](https://microbadger.com/images/qmcgaw/private-internet-access)
[![Image version](https://images.microbadger.com/badges/version/qmcgaw/private-internet-access.svg)](https://microbadger.com/images/qmcgaw/private-internet-access)
[![Join Slack channel](https://img.shields.io/badge/slack-@qdm12-yellow.svg?logo=slack)](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
## Features
- Based on Alpine 3.15 for a small Docker image of 29MB
- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- For **Mullvad**, **Ivpn** and **Windscribe**
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- Based on Alpine 3.12 for a small Docker image of 52MB
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn**, **NordVPN** and **PureVPN** servers
- Supports Openvpn only for now
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
- Choose the vpn network protocol, `udp` or `tcp`
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- Built in SOCKS5 proxy (Shadowsocks, tunnels TCP+UDP)
- Built in HTTP proxy (Tinyproxy, tunnels TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun#connect-to-it)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun#connect-to-it)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆
- 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. Requirements
- A VPN account with one of the service providers supported
- If you have a host or router firewall, please refer [to the firewall documentation](https://github.com/qdm12/gluetun/blob/master/doc/firewall.md)
1. On some devices you may need to setup your tunnel kernel module on your host with `insmod /lib/modules/tun.ko` or `modprobe tun`
- *Synology users*: please read [this part of the Wiki](https://github.com/qdm12/gluetun/wiki/Common-issues#synology)
1. Launch the container with:
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \
-e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \
qmcgaw/private-internet-access
```
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
Here's a docker-compose.yml for the laziest:
```bash
docker-compose up -d
```
```yml
version: "3"
services:
gluetun:
image: qmcgaw/gluetun
# container_name: gluetun
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
cap_add:
- NET_ADMIN
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
- VPNSP=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
- OPENVPN_USER=
- OPENVPN_PASSWORD=
# Wireguard:
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# - WIREGUARD_ADDRESS=10.64.222.21/32
# Timezone for accurate log times
- TZ=
Note that you can:
- Change the many [environment variables](#environment-variables) available
- Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the SOCKS5 proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
**If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/gluetun/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)**
1. You can update the image with `docker pull qmcgaw/private-internet-access:latest`. See the [wiki](https://github.com/qdm12/gluetun/wiki/Common-issues#use-a-release-tag) for more information on other tags available.
## Testing
Check the VPN IP address matches your expectations
```sh
docker run --rm --network=container:gluetun alpine:3.12 wget -qO- https://ipinfo.io
```
Want more testing? ▶ [see the Wiki](https://github.com/qdm12/gluetun/wiki/Testing)
## Environment variables
**TLDR**; only set the 🏁 marked environment variables to get started.
### VPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `VPNSP` | `private internet access` | `private internet access`, `mullvad`, `windscribe`, `surfshark`, `vyprvpn`, `nordvpn`, `purevpn` | VPN Service Provider |
| `IP_STATUS_FILE` | `/ip` | Any filepath | Filepath to store the public IP address assigned |
| `PROTOCOL` | `udp` | `udp` or `tcp` | Network protocol to use |
| `OPENVPN_VERBOSITY` | `1` | `0` to `6` | Openvpn verbosity level |
| `OPENVPN_ROOT` | `no` | `yes` or `no` | Run OpenVPN as root |
| `OPENVPN_TARGET_IP` | | Valid IP address | Specify a target VPN server (or gateway) IP address to use |
| `OPENVPN_CIPHER` | | i.e. `aes-256-gcm` | Specify a custom cipher to use. It will also set `ncp-disable` if using AES GCM for PIA |
| `OPENVPN_AUTH` | | i.e. `sha256` | Specify a custom auth algorithm to use |
*For all providers below, server location parameters are all optional. By default a random server is picked using the filter settings provided.*
- Private Internet Access
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | | One of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) | VPN server region |
| `PIA_ENCRYPTION` | `strong` | `normal`, `strong` | Encryption preset |
| `PORT_FORWARDING` | `off` | `on`, `off` | Enable port forwarding on the VPN server |
| `PORT_FORWARDING_STATUS_FILE` | `/forwarded_port` | Any filepath | Filepath to store the forwarded port number |
- Mullvad
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your user ID |
| `COUNTRY` | | One of the [Mullvad countries](https://mullvad.net/en/servers/#openvpn) | VPN server country |
| `CITY` | | One of the [Mullvad cities](https://mullvad.net/en/servers/#openvpn) | VPN server city |
| `ISP` | | One of the [Mullvad ISP](https://mullvad.net/en/servers/#openvpn) | VPN server ISP |
| `PORT` | | `80` or `443` for TCP; or `53` for UDP. Leave blank for default Mullvad server port | Custom VPN port to use |
- Windscribe
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | | One of the [Windscribe regions](https://windscribe.com/status) | VPN server region |
| `PORT` | | One from the [this list of ports](https://windscribe.com/getconfig/openvpn) | Custom VPN port to use |
- Surfshark
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your **service** username, found at the bottom of the [manual setup page](https://account.surfshark.com/setup/manual) |
| 🏁 `PASSWORD` | | | Your **service** password |
| `REGION` | | One of the [Surfshark regions](https://github.com/qdm12/gluetun/wiki/surfshark) | VPN server region |
- Cyberghost
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| 🏁 `CLIENT_KEY` | | | Your device client key content, **see below** |
| `REGION` | | One of the [Cyberghost countries](https://github.com/qdm12/gluetun/wiki/Cyberghost#regions) | VPN server country |
| `CYBERGHOST_GROUP` | `Premium UDP Europe` | One of the [server groups](https://github.com/qdm12/gluetun/wiki/Cyberghost#server-groups) | Server group |
To specify your client key, you can either:
- Bind mount it at `/files/client.key`, for example with `-v /yourpath/client.key:/files/client.key:ro`
- Convert it to a single line value using:
```sh
docker run -it --rm -v /yourpath/client.key:/files/client.key:ro qmcgaw/private-internet-access clientkey
```
And use the line produced as the value for the environment variable `CLIENT_KEY`.
- Vyprvpn
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | | One of the [VyprVPN regions](https://www.vyprvpn.com/server-locations) | VPN server region |
- NordVPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | | One of the NordVPN server country, i.e. `Switzerland` | VPN server country |
| `SERVER_NUMBER` | | Server integer number | Optional server number. For example `251` for `Italy #251` |
- PureVPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your user ID |
| 🏁 `REGION` | | One of the [PureVPN regions](https://support.purevpn.com/vpn-servers) | VPN server region |
| `COUNTRY` | | One of the [PureVPN countries](https://support.purevpn.com/vpn-servers) | VPN server country |
| `CITY` | | One of the [PureVPN cities](https://support.purevpn.com/vpn-servers) | VPN server city |
### DNS over TLS
None of the following values are required.
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `DOT` | `on` | `on`, `off` | Activate DNS over TLS with Unbound |
| `DOT_PROVIDERS` | `cloudflare` | `cloudflare`, `google`, `quad9`, `quadrant`, `cleanbrowsing`, `securedns`, `libredns` | Comma delimited list of DNS over TLS providers |
| `DOT_CACHING` | `on` | `on`, `off` | Unbound caching |
| `DOT_IPV6` | `off` | `on`, `off` | DNS IPv6 resolution |
| `DOT_PRIVATE_ADDRESS` | All private CIDRs ranges | | Comma separated list of CIDRs or single IP addresses Unbound won't resolve to. Note that the default setting prevents DNS rebinding |
| `DOT_VERBOSITY` | `1` | `0` to `5` | Unbound verbosity level |
| `DOT_VERBOSITY_DETAILS` | `0` | `0` to `4` | Unbound details verbosity level |
| `DOT_VALIDATION_LOGLEVEL` | `0` | `0` to `2` | Unbound validation log level |
| `DNS_UPDATE_PERIOD` | `24h` | i.e. `0`, `30s`, `5m`, `24h` | Period to update block lists and cryptographic files and restart Unbound. Set to `0` to deactivate updates |
| `BLOCK_MALICIOUS` | `on` | `on`, `off` | Block malicious hostnames and IPs with Unbound |
| `BLOCK_SURVEILLANCE` | `off` | `on`, `off` | Block surveillance hostnames and IPs with Unbound |
| `BLOCK_ADS` | `off` | `on`, `off` | Block ads hostnames and IPs with Unbound |
| `UNBLOCK` | |i.e. `domain1.com,x.domain2.co.uk` | Comma separated list of domain names to leave unblocked with Unbound |
| `DNS_PLAINTEXT_ADDRESS` | `1.1.1.1` | Any IP address | IP address to use as DNS resolver if `DOT` is `off` |
| `DNS_KEEP_NAMESERVER` | `off` | `on` or `off` | Keep the nameservers in /etc/resolv.conf untouched, but disabled DNS blocking features |
### Firewall
That one is important if you want to connect to the container from your LAN for example, using Shadowsocks or Tinyproxy.
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `FIREWALL` | `on` | `on` or `off` | Turn on or off the container built-in firewall. You should use it for **debugging purposes** only. |
| `EXTRA_SUBNETS` | | i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28` | Comma separated subnets allowed in the container firewall |
| `FIREWALL_VPN_INPUT_PORTS` | | i.e. `1000,8080` | Comma separated list of ports to allow from the VPN server side (useful for **vyprvpn** port forwarding) |
| `FIREWALL_DEBUG` | `off` | `on` or `off` | Prints every firewall related command. You should use it for **debugging purposes** only. |
### Shadowsocks
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `SHADOWSOCKS` | `off` | `on`, `off` | Enable the internal SOCKS5 proxy Shadowsocks |
| `SHADOWSOCKS_LOG` | `off` | `on`, `off` | Enable logging |
| `SHADOWSOCKS_PORT` | `8388` | `1024` to `65535` | Internal port number for Shadowsocks to listen on |
| `SHADOWSOCKS_PASSWORD` | | | Password to use to connect to Shadowsocks |
| `SHADOWSOCKS_METHOD` | `chacha20-ietf-poly1305` | `chacha20-ietf-poly1305`, `aes-128-gcm`, `aes-256-gcm` | Method to use for Shadowsocks |
### Tinyproxy
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `TINYPROXY` | `off` | `on`, `off` | Enable the internal HTTP proxy tinyproxy |
| `TINYPROXY_LOG` | `Info` | `Info`, `Connect`, `Notice`, `Warning`, `Error`, `Critical` | Tinyproxy log level |
| `TINYPROXY_PORT` | `8888` | `1024` to `65535` | Internal port number for Tinyproxy to listen on |
| `TINYPROXY_USER` | | | Username to use to connect to Tinyproxy |
| `TINYPROXY_PASSWORD` | | | Password to use to connect to Tinyproxy |
### System
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `TZ` | | i.e. `Europe/London` | Specify a timezone to use to have correct log times |
| `UID` | `1000` | | User ID to run as non root and for ownership of files written |
| `GID` | `1000` | | Group ID to run as non root and for ownership of files written |
### Other
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. |
## Connect to it
There are various ways to achieve this, depending on your use case.
- <details><summary>Connect containers in the same docker-compose.yml as Gluetun</summary><p>
Add `network_mode: "service:gluetun"` to your *docker-compose.yml* (no need for `depends_on`)
</p></details>
- <details><summary>Connect other containers to Gluetun</summary><p>
Add `--network=container:gluetun` when launching the container, provided Gluetun is already running
</p></details>
- <details><summary>Connect containers from another docker-compose.yml</summary><p>
Add `network_mode: "container:gluetun"` to your *docker-compose.yml*, provided Gluetun is already running
</p></details>
- <details><summary>Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)</summary><p>
You might want to use Shadowsocks instead which tunnels UDP as well as TCP, whereas Tinyproxy only tunnels TCP.
1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
1. Ensure the Gluetun container is launched with:
- port `8888` published `-p 8888:8888/tcp`
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
</p></details>
- <details><summary>Connect LAN devices through the built-in SOCKS5 proxy *Shadowsocks* (per app, system wide, etc.)</summary><p>
1. Setup a SOCKS5 proxy client, there is a list of [ShadowSocks clients for **all platforms**](https://shadowsocks.org/en/download/clients.html)
- **note** some clients do not tunnel UDP so your DNS queries will be done locally and not through Gluetun and its built in DNS over TLS
- Clients that support such UDP tunneling are, as far as I know:
- iOS: Potatso Lite
- OSX: ShadowsocksX
- Android: Shadowsocks by Max Lv
1. Ensure the Gluetun container is launched with:
- port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
- your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
1. With your SOCKS5 proxy client
- Enter the Docker host (i.e. `192.168.1.10`) as the server IP
- Enter port TCP (and UDP, if available) `8388` as the server port
- Use the password you have set with `SHADOWSOCKS_PASSWORD`
- Choose the encryption method/algorithm to the method you specified in `SHADOWSOCKS_METHOD`
1. If you set `SHADOWSOCKS_LOG` to `on`, (a lot) more information will be logged in the Docker logs
</p></details>
- <details><summary>Access ports of containers connected to Gluetun</summary><p>
In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to Gluetun,
publish ports `8000` and `9000` for the Gluetun container and access them as you would with any other container
</p></details>
- <details><summary>Access ports of containers connected to Gluetun, all in the same docker-compose.yml</summary><p>
In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to Gluetun, publish port `8000` and `9000` for the Gluetun container.
The docker-compose.yml file would look like:
```yml
version: '3.7'
services:
gluetun:
image: qmcgaw/private-internet-access
container_name: gluetun
cap_add:
- NET_ADMIN
environment:
- USER=js89ds7
- PASSWORD=8fd9s239G
ports:
- 8000:8000/tcp
- 9000:9000/tcp
abc:
image: abc
container_name: abc
network_mode: "service:gluetun"
xyz:
image: xyz
container_name: xyz
network_mode: "service:gluetun"
```
</p></details>
## Private Internet Access port forwarding
Note that [not all regions support port forwarding](https://www.privateinternetaccess.com/helpdesk/kb/articles/how-do-i-enable-port-forwarding-on-my-vpn).
When `PORT_FORWARDING=on`, a port will be forwarded on the VPN server side and written to the file specified by `PORT_FORWARDING_STATUS_FILE=/forwarded_port`.
It can be useful to mount this file as a volume to read it from other containers, for example to configure a torrenting client.
You can also use the HTTP control server (see below) to get the port forwarded.
## HTTP control server
See [its Wiki page](https://github.com/qdm12/gluetun/wiki/HTTP-control-server)
## Development and contributing
- Contribute with code: see [the Wiki](https://github.com/qdm12/gluetun/wiki/Contributing).
- [The list of existing contributors 👍](https://github.com/qdm12/gluetun/blob/master/.github/CONTRIBUTING.md#Contributors)
- [Github workflows](https://github.com/qdm12/gluetun/actions) to know what's building
- [List of issues and feature requests](https://github.com/qdm12/gluetun/issues)
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/master/LICENSE)
This repository is under an [MIT license](https://github.com/qdm12/gluetun/master/license)
## Support
Sponsor me on [Github](https://github.com/sponsors/qdm12), donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw) or subscribe to a VPN provider through one of my affiliate links:
[![https://github.com/sponsors/qdm12](https://raw.githubusercontent.com/qdm12/gluetun/master/doc/sponsors.jpg)](https://github.com/sponsors/qdm12)
[![https://www.paypal.me/qmcgaw](https://raw.githubusercontent.com/qdm12/gluetun/master/doc/paypal.jpg)](https://www.paypal.me/qmcgaw)
[![https://windscribe.com/?affid=mh7nyafu](https://raw.githubusercontent.com/qdm12/gluetun/master/doc/windscribe.jpg)](https://windscribe.com/?affid=mh7nyafu)
Feel also free to have a look at [the Kanban board](https://github.com/qdm12/gluetun/projects/1) and [contribute](#Development-and-contributing) to the code or the issues discussion.
Many thanks to @Frepke, @Ralph521, G. Mendez, M. Otmar Weber, J. Perez and A. Cooper for supporting me financially 🥇👍

View File

@@ -2,464 +2,324 @@ package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"github.com/qdm12/dns/pkg/unbound"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/configuration/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/configuration/sources/mux"
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gluetun/internal/httpproxy"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
gluetunLogging "github.com/qdm12/gluetun/internal/logging"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/gluetun/internal/tinyproxy"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"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/updated/pkg/dnscrypto"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
created = "an unknown date"
)
var (
errSetupRouting = errors.New("cannot setup routing")
errCreateUser = errors.New("cannot create user")
"github.com/qdm12/golibs/network"
)
func main() {
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
Created: created,
}
background := context.Background()
signalCtx, stop := signal.NotifyContext(background, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
ctx, cancel := context.WithCancel(background)
logger := logging.New(logging.Settings{
Level: logging.LevelInfo,
})
args := os.Args
tun := tun.New()
netLinker := netlink.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)
}()
select {
case <-signalCtx.Done():
stop()
fmt.Println("")
logger.Warn("Caught OS signal, shutting down")
cancel()
case err := <-errorCh:
stop()
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
}
logger.Error(err.Error())
cancel()
}
const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod)
select {
case <-errorCh:
if !timer.Stop() {
<-timer.C
}
logger.Info("Shutdown successful")
case <-timer.C:
logger.Warn("Shutdown timed out")
}
os.Exit(1)
ctx := context.Background()
os.Exit(_main(ctx, os.Args))
}
var (
errCommandUnknown = errors.New("command is unknown")
)
//nolint:gocognit,gocyclo
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger logging.ParentLogger, source sources.Source,
tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter,
cli cli.CLIer) error {
func _main(background context.Context, args []string) int {
if len(args) > 1 { // cli operation
var err error
switch args[1] {
case "healthcheck":
return cli.HealthCheck(ctx, source, logger)
err = cli.HealthCheck()
case "clientkey":
return cli.ClientKey(args[2:])
err = cli.ClientKey(args[2:])
case "openvpnconfig":
return cli.OpenvpnConfig(logger, source)
case "update":
return cli.Update(ctx, args[2:], logger)
case "format-servers":
return cli.FormatServers(args[2:])
err = cli.OpenvpnConfig()
default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
err = fmt.Errorf("command %q is unknown", args[1])
}
if err != nil {
fmt.Println(err)
return 1
}
return 0
}
ctx, cancel := context.WithCancel(background)
defer cancel()
logger := createLogger()
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)
}
fatalOnError := makeFatalOnError(logger, cancel)
allSettings, err := source.Read()
if err != nil {
return err
}
// TODO run this in a loop or in openvpn to reload from file without restarting
storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "})
storage, err := storage.New(storageLogger, constants.ServersData)
if err != nil {
return err
}
allServers := storage.GetServers()
err = allSettings.Validate(allServers)
if err != nil {
return err
}
logger.PatchLevel(*allSettings.Log.Level)
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
const clientTimeout = 15 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
client := network.NewClient(15 * time.Second)
// Create configurators
alpineConf := alpine.New()
ovpnConf := openvpn.New(
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
cmder, puid, pgid)
dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
fileManager := files.NewFileManager()
alpineConf := alpine.NewConfigurator(fileManager)
ovpnConf := openvpn.NewConfigurator(logger, fileManager)
dnsConf := dns.NewConfigurator(logger, client, fileManager)
routingConf := routing.NewRouting(logger, fileManager)
firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager)
tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger)
streamMerger := command.NewStreamMerger()
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "Unbound", getVersion: dnsConf.Version},
{name: "IPtables", getVersion: func(ctx context.Context) (version string, err error) {
return firewall.Version(ctx, cmder)
}},
paramsReader := params.NewReader(logger, fileManager)
fmt.Println(gluetunLogging.Splash(
paramsReader.GetVersion(),
paramsReader.GetVcsRef(),
paramsReader.GetBuildDate()))
printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){
"OpenVPN": ovpnConf.Version,
"Unbound": dnsConf.Version,
"IPtables": firewallConf.Version,
"TinyProxy": tinyProxyConf.Version,
})
if err != nil {
return err
}
allSettings, err := settings.GetAllSettings(paramsReader)
fatalOnError(err)
logger.Info(allSettings.String())
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
return err
}
if err := os.MkdirAll("/gluetun", 0644); err != nil {
return err
}
// Should never change
uid, gid := allSettings.System.UID, allSettings.System.GID
const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil {
return fmt.Errorf("%w: %s", errCreateUser, err)
}
if nonRootUsername != defaultUsername {
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
}
// set it for Unbound
// TODO remove this when migrating to qdm12/dns v2
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
allSettings.VPN.OpenVPN.ProcUser = nonRootUsername
err = alpineConf.CreateUser("nonrootuser", uid)
fatalOnError(err)
err = fileManager.SetOwnership("/etc/unbound", uid, gid)
fatalOnError(err)
err = fileManager.SetOwnership("/etc/tinyproxy", uid, gid)
fatalOnError(err)
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
return err
if allSettings.Firewall.Debug {
firewallConf.SetDebug()
routingConf.SetDebug()
}
firewallLogLevel := *allSettings.Log.Level
if *allSettings.Firewall.Debug {
firewallLogLevel = logging.LevelDebug
}
routingLogger := logger.NewChild(logging.Settings{
Prefix: "routing: ",
Level: firewallLogLevel,
})
routingConf := routing.New(netLinker, routingLogger)
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil {
return err
fatalOnError(err)
}
localNetworks, err := routingConf.LocalNetworks()
localSubnet, err := routingConf.LocalSubnet()
if err != nil {
return err
fatalOnError(err)
}
defaultIP, err := routingConf.DefaultIP()
if err != nil {
return err
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet)
if err := ovpnConf.CheckTUN(); err != nil {
logger.Warn(err)
err = ovpnConf.CreateTUN()
fatalOnError(err)
}
firewallLogger := logger.NewChild(logging.Settings{
Prefix: "firewall: ",
Level: firewallLogLevel,
})
firewallConf := firewall.NewConfig(firewallLogger, cmder,
defaultInterface, defaultGateway, localNetworks, defaultIP)
if err := routingConf.Setup(); err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
}
return fmt.Errorf("%w: %s", errSetupRouting, err)
connectedCh := make(chan struct{})
signalConnected := func() {
connectedCh <- struct{}{}
}
defer func() {
logger.Info("routing cleanup...")
if err := routingConf.TearDown(); err != nil {
logger.Error("cannot teardown routing: " + err.Error())
defer close(connectedCh)
go collectStreamLines(ctx, streamMerger, logger, signalConnected)
if allSettings.Firewall.Enabled {
err := firewallConf.SetEnabled(ctx, true) // disabled by default
fatalOnError(err)
}
err = firewallConf.SetAllowedSubnets(ctx, allSettings.Firewall.AllowedSubnets)
fatalOnError(err)
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
fatalOnError(err)
}
wg := &sync.WaitGroup{}
openvpnLooper := openvpn.NewLooper(allSettings.VPNSP, allSettings.OpenVPN, uid, gid,
ovpnConf, firewallConf, logger, client, fileManager, streamMerger, fatalOnError)
restartOpenvpn := openvpnLooper.Restart
portForward := openvpnLooper.PortForward
getOpenvpnSettings := openvpnLooper.GetSettings
getPortForwarded := openvpnLooper.GetPortForwarded
// wait for restartOpenvpn
go openvpnLooper.Run(ctx, wg)
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, logger, streamMerger, uid, gid)
restartUnbound := unboundLooper.Restart
// wait for restartUnbound
go unboundLooper.Run(ctx, wg)
publicIPLooper := publicip.NewLooper(client, logger, fileManager, allSettings.System.IPStatusFilepath, allSettings.PublicIPPeriod, uid, gid)
restartPublicIP := publicIPLooper.Restart
setPublicIPPeriod := publicIPLooper.SetPeriod
go publicIPLooper.Run(ctx)
go publicIPLooper.RunRestartTicker(ctx)
setPublicIPPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker
tinyproxyLooper := tinyproxy.NewLooper(tinyProxyConf, firewallConf, allSettings.TinyProxy, logger, streamMerger, uid, gid, defaultInterface)
restartTinyproxy := tinyproxyLooper.Restart
go tinyproxyLooper.Run(ctx, wg)
shadowsocksLooper := shadowsocks.NewLooper(firewallConf, allSettings.ShadowSocks, logger, defaultInterface)
restartShadowsocks := shadowsocksLooper.Restart
go shadowsocksLooper.Run(ctx, wg)
if allSettings.TinyProxy.Enabled {
restartTinyproxy()
}
if allSettings.ShadowSocks.Enabled {
restartShadowsocks()
}
go func() {
var restartTickerContext context.Context
var restartTickerCancel context.CancelFunc = func() {}
for {
select {
case <-ctx.Done():
restartTickerCancel()
return
case <-connectedCh: // blocks until openvpn is connected
restartTickerCancel()
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
go unboundLooper.RunRestartTicker(restartTickerContext)
onConnected(allSettings, logger, routingConf, portForward, restartUnbound, restartPublicIP)
}
}
}()
if err := firewallConf.SetOutboundSubnets(ctx, allSettings.Firewall.OutboundSubnets); err != nil {
return err
}
if err := routingConf.SetOutboundRoutes(allSettings.Firewall.OutboundSubnets); err != nil {
return err
}
httpServer := server.New("0.0.0.0:8000", logger, restartOpenvpn, restartUnbound, getOpenvpnSettings, getPortForwarded)
go httpServer.Run(ctx, wg)
if err := tun.Check(constants.TunnelDevice); err != nil {
logger.Info(err.Error() + "; creating it...")
err = tun.Create(constants.TunnelDevice)
if err != nil {
return err
// Start openvpn for the first time
restartOpenvpn()
signalsCh := make(chan os.Signal, 1)
signal.Notify(signalsCh,
syscall.SIGINT,
syscall.SIGTERM,
os.Interrupt,
)
shutdownErrorsCount := 0
select {
case signal := <-signalsCh:
logger.Warn("Caught OS signal %s, shutting down", signal)
cancel()
case <-ctx.Done():
logger.Warn("context canceled, shutting down")
}
logger.Info("Clearing ip status file %s", allSettings.System.IPStatusFilepath)
if err := fileManager.Remove(string(allSettings.System.IPStatusFilepath)); err != nil {
logger.Error(err)
shutdownErrorsCount++
}
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
logger.Info("Clearing forwarded port status file %s", allSettings.OpenVPN.Provider.PortForwarding.Filepath)
if err := fileManager.Remove(string(allSettings.OpenVPN.Provider.PortForwarding.Filepath)); err != nil {
logger.Error(err)
shutdownErrorsCount++
}
}
waiting, waited := context.WithTimeout(context.Background(), time.Second)
go func() {
defer waited()
wg.Wait()
}()
<-waiting.Done()
if waiting.Err() == context.DeadlineExceeded {
if shutdownErrorsCount > 0 {
logger.Warn("Shutdown had %d errors", shutdownErrorsCount)
}
logger.Warn("Shutdown timed out")
return 1
}
if shutdownErrorsCount > 0 {
logger.Warn("Shutdown had %d errors")
return 1
}
logger.Info("Shutdown successful")
return 0
}
if *allSettings.Firewall.Enabled {
err := firewallConf.SetEnabled(ctx, true) // disabled by default
func makeFatalOnError(logger logging.Logger, cancel context.CancelFunc) func(err error) {
return func(err error) {
if err != nil {
return err
logger.Error(err)
cancel()
}
}
}
for _, port := range allSettings.Firewall.InputPorts {
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
if err != nil {
return err
}
} // TODO move inside firewall?
// Shutdown settings
const totalShutdownTimeout = 3 * time.Second
const defaultShutdownTimeout = 400 * time.Millisecond
defaultShutdownOnSuccess := func(goRoutineName string) {
logger.Info(goRoutineName + ": terminated ✔️")
func createLogger() logging.Logger {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
if err != nil {
panic(err)
}
defaultShutdownOnFailure := func(goRoutineName string, err error) {
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
}
defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess)}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
portForwardLogger := logger.NewChild(logging.Settings{Prefix: "port forwarding: "})
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
httpClient, firewallConf, portForwardLogger)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
"port forwarding", goroutine.OptionTimeout(time.Second))
go portForwardLooper.Run(portForwardCtx, portForwardDone)
unboundLogger := logger.NewChild(logging.Settings{Prefix: "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)
publicIPLooper := publicip.NewLoop(httpClient,
logger.NewChild(logging.Settings{Prefix: "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)
vpnLogger := logger.NewChild(logging.Settings{Prefix: "vpn: "})
vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts,
allServers, 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.NewLooper(allSettings.Updater,
allServers, storage, vpnLooper.SetServers, httpClient,
logger.NewChild(logging.Settings{Prefix: "updater: "}))
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(updaterCtx, updaterDone)
tickersGroupHandler.Add(updaterHandler)
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop(
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks,
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := fmt.Sprintf(":%d", *allSettings.ControlServer.Port)
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.NewChild(logging.Settings{Prefix: "http server: "}),
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper)
go httpServer.Run(httpServerCtx, httpServerDone)
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call
// until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
<-ctx.Done()
return orderHandler.Shutdown(context.Background())
return logger
}
type printVersionElement struct {
name string
getVersion func(ctx context.Context) (version string, err error)
}
type infoer interface {
Info(s string)
}
func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement) (err error) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
func printVersions(ctx context.Context, logger logging.Logger, versionFunctions map[string]func(ctx context.Context) (string, error)) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
for _, element := range elements {
version, err := element.getVersion(ctx)
for name, f := range versionFunctions {
version, err := f(ctx)
if err != nil {
return err
logger.Error(err)
} else {
logger.Info("%s version: %s", name, version)
}
}
}
func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger, logger logging.Logger, signalConnected func()) {
// Blocking line merging paramsReader for all programs: openvpn, tinyproxy, unbound and shadowsocks
logger.Info("Launching standard output merger")
streamMerger.CollectLines(ctx, func(line string) {
line, level := gluetunLogging.PostProcessLine(line)
if line == "" {
return
}
switch level {
case logging.InfoLevel:
logger.Info(line)
case logging.WarnLevel:
logger.Warn(line)
case logging.ErrorLevel:
logger.Error(line)
}
if strings.Contains(line, "Initialization Sequence Completed") {
signalConnected()
}
}, func(err error) {
logger.Warn(err)
})
}
func onConnected(allSettings settings.Settings, logger logging.Logger, routingConf routing.Routing,
portForward, restartUnbound, restartPublicIP func(),
) {
restartUnbound()
restartPublicIP()
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
time.AfterFunc(5*time.Second, portForward)
}
defaultInterface, _, err := routingConf.DefaultRoute()
if err != nil {
logger.Warn(err)
} else {
vpnGatewayIP, err := routingConf.VPNGatewayIP(defaultInterface)
if err != nil {
logger.Warn(err)
} else {
logger.Info("Gateway VPN IP address: %s", vpnGatewayIP)
}
logger.Info(element.name + " version: " + version)
}
return nil
}

365
cmd/hostfinder/main.go Normal file
View File

@@ -0,0 +1,365 @@
package main
import (
"context"
"flag"
"fmt"
"net"
"os"
"sort"
)
func main() {
ctx := context.Background()
os.Exit(_main(ctx))
}
func _main(ctx context.Context) int {
fmt.Println("Host finder for Cyberghost")
resolverAddress := flag.String("resolver", "1.1.1.1", "DNS Resolver IP address to use")
flag.Parse()
resolver := newResolver(*resolverAddress)
lookupIP := newLookupIP(resolver)
const domain = "cg-dialup.net"
groups := getCyberghostGroups()
countryCodes := getCountryCodes()
type result struct {
groupName string
region string
subdomain string
exists bool
}
resultsChannel := make(chan result)
const maxGoroutines = 10
guard := make(chan struct{}, maxGoroutines)
fmt.Print("Subdomains found: ")
for groupName, groupID := range groups {
for country, countryCode := range countryCodes {
go func(groupName, groupID, country, countryCode string) {
r := result{
region: country,
groupName: groupName,
subdomain: fmt.Sprintf("%s-%s", groupID, countryCode),
}
fqdn := fmt.Sprintf("%s.%s", r.subdomain, domain)
guard <- struct{}{}
ips, err := lookupIP(ctx, fqdn)
<-guard
if err == nil && len(ips) > 0 {
r.exists = true
}
resultsChannel <- r
}(groupName, groupID, country, countryCode)
}
}
results := make([]result, len(groups)*len(countryCodes))
for i := range results {
results[i] = <-resultsChannel
fmt.Printf("%s ", results[i].subdomain)
}
fmt.Print("\n\n")
sort.Slice(results, func(i, j int) bool {
return results[i].region < results[j].region
})
for _, r := range results {
if r.exists {
// Use in resolver program
fmt.Printf("{subdomain: %q, region: %q, group: %q},\n", r.subdomain, r.region, r.groupName)
}
}
return 0
}
func newResolver(ip string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", net.JoinHostPort(ip, "53"))
},
}
}
type lookupIPFunc func(ctx context.Context, host string) (ips []net.IP, err error)
func newLookupIP(r *net.Resolver) lookupIPFunc {
return func(ctx context.Context, host string) (ips []net.IP, err error) {
addresses, err := r.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
ips = make([]net.IP, len(addresses))
for i := range addresses {
ips[i] = addresses[i].IP
}
return ips, nil
}
}
func getCyberghostGroups() map[string]string {
return map[string]string{
"Premium UDP Europe": "87-1",
"Premium UDP USA": "94-1",
"Premium UDP Asia": "95-1",
"NoSpy UDP Europe": "87-8",
"Premium TCP Europe": "97-1",
"Premium TCP USA": "93-1",
"Premium TCP Asia": "96-1",
"NoSpy TCP Europe": "97-8",
}
}
func getCountryCodes() map[string]string {
return map[string]string{
"Afghanistan": "af",
"Aland Islands": "ax",
"Albania": "al",
"Algeria": "dz",
"American Samoa": "as",
"Andorra": "ad",
"Angola": "ao",
"Anguilla": "ai",
"Antarctica": "aq",
"Antigua and Barbuda": "ag",
"Argentina": "ar",
"Armenia": "am",
"Aruba": "aw",
"Australia": "au",
"Austria": "at",
"Azerbaijan": "az",
"Bahamas": "bs",
"Bahrain": "bh",
"Bangladesh": "bd",
"Barbados": "bb",
"Belarus": "by",
"Belgium": "be",
"Belize": "bz",
"Benin": "bj",
"Bermuda": "bm",
"Bhutan": "bt",
"Bolivia": "bo",
"Bonaire": "bq",
"Bosnia and Herzegovina": "ba",
"Botswana": "bw",
"Bouvet Island": "bv",
"Brazil": "br",
"British Indian Ocean Territory": "io",
"British Virgin Islands": "vg",
"Brunei Darussalam": "bn",
"Bulgaria": "bg",
"Burkina Faso": "bf",
"Burundi": "bi",
"Cambodia": "kh",
"Cameroon": "cm",
"Canada": "ca",
"Cape Verde": "cv",
"Cayman Islands": "ky",
"Central African Republic": "cf",
"Chad": "td",
"Chile": "cl",
"China": "cn",
"Christmas Island": "cx",
"Cocos Islands": "cc",
"Colombia": "co",
"Comoros": "km",
"Congo": "cg",
"Cook Islands": "ck",
"Costa Rica": "cr",
"Cote d'Ivoire": "ci",
"Croatia": "hr",
"Cuba": "cu",
"Curacao": "cw",
"Cyprus": "cy",
"Czech Republic": "cz",
"Democratic Republic of the Congo": "cd",
"Denmark": "dk",
"Djibouti": "dj",
"Dominica": "dm",
"Dominican Republic": "do",
"Ecuador": "ec",
"Egypt": "eg",
"El Salvador": "sv",
"Equatorial Guinea": "gq",
"Eritrea": "er",
"Estonia": "ee",
"Ethiopia": "et",
"Falkland Islands": "fk",
"Faroe Islands": "fo",
"Fiji": "fj",
"Finland": "fi",
"France": "fr",
"French Guiana": "gf",
"French Polynesia": "pf",
"French Southern Territories": "tf",
"Gabon": "ga",
"Gambia": "gm",
"Georgia": "ge",
"Germany": "de",
"Ghana": "gh",
"Gibraltar": "gi",
"Greece": "gr",
"Greenland": "gl",
"Grenada": "gd",
"Guadeloupe": "gp",
"Guam": "gu",
"Guatemala": "gt",
"Guernsey": "gg",
"Guinea-Bissau": "gw",
"Guinea": "gn",
"Guyana": "gy",
"Haiti": "ht",
"Heard Island and McDonald Islands": "hm",
"Honduras": "hn",
"Hong Kong": "hk",
"Hungary": "hu",
"Iceland": "is",
"India": "in",
"Indonesia": "id",
"Iran": "ir",
"Iraq": "iq",
"Ireland": "ie",
"Isle of Man": "im",
"Israel": "il",
"Italy": "it",
"Jamaica": "jm",
"Japan": "jp",
"Jersey": "je",
"Jordan": "jo",
"Kazakhstan": "kz",
"Kenya": "ke",
"Kiribati": "ki",
"Korea": "kr",
"Kuwait": "kw",
"Kyrgyzstan": "kg",
"Lao People's Democratic Republic": "la",
"Latvia": "lv",
"Lebanon": "lb",
"Lesotho": "ls",
"Liberia": "lr",
"Libya": "ly",
"Liechtenstein": "li",
"Lithuania": "lt",
"Luxembourg": "lu",
"Macao": "mo",
"Macedonia": "mk",
"Madagascar": "mg",
"Malawi": "mw",
"Malaysia": "my",
"Maldives": "mv",
"Mali": "ml",
"Malta": "mt",
"Marshall Islands": "mh",
"Martinique": "mq",
"Mauritania": "mr",
"Mauritius": "mu",
"Mayotte": "yt",
"Mexico": "mx",
"Micronesia": "fm",
"Moldova": "md",
"Monaco": "mc",
"Mongolia": "mn",
"Montenegro": "me",
"Montserrat": "ms",
"Morocco": "ma",
"Mozambique": "mz",
"Myanmar": "mm",
"Namibia": "na",
"Nauru": "nr",
"Nepal": "np",
"Netherlands": "nl",
"New Caledonia": "nc",
"New Zealand": "nz",
"Nicaragua": "ni",
"Niger": "ne",
"Nigeria": "ng",
"Niue": "nu",
"Norfolk Island": "nf",
"Northern Mariana Islands": "mp",
"Norway": "no",
"Oman": "om",
"Pakistan": "pk",
"Palau": "pw",
"Palestine, State of": "ps",
"Panama": "pa",
"Papua New Guinea": "pg",
"Paraguay": "py",
"Peru": "pe",
"Philippines": "ph",
"Pitcairn": "pn",
"Poland": "pl",
"Portugal": "pt",
"Puerto Rico": "pr",
"Qatar": "qa",
"Reunion": "re",
"Romania": "ro",
"Russian Federation": "ru",
"Rwanda": "rw",
"Saint Barthelemy": "bl",
"Saint Helena": "sh",
"Saint Kitts and Nevis": "kn",
"Saint Lucia": "lc",
"Saint Martin": "mf",
"Saint Pierre and Miquelon": "pm",
"Saint Vincent and the Grenadines": "vc",
"Samoa": "ws",
"San Marino": "sm",
"Sao Tome and Principe": "st",
"Saudi Arabia": "sa",
"Senegal": "sn",
"Serbia": "rs",
"Seychelles": "sc",
"Sierra Leone": "sl",
"Singapore": "sg",
"Sint Maarten": "sx",
"Slovakia": "sk",
"Slovenia": "si",
"Solomon Islands": "sb",
"Somalia": "so",
"South Africa": "za",
"South Georgia and the South Sandwich Islands": "gs",
"South Sudan": "ss",
"Spain": "es",
"Sri Lanka": "lk",
"Sudan": "sd",
"Suriname": "sr",
"Svalbard and Jan Mayen": "sj",
"Swaziland": "sz",
"Sweden": "se",
"Switzerland": "ch",
"Syrian Arab Republic": "sy",
"Taiwan": "tw",
"Tajikistan": "tj",
"Tanzania": "tz",
"Thailand": "th",
"Timor-Leste": "tl",
"Togo": "tg",
"Tokelau": "tk",
"Tonga": "to",
"Trinidad and Tobago": "tt",
"Tunisia": "tn",
"Turkey": "tr",
"Turkmenistan": "tm",
"Turks and Caicos Islands": "tc",
"Tuvalu": "tv",
"Uganda": "ug",
"Ukraine": "ua",
"United Arab Emirates": "ae",
"United Kingdom": "gb",
"United States Minor Outlying Islands": "um",
"United States": "us",
"Uruguay": "uy",
"US Virgin Islands": "vi",
"Uzbekistan": "uz",
"Vanuatu": "vu",
"Vatican City State": "va",
"Venezuela": "ve",
"Vietnam": "vn",
"Wallis and Futuna": "wf",
"Western Sahara": "eh",
"Yemen": "ye",
"Zambia": "zm",
"Zimbabwe": "zw",
}
}

View File

@@ -0,0 +1,111 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/qdm12/golibs/network"
)
func main() {
os.Exit(_main())
}
func _main() int {
provider := flag.String("provider", "purevpn", "VPN provider to map location to subdomain, can be 'purevpn'")
flag.Parse()
client := network.NewClient(5 * time.Second)
switch *provider {
case "purevpn":
servers, warnings, err := purevpn(client)
if err != nil {
fmt.Println(err)
return 1
}
for _, server := range servers {
fmt.Printf(
"{subdomain: %q, region: %q, country: %q, city: %q},\n",
server.subdomain, server.region, server.country, server.city,
)
}
fmt.Print("\n\n")
for _, warning := range warnings {
fmt.Println(warning)
}
default:
fmt.Printf("Provider %q is not supported\n", *provider)
return 1
}
return 0
}
type purevpnServer struct {
region string
country string
city string
subdomain string // without -tcp or -udp suffix
}
func purevpn(client network.Client) (servers []purevpnServer, warnings []string, err error) {
content, status, err := client.GetContent("https://support.purevpn.com/vpn-servers")
if err != nil {
return nil, nil, err
} else if status != http.StatusOK {
return nil, nil, fmt.Errorf("HTTP status %d from Purevpn", status)
}
const jsonPrefix = "<script>var servers = "
const jsonSuffix = "</script>"
s := string(content)
jsonPrefixIndex := strings.Index(s, jsonPrefix)
if jsonPrefixIndex == -1 {
return nil, nil, fmt.Errorf("cannot find prefix %s in html", jsonPrefix)
}
if len(s[jsonPrefixIndex:]) == len(jsonPrefix) {
return nil, nil, fmt.Errorf("no body after json prefix %s", jsonPrefix)
}
s = s[jsonPrefixIndex+len(jsonPrefix):]
endIndex := strings.Index(s, jsonSuffix)
s = s[:endIndex]
var data []struct {
Region string `json:"region_name"`
Country string `json:"country_name"`
City string `json:"city_name"`
TCP string `json:"tcp"`
UDP string `json:"udp"`
}
if err := json.Unmarshal([]byte(s), &data); err != nil {
return nil, nil, err
}
sort.Slice(data, func(i, j int) bool {
if data[i].Region == data[j].Region {
if data[i].Country == data[j].Country {
return data[i].City < data[j].City
}
return data[i].Country < data[j].Country
}
return data[i].Region < data[j].Region
})
for i := range data {
if data[i].UDP == "" && data[i].TCP == "" {
warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP and UDP for openvpn", data[i].Region, data[i].Country, data[i].City))
continue
}
if data[i].UDP == "" || data[i].TCP == "" {
warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP or udp for openvpn", data[i].Region, data[i].Country, data[i].City))
}
servers = append(servers, purevpnServer{
region: data[i].Region,
country: data[i].Country,
city: data[i].City,
subdomain: strings.TrimSuffix(data[i].TCP, "-tcp.pointtoserver.com"),
})
}
return servers, warnings, nil
}

106
cmd/mapper/main.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/network"
)
func main() {
os.Exit(_main())
}
func _main() int {
provider := flag.String("provider", "nordvpn", "VPN provider to map region to IP addresses using their API, can be 'nordvpn'")
flag.Parse()
client := network.NewClient(30 * time.Second) // big file so 30 seconds
switch *provider {
case "nordvpn":
servers, ignoredServers, err := nordvpn(client)
if err != nil {
fmt.Println(err)
return 1
}
for _, server := range servers {
fmt.Printf(
"{Region: %q, Number: %d, TCP: %t, UDP: %t, IP: net.IP{%s}},\n",
server.Region, server.Number, server.TCP, server.UDP, strings.ReplaceAll(server.IP.String(), ".", ", "),
)
}
fmt.Print("\n\n")
for _, serverName := range ignoredServers {
fmt.Printf("ignored server %q because it does not support both UDP and TCP\n", serverName)
}
default:
fmt.Printf("Provider %q is not supported\n", *provider)
return 1
}
return 0
}
func nordvpn(client network.Client) (servers []models.NordvpnServer, ignoredServers []string, err error) {
content, status, err := client.GetContent("https://nordvpn.com/api/server")
if err != nil {
return nil, nil, err
} else if status != http.StatusOK {
return nil, nil, fmt.Errorf("HTTP status %d from NordVPN API", status)
}
response := []struct {
IPAddress string `json:"ip_address"`
Name string `json:"name"`
Country string `json:"country"`
Features struct {
UDP bool `json:"openvpn_udp"`
TCP bool `json:"openvpn_tcp"`
} `json:"features"`
}{}
if err := json.Unmarshal(content, &response); err != nil {
return nil, nil, err
}
for _, element := range response {
if !element.Features.TCP && !element.Features.UDP {
ignoredServers = append(ignoredServers, element.Name)
}
ip := net.ParseIP(element.IPAddress)
if ip == nil {
return nil, nil, fmt.Errorf("IP address %q is not valid for server %q", element.IPAddress, element.Name)
}
i := strings.IndexRune(element.Name, '#')
if i < 0 {
return nil, nil, fmt.Errorf("No ID in server name %q", element.Name)
}
idString := element.Name[i+1:]
idUint64, err := strconv.ParseUint(idString, 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("Bad ID in server name %q", element.Name)
}
id := uint16(idUint64)
server := models.NordvpnServer{
Region: element.Country,
Number: id,
IP: ip,
TCP: element.Features.TCP,
UDP: element.Features.UDP,
}
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {
if servers[i].Region == servers[j].Region {
return servers[i].Number < servers[j].Number
}
return servers[i].Region < servers[j].Region
})
return servers, ignoredServers, nil
}

152
cmd/ovpnparser/main.go Normal file
View File

@@ -0,0 +1,152 @@
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/qdm12/golibs/network"
)
func main() {
os.Exit(_main())
}
// Find subdomains from .ovpn files contained in a .zip file
func _main() int {
provider := flag.String("provider", "surfshark", "VPN provider to parse openvpn files for, can be 'surfshark' or 'vyprvpn")
flag.Parse()
var urls []string
var suffix string
switch *provider {
case "surfshark":
urls = []string{
"https://account.surfshark.com/api/v1/server/configurations",
"https://v2uploads.zopim.io/p/2/L/p2LbwLkvfQoSdzOl6VEltzQA6StiZqrs/12500634259669c77012765139bcfe4f4c90db1e.zip",
}
suffix = ".prod.surfshark.com"
case "vyprvpn":
urls = []string{
"https://support.vyprvpn.com/hc/article_attachments/360052617332/Vypr_OpenVPN_20200320.zip",
}
suffix = ".vyprvpn.com"
default:
fmt.Printf("Provider %q is not supported\n", *provider)
return 1
}
contents, err := fetchAndExtractFiles(urls...)
if err != nil {
fmt.Println(err)
return 1
}
uniqueSubdomainsToFilename := make(map[string]string)
for fileName, content := range contents {
subdomain, err := extractInformation(content, suffix)
if err != nil {
fmt.Println(err)
return 1
} else if len(subdomain) > 0 {
fileName = strings.TrimSuffix(fileName, ".ovpn")
fileName = strings.ReplaceAll(fileName, " - ", " ")
uniqueSubdomainsToFilename[subdomain] = fileName
}
}
type subdomainFilename struct {
subdomain string
fileName string
}
subdomains := make([]subdomainFilename, len(uniqueSubdomainsToFilename))
i := 0
for subdomain, fileName := range uniqueSubdomainsToFilename {
subdomains[i] = subdomainFilename{
subdomain: subdomain,
fileName: fileName,
}
i++
}
sort.Slice(subdomains, func(i, j int) bool {
return subdomains[i].subdomain < subdomains[j].subdomain
})
fmt.Println("Subdomain Filename")
for i := range subdomains {
fmt.Printf("%s %s\n", subdomains[i].subdomain, subdomains[i].fileName)
}
return 0
}
func fetchAndExtractFiles(urls ...string) (contents map[string][]byte, err error) {
client := network.NewClient(10 * time.Second)
contents = make(map[string][]byte)
for _, url := range urls {
zipBytes, status, err := client.GetContent(url)
if err != nil {
return nil, err
} else if status != http.StatusOK {
return nil, fmt.Errorf("Getting %s results in HTTP status code %d", url, status)
}
newContents, err := zipExtractAll(zipBytes)
if err != nil {
return nil, err
}
for fileName, content := range newContents {
contents[fileName] = content
}
}
return contents, nil
}
func zipExtractAll(zipBytes []byte) (contents map[string][]byte, err error) {
r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
return nil, err
}
contents = map[string][]byte{}
for _, zf := range r.File {
fileName := filepath.Base(zf.Name)
if !strings.HasSuffix(fileName, ".ovpn") {
continue
}
f, err := zf.Open()
if err != nil {
return nil, err
}
defer f.Close()
contents[fileName], err = ioutil.ReadAll(f)
if err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
}
return contents, nil
}
func extractInformation(content []byte, suffix string) (subdomain string, err error) {
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "remote ") {
words := strings.Fields(line)
if len(words) < 2 {
return "", fmt.Errorf("not enough words on line %q", line)
}
host := words[1]
if net.ParseIP(host) != nil {
return "", nil // ignore IP addresses
}
return strings.TrimSuffix(host, suffix), nil
}
}
return "", fmt.Errorf("could not find remote line in: %s", string(content))
}

919
cmd/resolver/main.go Normal file
View File

@@ -0,0 +1,919 @@
package main
import (
"bytes"
"context"
"flag"
"fmt"
"net"
"os"
"sort"
"strings"
)
func main() {
ctx := context.Background()
os.Exit(_main(ctx))
}
func _main(ctx context.Context) int {
resolverAddress := flag.String("resolver", "1.1.1.1", "DNS Resolver IP address to use")
provider := flag.String("provider", "pia", "VPN provider to resolve for, 'pia', 'windscribe', 'cyberghost', 'vyprvpn' or 'purevpn'")
region := flag.String("region", "all", "Comma separated list of VPN provider region names to resolve for, use 'all' to resolve all")
flag.Parse()
resolver := newResolver(*resolverAddress)
lookupIP := newLookupIP(resolver)
var domain string
var servers []server
switch *provider {
case "pia":
domain = "privateinternetaccess.com"
servers = piaServers()
case "windscribe":
domain = "windscribe.com"
servers = windscribeServers()
case "surfshark":
domain = "prod.surfshark.com"
servers = surfsharkServers()
case "cyberghost":
domain = "cg-dialup.net"
servers = cyberghostServers()
case "vyprvpn":
domain = "vyprvpn.com"
servers = vyprvpnServers()
case "purevpn":
domain = "pointtoserver.com"
servers = purevpnServers()
default:
fmt.Printf("Provider %q is not supported\n", *provider)
return 1
}
if *region != "all" {
regions := strings.Split(*region, ",")
uniqueRegions := make(map[string]struct{})
for _, r := range regions {
uniqueRegions[r] = struct{}{}
}
for i := range servers {
if _, ok := uniqueRegions[servers[i].region]; !ok {
servers[i] = servers[len(servers)-1]
servers = servers[:len(servers)-1]
}
}
}
stringChannel := make(chan string)
errorChannel := make(chan error)
const maxGoroutines = 10
guard := make(chan struct{}, maxGoroutines)
for _, s := range servers {
go func(s server) {
guard <- struct{}{}
ips, err := resolveRepeat(ctx, lookupIP, s.subdomain+"."+domain, 3)
<-guard
if err != nil {
errorChannel <- err
return
}
stringChannel <- formatLine(*provider, s, ips)
}(s)
}
var lines []string
var errors []error
for range servers {
select {
case err := <-errorChannel:
errors = append(errors, err)
case s := <-stringChannel:
lines = append(lines, s)
}
}
sort.Slice(lines, func(i, j int) bool {
return lines[i] < lines[j]
})
for _, s := range lines {
fmt.Println(s)
}
if len(errors) > 0 {
fmt.Printf("\n%d errors occurred, described below\n\n", len(errors))
for _, err := range errors {
fmt.Println(err)
}
return 1
}
return 0
}
func formatLine(provider string, s server, ips []net.IP) string {
ipStrings := make([]string, len(ips))
for i := range ips {
ipStrings[i] = fmt.Sprintf("{%s}", strings.ReplaceAll(ips[i].String(), ".", ", "))
}
ipString := strings.Join(ipStrings, ", ")
switch provider {
case "pia":
return fmt.Sprintf(
"{Region: %q, IPs: []net.IP{%s}},",
s.region, ipString,
)
case "windscribe":
return fmt.Sprintf(
"{Region: %q, IPs: []net.IP{%s}},",
s.region, ipString,
)
case "surfshark":
return fmt.Sprintf(
"{Region: %q, IPs: []net.IP{%s}},",
s.region, ipString,
)
case "cyberghost":
return fmt.Sprintf(
"{Region: %q, Group: %q, IPs: []net.IP{%s}},",
s.region, s.group, ipString,
)
case "vyprvpn":
return fmt.Sprintf(
"{Region: %q, IPs: []net.IP{%s}},",
s.region, ipString,
)
case "purevpn":
return fmt.Sprintf(
"{Region: %q, Country: %q, City: %q, IPs: []net.IP{%s}},",
s.region, s.country, s.city, ipString,
)
}
return ""
}
type lookupIPFunc func(ctx context.Context, host string) (ips []net.IP, err error)
func newLookupIP(r *net.Resolver) lookupIPFunc {
return func(ctx context.Context, host string) (ips []net.IP, err error) {
addresses, err := r.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
ips = make([]net.IP, len(addresses))
for i := range addresses {
ips[i] = addresses[i].IP
}
return ips, nil
}
}
func newResolver(ip string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", net.JoinHostPort(ip, "53"))
},
}
}
func resolveRepeat(ctx context.Context, lookupIP lookupIPFunc, host string, n int) (ips []net.IP, err error) {
for i := 0; i < n; i++ {
newIPs, err := lookupIP(ctx, host)
if err != nil {
return nil, err
}
ips = append(ips, newIPs...)
}
return uniqueSortedIPs(ips), nil
}
func uniqueSortedIPs(ips []net.IP) []net.IP {
uniqueIPs := make(map[string]struct{})
for _, ip := range ips {
uniqueIPs[ip.String()] = struct{}{}
}
ips = make([]net.IP, len(uniqueIPs))
i := 0
for ip := range uniqueIPs {
ips[i] = net.ParseIP(ip)
i++
}
sort.Slice(ips, func(i, j int) bool {
return bytes.Compare(ips[i], ips[j]) < 0
})
return ips
}
type server struct {
subdomain string
region string
group string // only for cyberghost
country string // only for purevpn
city string // only for purevpn
}
func piaServers() []server {
return []server{
{subdomain: "au-melbourne", region: "AU Melbourne"},
{subdomain: "au-perth", region: "AU Perth"},
{subdomain: "au-sydney", region: "AU Sydney"},
{subdomain: "austria", region: "Austria"},
{subdomain: "belgium", region: "Belgium"},
{subdomain: "ca-montreal", region: "CA Montreal"},
{subdomain: "ca-toronto", region: "CA Toronto"},
{subdomain: "ca-vancouver", region: "CA Vancouver"},
{subdomain: "czech", region: "Czech Republic"},
{subdomain: "de-berlin", region: "DE Berlin"},
{subdomain: "de-frankfurt", region: "DE Frankfurt"},
{subdomain: "denmark", region: "Denmark"},
{subdomain: "fi", region: "Finlan"},
{subdomain: "france", region: "France"},
{subdomain: "hungary", region: "Hungary"},
{subdomain: "in", region: "India"},
{subdomain: "ireland", region: "Ireland"},
{subdomain: "israel", region: "Israel"},
{subdomain: "italy", region: "Italy"},
{subdomain: "japan", region: "Japan"},
{subdomain: "lu", region: "Luxembourg"},
{subdomain: "mexico", region: "Mexico"},
{subdomain: "nl", region: "Netherlands"},
{subdomain: "nz", region: "New Zealand"},
{subdomain: "no", region: "Norway"},
{subdomain: "poland", region: "Poland"},
{subdomain: "ro", region: "Romania"},
{subdomain: "sg", region: "Singapore"},
{subdomain: "spain", region: "Spain"},
{subdomain: "sweden", region: "Sweden"},
{subdomain: "swiss", region: "Switzerland"},
{subdomain: "ae", region: "UAE"},
{subdomain: "uk-london", region: "UK London"},
{subdomain: "uk-manchester", region: "UK Manchester"},
{subdomain: "uk-southampton", region: "UK Southampton"},
{subdomain: "us-atlanta", region: "US Atlanta"},
{subdomain: "us-california", region: "US California"},
{subdomain: "us-chicago", region: "US Chicago"},
{subdomain: "us-dallas", region: "US Dallas"},
{subdomain: "us-denver", region: "US Denver"},
{subdomain: "us-east", region: "US East"},
{subdomain: "us-florida", region: "US Florida"},
{subdomain: "us-houston", region: "US Houston"},
{subdomain: "us-lasvegas", region: "US Las Vegas"},
{subdomain: "us-newyorkcity", region: "US New York City"},
{subdomain: "us-seattle", region: "US Seattle"},
{subdomain: "us-siliconvalley", region: "US Silicon Valley"},
{subdomain: "us-washingtondc", region: "US Washington DC"},
{subdomain: "us-west", region: "US West"},
}
}
func windscribeServers() []server {
return []server{
{subdomain: "al", region: "Albania"},
{subdomain: "ar", region: "Argentina"},
{subdomain: "au", region: "Australia"},
{subdomain: "at", region: "Austria"},
{subdomain: "az", region: "Azerbaijan"},
{subdomain: "be", region: "Belgium"},
{subdomain: "ba", region: "Bosnia"},
{subdomain: "br", region: "Brazil"},
{subdomain: "bg", region: "Bulgaria"},
{subdomain: "ca", region: "Canada East"},
{subdomain: "ca-west", region: "Canada West"},
{subdomain: "co", region: "Colombia"},
{subdomain: "hr", region: "Croatia"},
{subdomain: "cy", region: "Cyprus"},
{subdomain: "cz", region: "Czech republic"},
{subdomain: "dk", region: "Denmark"},
{subdomain: "ee", region: "Estonia"},
{subdomain: "aq", region: "Fake antarctica"},
{subdomain: "fi", region: "Finland"},
{subdomain: "fr", region: "France"},
{subdomain: "ge", region: "Georgia"},
{subdomain: "de", region: "Germany"},
{subdomain: "gr", region: "Greece"},
{subdomain: "hk", region: "Hong kong"},
{subdomain: "hu", region: "Hungary"},
{subdomain: "is", region: "Iceland"},
{subdomain: "in", region: "India"},
{subdomain: "id", region: "Indonesia"},
{subdomain: "ie", region: "Ireland"},
{subdomain: "il", region: "Israel"},
{subdomain: "it", region: "Italy"},
{subdomain: "jp", region: "Japan"},
{subdomain: "lv", region: "Latvia"},
{subdomain: "lt", region: "Lithuania"},
{subdomain: "mk", region: "Macedonia"},
{subdomain: "my", region: "Malaysia"},
{subdomain: "mx", region: "Mexico"},
{subdomain: "md", region: "Moldova"},
{subdomain: "nl", region: "Netherlands"},
{subdomain: "nz", region: "New zealand"},
{subdomain: "no", region: "Norway"},
{subdomain: "ph", region: "Philippines"},
{subdomain: "pl", region: "Poland"},
{subdomain: "pt", region: "Portugal"},
{subdomain: "ro", region: "Romania"},
{subdomain: "ru", region: "Russia"},
{subdomain: "rs", region: "Serbia"},
{subdomain: "sg", region: "Singapore"},
{subdomain: "sk", region: "Slovakia"},
{subdomain: "si", region: "Slovenia"},
{subdomain: "za", region: "South Africa"},
{subdomain: "kr", region: "South Korea"},
{subdomain: "es", region: "Spain"},
{subdomain: "se", region: "Sweden"},
{subdomain: "ch", region: "Switzerland"},
{subdomain: "th", region: "Thailand"},
{subdomain: "tn", region: "Tunisia"},
{subdomain: "tr", region: "Turkey"},
{subdomain: "ua", region: "Ukraine"},
{subdomain: "ae", region: "United Arab Emirates"},
{subdomain: "uk", region: "United Kingdom"},
{subdomain: "us-central", region: "US Central"},
{subdomain: "us-east", region: "US East"},
{subdomain: "us-west", region: "US West"},
{subdomain: "vn", region: "Vietnam"},
{subdomain: "wf-ca", region: "Windflix CA"},
{subdomain: "wf-jp", region: "Windflix JP"},
{subdomain: "wf-uk", region: "Windflix UK"},
{subdomain: "wf-us", region: "Windflix US"},
}
}
func surfsharkServers() []server {
return []server{
{subdomain: "ae-dub", region: "United Arab Emirates"},
{subdomain: "al-tia", region: "Albania"},
{subdomain: "at-vie", region: "Austria"},
{subdomain: "au-adl", region: "Australia Adelaide"},
{subdomain: "au-bne", region: "Australia Brisbane"},
{subdomain: "au-mel", region: "Australia Melbourne"},
{subdomain: "au-per", region: "Australia Perth"},
{subdomain: "au-syd", region: "Australia Sydney"},
{subdomain: "au-us", region: "Australia US"},
{subdomain: "az-bak", region: "Azerbaijan"},
{subdomain: "ba-sjj", region: "Bosnia and Herzegovina"},
{subdomain: "be-bru", region: "Belgium"},
{subdomain: "bg-sof", region: "Bulgaria"},
{subdomain: "br-sao", region: "Brazil"},
{subdomain: "ca-mon", region: "Canada Montreal"},
{subdomain: "ca-tor", region: "Canada Toronto"},
{subdomain: "ca-us", region: "Canada US"},
{subdomain: "ca-van", region: "Canada Vancouver"},
{subdomain: "ch-zur", region: "Switzerland"},
{subdomain: "cl-san", region: "Chile"},
{subdomain: "co-bog", region: "Colombia"},
{subdomain: "cr-sjn", region: "Costa Rica"},
{subdomain: "cy-nic", region: "Cyprus"},
{subdomain: "cz-prg", region: "Czech Republic"},
{subdomain: "de-ber", region: "Germany Berlin"},
{subdomain: "de-fra", region: "Germany Frankfurt am Main"},
{subdomain: "de-fra-st001", region: "Germany Frankfurt am Main st001"},
{subdomain: "de-fra-st002", region: "Germany Frankfurt am Main st002"},
{subdomain: "de-fra-st003", region: "Germany Frankfurt am Main st003"},
{subdomain: "de-muc", region: "Germany Munich"},
{subdomain: "de-nue", region: "Germany Nuremberg"},
{subdomain: "de-sg", region: "Germany Singapour"},
{subdomain: "de-uk", region: "Germany UK"},
{subdomain: "dk-cph", region: "Denmark"},
{subdomain: "ee-tll", region: "Estonia"},
{subdomain: "es-bcn", region: "Spain Barcelona"},
{subdomain: "es-mad", region: "Spain Madrid"},
{subdomain: "es-vlc", region: "Spain Valencia"},
{subdomain: "fi-hel", region: "Finland"},
{subdomain: "fr-bod", region: "France Bordeaux"},
{subdomain: "fr-mrs", region: "France Marseilles"},
{subdomain: "fr-par", region: "France Paris"},
{subdomain: "fr-se", region: "France Sweden"},
{subdomain: "gr-ath", region: "Greece"},
{subdomain: "hk-hkg", region: "Hong Kong"},
{subdomain: "hr-zag", region: "Croatia"},
{subdomain: "hu-bud", region: "Hungary"},
{subdomain: "id-jak", region: "Indonesia"},
{subdomain: "ie-dub", region: "Ireland"},
{subdomain: "il-tlv", region: "Israel"},
{subdomain: "in-chn", region: "India Chennai"},
{subdomain: "in-idr", region: "India Indore"},
{subdomain: "in-mum", region: "India Mumbai"},
{subdomain: "in-uk", region: "India UK"},
{subdomain: "is-rkv", region: "Iceland"},
{subdomain: "it-mil", region: "Italy Milan"},
{subdomain: "it-rom", region: "Italy Rome"},
{subdomain: "jp-tok", region: "Japan Tokyo"},
{subdomain: "jp-tok-st001", region: "Japan Tokyo st001"},
{subdomain: "jp-tok-st002", region: "Japan Tokyo st002"},
{subdomain: "jp-tok-st003", region: "Japan Tokyo st003"},
{subdomain: "jp-tok-st004", region: "Japan Tokyo st004"},
{subdomain: "jp-tok-st005", region: "Japan Tokyo st005"},
{subdomain: "jp-tok-st006", region: "Japan Tokyo st006"},
{subdomain: "jp-tok-st007", region: "Japan Tokyo st007"},
{subdomain: "kr-seo", region: "Korea"},
{subdomain: "kz-ura", region: "Kazakhstan"},
{subdomain: "lu-ste", region: "Luxembourg"},
{subdomain: "lv-rig", region: "Latvia"},
{subdomain: "ly-tip", region: "Libya"},
{subdomain: "md-chi", region: "Moldova"},
{subdomain: "mk-skp", region: "North Macedonia"},
{subdomain: "my-kul", region: "Malaysia"},
{subdomain: "ng-lag", region: "Nigeria"},
{subdomain: "nl-ams", region: "Netherlands Amsterdam"},
{subdomain: "nl-ams-st001", region: "Netherlands Amsterdam st001"},
{subdomain: "nl-us", region: "Netherlands US"},
{subdomain: "no-osl", region: "Norway"},
{subdomain: "nz-akl", region: "New Zealand"},
{subdomain: "ph-mnl", region: "Philippines"},
{subdomain: "pl-gdn", region: "Poland Gdansk"},
{subdomain: "pl-waw", region: "Poland Warsaw"},
{subdomain: "pt-lis", region: "Portugal Lisbon"},
{subdomain: "pt-lou", region: "Portugal Loule"},
{subdomain: "pt-opo", region: "Portugal Porto"},
{subdomain: "py-asu", region: "Paraguay"},
{subdomain: "ro-buc", region: "Romania"},
{subdomain: "rs-beg", region: "Serbia"},
{subdomain: "ru-mos", region: "Russia Moscow"},
{subdomain: "ru-spt", region: "Russia St. Petersburg"},
{subdomain: "se-sto", region: "Sweden"},
{subdomain: "sg-hk", region: "Singapore Hong Kong"},
{subdomain: "sg-nl", region: "Singapore Netherlands"},
{subdomain: "sg-sng", region: "Singapore"},
{subdomain: "sg-sng-st001", region: "Singapore st001"},
{subdomain: "sg-sng-st002", region: "Singapore st002"},
{subdomain: "sg-sng-st003", region: "Singapore st003"},
{subdomain: "sg-sng-st004", region: "Singapore st004"},
{subdomain: "si-lju", region: "Slovenia"},
{subdomain: "sk-bts", region: "Slovekia"},
{subdomain: "th-bkk", region: "Thailand"},
{subdomain: "tr-bur", region: "Turkey"},
{subdomain: "tw-tai", region: "Taiwan"},
{subdomain: "ua-iev", region: "Ukraine"},
{subdomain: "uk-de", region: "UK Germany"},
{subdomain: "uk-fr", region: "UK France"},
{subdomain: "uk-gla", region: "UK Glasgow"},
{subdomain: "uk-lon", region: "UK London"},
{subdomain: "uk-lon-st001", region: "UK London st001"},
{subdomain: "uk-lon-st002", region: "UK London st002"},
{subdomain: "uk-lon-st003", region: "UK London st003"},
{subdomain: "uk-lon-st004", region: "UK London st004"},
{subdomain: "uk-lon-st005", region: "UK London st005"},
{subdomain: "uk-man", region: "UK Manchester"},
{subdomain: "us-atl", region: "US Atlanta"},
{subdomain: "us-bdn", region: "US Bend"},
{subdomain: "us-bos", region: "US Boston"},
{subdomain: "us-buf", region: "US Buffalo"},
{subdomain: "us-chi", region: "US Chicago"},
{subdomain: "us-clt", region: "US Charlotte"},
{subdomain: "us-dal", region: "US Dallas"},
{subdomain: "us-den", region: "US Denver"},
{subdomain: "us-dtw", region: "US Gahanna"},
{subdomain: "us-hou", region: "US Houston"},
{subdomain: "us-kan", region: "US Kansas City"},
{subdomain: "us-las", region: "US Las Vegas"},
{subdomain: "us-lax", region: "US Los Angeles"},
{subdomain: "us-ltm", region: "US Latham"},
{subdomain: "us-mia", region: "US Miami"},
{subdomain: "us-mnz", region: "US Maryland"},
{subdomain: "us-nl", region: "US Netherlands"},
{subdomain: "us-nyc", region: "US New York City"},
{subdomain: "us-nyc-mp001", region: "US New York City mp001"},
{subdomain: "us-nyc-st001", region: "US New York City st001"},
{subdomain: "us-nyc-st002", region: "US New York City st002"},
{subdomain: "us-nyc-st003", region: "US New York City st003"},
{subdomain: "us-nyc-st004", region: "US New York City st004"},
{subdomain: "us-nyc-st005", region: "US New York City st005"},
{subdomain: "us-orl", region: "US Orlando"},
{subdomain: "us-phx", region: "US Phoenix"},
{subdomain: "us-pt", region: "US Portugal"},
{subdomain: "us-sea", region: "US Seatle"},
{subdomain: "us-sfo", region: "US San Francisco"},
{subdomain: "us-slc", region: "US Salt Lake City"},
{subdomain: "us-stl", region: "US Saint Louis"},
{subdomain: "us-tpa", region: "US Tampa"},
{subdomain: "vn-hcm", region: "Vietnam"},
{subdomain: "za-jnb", region: "South Africa"},
}
}
func cyberghostServers() []server {
return []server{
{subdomain: "97-1-al", region: "Albania", group: "Premium TCP Europe"},
{subdomain: "87-1-al", region: "Albania", group: "Premium UDP Europe"},
{subdomain: "87-1-dz", region: "Algeria", group: "Premium UDP Europe"},
{subdomain: "97-1-dz", region: "Algeria", group: "Premium TCP Europe"},
{subdomain: "97-1-ad", region: "Andorra", group: "Premium TCP Europe"},
{subdomain: "87-1-ad", region: "Andorra", group: "Premium UDP Europe"},
{subdomain: "94-1-ar", region: "Argentina", group: "Premium UDP USA"},
{subdomain: "93-1-ar", region: "Argentina", group: "Premium TCP USA"},
{subdomain: "87-1-am", region: "Armenia", group: "Premium UDP Europe"},
{subdomain: "97-1-am", region: "Armenia", group: "Premium TCP Europe"},
{subdomain: "95-1-au", region: "Australia", group: "Premium UDP Asia"},
{subdomain: "96-1-au", region: "Australia", group: "Premium TCP Asia"},
{subdomain: "97-1-at", region: "Austria", group: "Premium TCP Europe"},
{subdomain: "87-1-at", region: "Austria", group: "Premium UDP Europe"},
{subdomain: "93-1-bs", region: "Bahamas", group: "Premium TCP USA"},
{subdomain: "94-1-bs", region: "Bahamas", group: "Premium UDP USA"},
{subdomain: "95-1-bd", region: "Bangladesh", group: "Premium UDP Asia"},
{subdomain: "96-1-bd", region: "Bangladesh", group: "Premium TCP Asia"},
{subdomain: "97-1-by", region: "Belarus", group: "Premium TCP Europe"},
{subdomain: "87-1-by", region: "Belarus", group: "Premium UDP Europe"},
{subdomain: "97-1-be", region: "Belgium", group: "Premium TCP Europe"},
{subdomain: "87-1-be", region: "Belgium", group: "Premium UDP Europe"},
{subdomain: "87-1-ba", region: "Bosnia and Herzegovina", group: "Premium UDP Europe"},
{subdomain: "97-1-ba", region: "Bosnia and Herzegovina", group: "Premium TCP Europe"},
{subdomain: "94-1-br", region: "Brazil", group: "Premium UDP USA"},
{subdomain: "93-1-br", region: "Brazil", group: "Premium TCP USA"},
{subdomain: "87-1-bg", region: "Bulgaria", group: "Premium UDP Europe"},
{subdomain: "97-1-bg", region: "Bulgaria", group: "Premium TCP Europe"},
{subdomain: "96-1-kh", region: "Cambodia", group: "Premium TCP Asia"},
{subdomain: "95-1-kh", region: "Cambodia", group: "Premium UDP Asia"},
{subdomain: "93-1-ca", region: "Canada", group: "Premium TCP USA"},
{subdomain: "94-1-ca", region: "Canada", group: "Premium UDP USA"},
{subdomain: "93-1-cl", region: "Chile", group: "Premium TCP USA"},
{subdomain: "94-1-cl", region: "Chile", group: "Premium UDP USA"},
{subdomain: "96-1-cn", region: "China", group: "Premium TCP Asia"},
{subdomain: "95-1-cn", region: "China", group: "Premium UDP Asia"},
{subdomain: "94-1-co", region: "Colombia", group: "Premium UDP USA"},
{subdomain: "93-1-co", region: "Colombia", group: "Premium TCP USA"},
{subdomain: "93-1-cr", region: "Costa Rica", group: "Premium TCP USA"},
{subdomain: "94-1-cr", region: "Costa Rica", group: "Premium UDP USA"},
{subdomain: "87-1-cy", region: "Cyprus", group: "Premium UDP Europe"},
{subdomain: "97-1-cy", region: "Cyprus", group: "Premium TCP Europe"},
{subdomain: "97-1-cz", region: "Czech Republic", group: "Premium TCP Europe"},
{subdomain: "87-1-cz", region: "Czech Republic", group: "Premium UDP Europe"},
{subdomain: "87-1-dk", region: "Denmark", group: "Premium UDP Europe"},
{subdomain: "97-1-dk", region: "Denmark", group: "Premium TCP Europe"},
{subdomain: "87-1-eg", region: "Egypt", group: "Premium UDP Europe"},
{subdomain: "97-1-eg", region: "Egypt", group: "Premium TCP Europe"},
{subdomain: "87-1-ee", region: "Estonia", group: "Premium UDP Europe"},
{subdomain: "97-1-ee", region: "Estonia", group: "Premium TCP Europe"},
{subdomain: "97-1-fi", region: "Finland", group: "Premium TCP Europe"},
{subdomain: "87-1-fi", region: "Finland", group: "Premium UDP Europe"},
{subdomain: "87-1-fr", region: "France", group: "Premium UDP Europe"},
{subdomain: "97-1-fr", region: "France", group: "Premium TCP Europe"},
{subdomain: "87-1-ge", region: "Georgia", group: "Premium UDP Europe"},
{subdomain: "97-1-ge", region: "Georgia", group: "Premium TCP Europe"},
{subdomain: "97-1-de", region: "Germany", group: "Premium TCP Europe"},
{subdomain: "87-1-de", region: "Germany", group: "Premium UDP Europe"},
{subdomain: "87-1-gr", region: "Greece", group: "Premium UDP Europe"},
{subdomain: "97-1-gr", region: "Greece", group: "Premium TCP Europe"},
{subdomain: "97-1-gl", region: "Greenland", group: "Premium TCP Europe"},
{subdomain: "87-1-gl", region: "Greenland", group: "Premium UDP Europe"},
{subdomain: "96-1-hk", region: "Hong Kong", group: "Premium TCP Asia"},
{subdomain: "95-1-hk", region: "Hong Kong", group: "Premium UDP Asia"},
{subdomain: "87-1-hu", region: "Hungary", group: "Premium UDP Europe"},
{subdomain: "97-1-hu", region: "Hungary", group: "Premium TCP Europe"},
{subdomain: "97-1-is", region: "Iceland", group: "Premium TCP Europe"},
{subdomain: "87-1-is", region: "Iceland", group: "Premium UDP Europe"},
{subdomain: "87-1-in", region: "India", group: "Premium UDP Europe"},
{subdomain: "97-1-in", region: "India", group: "Premium TCP Europe"},
{subdomain: "95-1-id", region: "Indonesia", group: "Premium UDP Asia"},
{subdomain: "96-1-id", region: "Indonesia", group: "Premium TCP Asia"},
{subdomain: "87-1-ir", region: "Iran", group: "Premium UDP Europe"},
{subdomain: "97-1-ir", region: "Iran", group: "Premium TCP Europe"},
{subdomain: "87-1-ie", region: "Ireland", group: "Premium UDP Europe"},
{subdomain: "97-1-ie", region: "Ireland", group: "Premium TCP Europe"},
{subdomain: "87-1-im", region: "Isle of Man", group: "Premium UDP Europe"},
{subdomain: "97-1-im", region: "Isle of Man", group: "Premium TCP Europe"},
{subdomain: "87-1-il", region: "Israel", group: "Premium UDP Europe"},
{subdomain: "97-1-il", region: "Israel", group: "Premium TCP Europe"},
{subdomain: "97-1-it", region: "Italy", group: "Premium TCP Europe"},
{subdomain: "87-1-it", region: "Italy", group: "Premium UDP Europe"},
{subdomain: "95-1-jp", region: "Japan", group: "Premium UDP Asia"},
{subdomain: "96-1-jp", region: "Japan", group: "Premium TCP Asia"},
{subdomain: "97-1-kz", region: "Kazakhstan", group: "Premium TCP Europe"},
{subdomain: "87-1-kz", region: "Kazakhstan", group: "Premium UDP Europe"},
{subdomain: "95-1-ke", region: "Kenya", group: "Premium UDP Asia"},
{subdomain: "96-1-ke", region: "Kenya", group: "Premium TCP Asia"},
{subdomain: "95-1-kr", region: "Korea", group: "Premium UDP Asia"},
{subdomain: "96-1-kr", region: "Korea", group: "Premium TCP Asia"},
{subdomain: "97-1-lv", region: "Latvia", group: "Premium TCP Europe"},
{subdomain: "87-1-lv", region: "Latvia", group: "Premium UDP Europe"},
{subdomain: "97-1-li", region: "Liechtenstein", group: "Premium TCP Europe"},
{subdomain: "87-1-li", region: "Liechtenstein", group: "Premium UDP Europe"},
{subdomain: "97-1-lt", region: "Lithuania", group: "Premium TCP Europe"},
{subdomain: "87-1-lt", region: "Lithuania", group: "Premium UDP Europe"},
{subdomain: "87-1-lu", region: "Luxembourg", group: "Premium UDP Europe"},
{subdomain: "97-1-lu", region: "Luxembourg", group: "Premium TCP Europe"},
{subdomain: "96-1-mo", region: "Macao", group: "Premium TCP Asia"},
{subdomain: "95-1-mo", region: "Macao", group: "Premium UDP Asia"},
{subdomain: "97-1-mk", region: "Macedonia", group: "Premium TCP Europe"},
{subdomain: "87-1-mk", region: "Macedonia", group: "Premium UDP Europe"},
{subdomain: "95-1-my", region: "Malaysia", group: "Premium UDP Asia"},
{subdomain: "96-1-my", region: "Malaysia", group: "Premium TCP Asia"},
{subdomain: "87-1-mt", region: "Malta", group: "Premium UDP Europe"},
{subdomain: "97-1-mt", region: "Malta", group: "Premium TCP Europe"},
{subdomain: "93-1-mx", region: "Mexico", group: "Premium TCP USA"},
{subdomain: "94-1-mx", region: "Mexico", group: "Premium UDP USA"},
{subdomain: "87-1-md", region: "Moldova", group: "Premium UDP Europe"},
{subdomain: "97-1-md", region: "Moldova", group: "Premium TCP Europe"},
{subdomain: "87-1-mc", region: "Monaco", group: "Premium UDP Europe"},
{subdomain: "97-1-mc", region: "Monaco", group: "Premium TCP Europe"},
{subdomain: "96-1-mn", region: "Mongolia", group: "Premium TCP Asia"},
{subdomain: "95-1-mn", region: "Mongolia", group: "Premium UDP Asia"},
{subdomain: "87-1-me", region: "Montenegro", group: "Premium UDP Europe"},
{subdomain: "97-1-me", region: "Montenegro", group: "Premium TCP Europe"},
{subdomain: "97-1-ma", region: "Morocco", group: "Premium TCP Europe"},
{subdomain: "87-1-ma", region: "Morocco", group: "Premium UDP Europe"},
{subdomain: "97-1-nl", region: "Netherlands", group: "Premium TCP Europe"},
{subdomain: "87-1-nl", region: "Netherlands", group: "Premium UDP Europe"},
{subdomain: "95-1-nz", region: "New Zealand", group: "Premium UDP Asia"},
{subdomain: "96-1-nz", region: "New Zealand", group: "Premium TCP Asia"},
{subdomain: "87-1-ng", region: "Nigeria", group: "Premium UDP Europe"},
{subdomain: "97-1-ng", region: "Nigeria", group: "Premium TCP Europe"},
{subdomain: "97-1-no", region: "Norway", group: "Premium TCP Europe"},
{subdomain: "87-1-no", region: "Norway", group: "Premium UDP Europe"},
{subdomain: "97-1-pk", region: "Pakistan", group: "Premium TCP Europe"},
{subdomain: "87-1-pk", region: "Pakistan", group: "Premium UDP Europe"},
{subdomain: "97-1-pa", region: "Panama", group: "Premium TCP Europe"},
{subdomain: "87-1-pa", region: "Panama", group: "Premium UDP Europe"},
{subdomain: "95-1-ph", region: "Philippines", group: "Premium UDP Asia"},
{subdomain: "96-1-ph", region: "Philippines", group: "Premium TCP Asia"},
{subdomain: "97-1-pl", region: "Poland", group: "Premium TCP Europe"},
{subdomain: "87-1-pl", region: "Poland", group: "Premium UDP Europe"},
{subdomain: "97-1-pt", region: "Portugal", group: "Premium TCP Europe"},
{subdomain: "87-1-pt", region: "Portugal", group: "Premium UDP Europe"},
{subdomain: "97-1-qa", region: "Qatar", group: "Premium TCP Europe"},
{subdomain: "87-1-qa", region: "Qatar", group: "Premium UDP Europe"},
{subdomain: "87-1-ro", region: "Romania", group: "Premium UDP Europe"},
{subdomain: "87-8-ro", region: "Romania", group: "NoSpy UDP Europe"},
{subdomain: "97-8-ro", region: "Romania", group: "NoSpy TCP Europe"},
{subdomain: "97-1-ro", region: "Romania", group: "Premium TCP Europe"},
{subdomain: "87-1-ru", region: "Russian Federation", group: "Premium UDP Europe"},
{subdomain: "97-1-ru", region: "Russian Federation", group: "Premium TCP Europe"},
{subdomain: "97-1-sa", region: "Saudi Arabia", group: "Premium TCP Europe"},
{subdomain: "87-1-sa", region: "Saudi Arabia", group: "Premium UDP Europe"},
{subdomain: "97-1-rs", region: "Serbia", group: "Premium TCP Europe"},
{subdomain: "87-1-rs", region: "Serbia", group: "Premium UDP Europe"},
{subdomain: "95-1-sg", region: "Singapore", group: "Premium UDP Asia"},
{subdomain: "96-1-sg", region: "Singapore", group: "Premium TCP Asia"},
{subdomain: "87-1-sk", region: "Slovakia", group: "Premium UDP Europe"},
{subdomain: "97-1-sk", region: "Slovakia", group: "Premium TCP Europe"},
{subdomain: "87-1-si", region: "Slovenia", group: "Premium UDP Europe"},
{subdomain: "97-1-si", region: "Slovenia", group: "Premium TCP Europe"},
{subdomain: "87-1-za", region: "South Africa", group: "Premium UDP Europe"},
{subdomain: "95-1-za", region: "South Africa", group: "Premium UDP Asia"},
{subdomain: "97-1-za", region: "South Africa", group: "Premium TCP Europe"},
{subdomain: "96-1-za", region: "South Africa", group: "Premium TCP Asia"},
{subdomain: "97-1-es", region: "Spain", group: "Premium TCP Europe"},
{subdomain: "87-1-es", region: "Spain", group: "Premium UDP Europe"},
{subdomain: "97-1-lk", region: "Sri Lanka", group: "Premium TCP Europe"},
{subdomain: "87-1-lk", region: "Sri Lanka", group: "Premium UDP Europe"},
{subdomain: "97-1-se", region: "Sweden", group: "Premium TCP Europe"},
{subdomain: "87-1-se", region: "Sweden", group: "Premium UDP Europe"},
{subdomain: "87-1-ch", region: "Switzerland", group: "Premium UDP Europe"},
{subdomain: "97-1-ch", region: "Switzerland", group: "Premium TCP Europe"},
{subdomain: "96-1-tw", region: "Taiwan", group: "Premium TCP Asia"},
{subdomain: "95-1-tw", region: "Taiwan", group: "Premium UDP Asia"},
{subdomain: "96-1-th", region: "Thailand", group: "Premium TCP Asia"},
{subdomain: "95-1-th", region: "Thailand", group: "Premium UDP Asia"},
{subdomain: "87-1-tr", region: "Turkey", group: "Premium UDP Europe"},
{subdomain: "97-1-tr", region: "Turkey", group: "Premium TCP Europe"},
{subdomain: "97-1-ua", region: "Ukraine", group: "Premium TCP Europe"},
{subdomain: "87-1-ua", region: "Ukraine", group: "Premium UDP Europe"},
{subdomain: "87-1-ae", region: "United Arab Emirates", group: "Premium UDP Europe"},
{subdomain: "97-1-ae", region: "United Arab Emirates", group: "Premium TCP Europe"},
{subdomain: "97-1-gb", region: "United Kingdom", group: "Premium TCP Europe"},
{subdomain: "87-1-gb", region: "United Kingdom", group: "Premium UDP Europe"},
{subdomain: "94-1-us", region: "United States", group: "Premium UDP USA"},
{subdomain: "93-1-us", region: "United States", group: "Premium TCP USA"},
{subdomain: "87-1-ve", region: "Venezuela", group: "Premium UDP Europe"},
{subdomain: "97-1-ve", region: "Venezuela", group: "Premium TCP Europe"},
{subdomain: "95-1-vn", region: "Vietnam", group: "Premium UDP Asia"},
{subdomain: "96-1-vn", region: "Vietnam", group: "Premium TCP Asia"},
}
}
func vyprvpnServers() []server {
return []server{
{subdomain: "ae1", region: "Dubai"},
{subdomain: "ar1", region: "Argentina"},
{subdomain: "at1", region: "Austria"},
{subdomain: "au1", region: "Australia Sydney"},
{subdomain: "au2", region: "Australia Melbourne"},
{subdomain: "au3", region: "Australia Perth"},
{subdomain: "be1", region: "Belgium"},
{subdomain: "bg1", region: "Bulgaria"},
{subdomain: "bh1", region: "Bahrain"},
{subdomain: "br1", region: "Brazil"},
{subdomain: "ca1", region: "Canada"},
{subdomain: "ch1", region: "Switzerland"},
{subdomain: "co1", region: "Columbia"},
{subdomain: "cr1", region: "Costa Rica"},
{subdomain: "cz1", region: "Czech Republic"},
{subdomain: "de1", region: "Germany"},
{subdomain: "dk1", region: "Denmark"},
{subdomain: "dz1", region: "Algeria"},
{subdomain: "eg1", region: "Egypt"},
{subdomain: "es1", region: "Spain"},
{subdomain: "eu1", region: "Netherlands"},
{subdomain: "fi1", region: "Finland"},
{subdomain: "fr1", region: "France"},
{subdomain: "gr1", region: "Greece"},
{subdomain: "hk1", region: "Hong Kong"},
{subdomain: "id1", region: "Indonesia"},
{subdomain: "ie1", region: "Ireland"},
{subdomain: "il1", region: "Israel"},
{subdomain: "in1", region: "India"},
{subdomain: "is1", region: "Iceland"},
{subdomain: "it1", region: "Italy"},
{subdomain: "jp1", region: "Japan"},
{subdomain: "kr1", region: "South Korea"},
{subdomain: "li1", region: "Liechtenstein"},
{subdomain: "lt1", region: "Lithuania"},
{subdomain: "lu1", region: "Luxembourg"},
{subdomain: "lv1", region: "Latvia"},
{subdomain: "mh1", region: "Marshall Islands"},
{subdomain: "mo1", region: "Macao"},
{subdomain: "mv1", region: "Maldives"},
{subdomain: "mx1", region: "Mexico"},
{subdomain: "my1", region: "Malaysia"},
{subdomain: "no1", region: "Norway"},
{subdomain: "nz1", region: "New Zealand"},
{subdomain: "pa1", region: "Panama"},
{subdomain: "ph1", region: "Philippines"},
{subdomain: "pk1", region: "Pakistan"},
{subdomain: "pl1", region: "Poland"},
{subdomain: "pt1", region: "Portugal"},
{subdomain: "qa1", region: "Qatar"},
{subdomain: "ro1", region: "Romania"},
{subdomain: "ru1", region: "Russia"},
{subdomain: "sa1", region: "Saudi Arabia"},
{subdomain: "se1", region: "Sweden"},
{subdomain: "sg1", region: "Singapore"},
{subdomain: "si1", region: "Slovenia"},
{subdomain: "sk1", region: "Slovakia"},
{subdomain: "sv1", region: "El Salvador"},
{subdomain: "th1", region: "Thailand"},
{subdomain: "tr1", region: "Turkey"},
{subdomain: "tw1", region: "Taiwan"},
{subdomain: "ua1", region: "Ukraine"},
{subdomain: "uk1", region: "United Kingdom"},
{subdomain: "us1", region: "USA Los Angeles"},
{subdomain: "us2", region: "USA Washington DC"},
{subdomain: "us3", region: "USA Austin"},
{subdomain: "us4", region: "USA Miami"},
{subdomain: "us5", region: "USA New York"},
{subdomain: "us6", region: "USA Chicago"},
{subdomain: "us7", region: "USA San Francisco"},
{subdomain: "us8", region: "USA Seattle"},
{subdomain: "uy1", region: "Uruguay"},
{subdomain: "vn1", region: "Vietnam"},
}
}
func purevpnServers() []server {
servers := []server{
{subdomain: "vlus-dz1-ovpn", region: "Africa", country: "Algeria", city: "Algiers"},
{subdomain: "vlus-ao1-ovpn", region: "Africa", country: "Angola", city: "Benguela"},
{subdomain: "vleu-cv-ovpn", region: "Africa", country: "Cape Verde", city: "Praia"},
{subdomain: "vlus-eg1-ovpn", region: "Africa", country: "Egypt", city: "Cairo"},
{subdomain: "et1-ovpn", region: "Africa", country: "Ethiopia", city: "Addis Ababa"},
{subdomain: "gh1-ovpn", region: "Africa", country: "Ghana", city: "Accra"},
{subdomain: "ke1-ovpn", region: "Africa", country: "Kenya", city: "Mombasa"},
{subdomain: "vlus-mg1-ovpn", region: "Africa", country: "Madagascar", city: "Antananarivo"},
{subdomain: "vlus-mr1-ovpn", region: "Africa", country: "Mauritania", city: "Nouakchott"},
{subdomain: "mu1-ovpn", region: "Africa", country: "Mauritius", city: "Port Louis"},
{subdomain: "ma1-ovpn", region: "Africa", country: "Morocco", city: "Rabat"},
{subdomain: "vlus-ne1-ovpn", region: "Africa", country: "Niger", city: "Niamey"},
{subdomain: "ng1-ovpn", region: "Africa", country: "Nigeria", city: "Suleja"},
{subdomain: "vlus-sn1-ovpn", region: "Africa", country: "Senegal", city: "Dakar"},
{subdomain: "sc1-ovpn", region: "Africa", country: "Seychelles", city: "Victoria"},
{subdomain: "za2-ovpn", region: "Africa", country: "South Africa", city: "Johannesburg"},
{subdomain: "vlus-tz1-ovpn", region: "Africa", country: "Tanzania", city: "Dar Es Salaam"},
{subdomain: "vlus-tn1-ovpn", region: "Africa", country: "Tunisia", city: "Tunis"},
{subdomain: "vlus-af1-ovpn", region: "Asia", country: "Afghanistan", city: "Kabul"},
{subdomain: "sg2-ovpn", region: "Asia", country: "Armenia", city: "Singapore"},
{subdomain: "az1-ovpn", region: "Asia", country: "Azerbaijan", city: "Baku"},
{subdomain: "vlus-bd1-ovpn", region: "Asia", country: "Bangladesh", city: "Dhaka"},
{subdomain: "bn2-ovpn", region: "Asia", country: "Brunei Darussalam", city: "Bandar Seri Begawan"},
{subdomain: "kh1-ovpn", region: "Asia", country: "Cambodia", city: "Phnom Penh"},
{subdomain: "hk2-ovpn", region: "Asia", country: "Hong Kong (SAR)", city: "Hong Kong"},
{subdomain: "in2-ovpn", region: "Asia", country: "India", city: "Chennai"},
{subdomain: "idn1-ovpn", region: "Asia", country: "Indonesia", city: "Jakarta"},
{subdomain: "jp-tk1-ovpn", region: "Asia", country: "Japan", city: "Tokyo"},
{subdomain: "vlus-kz1-ovpn", region: "Asia", country: "Kazakhstan", city: "Almaty"},
{subdomain: "kr2-ovpn", region: "Asia", country: "Korea, South", city: "Seoul"},
{subdomain: "vlus-kg1-ovpn", region: "Asia", country: "Kyrgyzstan", city: "Bishkek"},
{subdomain: "vlus-la1-ovpn", region: "Asia", country: "Laos", city: "Vientiane"},
{subdomain: "mo1-ovpn", region: "Asia", country: "Macao", city: "Beyrouth"},
{subdomain: "my2-ovpn", region: "Asia", country: "Malaysia", city: "Johor Baharu"},
{subdomain: "my-kl2-ovpn", region: "Asia", country: "Malaysia", city: "Kuala Lumpur"},
{subdomain: "vlus-mn1-ovpn", region: "Asia", country: "Mongolia", city: "Ulaanbaatar"},
{subdomain: "pk1-ovpn", region: "Asia", country: "Pakistan", city: "Islamabad"},
{subdomain: "vlus-pg1-ovpn", region: "Asia", country: "Papua New Guinea", city: "Port Moresby"},
{subdomain: "vlap-ph2-ovpn", region: "Asia", country: "Philippines", city: "Manila"},
{subdomain: "vlus-lk1-ovpn", region: "Asia", country: "Sri Lanka", city: "Colombo"},
{subdomain: "tw2-ovpn", region: "Asia", country: "Taiwan", city: "Taipei"},
{subdomain: "vlus-tj-ovpn", region: "Asia", country: "Tajikistan", city: "Dushanbe"},
{subdomain: "vlap-th2-ovpn", region: "Asia", country: "Thailand", city: "Bangkok"},
{subdomain: "tr2-ovpn", region: "Asia", country: "Turkey", city: "Istanbul"},
{subdomain: "vlus-tm1-ovpn", region: "Asia", country: "Turkmenistan", city: "Ashgabat"},
{subdomain: "vlus-uz-ovpn", region: "Asia", country: "Uzbekistan", city: "Tashkent"},
{subdomain: "vlap-vn2-ovpn", region: "Asia", country: "Vietnam", city: "Hanoi"},
{subdomain: "al1-ovpn", region: "Europe", country: "Albania", city: "Tirane"},
{subdomain: "vleu-am1-ovpn", region: "Europe", country: "Armenia", city: "Yerevan"},
{subdomain: "at2-ovpn", region: "Europe", country: "Austria", city: "Vienna"},
{subdomain: "vleu-be2-ovpn", region: "Europe", country: "Belgium", city: "Brussels"},
{subdomain: "ba1-ovpn", region: "Europe", country: "Bosnia and Herzegovina", city: "Sarajevo"},
{subdomain: "bg2-ovpn", region: "Europe", country: "Bulgaria", city: "Sofia"},
{subdomain: "vlus-hr1-ovpn", region: "Europe", country: "Croatia", city: "Zagreb"},
{subdomain: "cy1-ovpn", region: "Europe", country: "Cyprus", city: "Nicosia"},
{subdomain: "dk2-ovpn", region: "Europe", country: "Denmark", city: "Copenhagen"},
{subdomain: "ee1-ovpn", region: "Europe", country: "Estonia", city: "Tallinn"},
{subdomain: "fr2-ovpn", region: "Europe", country: "France", city: "Paris"},
{subdomain: "vlus-ge1-ovpn", region: "Europe", country: "Georgia", city: "Tbilisi"},
{subdomain: "de2-ovpn", region: "Europe", country: "Germany", city: "Frankfurt"},
{subdomain: "de2-ovpn", region: "Europe", country: "Germany", city: "Munich"},
{subdomain: "de-ao1-ovpn", region: "Europe", country: "Germany", city: "Nuremberg"},
{subdomain: "gr2-ovpn", region: "Europe", country: "Greece", city: "Thessaloniki"},
{subdomain: "hu2-ovpn", region: "Europe", country: "Hungary", city: "Budapest"},
{subdomain: "is1-ovpn", region: "Europe", country: "Iceland", city: "Reykjavik"},
{subdomain: "ie2-ovpn", region: "Europe", country: "Ireland", city: "Dublin"},
{subdomain: "im1-ovpn", region: "Europe", country: "Isle of Man", city: "Onchan"},
{subdomain: "vlus-it1-ovpn", region: "Europe", country: "Italy", city: "Milano"},
{subdomain: "lv1-ovpn", region: "Europe", country: "Latvia", city: "RIGA"},
{subdomain: "li1-ovpn", region: "Europe", country: "Liechtenstein", city: "Vaduz"},
{subdomain: "lt1-ovpn", region: "Europe", country: "Lithuania", city: "Vilnius"},
{subdomain: "lu2-ovpn", region: "Europe", country: "Luxembourg", city: "Luxembourg"},
{subdomain: "mt1-ovpn", region: "Europe", country: "Malta", city: "Sliema"},
{subdomain: "mn1-ovpn", region: "Europe", country: "Monaco", city: "Monaco"},
{subdomain: "vleu-me1-ovpn", region: "Europe", country: "Montenegro", city: "Podgorica"},
{subdomain: "nl2-ovpn", region: "Europe", country: "Netherlands", city: "Amsterdam"},
{subdomain: "vleu-no2-ovpn", region: "Europe", country: "Norway", city: "Oslo"},
{subdomain: "pl2-ovpn", region: "Europe", country: "Poland", city: "Warsaw"},
{subdomain: "pt2-ovpn", region: "Europe", country: "Portugal", city: "Lisbon"},
{subdomain: "ro2-ovpn", region: "Europe", country: "Romania", city: "Bucharest"},
{subdomain: "rs2-ovpn", region: "Europe", country: "Serbia", city: "Niš"},
{subdomain: "sk1-ovpn", region: "Europe", country: "Slovakia", city: "Bratislava"},
{subdomain: "si1-ovpn", region: "Europe", country: "Slovenia", city: "Ljubljana"},
{subdomain: "es-ovpn", region: "Europe", country: "Spain", city: "Barcelona"},
{subdomain: "vlus-se1-ovpn", region: "Europe", country: "Sweden", city: "Stockholm"},
{subdomain: "ch2-ovpn", region: "Europe", country: "Switzerland", city: "Zurich"},
{subdomain: "ukg2-ovpn", region: "Europe", country: "United Kingdom", city: "Gosport"},
{subdomain: "ukl2-ovpn", region: "Europe", country: "United Kingdom", city: "London"},
{subdomain: "ukm2-ovpn", region: "Europe", country: "United Kingdom", city: "Maidenhead"},
{subdomain: "vlus-uk-man1-ovpn", region: "Europe", country: "United Kingdom", city: "Manchester"},
{subdomain: "bh-ovpn", region: "Middle East", country: "Bahrain", city: "Manama"},
{subdomain: "vlus-jo1-ovpn", region: "Middle East", country: "Jordan", city: "Amman"},
{subdomain: "vlus-kw1-ovpn", region: "Middle East", country: "Kuwait", city: "Kuwait"},
{subdomain: "om1-ovpn", region: "Middle East", country: "Oman", city: "Salalah"},
{subdomain: "qa1-ovpn", region: "Middle East", country: "Qatar", city: "Doha"},
{subdomain: "sa1-ovpn", region: "Middle East", country: "Saudi Arabia", city: "Jeddah"},
{subdomain: "ae2-ovpn", region: "Middle East", country: "United Arab Emirates", city: "Dubai"},
{subdomain: "aw1-ovpn", region: "North America", country: "Aruba", city: "Oranjestad"},
{subdomain: "vleu-bb-ovpn", region: "North America", country: "Barbados", city: "Bridgetown"},
{subdomain: "bz1-ovpn", region: "North America", country: "Belize", city: "Belmopan"},
{subdomain: "vleu-bm-ovpn", region: "North America", country: "Bermuda", city: "Hamilton"},
{subdomain: "caq1-ovpn", region: "North America", country: "Canada", city: "Montreal"},
{subdomain: "cato-ovpn", region: "North America", country: "Canada", city: "Toronto"},
{subdomain: "cav2-ovpn", region: "North America", country: "Canada", city: "Vancouver"},
{subdomain: "vleu-ky-ovpn", region: "North America", country: "Cayman Islands", city: "George Town"},
{subdomain: "vlus-cr1-ovpn", region: "North America", country: "Costa Rica", city: "San Jose"},
{subdomain: "vleu-dm-ovpn", region: "North America", country: "Dominica", city: "Roseau"},
{subdomain: "vleu-do-ovpn", region: "North America", country: "Dominican Republic", city: "Santo Domingo"},
{subdomain: "vleu-sv-ovpn", region: "North America", country: "El Salvador", city: "San Salvador"},
{subdomain: "vleu-gd-ovpn", region: "North America", country: "Grenada", city: "St George's"},
{subdomain: "vleu-gt-ovpn", region: "North America", country: "Guatemala", city: "Guatemala"},
{subdomain: "vleu-ht1-ovpn", region: "North America", country: "Haiti", city: "PORT-AU-PRINCE"},
{subdomain: "vleu-hn-ovpn", region: "North America", country: "Honduras", city: "TEGUCIGALPA"},
{subdomain: "jm1-ovpn", region: "North America", country: "Jamaica", city: "Kingston"},
{subdomain: "vlus-mx2-ovpn", region: "North America", country: "Mexico", city: "Mexico City"},
{subdomain: "vleu-ms-ovpn", region: "North America", country: "Montserrat", city: "plymouth"},
{subdomain: "pr1-ovpn", region: "North America", country: "Puerto Rico", city: "San Juan"},
{subdomain: "vleu-lc-ovpn", region: "North America", country: "Saint Lucia", city: "Castries"},
{subdomain: "bs1-ovpn", region: "North America", country: "The Bahamas", city: "Freeport"},
{subdomain: "vleu-tt-ovpn", region: "North America", country: "Trinidad and Tobago", city: "Port of Spain"},
{subdomain: "vleu-tc-ovpn", region: "North America", country: "Turks and Caicos Islands", city: "Balfour Town"},
{subdomain: "usva-ovpn", region: "North America", country: "United States", city: "Ashburn"},
{subdomain: "usil2-ovpn", region: "North America", country: "United States", city: "Chicago"},
{subdomain: "usoh1-ovpn", region: "North America", country: "United States", city: "Columbus"},
{subdomain: "usga2-ovpn", region: "North America", country: "United States", city: "Georgia"},
{subdomain: "ustx2-ovpn", region: "North America", country: "United States", city: "Houston"},
{subdomain: "usla2-ovpn", region: "North America", country: "United States", city: "Los Angeles"},
{subdomain: "usfl2-ovpn", region: "North America", country: "United States", city: "Miami"},
{subdomain: "usnj2-ovpn", region: "North America", country: "United States", city: "New Jersey"},
{subdomain: "usny2-ovpn", region: "North America", country: "United States", city: "New York"},
{subdomain: "usphx2-ovpn", region: "North America", country: "United States", city: "Phoenix"},
{subdomain: "usut2-ovpn", region: "North America", country: "United States", city: "Salt Lake City"},
{subdomain: "ussf2-ovpn", region: "North America", country: "United States", city: "San Francisco"},
{subdomain: "ussa-ovpn", region: "North America", country: "United States", city: "Seattle"},
{subdomain: "uswdc2-ovpn", region: "North America", country: "United States", city: "Washington, D.C."},
{subdomain: "au-bn-ovpn", region: "Oceania", country: "Australia", city: "Brisbane"},
{subdomain: "au-me1-ovpn", region: "Oceania", country: "Australia", city: "Melbourne"},
{subdomain: "au2-pe-ovpn", region: "Oceania", country: "Australia", city: "Perth"},
{subdomain: "au-sd2-ovpn", region: "Oceania", country: "Australia", city: "Sydney"},
{subdomain: "nz2-ovpn", region: "Oceania", country: "New Zealand", city: "Auckland"},
{subdomain: "vlus-ar1-ovpn", region: "South America", country: "Argentina", city: "Buenos Aires"},
{subdomain: "vleu-bo-ovpn", region: "South America", country: "Bolivia", city: "Sucre"},
{subdomain: "br2-ovpn", region: "South America", country: "Brazil", city: "Sao Paulo"},
{subdomain: "vg1-ovpn", region: "South America", country: "British Virgin Island", city: "Road Town"},
{subdomain: "vlbr-cl-ovpn", region: "South America", country: "Chile", city: "Santiago"},
{subdomain: "co1-ovpn", region: "South America", country: "Colombia", city: "Bogota"},
{subdomain: "ec1-ovpn", region: "South America", country: "Ecuador", city: "Quito"},
{subdomain: "vleu-gy-ovpn", region: "South America", country: "Guyana", city: "Georgetown"},
{subdomain: "pa2-ovpn", region: "South America", country: "Panama", city: "Panama City"},
{subdomain: "vleu-py-ovpn", region: "South America", country: "Paraguay", city: "Asuncion"},
{subdomain: "pe1-ovpn", region: "South America", country: "Peru", city: "Lima"},
{subdomain: "vleu-sr-ovpn", region: "South America", country: "Suriname", city: "Paramaribo"},
}
for i := range servers {
servers[i].subdomain += "-udp"
}
return servers
}

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

BIN
doc/paypal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
doc/sponsors.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
doc/windscribe.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

39
docker-compose.yml Normal file
View File

@@ -0,0 +1,39 @@
version: "3.7"
services:
gluetun:
image: qmcgaw/private-internet-access
container_name: gluetun
cap_add:
- NET_ADMIN
network_mode: bridge
ports:
- 8888:8888/tcp # Tinyproxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
- 8000:8000/tcp # Built-in HTTP control server
# command:
environment:
# More variables are available, see the readme table
- VPNSP=private internet access
# Timezone for accurate logs times
- TZ=
# All VPN providers
- USER=js89ds7
# All VPN providers but Mullvad
- PASSWORD=8fd9s239G
# Cyberghost only
- CLIENT_KEY=
# All VPN providers but Mullvad
- REGION=Austria
# Mullvad only
- COUNTRY=Sweden
# Allow for example your LAN, set to: 192.168.1.0/24
- EXTRA_SUBNETS=
restart: always

46
go.mod
View File

@@ -1,43 +1,13 @@
module github.com/qdm12/gluetun
go 1.17
go 1.15
require (
github.com/breml/rootcerts v0.2.1
github.com/fatih/color v1.13.0
github.com/golang/mock v1.6.0
github.com/qdm12/dns v1.11.0
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/gotree v0.2.0
github.com/qdm12/govalid v0.1.0
github.com/qdm12/ss-server v0.4.0
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
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-20201222180813-1025295fd063 // 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.0-20200313102051-9f266ea9e77c // indirect
github.com/fatih/color v1.9.0
github.com/golang/mock v1.4.4
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307
github.com/stretchr/testify v1.6.1
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed
)

271
go.sum
View File

@@ -1,251 +1,142 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.1 h1:GZMVDXOs945764NFck0vtHSjktKYubOFM0kjf5HAuwc=
github.com/breml/rootcerts v0.2.1/go.mod h1:24FDtzYMpqIeYC7QzaE8VPRQaFZU5TIUDlyk8qwjD88=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/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/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8=
github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4=
github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU=
github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307 h1:+LhVxIKpZgUM8ZcopIuc3Yjk+p76dWRdYLQiAA7caZM=
github.com/qdm12/ss-server v0.0.0-20200819005413-6b516c299307/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
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/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/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-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/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19 h1:ab2jcw2W91Rz07eHAb8Lic7sFQKO0NhBftjv6m/gL/0=
golang.zx2c4.com/wireguard v0.0.0-20210805125648-3957e9b9dd19/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
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=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722 h1:Qws2rZnQudC58cIagVucPQDLmMi3kAXgxscsgD0v6DU=
inet.af/netaddr v0.0.0-20210718074554-06ca8145d722/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,29 +1,25 @@
// Package alpine defines a configurator to interact with the Alpine operating system.
package alpine
import (
"os/user"
"github.com/qdm12/golibs/files"
)
var _ Alpiner = (*Alpine)(nil)
type Alpiner interface {
UserCreater
VersionGetter
type Configurator interface {
CreateUser(username string, uid int) error
}
type Alpine struct {
alpineReleasePath string
passwdPath string
lookupID func(uid string) (*user.User, error)
lookup func(username string) (*user.User, error)
type configurator struct {
fileManager files.FileManager
lookupUID func(uid string) (*user.User, error)
lookupUser func(username string) (*user.User, error)
}
func New() *Alpine {
return &Alpine{
alpineReleasePath: "/etc/alpine-release",
passwdPath: "/etc/passwd",
lookupID: user.LookupId,
lookup: user.Lookup,
func NewConfigurator(fileManager files.FileManager) Configurator {
return &configurator{
fileManager: fileManager,
lookupUID: user.LookupId,
lookupUser: user.Lookup,
}
}

View File

@@ -1,58 +1,38 @@
package alpine
import (
"errors"
"fmt"
"os"
"os/user"
"strconv"
)
var (
ErrUserAlreadyExists = errors.New("user already exists")
)
type UserCreater interface {
CreateUser(username string, uid int) (createdUsername string, err error)
}
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)
u, err := a.lookupID(UIDStr)
// CreateUser creates a user in Alpine with the given UID
func (c *configurator) CreateUser(username string, uid int) error {
UIDStr := fmt.Sprintf("%d", uid)
u, err := c.lookupUID(UIDStr)
_, unknownUID := err.(user.UnknownUserIdError)
if err != nil && !unknownUID {
return "", err
}
if u != nil {
return fmt.Errorf("cannot create user: %w", err)
} else if u != nil {
if u.Username == username {
return "", nil
return nil
}
return u.Username, nil
return fmt.Errorf("user with ID %d exists with username %q instead of %q", uid, u.Username, username)
}
u, err = a.lookup(username)
u, err = c.lookupUser(username)
_, unknownUsername := err.(user.UnknownUserError)
if err != nil && !unknownUsername {
return "", err
return fmt.Errorf("cannot create user: %w", err)
} else if u != nil {
return fmt.Errorf("cannot create user: user with name %s already exists for ID %s instead of %d", username, u.Uid, uid)
}
if u != nil {
return "", fmt.Errorf("%w: with name %s for ID %s instead of %d",
ErrUserAlreadyExists, username, u.Uid, uid)
}
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
passwd, err := c.fileManager.ReadFile("/etc/passwd")
if err != nil {
return "", err
}
s := fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid)
_, err = file.WriteString(s)
if err != nil {
_ = file.Close()
return "", err
return fmt.Errorf("cannot create user: %w", err)
}
passwd = append(passwd, []byte(fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid))...)
return username, file.Close()
if err := c.fileManager.WriteToFile("/etc/passwd", passwd); err != nil {
return fmt.Errorf("cannot create user: %w", err)
}
return nil
}

View File

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

View File

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

View File

@@ -1,22 +1,74 @@
// Package cli defines an interface CLI to run command line operations.
package cli
var _ CLIer = (*CLI)(nil)
import (
"flag"
"fmt"
"strings"
type CLIer interface {
ClientKeyFormatter
HealthChecker
OpenvpnConfigMaker
Updater
ServersFormatter
}
"net"
type CLI struct {
repoServersPath string
}
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
)
func New() *CLI {
return &CLI{
repoServersPath: "./internal/storage/servers.json",
func ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", "/files/client.key", "file path to the client.key file")
if err := flagSet.Parse(args); err != nil {
return err
}
fileManager := files.NewFileManager()
data, err := fileManager.ReadFile(*filepath)
if err != nil {
return err
}
s := string(data)
s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "")
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
fmt.Println(s)
return nil
}
func HealthCheck() error {
ips, err := net.LookupIP("github.com")
if err != nil {
return fmt.Errorf("cannot resolve github.com (%s)", err)
} else if len(ips) == 0 {
return fmt.Errorf("resolved no IP addresses for github.com")
}
return nil
}
func OpenvpnConfig() error {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
if err != nil {
return err
}
paramsReader := params.NewReader(logger, files.NewFileManager())
allSettings, err := settings.GetAllSettings(paramsReader)
if err != nil {
return err
}
providerConf := provider.New(allSettings.OpenVPN.Provider.Name)
connections, err := providerConf.GetOpenVPNConnections(allSettings.OpenVPN.Provider.ServerSelection)
if err != nil {
return err
}
lines := providerConf.BuildConf(
connections,
allSettings.OpenVPN.Verbosity,
allSettings.System.UID,
allSettings.System.GID,
allSettings.OpenVPN.Root,
allSettings.OpenVPN.Cipher,
allSettings.OpenVPN.Auth,
allSettings.OpenVPN.Provider.ExtraConfigOptions,
)
fmt.Println(strings.Join(lines, "\n"))
return nil
}

View File

@@ -1,45 +0,0 @@
package cli
import (
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/qdm12/gluetun/internal/constants"
)
type ClientKeyFormatter interface {
ClientKey(args []string) error
}
func (c *CLI) ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
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)
if err != nil {
return err
}
data, err := io.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
if err != nil {
return err
}
s := string(data)
s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "")
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
fmt.Println(s)
return nil
}

View File

@@ -1,133 +0,0 @@
package cli
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/storage"
)
type ServersFormatter interface {
FormatServers(args []string) error
}
var (
ErrFormatNotRecognized = errors.New("format is not recognized")
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrOpenOutputFile = errors.New("cannot open output file")
ErrWriteOutput = errors.New("cannot write to output file")
ErrCloseOutputFile = errors.New("cannot close output file")
)
func (c *CLI) FormatServers(args []string) error {
var format, output string
var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad,
nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark,
torguard, vpnUnlimited, vyprvpn, wevpn, windscribe 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")
flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers")
flagSet.BoolVar(&expressvpn, "expressvpn", false, "Format ExpressVPN servers")
flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers")
flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers")
flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers")
flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers")
flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers")
flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers")
flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers")
flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers")
flagSet.BoolVar(&privado, "privado", false, "Format Privado servers")
flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers")
flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers")
flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers")
flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers")
flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers")
flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers")
flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers")
flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers")
flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers")
if err := flagSet.Parse(args); err != nil {
return err
}
if format != "markdown" {
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
}
logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("%w: %s", ErrNewStorage, err)
}
currentServers := storage.GetServers()
var formatted string
switch {
case cyberghost:
formatted = currentServers.Cyberghost.ToMarkdown()
case expressvpn:
formatted = currentServers.Expressvpn.ToMarkdown()
case fastestvpn:
formatted = currentServers.Fastestvpn.ToMarkdown()
case hideMyAss:
formatted = currentServers.HideMyAss.ToMarkdown()
case ipvanish:
formatted = currentServers.Ipvanish.ToMarkdown()
case ivpn:
formatted = currentServers.Ivpn.ToMarkdown()
case mullvad:
formatted = currentServers.Mullvad.ToMarkdown()
case nordvpn:
formatted = currentServers.Nordvpn.ToMarkdown()
case perfectPrivacy:
formatted = currentServers.Perfectprivacy.ToMarkdown()
case pia:
formatted = currentServers.Pia.ToMarkdown()
case privado:
formatted = currentServers.Privado.ToMarkdown()
case privatevpn:
formatted = currentServers.Privatevpn.ToMarkdown()
case protonvpn:
formatted = currentServers.Protonvpn.ToMarkdown()
case purevpn:
formatted = currentServers.Purevpn.ToMarkdown()
case surfshark:
formatted = currentServers.Surfshark.ToMarkdown()
case torguard:
formatted = currentServers.Torguard.ToMarkdown()
case vpnUnlimited:
formatted = currentServers.VPNUnlimited.ToMarkdown()
case vyprvpn:
formatted = currentServers.Vyprvpn.ToMarkdown()
case wevpn:
formatted = currentServers.Wevpn.ToMarkdown()
case windscribe:
formatted = currentServers.Windscribe.ToMarkdown()
default:
return ErrProviderUnspecified
}
output = filepath.Clean(output)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenOutputFile, err)
}
_, err = fmt.Fprint(file, formatted)
if err != nil {
_ = file.Close()
return fmt.Errorf("%w: %s", ErrWriteOutput, err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("%w: %s", ErrCloseOutputFile, err)
}
return nil
}

View File

@@ -1,42 +0,0 @@
package cli
import (
"context"
"net"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/healthcheck"
)
type HealthChecker interface {
HealthCheck(ctx context.Context, source sources.Source, warner Warner) error
}
func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error {
// Extract the health server port from the configuration.
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
}
const timeout = 10 * time.Second
httpClient := &http.Client{Timeout: timeout}
client := healthcheck.NewClient(httpClient)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
url := "http://127.0.0.1:" + port
return client.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,51 +0,0 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/sources"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage"
)
type OpenvpnConfigMaker interface {
OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error
}
type OpenvpnConfigLogger interface {
Info(s string)
Warn(s string)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error {
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return err
}
allServers := storage.GetServers()
allSettings, err := source.Read()
if err != nil {
return err
}
if err = allSettings.Validate(allServers); err != nil {
return err
}
providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection)
if err != nil {
return err
}
lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN)
if err != nil {
return err
}
fmt.Println(strings.Join(lines, "\n"))
return nil
}

View File

@@ -1,132 +0,0 @@
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrDNSAddress = errors.New("DNS address is not valid")
ErrNoProviderSpecified = errors.New("no provider was specified")
ErrNewStorage = errors.New("cannot create storage")
ErrUpdateServerInformation = errors.New("cannot update server information")
ErrWriteToFile = errors.New("cannot write updated information to file")
)
type Updater interface {
Update(ctx context.Context, args []string, logger UpdaterLogger) error
}
type UpdaterLogger interface {
Info(s string)
Warn(s string)
Error(s string)
}
func boolPtr(b bool) *bool { return &b }
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{CLI: boolPtr(true)}
var endUserMode, maintainerMode, updateAll bool
var dnsAddress, csvProviders string
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&maintainerMode, "maintainer", false,
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use")
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")
if err := flagSet.Parse(args); err != nil {
return err
}
if !endUserMode && !maintainerMode {
return ErrModeUnspecified
}
options.DNSAddress = net.ParseIP(dnsAddress)
if options.DNSAddress == nil {
return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress)
}
if updateAll {
for _, provider := range constants.AllProviders() {
if provider == constants.Custom {
continue
}
options.Providers = append(options.Providers, provider)
}
} else {
if csvProviders == "" {
return ErrNoProviderSpecified
}
options.Providers = strings.Split(csvProviders, ",")
}
options.SetDefaults()
err := options.Validate()
if err != nil {
return fmt.Errorf("options validation failed: %w", err)
}
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("%w: %s", ErrNewStorage, err)
}
currentServers := storage.GetServers()
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
}
if endUserMode {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
}
}
if maintainerMode {
if err := writeToEmbeddedJSON(c.repoServersPath, allServers); err != nil {
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
}
}
return nil
}
func writeToEmbeddedJSON(repoServersPath string,
allServers models.AllServers) error {
const perms = 0600
f, err := os.OpenFile(repoServersPath,
os.O_TRUNC|os.O_WRONLY|os.O_CREATE, perms)
if err != nil {
return err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
return encoder.Encode(allServers)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +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")
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")
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientCertMissing = errors.New("client certificate is missing")
ErrOpenVPNClientCertNotValid = errors.New("client certificate is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNClientKeyNotValid = errors.New("client key is not valid")
ErrOpenVPNConfigFile = errors.New("custom configuration file error")
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")
ErrPortForwardingFilepathNotValid = errors.New("port forwarding filepath given is not valid")
ErrPublicIPFilepathNotValid = errors.New("public IP address file path is not valid")
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
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")
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")
ErrWireguardPreSharedKeyNotValid = errors.New("pre-shared key is not valid")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPrivateKeyNotValid = errors.New("private key is not valid")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
)

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,83 +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("%w: %s",
ErrServerAddressNotValid, err)
}
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings validation failed: %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, "github.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,45 +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 ErrValueNotOneOf = errors.New("value is not one of the possible choices")
func AreAllOneOf(values, choices []string) (err error) {
set := make(map[string]struct{}, len(choices))
for _, choice := range choices {
choice = strings.ToLower(choice)
set[choice] = struct{}{}
}
for _, value := range values {
_, ok := set[value]
if !ok {
return fmt.Errorf("%w: value %q, choices available are %s",
ErrValueNotOneOf, value, strings.Join(choices, ", "))
}
}
return nil
}
func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) {
for _, choice := range choices {
if port == choice {
return true
}
}
return false
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,112 +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/golibs/logging"
"github.com/qdm12/gotree"
)
// 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 *logging.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, logging.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,320 +0,0 @@
package settings
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/openvpn/parse"
"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
// Root is true if OpenVPN is to be run as root,
// and false otherwise. It cannot be nil in the
// internal state.
Root *bool
// ProcUser is the OpenVPN process OS username
// to use. It cannot be nil in the internal state.
// This is set and injected at runtime.
// TODO only use ProcUser and not Root field.
ProcUser string
// Verbosity is the OpenVPN verbosity level from 0 to 6.
// 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
}
func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{constants.Openvpn24, constants.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 == constants.Custom
if !isCustom && o.User == "" {
return ErrOpenVPNUserIsEmpty
}
if !isCustom && o.Password == "" {
return ErrOpenVPNPasswordIsEmpty
}
// Validate ConfFile
if isCustom {
if *o.ConfFile == "" {
return fmt.Errorf("%w: no file path specified", ErrOpenVPNConfigFile)
}
err := helpers.FileExists(*o.ConfFile)
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
}
}
// Check client certificate
switch vpnProvider {
case
constants.Cyberghost,
constants.VPNUnlimited:
if *o.ClientCrt == "" {
return ErrOpenVPNClientCertMissing
}
}
if *o.ClientCrt != "" {
_, err = parse.ExtractCert([]byte(*o.ClientCrt))
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNClientCertNotValid, err)
}
}
// Check client key
switch vpnProvider {
case
constants.Cyberghost,
constants.VPNUnlimited,
constants.Wevpn:
if *o.ClientKey == "" {
return ErrOpenVPNClientKeyMissing
}
}
if *o.ClientKey != "" {
_, err = parse.ExtractPrivateKey([]byte(*o.ClientKey))
if err != nil {
return fmt.Errorf("%w: %s", ErrOpenVPNClientKeyNotValid, err)
}
}
// Validate MSSFix
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)
}
// Validate Verbosity
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 (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,
Root: helpers.CopyBoolPtr(o.Root),
ProcUser: o.ProcUser,
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.Root = helpers.MergeWithBool(o.Root, other.Root)
o.ProcUser = helpers.MergeWithString(o.ProcUser, other.ProcUser)
o.Verbosity = helpers.MergeWithInt(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.Root = helpers.OverrideWithBool(o.Root, other.Root)
o.ProcUser = helpers.OverrideWithString(o.ProcUser, other.ProcUser)
o.Verbosity = helpers.OverrideWithInt(o.Verbosity, other.Verbosity)
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
}
func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = helpers.DefaultString(o.Version, constants.Openvpn25)
if vpnProvider == constants.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 == constants.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong
}
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.Root = helpers.DefaultBool(o.Root, true)
o.ProcUser = helpers.DefaultString(o.ProcUser, "root")
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
}
func (o OpenVPN) String() string {
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)
}
processUser := "root"
if !*o.Root {
processUser = "some non root user" // TODO
if o.ProcUser != "" {
processUser = o.ProcUser
}
}
node.Appendf("Run OpenVPN as: %s", processUser)
node.Appendf("Verbosity level: %d", *o.Verbosity)
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,170 +0,0 @@
package settings
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"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("%w: %s", ErrOpenVPNConfigFile, err)
}
}
// Validate TCP
if *o.TCP && helpers.IsOneOf(vpnProvider,
constants.Perfectprivacy,
constants.Privado,
constants.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 constants.Cyberghost, constants.HideMyAss,
constants.PrivateInternetAccess, constants.Privatevpn,
constants.Protonvpn, constants.Torguard:
// no custom port allowed
case constants.Expressvpn, constants.Fastestvpn,
constants.Ipvanish, constants.Nordvpn,
constants.Privado, constants.Purevpn,
constants.Surfshark, constants.VPNUnlimited,
constants.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
case constants.Ivpn:
allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050}
case constants.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case constants.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case constants.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case constants.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 == constants.PrivateInternetAccess {
validEncryptionPresets := []string{
constants.PIAEncryptionPresetNone,
constants.PIAEncryptionPresetNormal,
constants.PIAEncryptionPresetStrong,
}
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 == constants.PrivateInternetAccess {
defaultEncPreset = constants.PIAEncryptionPresetStrong
}
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"
"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{constants.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("%w: %s", ErrPortForwardingFilepathNotValid, 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"
"github.com/qdm12/gluetun/internal/models"
"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, allServers models.AllServers) (err error) {
// Validate Name
var validNames []string
if vpnType == constants.OpenVPN {
validNames = constants.AllProviders()
validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard
validNames = []string{
constants.Custom,
constants.Ivpn,
constants.Mullvad,
constants.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, allServers)
if err != nil {
return fmt.Errorf("server selection settings validation failed: %w", err)
}
err = p.PortForwarding.validate(*p.Name)
if err != nil {
return fmt.Errorf("port forwarding settings validation failed: %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, constants.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("%w: %s", ErrPublicIPFilepathNotValid, 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,71 +0,0 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gotree"
)
// ControlServer contains settings to customize the control server operation.
type ControlServer struct {
// Port is the listening port to use.
// It can be set to 0 to bind to a random port.
// It cannot be nil in the internal state.
// TODO change to address
Port *uint16
// 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) {
uid := os.Getuid()
const maxPrivilegedPort uint16 = 1023
if uid != 0 && *c.Port <= maxPrivilegedPort {
return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, *c.Port, uid)
}
return nil
}
func (c *ControlServer) copy() (copied ControlServer) {
return ControlServer{
Port: helpers.CopyUint16Ptr(c.Port),
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.Port = helpers.MergeWithUint16(c.Port, other.Port)
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.Port = helpers.MergeWithUint16(c.Port, other.Port)
c.Log = helpers.MergeWithBool(c.Log, other.Log)
}
func (c *ControlServer) setDefaults() {
const defaultPort = 8000
c.Port = helpers.DefaultUint16(c.Port, defaultPort)
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 port: %d", *c.Port)
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
return node
}

View File

@@ -1,389 +0,0 @@
package settings
import (
"fmt"
"net"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"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 only VPN provider owned servers
// should be filtered. This is used with Mullvad.
OwnedOnly *bool
// FreeOnly is true if only free VPN servers
// should be filtered. This is used with ProtonVPN.
FreeOnly *bool
// FreeOnly is true if only free VPN servers
// should be filtered. This is used with ProtonVPN.
StreamOnly *bool
// MultiHopOnly is true if only multihop VPN servers
// 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
}
func (ss *ServerSelection) validate(vpnServiceProvider string,
allServers models.AllServers) (err error) {
switch ss.VPN {
case constants.OpenVPN, constants.Wireguard:
default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
}
var countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices []string
switch vpnServiceProvider {
case constants.Custom:
case constants.Cyberghost:
servers := allServers.GetCyberghost()
countryChoices = constants.CyberghostCountryChoices(servers)
hostnameChoices = constants.CyberghostHostnameChoices(servers)
case constants.Expressvpn:
servers := allServers.GetExpressvpn()
countryChoices = constants.ExpressvpnCountriesChoices(servers)
cityChoices = constants.ExpressvpnCityChoices(servers)
hostnameChoices = constants.ExpressvpnHostnameChoices(servers)
case constants.Fastestvpn:
servers := allServers.GetFastestvpn()
countryChoices = constants.FastestvpnCountriesChoices(servers)
hostnameChoices = constants.FastestvpnHostnameChoices(servers)
case constants.HideMyAss:
servers := allServers.GetHideMyAss()
countryChoices = constants.HideMyAssCountryChoices(servers)
regionChoices = constants.HideMyAssRegionChoices(servers)
cityChoices = constants.HideMyAssCityChoices(servers)
hostnameChoices = constants.HideMyAssHostnameChoices(servers)
case constants.Ipvanish:
servers := allServers.GetIpvanish()
countryChoices = constants.IpvanishCountryChoices(servers)
cityChoices = constants.IpvanishCityChoices(servers)
hostnameChoices = constants.IpvanishHostnameChoices(servers)
case constants.Ivpn:
servers := allServers.GetIvpn()
countryChoices = constants.IvpnCountryChoices(servers)
cityChoices = constants.IvpnCityChoices(servers)
ispChoices = constants.IvpnISPChoices(servers)
hostnameChoices = constants.IvpnHostnameChoices(servers)
case constants.Mullvad:
servers := allServers.GetMullvad()
countryChoices = constants.MullvadCountryChoices(servers)
cityChoices = constants.MullvadCityChoices(servers)
ispChoices = constants.MullvadISPChoices(servers)
hostnameChoices = constants.MullvadHostnameChoices(servers)
case constants.Nordvpn:
servers := allServers.GetNordvpn()
regionChoices = constants.NordvpnRegionChoices(servers)
hostnameChoices = constants.NordvpnHostnameChoices(servers)
case constants.Perfectprivacy:
servers := allServers.GetPerfectprivacy()
cityChoices = constants.PerfectprivacyCityChoices(servers)
case constants.Privado:
servers := allServers.GetPrivado()
countryChoices = constants.PrivadoCountryChoices(servers)
regionChoices = constants.PrivadoRegionChoices(servers)
cityChoices = constants.PrivadoCityChoices(servers)
hostnameChoices = constants.PrivadoHostnameChoices(servers)
case constants.PrivateInternetAccess:
servers := allServers.GetPia()
regionChoices = constants.PIAGeoChoices(servers)
hostnameChoices = constants.PIAHostnameChoices(servers)
nameChoices = constants.PIANameChoices(servers)
case constants.Privatevpn:
servers := allServers.GetPrivatevpn()
countryChoices = constants.PrivatevpnCountryChoices(servers)
cityChoices = constants.PrivatevpnCityChoices(servers)
hostnameChoices = constants.PrivatevpnHostnameChoices(servers)
case constants.Protonvpn:
servers := allServers.GetProtonvpn()
countryChoices = constants.ProtonvpnCountryChoices(servers)
regionChoices = constants.ProtonvpnRegionChoices(servers)
cityChoices = constants.ProtonvpnCityChoices(servers)
nameChoices = constants.ProtonvpnNameChoices(servers)
hostnameChoices = constants.ProtonvpnHostnameChoices(servers)
case constants.Purevpn:
servers := allServers.GetPurevpn()
countryChoices = constants.PurevpnCountryChoices(servers)
regionChoices = constants.PurevpnRegionChoices(servers)
cityChoices = constants.PurevpnCityChoices(servers)
hostnameChoices = constants.PurevpnHostnameChoices(servers)
case constants.Surfshark:
servers := allServers.GetSurfshark()
countryChoices = constants.SurfsharkCountryChoices(servers)
cityChoices = constants.SurfsharkCityChoices(servers)
hostnameChoices = constants.SurfsharkHostnameChoices(servers)
regionChoices = constants.SurfsharkRegionChoices(servers)
// TODO v4 remove
regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...)
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
// Retro compatibility
// TODO remove in v4
*ss = surfsharkRetroRegion(*ss)
case constants.Torguard:
servers := allServers.GetTorguard()
countryChoices = constants.TorguardCountryChoices(servers)
cityChoices = constants.TorguardCityChoices(servers)
hostnameChoices = constants.TorguardHostnameChoices(servers)
case constants.VPNUnlimited:
servers := allServers.GetVPNUnlimited()
countryChoices = constants.VPNUnlimitedCountryChoices(servers)
cityChoices = constants.VPNUnlimitedCityChoices(servers)
hostnameChoices = constants.VPNUnlimitedHostnameChoices(servers)
case constants.Vyprvpn:
servers := allServers.GetVyprvpn()
regionChoices = constants.VyprvpnRegionChoices(servers)
case constants.Wevpn:
servers := allServers.GetWevpn()
cityChoices = constants.WevpnCityChoices(servers)
hostnameChoices = constants.WevpnHostnameChoices(servers)
case constants.Windscribe:
servers := allServers.GetWindscribe()
regionChoices = constants.WindscribeRegionChoices(servers)
cityChoices = constants.WindscribeCityChoices(servers)
hostnameChoices = constants.WindscribeHostnameChoices(servers)
default:
return fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, vpnServiceProvider)
}
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
ispChoices, nameChoices, hostnameChoices)
if err != nil {
return err // already wrapped error
}
if ss.VPN == constants.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("OpenVPN server selection settings validation failed: %w", err)
}
} else {
err = ss.Wireguard.validate(vpnServiceProvider)
if err != nil {
return fmt.Errorf("Wireguard server selection settings validation failed: %w", err)
}
}
return 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,
countryChoices, regionChoices, cityChoices, ispChoices,
nameChoices, hostnameChoices []string) (err error) {
if countryChoices != nil {
if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
}
}
if regionChoices != nil {
if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
}
}
if cityChoices != nil {
if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
}
}
if ispChoices != nil {
if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
}
}
if hostnameChoices != nil {
if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
}
}
if nameChoices != nil {
if err := helpers.AreAllOneOf(settings.Names, nameChoices); 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, constants.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 == constants.OpenVPN {
node.AppendNode(ss.OpenVPN.toLinesNode())
} else {
node.AppendNode(ss.Wireguard.toLinesNode())
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
ss.setDefaults(provider)
return ss
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,117 +0,0 @@
package settings
import (
"fmt"
"net"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gotree"
)
// Updater contains settings to configure the VPN
// server information updater.
type Updater struct {
// Period is the period for which the updater
// should run. It can be set to 0 to disable the
// updater. It cannot be nil in the internal state.
// TODO change to value and add Enabled field.
Period *time.Duration
// DNSAddress is the DNS server address to use
// to resolve VPN server hostnames to IP addresses.
// It cannot be nil in the internal state.
DNSAddress net.IP
// Providers is the list of VPN service providers
// to update server information for.
Providers []string
// CLI is to precise the updater is running in CLI
// mode. This is set automatically and cannot be set
// by settings sources. It cannot be nil in the
// internal state.
CLI *bool
}
func (u Updater) Validate() (err error) {
const minPeriod = time.Minute
if *u.Period > 0 && *u.Period < minPeriod {
return fmt.Errorf("%w: %d must be larger than %s",
ErrUpdaterPeriodTooSmall, *u.Period, minPeriod)
}
for i, provider := range u.Providers {
valid := false
for _, validProvider := range constants.AllProviders() {
if validProvider == constants.Custom {
continue
}
if provider == validProvider {
valid = true
break
}
}
if !valid {
return fmt.Errorf("%w: %s at index %d",
ErrVPNProviderNameNotValid, provider, i)
}
}
return nil
}
func (u *Updater) copy() (copied Updater) {
return Updater{
Period: helpers.CopyDurationPtr(u.Period),
DNSAddress: helpers.CopyIP(u.DNSAddress),
Providers: helpers.CopyStringSlice(u.Providers),
CLI: u.CLI,
}
}
// mergeWith merges the other settings into any
// unset field of the receiver settings object.
func (u *Updater) mergeWith(other Updater) {
u.Period = helpers.MergeWithDuration(u.Period, other.Period)
u.DNSAddress = helpers.MergeWithIP(u.DNSAddress, other.DNSAddress)
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (u *Updater) overrideWith(other Updater) {
u.Period = helpers.OverrideWithDuration(u.Period, other.Period)
u.DNSAddress = helpers.OverrideWithIP(u.DNSAddress, other.DNSAddress)
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
}
func (u *Updater) SetDefaults() {
u.Period = helpers.DefaultDuration(u.Period, 0)
u.DNSAddress = helpers.DefaultIP(u.DNSAddress, net.IPv4(1, 1, 1, 1))
u.CLI = helpers.DefaultBool(u.CLI, false)
}
func (u Updater) String() string {
return u.toLinesNode().String()
}
func (u Updater) toLinesNode() (node *gotree.Node) {
if *u.Period == 0 {
return nil
}
node = gotree.New("Server data updater settings:")
node.Appendf("Update period: %s", *u.Period)
node.Appendf("DNS address: %s", u.DNSAddress)
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
if *u.CLI {
node.Appendf("CLI mode: enabled")
}
return node
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
package env
import (
"fmt"
"net"
"os"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (r *Reader) readDNS() (dns settings.DNS, err error) {
dns.ServerAddress, err = r.readDNSServerAddress()
if err != nil {
return dns, err
}
dns.KeepNameserver, err = envToBoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return dns, fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err)
}
dns.DoT, err = r.readDoT()
if err != nil {
return dns, fmt.Errorf("cannot read DoT settings: %w", err)
}
return dns, nil
}
func (r *Reader) readDNSServerAddress() (address net.IP, err error) {
s := os.Getenv("DNS_PLAINTEXT_ADDRESS")
if s == "" {
return nil, nil
}
address = net.ParseIP(s)
if address == nil {
return nil, fmt.Errorf("environment variable DNS_PLAINTEXT_ADDRESS: %w: %s", ErrIPAddressParse, s)
}
// TODO remove in v4
if !address.Equal(net.IPv4(127, 0, 0, 1)) { //nolint:gomnd
r.warner.Warn("DNS_PLAINTEXT_ADDRESS is set to " + s +
" so the DNS over TLS (DoT) server will not be used." +
" The default value changed to 127.0.0.1 so it uses the internal DoT server." +
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" +
" corresponding to the first DoT provider chosen is used.")
}
return address, nil
}

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