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
796 changed files with 17786 additions and 358144 deletions

View File

@@ -1,4 +0,0 @@
.dockerignore
devcontainer.json
Dockerfile
README.md

View File

@@ -1,2 +0,0 @@
FROM qmcgaw/godevcontainer:v0.20-alpine
RUN apk add wireguard-tools htop openssl

View File

@@ -1,48 +0,0 @@
# Development container
Development container that can be used with VSCode.
It works on Linux, Windows (WSL2) and OSX.
## Requirements
- [VS code](https://code.visualstudio.com/download) installed
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
## Setup
1. Create the following files and directory on your host if you don't have them:
```sh
touch ~/.gitconfig ~/.zsh_history
mkdir -p ~/.ssh
```
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
## Customization
For any customization to take effect, you should "rebuild and reopen":
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
2. Select `Dev Containers: Rebuild Container`
Changes you can make are notably:
- Changes to the Docker image in [Dockerfile](Dockerfile)
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
```json
// Welcome script
{
"source": "/yourpath/.welcome.sh",
"target": "/root/.welcome.sh",
"type": "bind"
},
```
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).

View File

@@ -1,108 +1,115 @@
{
"name": "gluetun-dev",
// User defined settings
"containerEnv": {
"TZ": ""
},
// Fixed settings
"build": {
"dockerfile": "./Dockerfile"
},
"postCreateCommand": "~/.windows.sh && go mod download",
"capAdd": [
"NET_ADMIN", // Gluetun specific
"SYS_PTRACE" // for dlv Go debugging
"name": "pia-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"securityOpt": [
"seccomp=unconfined" // for dlv Go debugging
"service": "vscode",
"runServices": [
"vscode"
],
"mounts": [
// Zsh commands history persistence
{
"source": "${localEnv:HOME}/.zsh_history",
"target": "/root/.zsh_history",
"type": "bind"
},
// Git configuration file
{
"source": "${localEnv:HOME}/.gitconfig",
"target": "/root/.gitconfig",
"type": "bind"
},
// SSH directory for Linux, OSX and WSL
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
// created in the container. On Windows, files are copied
// from /mnt/ssh to ~/.ssh to fix permissions.
{
"source": "${localEnv:HOME}/.ssh",
"target": "/mnt/ssh",
"type": "bind"
},
// Docker socket to access the host Docker server
{
"source": "/var/run/docker.sock",
"target": "/var/run/docker.sock",
"type": "bind"
}
"shutdownAction": "stopCompose",
"postCreateCommand": "go mod download",
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",
"IBM.output-colorizer",
"eamodio.gitlens",
"mhutchie.git-graph",
"davidanson.vscode-markdownlint",
"shardulm94.trailing-spaces",
"alefragnani.Bookmarks",
"Gruntfuggly.todo-tree",
"mohsen1.prettify-json",
"quicktype.quicktype",
"spikespaz.vscode-smoothtype",
"stkb.rewrap",
"vscode-icons-team.vscode-icons"
],
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"eamodio.gitlens", // IDE Git information
"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
"github.copilot" // AI code completion
],
"settings": {
"files.eol": "\n",
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"[go.mod]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"formatting.gofumpt": true,
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true
"settings": {
// 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",
"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=integration"
},
"gopls.env": {
"GOFLAGS": "-tags=integration"
},
"go.testEnvVars": {},
"go.testFlags": [
"-v",
// "-race"
],
"go.testTimeout": "600s",
"go.coverOnSingleTestFile": true,
"go.coverOnSingleTest": true
}
}

View File

@@ -0,0 +1,15 @@
version: "3.7"
services:
vscode:
image: qmcgaw/godevcontainer
volumes:
- ../:/workspace
- ~/.ssh:/home/vscode/.ssh:ro
- ~/.ssh:/root/.ssh:ro
- /var/run/docker.sock:/var/run/docker.sock
cap_add:
- SYS_PTRACE
security_opt:
- 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/blob/main/contributing/development.md)
- [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,117 +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!
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
- you do not fill out **the title of the issue** ☝️
- you do not provide the **Gluetun version** as requested below
- you provide **less than 10 lines of logs** as requested below
- type: dropdown
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/blob/main/setup/docker-image-tags.md) 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:
- AirVPN
- Custom
- Cyberghost
- ExpressVPN
- FastestVPN
- Giganews
- HideMyAss
- IPVanish
- IVPN
- Mullvad
- NordVPN
- Privado
- Private Internet Access
- PrivateVPN
- ProtonVPN
- PureVPN
- SlickVPN
- Surfshark
- TorGuard
- VPNSecure.me
- 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
- Unraid
- 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 MUST 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 (at least 10 lines)
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: plain text
validations:
required: true
- type: textarea
id: config
attributes:
label: Share your configuration
description: Share your configuration such as `docker-compose.yml`. Ensure to remove credentials.
render: yml

View File

@@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new/choose
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's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.

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"

203
.github/labels.yml vendored
View File

@@ -1,152 +1,51 @@
- name: "Status: 🗯️ Waiting for feedback"
color: "f7d692"
- name: "Status: 🔴 Blocked"
color: "f7d692"
description: "Blocked by another issue or pull request"
- name: "Status: 📌 Before next release"
color: "f7d692"
description: "Has to be done before the next release"
- name: "Status: 🔒 After next release"
color: "f7d692"
description: "Will be done after the next release"
- name: "Status: 🟡 Nearly resolved"
color: "f7d692"
description: "This might be resolved or is about to be resolved"
- name: "Closed: ⚰️ Inactive"
color: "959a9c"
description: "No answer was received for weeks"
- name: "Closed: 👥 Duplicate"
color: "959a9c"
description: "Issue duplicates an existing issue"
- name: "Closed: 🗑️ Bad issue"
color: "959a9c"
- name: "Closed: ☠️ cannot be done"
color: "959a9c"
- name: "Priority: 🚨 Urgent"
color: "03adfc"
- name: "Priority: 💤 Low priority"
color: "03adfc"
- name: "Complexity: ☣️ Hard to do"
color: "ff9efc"
- name: "Complexity: 🟩 Easy to do"
color: "ff9efc"
- name: "Popularity: ❤️‍🔥 extreme"
color: "ffc7ea"
- name: "Popularity: ❤️ high"
color: "ffc7ea"
# VPN providers
- name: "☁️ AirVPN"
color: "cfe8d4"
- name: "☁️ Custom"
color: "cfe8d4"
- name: "☁️ Cyberghost"
color: "cfe8d4"
- name: "☁️ Giganews"
color: "cfe8d4"
- name: "☁️ HideMyAss"
color: "cfe8d4"
- name: "☁️ IPVanish"
color: "cfe8d4"
- name: "☁️ IVPN"
color: "cfe8d4"
- name: "☁️ ExpressVPN"
color: "cfe8d4"
- name: "☁️ FastestVPN"
color: "cfe8d4"
- name: "☁️ Mullvad"
color: "cfe8d4"
- name: "☁️ NordVPN"
color: "cfe8d4"
- name: "☁️ Perfect Privacy"
color: "cfe8d4"
- name: "☁️ PIA"
color: "cfe8d4"
- name: "☁️ Privado"
color: "cfe8d4"
- name: "☁️ PrivateVPN"
color: "cfe8d4"
- name: "☁️ ProtonVPN"
color: "cfe8d4"
- name: "☁️ PureVPN"
color: "cfe8d4"
- name: "☁️ SlickVPN"
color: "cfe8d4"
- name: "☁️ Surfshark"
color: "cfe8d4"
- name: "☁️ Torguard"
color: "cfe8d4"
- name: "☁️ VPNSecure.me"
color: "cfe8d4"
- name: "☁️ VPNUnlimited"
color: "cfe8d4"
- name: "☁️ Vyprvpn"
color: "cfe8d4"
- name: "☁️ WeVPN"
color: "cfe8d4"
- name: "☁️ Windscribe"
color: "cfe8d4"
- name: "Category: User error 🤦"
from_name: "Category: Config problem 📝"
color: "ffc7ea"
- name: "Category: Healthcheck 🩺"
color: "ffc7ea"
- name: "Category: Documentation ✒️"
description: "A problem with the readme or a code comment."
color: "ffc7ea"
- name: "Category: Maintenance ⛓️"
description: "Anything related to code or other maintenance"
color: "ffc7ea"
- name: "Category: Logs 📚"
description: "Something to change in logs"
color: "ffc7ea"
- name: "Category: Good idea 🎯"
description: "This is a good idea, judged by the maintainers"
color: "ffc7ea"
- name: "Category: Motivated! 🙌"
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
color: "ffc7ea"
- name: "Category: Foolproof settings 👼"
color: "ffc7ea"
- name: "Category: Label missing ❗"
color: "ffc7ea"
- name: "Category: updater ♻️"
color: "ffc7ea"
description: "Concerns the code to update servers data"
- name: "Category: New provider 🆕"
color: "ffc7ea"
- name: "Category: OpenVPN 🔐"
color: "ffc7ea"
- name: "Category: Wireguard 🔐"
color: "ffc7ea"
- name: "Category: DNS 📠"
color: "ffc7ea"
- name: "Category: Firewall ⛓️"
color: "ffc7ea"
- name: "Category: Routing 🛤️"
color: "ffc7ea"
- name: "Category: IPv6 🛰️"
color: "ffc7ea"
- name: "Category: VPN port forwarding 📥"
color: "ffc7ea"
- name: "Category: HTTP proxy 🔁"
color: "ffc7ea"
- name: "Category: Shadowsocks 🔁"
color: "ffc7ea"
- name: "Category: control server ⚙️"
color: "ffc7ea"
- name: "Category: kernel 🧠"
color: "ffc7ea"
- name: "Category: public IP service 💬"
color: "ffc7ea"
- name: "Category: servers storage 📦"
color: "ffc7ea"
- name: "Category: Performance 🚀"
color: "ffc7ea"
- name: "Category: Investigation 🔍"
color: "ffc7ea"
- name: ":robot: bot"
color: "69cde9"
description: ""
- name: ":bug: bug"
color: "b60205"
description: ""
- name: ":game_die: dependencies"
color: "0366d6"
description: ""
- name: ":memo: documentation"
color: "c5def5"
description: ""
- 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: ""
- name: ":question: question"
color: "3f51b5"
description: ""
- 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,35 +0,0 @@
name: No trigger file paths
on:
push:
branches:
- master
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
paths-ignore:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required verify workflow.
run: exit 0

View File

@@ -1,150 +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:
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v4
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
exclude: |
./internal/storage/servers.json
*.md
- name: Linting
run: docker build --target lint .
- name: Mocks check
run: docker build --target mocks .
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
- name: Build final image
run: docker build -t final-image .
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "^1.23"
- uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
publish:
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, codeql]
permissions:
actions: read
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
ghcr.io/qdm12/gluetun
qmcgaw/gluetun
qmcgaw/private-internet-access
tags: |
type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: qdm12
password: ${{ github.token }}
- 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@v6
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,21 +0,0 @@
name: Closed issue
on:
issues:
types: [closed]
jobs:
comment:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ github.token }}
issue-number: ${{ github.event.issue.number }}
body: |
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
This is an automated comment setup because @qdm12 is the sole maintainer of this project
which became too popular to monitor issues closed.

View File

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

View File

@@ -0,0 +1,19 @@
name: Docker Hub description
on:
push:
branches: [master]
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker Hub Description
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,17 +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:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crazy-max/ghaction-github-labeler@v5
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

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

View File

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

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

@@ -0,0 +1,16 @@
name: Misspells
on:
pull_request:
branches: [master]
push:
branches: [master]
jobs:
misspell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: reviewdog/action-misspell@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
locale: "US"
level: error

View File

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

1
.gitignore vendored
View File

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

View File

@@ -1,97 +1,47 @@
linters-settings:
maligned:
suggest-new: true
misspell:
locale: US
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- err113
- containedctx
- maintidx
- path: "internal\\/server\\/.+\\.go"
linters:
- dupl
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters:
- ireturn
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters:
- ireturn
linters:
disable-all: true
enable:
# - cyclop
# - errorlint
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- copyloopvar
- decorder
- deadcode
- dogsled
- dupl
- dupword
- durationcheck
- err113
- errchkjson
- errname
- exhaustive
- fatcontext
- forcetypeassert
- gci
- gocheckcompilerdirectives
- errcheck
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- gofumpt
- goheader
- goimports
- gomoddirectives
- goprintffuncname
- golint
- gosec
- gosmopolitan
- grouper
- importas
- interfacebloat
- intrange
- ireturn
- lll
- maintidx
- makezero
- mirror
- gosimple
- govet
- ineffassign
- interfacer
- maligned
- misspell
- mnd
- musttag
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nosprintfhostport
- paralleltest
- prealloc
- predeclared
- promlinter
- reassign
- revive
- rowserrcheck
- sqlclosecheck
- tagalign
- tenv
- thelper
- tparallel
- scopelint
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- usestdlibvars
- wastedassign
- unused
- varcheck
- whitespace
- zerologlint
run:
skip-dirs:
- .devcontainer
- .github
- postgres

View File

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

View File

@@ -1,8 +1,9 @@
{
// This list should be kept to the strict minimum
// to develop this project.
"recommendations": [
"golang.go",
"davidanson.vscode-markdownlint",
],
"recommendations": [
"shardulm94.trailing-spaces",
"ms-azuretools.vscode-docker",
"davidanson.vscode-markdownlint",
"IBM.output-colorizer",
"golang.go"
]
}

35
.vscode/launch.json vendored
View File

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

116
.vscode/settings.json vendored
View File

@@ -1,29 +1,91 @@
{
// The settings should be kept to the strict minimum
// to develop this project.
"files.eol": "\n",
"editor.formatOnSave": true,
"go.buildTags": "linux",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package"
// 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,174 +1,76 @@
ARG ALPINE_VERSION=3.20
ARG GO_ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.61.0
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64
ARG ALPINE_VERSION=3.12
ARG GO_VERSION=1.15
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
RUN apk --update add git g++ findutils
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
RUN apk --update add git
ENV CGO_ENABLED=0
COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen
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
COPY cmd/ ./cmd/
RUN go mod download 2>&1
COPY cmd/gluetun/main.go .
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 go test ./...
RUN golangci-lint run --timeout=10m
FROM --platform=${BUILDPLATFORM} base AS mocks
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git config core.fileMode false && \
git add -A && \
git commit -m "snapshot" && \
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
go generate -run "mockgen" ./... && \
git diff --exit-code && \
rm -rf .git/
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
RUN go build -trimpath -ldflags="-s -w" -o entrypoint main.go
FROM alpine:${ALPINE_VERSION}
ARG VERSION=unknown
ARG CREATED="an unknown date"
ARG COMMIT=unknown
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=$CREATED \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$COMMIT \
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 swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
ENV VPN_SERVICE_PROVIDER=pia \
VPN_TYPE=openvpn \
# Common VPN options
VPN_INTERFACE=tun0 \
# OpenVPN
OPENVPN_ENDPOINT_IP= \
OPENVPN_ENDPOINT_PORT= \
OPENVPN_PROTOCOL=udp \
OPENVPN_USER= \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.4 \
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_FLAGS= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_PROCESS_USER=root \
OPENVPN_MSSFIX= \
OPENVPN_CUSTOM_CONFIG= \
# Wireguard
WIREGUARD_ENDPOINT_IP= \
WIREGUARD_ENDPOINT_PORT= \
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ALLOWED_IPS= \
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
WIREGUARD_ADDRESSES= \
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
WIREGUARD_MTU=1320 \
WIREGUARD_IMPLEMENTATION=auto \
# VPN server filtering
SERVER_REGIONS= \
SERVER_COUNTRIES= \
SERVER_CITIES= \
SERVER_HOSTNAMES= \
SERVER_CATEGORIES= \
# # Mullvad only:
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= \
OWNED_ONLY=no \
# # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
VPN_PORT_FORWARDING=off \
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
VPN_PORT_FORWARDING_USERNAME= \
VPN_PORT_FORWARDING_PASSWORD= \
# # Cyberghost only:
OPENVPN_CERT= \
OPENVPN_KEY= \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# # VPNSecure only:
OPENVPN_ENCRYPTED_KEY= \
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
OPENVPN_KEY_PASSPHRASE= \
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
# # Nordvpn only:
# Mullvad and Windscribe only
PORT= \
# Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \
# NordVPN only
SERVER_NUMBER= \
# # PIA only:
SERVER_NAMES= \
# # VPNUnlimited and ProtonVPN only:
STREAM_ONLY= \
FREE_ONLY= \
# # ProtonVPN only:
SECURE_CORE_ONLY= \
TOR_ONLY= \
# # Surfshark only:
MULTIHOP_ONLY= \
# # VPN Secure only:
PREMIUM_ONLY= \
# # PIA and ProtonVPN only:
PORT_FORWARD_ONLY= \
# Firewall
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
FIREWALL_VPN_INPUT_PORTS= \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# Logging
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
HEALTH_SUCCESS_WAIT_DURATION=5s \
HEALTH_VPN_DURATION_INITIAL=6s \
HEALTH_VPN_DURATION_ADDITION=5s \
# Openvpn
OPENVPN_CIPHER= \
OPENVPN_AUTH= \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:0:0/96 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
DOT_CACHING=on \
DOT_IPV6=off \
BLOCK_MALICIOUS=on \
@@ -176,63 +78,31 @@ ENV VPN_SERVICE_PROVIDER=pia \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy
HTTPPROXY= \
HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_STEALTH=off \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
# 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_LISTENING_ADDRESS=":8388" \
SHADOWSOCKS_PORT=8388 \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
# Server data updater
UPDATER_PERIOD=0 \
UPDATER_MIN_RATIO=0.8 \
UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_ENABLED=on \
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
PUBLICIP_API_TOKEN= \
# Storage
STORAGE_FILEPATH=/gluetun/servers.json \
# Pprof
PPROF_ENABLED=no \
PPROF_BLOCK_PROFILE_RATE=0 \
PPROF_MUTEX_PROFILE_RATE=0 \
PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras
VERSION_INFORMATION=on \
TZ= \
PUID= \
PGID=
ENTRYPOINT ["/gluetun-entrypoint"]
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305
ENTRYPOINT ["/entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l wget && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn\~2.4 && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.16/main" openssl\~1.1 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
apk del openvpn && \
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
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 && \
mkdir /gluetun
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
deluser tinyproxy && \
deluser unbound
COPY --from=builder /tmp/gobuild/entrypoint /entrypoint

449
README.md
View File

@@ -1,129 +1,378 @@
# Gluetun VPN client
Lightweight swiss-knife-like VPN client to multiple VPN service providers
*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*
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
**ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)*
[![Build status](https://github.com/qdm12/gluetun/actions/workflows/ci.yml/badge.svg)](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
[![Docker pulls qmcgaw/gluetun](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![Docker pulls qmcgaw/private-internet-access](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![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 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 [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Suggestion?
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
- 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.20 for a small Docker image of 35.6MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- 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 server (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/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- 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 [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
- Built in 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
- 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-wiki/issues/new/choose)
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/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- 8888:8888/tcp # HTTP proxy
- 8388:8388/tcp # Shadowsocks
- 8388:8388/udp # Shadowsocks
volumes:
- /yourpath:/gluetun
environment:
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
- VPN_SERVICE_PROVIDER=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
- OPENVPN_USER=
- OPENVPN_PASSWORD=
# Wireguard:
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
# - WIREGUARD_ADDRESSES=10.64.222.21/32
# Timezone for accurate log times
- TZ=
# Server list updater
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
- UPDATER_PERIOD=
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
```
🆕 Image also available as `ghcr.io/qdm12/gluetun`
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/blob/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,603 +2,324 @@ package main
import (
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"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/openvpn/extract"
"github.com/qdm12/gluetun/internal/portforward"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/tun"
updater "github.com/qdm12/gluetun/internal/updater/loop"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
"github.com/qdm12/gluetun/internal/vpn"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/reader/sources/env"
"github.com/qdm12/goshutdown"
"github.com/qdm12/goshutdown/goroutine"
"github.com/qdm12/goshutdown/group"
"github.com/qdm12/goshutdown/order"
"github.com/qdm12/gosplash"
"github.com/qdm12/log"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
created = "an unknown date"
"github.com/qdm12/gluetun/internal/tinyproxy"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
)
func main() {
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
Created: created,
}
background := context.Background()
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(background)
logger := log.New(log.SetLevel(log.LevelInfo))
args := os.Args
tun := tun.New()
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
netLinker := netlink.New(netLinkDebugLogger)
cli := cli.New()
cmder := command.New()
reader := reader.New(reader.Settings{
Sources: []reader.Source{
secrets.New(logger),
files.New(logger),
env.New(env.Settings{}),
},
HandleDeprecatedKey: func(source, deprecatedKey, currentKey string) {
logger.Warn("You are using the old " + source + " " + deprecatedKey +
", please consider changing it to " + currentKey)
},
})
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
}()
// Wait for OS signal or run error
var err error
select {
case receivedSignal := <-signalCh:
signal.Stop(signalCh)
fmt.Println("")
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
cancel()
case err = <-errorCh:
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
}
logger.Error(err.Error())
cancel()
}
// Shutdown timed sequence, and force exit on second OS signal
const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod)
select {
case shutdownErr := <-errorCh:
timer.Stop()
if shutdownErr != nil {
logger.Warnf("Shutdown failed: %s", shutdownErr)
os.Exit(1)
}
logger.Info("Shutdown successful")
if err != nil {
os.Exit(1)
}
os.Exit(0)
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,maintidx
func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger log.LoggerInterface, reader *reader.Reader,
tun Tun, netLinker netLinker, cmder RunStarter,
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, reader, logger)
err = cli.HealthCheck()
case "clientkey":
return cli.ClientKey(args[2:])
err = cli.ClientKey(args[2:])
case "openvpnconfig":
return cli.OpenvpnConfig(logger, reader, netLinker)
case "update":
return cli.Update(ctx, args[2:], logger)
case "format-servers":
return cli.FormatServers(args[2:])
case "genkey":
return cli.GenKey(args[2:])
err = cli.OpenvpnConfig()
default:
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
err = fmt.Errorf("command %q is unknown", args[1])
}
}
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00: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,
Created: buildInfo.Created,
Announcement: "All control server routes will become private by default after the v3.41.0 release",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
GithubSponsor: "qdm12",
}
for _, line := range gosplash.MakeLines(splashSettings) {
fmt.Println(line)
}
var allSettings settings.Settings
err = allSettings.Read(reader, logger)
if err != nil {
return err
}
allSettings.SetDefaults()
// Note: no need to validate minimal settings for the firewall:
// - global log level is parsed below
// - firewall Debug and Enabled are booleans parsed from source
logLevel, err := log.ParseLevel(allSettings.Log.Level)
if err != nil {
return fmt.Errorf("log level: %w", err)
}
logger.Patch(log.SetLevel(logLevel))
netLinker.PatchLoggerLevel(logLevel)
routingLogger := logger.New(log.SetComponent("routing"))
if *allSettings.Firewall.Debug { // To remove in v4
routingLogger.Patch(log.SetLevel(log.LevelDebug))
}
routingConf := routing.New(netLinker, routingLogger)
defaultRoutes, err := routingConf.DefaultRoutes()
if err != nil {
return err
}
localNetworks, err := routingConf.LocalNetworks()
if err != nil {
return err
}
firewallLogger := logger.New(log.SetComponent("firewall"))
if *allSettings.Firewall.Debug { // To remove in v4
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks)
if err != nil {
return err
}
if *allSettings.Firewall.Enabled {
err = firewallConf.SetEnabled(ctx, true)
if err != nil {
return err
fmt.Println(err)
return 1
}
return 0
}
ctx, cancel := context.WithCancel(background)
defer cancel()
logger := createLogger()
// TODO run this in a loop or in openvpn to reload from file without restarting
storageLogger := logger.New(log.SetComponent("storage"))
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
if err != nil {
return err
}
fatalOnError := makeFatalOnError(logger, cancel)
ipv6Supported, err := netLinker.IsIPv6Supported()
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil {
return err
}
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
pprofServer, err := pprof.New(allSettings.Pprof)
if err != nil {
return fmt.Errorf("creating Pprof server: %w", err)
}
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.New(log.SetComponent("openvpn configurator")),
cmder, puid, pgid)
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: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "IPtables", getVersion: firewallConf.Version},
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())
for _, warning := range allSettings.Warnings() {
logger.Warn(warning)
// Should never change
uid, gid := allSettings.System.UID, allSettings.System.GID
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 allSettings.Firewall.Debug {
firewallConf.SetDebug()
routingConf.SetDebug()
}
const permission = fs.FileMode(0o644)
err = os.MkdirAll("/tmp/gluetun", permission)
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
if err != nil {
return err
}
err = os.MkdirAll("/gluetun", permission)
if err != nil {
return err
fatalOnError(err)
}
const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
localSubnet, err := routingConf.LocalSubnet()
if err != nil {
return fmt.Errorf("creating user: %w", err)
fatalOnError(err)
}
if nonRootUsername != defaultUsername {
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
}
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
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("setting up routing: %w", err)
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localSubnet)
if err := ovpnConf.CheckTUN(); err != nil {
logger.Warn(err)
err = ovpnConf.CreateTUN()
fatalOnError(err)
}
defer func() {
routingLogger.Info("routing cleanup...")
if err := routingConf.TearDown(); err != nil {
routingLogger.Error("cannot teardown routing: " + err.Error())
connectedCh := make(chan struct{})
signalConnected := func() {
connectedCh <- struct{}{}
}
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)
err = routingConf.AddLocalRules(localNetworks)
if err != nil {
return fmt.Errorf("adding local rules: %w", err)
}
const tunDevice = "/dev/net/tun"
err = tun.Check(tunDevice)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
}
logger.Info(err.Error() + "; creating it...")
err = tun.Create(tunDevice)
if err != nil {
return fmt.Errorf("creating tun device: %w", err)
}
}
for _, port := range allSettings.Firewall.InputPorts {
for _, defaultRoute := range defaultRoutes {
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
if err != nil {
return err
}
}
} // TODO move inside firewall?
// Shutdown settings
const totalShutdownTimeout = 3 * time.Second
const defaultShutdownTimeout = 400 * time.Millisecond
defaultShutdownOnSuccess := func(goRoutineName string) {
logger.Info(goRoutineName + ": terminated ✔️")
}
defaultShutdownOnFailure := func(goRoutineName string, err error) {
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
}
defaultGroupOptions := []group.Option{
group.OptionTimeout(defaultShutdownTimeout),
group.OptionOnSuccess(defaultShutdownOnSuccess),
}
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
if *allSettings.Pprof.Enabled {
// TODO run in run loop so this can be patched at runtime
pprofReady := make(chan struct{})
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
otherGroupHandler.Add(pprofHandler)
<-pprofReady
}
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardRunError, err := portForwardLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting port forwarding loop: %w", err)
}
dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
}
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
go dnsLooper.Run(dnsCtx, dnsDone)
otherGroupHandler.Add(dnsHandler)
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler)
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
logger.New(log.SetComponent("ip getter")))
if err != nil {
return fmt.Errorf("creating public ip loop: %w", err)
}
publicIPRunError, err := publicIPLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting public ip loop: %w", err)
}
updaterLogger := logger.New(log.SetComponent("updater"))
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone)
updaterLooper := updater.NewLoop(allSettings.Updater,
providers, storage, httpClient, updaterLogger)
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(updaterCtx, updaterDone)
tickersGroupHandler.Add(updaterHandler)
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
controlGroupHandler.Add(updaterTickerHandler)
httpProxyLooper := httpproxy.NewLoop(
logger.New(log.SetComponent("http proxy")),
allSettings.HTTPProxy)
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
otherGroupHandler.Add(httpProxyHandler)
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
logger.New(log.SetComponent("shadowsocks")))
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
otherGroupHandler.Add(shadowsocksHandler)
controlServerAddress := *allSettings.ControlServer.Address
controlServerLogging := *allSettings.ControlServer.Log
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported)
if err != nil {
return fmt.Errorf("setting up control server: %w", err)
}
httpServerReady := make(chan struct{})
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
<-httpServerReady
controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.New(log.SetComponent("healthcheck"))
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
go healthcheckServer.Run(healthServerCtx, healthServerDone)
orderHandler := goshutdown.NewOrderHandler("gluetun",
order.OptionTimeout(totalShutdownTimeout),
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call
// until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
// Start openvpn for the first time
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():
stoppers := []interface {
String() string
Stop() error
}{
portForwardLooper, publicIPLooper,
}
for _, stopper := range stoppers {
err := stopper.Stop()
if err != nil {
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
}
}
case err := <-portForwardRunError:
logger.Errorf("port forwarding loop crashed: %s", err)
case err := <-publicIPRunError:
logger.Errorf("public IP loop crashed: %s", err)
logger.Warn("context canceled, shutting down")
}
return orderHandler.Shutdown(context.Background())
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
}
type printVersionElement struct {
name string
getVersion func(ctx context.Context) (version string, err error)
}
type infoer interface {
Info(s string)
}
func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement,
) (err error) {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for _, element := range elements {
version, err := element.getVersion(ctx)
func makeFatalOnError(logger logging.Logger, cancel context.CancelFunc) func(err error) {
return func(err error) {
if err != nil {
return fmt.Errorf("getting %s version: %w", element.name, err)
logger.Error(err)
cancel()
}
logger.Info(element.name + " version: " + version)
}
return nil
}
type netLinker interface {
Addresser
Router
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsIPv6Supported() (ok bool, err error)
PatchLoggerLevel(level log.Level)
func createLogger() logging.Logger {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
if err != nil {
panic(err)
}
return logger
}
type Addresser interface {
AddrList(link netlink.Link, family int) (
addresses []netlink.Addr, err error)
AddrReplace(link netlink.Link, addr netlink.Addr) error
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 name, f := range versionFunctions {
version, err := f(ctx)
if err != nil {
logger.Error(err)
} else {
logger.Info("%s version: %s", name, version)
}
}
}
type Router interface {
RouteList(family int) (routes []netlink.Route, err error)
RouteAdd(route netlink.Route) error
RouteDel(route netlink.Route) error
RouteReplace(route netlink.Route) error
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)
})
}
type Ruler interface {
RuleList(family int) (rules []netlink.Rule, err error)
RuleAdd(rule netlink.Rule) error
RuleDel(rule netlink.Rule) error
}
type Linker interface {
LinkList() (links []netlink.Link, err error)
LinkByName(name string) (link netlink.Link, err error)
LinkByIndex(index int) (link netlink.Link, err error)
LinkAdd(link netlink.Link) (linkIndex int, err error)
LinkDel(link netlink.Link) (err error)
LinkSetUp(link netlink.Link) (linkIndex int, err error)
LinkSetDown(link netlink.Link) (err error)
}
type clier interface {
ClientKey(args []string) error
FormatServers(args []string) error
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
GenKey(args []string) error
}
type Tun interface {
Check(tunDevice string) error
Create(tunDevice string) error
}
type RunStarter interface {
Run(cmd *exec.Cmd) (output string, err error)
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, err error)
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)
}
}
}

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

65
go.mod
View File

@@ -1,62 +1,13 @@
module github.com/qdm12/gluetun
go 1.23
go 1.15
require (
github.com/breml/rootcerts v0.2.18
github.com/fatih/color v1.18.0
github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.11
github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.2
github.com/qdm12/dns/v2 v2.0.0-rc8
github.com/qdm12/gosettings v0.4.3
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.2.0
github.com/qdm12/gotree v0.3.0
github.com/qdm12/log v0.1.0
github.com/qdm12/ss-server v0.6.0
github.com/stretchr/testify v1.9.0
github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/net v0.30.0
golang.org/x/sys v0.26.0
golang.org/x/text v0.19.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gopkg.in/ini.v1 v1.67.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/qdm12/goservices v0.1.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
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
)

267
go.sum
View File

@@ -1,161 +1,142 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/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/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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
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/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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.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/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/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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-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.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/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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
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.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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.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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
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

@@ -2,20 +2,24 @@ package alpine
import (
"os/user"
"github.com/qdm12/golibs/files"
)
type Alpine struct {
alpineReleasePath string
passwdPath string
lookupID func(uid string) (*user.User, error)
lookup func(username string) (*user.User, error)
type Configurator interface {
CreateUser(username string, uid int) error
}
func New() *Alpine {
return &Alpine{
alpineReleasePath: "/etc/alpine-release",
passwdPath: "/etc/passwd",
lookupID: user.LookupId,
lookup: user.Lookup,
type configurator struct {
fileManager files.FileManager
lookupUID func(uid string) (*user.User, error)
lookupUser func(username string) (*user.User, error)
}
func NewConfigurator(fileManager files.FileManager) Configurator {
return &configurator{
fileManager: fileManager,
lookupUID: user.LookupId,
lookupUser: user.Lookup,
}
}

View File

@@ -1,54 +1,38 @@
package alpine
import (
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"strconv"
)
var ErrUserAlreadyExists = errors.New("user already exists")
// CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := strconv.Itoa(uid)
u, err := a.lookupID(UIDStr)
// 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)
}
const permission = fs.FileMode(0o644)
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
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,27 +0,0 @@
package alpine
import (
"context"
"io"
"os"
"strings"
)
func (a *Alpine) Version(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) error {
return nil
}

View File

@@ -1,11 +1,74 @@
package cli
type CLI struct {
repoServersPath string
import (
"flag"
"fmt"
"strings"
"net"
"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 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 New() *CLI {
return &CLI{
repoServersPath: "./internal/storage/servers.json",
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,37 +0,0 @@
package cli
import (
"flag"
"fmt"
"io"
"os"
"strings"
)
func (c *CLI) ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate?
filepath := flagSet.String("path", openVPNClientKeyPath, "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
}
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,113 +0,0 @@
package cli
import (
"errors"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/storage"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
)
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
provider string, titleCaser cases.Caser,
) {
boolPtr, ok := providerToFormat[provider]
if !ok {
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
}
flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers")
}
func (c *CLI) FormatServers(args []string) error {
var format, output string
allProviders := providers.All()
allProviderFlags := make([]string, len(allProviders))
for i, provider := range allProviders {
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
}
providersToFormat := make(map[string]*bool, len(allProviders))
for _, provider := range allProviderFlags {
providersToFormat[provider] = new(bool)
}
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
titleCaser := cases.Title(language.English)
for _, provider := range allProviderFlags {
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
}
if err := flagSet.Parse(args); err != nil {
return err
}
// Note the format is validated by storage.Format
// Verify only one provider is set to be formatted.
var providers []string
for provider, formatPtr := range providersToFormat {
if *formatPtr {
providers = append(providers, provider)
}
}
switch len(providers) {
case 0:
return fmt.Errorf("%w", ErrProviderUnspecified)
case 1:
default:
return fmt.Errorf("%w: %d specified: %s",
ErrMultipleProvidersToFormat, len(providers),
strings.Join(providers, ", "))
}
var providerToFormat string
for _, providerToFormat = range allProviders {
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
break
}
}
logger := newNoopLogger()
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("creating servers storage: %w", err)
}
formatted, err := storage.Format(providerToFormat, format)
if err != nil {
return fmt.Errorf("formatting servers: %w", err)
}
output = filepath.Clean(output)
const permission = fs.FileMode(0o644)
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
if err != nil {
return fmt.Errorf("opening output file: %w", err)
}
_, err = fmt.Fprint(file, formatted)
if err != nil {
_ = file.Close()
return fmt.Errorf("writing to output file: %w", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("closing output file: %w", err)
}
return nil
}

View File

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

View File

@@ -1,42 +0,0 @@
package cli
import (
"context"
"net"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gosettings/reader"
)
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
// Extract the health server port from the configuration.
var config settings.Health
err = config.Read(reader)
if err != nil {
return err
}
config.SetDefaults()
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,9 +0,0 @@
package cli
import "github.com/qdm12/gluetun/internal/configuration/settings"
type Source interface {
Read() (settings settings.Settings, err error)
ReadHealth() (health settings.Health, err error)
String() string
}

View File

@@ -1,9 +0,0 @@
package cli
type noopLogger struct{}
func newNoopLogger() *noopLogger {
return new(noopLogger)
}
func (l *noopLogger) Info(string) {}

View File

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

View File

@@ -1,113 +0,0 @@
package cli
import (
"context"
"errors"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
)
var (
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
ErrNoProviderSpecified = errors.New("no provider was specified")
)
type UpdaterLogger interface {
Info(s string)
Warn(s string)
Error(s string)
}
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
options := settings.Updater{}
var endUserMode, maintainerMode, updateAll bool
var csvProviders, ipToken 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(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
const defaultMinRatio = 0.8
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
"Minimum ratio of servers to find for the update to succeed")
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
if err := flagSet.Parse(args); err != nil {
return err
}
if !endUserMode && !maintainerMode {
return fmt.Errorf("%w", ErrModeUnspecified)
}
if updateAll {
options.Providers = providers.All()
} else {
if csvProviders == "" {
return fmt.Errorf("%w", ErrNoProviderSpecified)
}
options.Providers = strings.Split(csvProviders, ",")
}
options.SetDefaults(options.Providers[0])
err := options.Validate()
if err != nil {
return fmt.Errorf("options validation failed: %w", err)
}
storage, err := storage.New(logger, constants.ServersData)
if err != nil {
return fmt.Errorf("creating servers storage: %w", err)
}
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
}
fetchers, err := api.New(nameTokenPairs, httpClient)
if err != nil {
return fmt.Errorf("creating public IP fetchers: %w", err)
}
ipFetcher := api.NewResilient(fetchers, logger)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
updater := updater.New(httpClient, storage, providers, logger)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
if err != nil {
return fmt.Errorf("updating server information: %w", err)
}
if maintainerMode {
err := storage.FlushToFile(c.repoServersPath)
if err != nil {
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
}
}
return nil
}

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
package command
import "io"
type execCmd interface {
CombinedOutput() ([]byte, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
Start() error
Wait() error
}

View File

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

View File

@@ -1,108 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces_local.go
// Package command is a generated GoMock package.
package command
import (
io "io"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockexecCmd is a mock of execCmd interface.
type MockexecCmd struct {
ctrl *gomock.Controller
recorder *MockexecCmdMockRecorder
}
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
type MockexecCmdMockRecorder struct {
mock *MockexecCmd
}
// NewMockexecCmd creates a new mock instance.
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
mock := &MockexecCmd{ctrl: ctrl}
mock.recorder = &MockexecCmdMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
return m.recorder
}
// CombinedOutput mocks base method.
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CombinedOutput")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CombinedOutput indicates an expected call of CombinedOutput.
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
}
// Start mocks base method.
func (m *MockexecCmd) Start() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Start")
ret0, _ := ret[0].(error)
return ret0
}
// Start indicates an expected call of Start.
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
}
// StderrPipe mocks base method.
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StderrPipe")
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StderrPipe indicates an expected call of StderrPipe.
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
}
// StdoutPipe mocks base method.
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StdoutPipe")
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StdoutPipe indicates an expected call of StdoutPipe.
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
}
// Wait mocks base method.
func (m *MockexecCmd) Wait() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait")
ret0, _ := ret[0].(error)
return ret0
}
// Wait indicates an expected call of Wait.
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
}

View File

@@ -1,30 +0,0 @@
package command
import (
"os/exec"
"strings"
)
// Run runs a command in a blocking manner, returning its output and
// an error if it failed.
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
return run(cmd)
}
func run(cmd execCmd) (output string, err error) {
stdout, err := cmd.CombinedOutput()
output = string(stdout)
output = strings.TrimSuffix(output, "\n")
lines := stringToLines(output)
for i := range lines {
lines[i] = strings.TrimPrefix(lines[i], "'")
lines[i] = strings.TrimSuffix(lines[i], "'")
}
output = strings.Join(lines, "\n")
return output, err
}
func stringToLines(s string) (lines []string) {
s = strings.TrimSuffix(s, "\n")
return strings.Split(s, "\n")
}

View File

@@ -1,54 +0,0 @@
package command
import (
"errors"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_run(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
testCases := map[string]struct {
stdout []byte
cmdErr error
output string
err error
}{
"no output": {},
"cmd error": {
stdout: []byte("'hello \nworld'\n"),
cmdErr: errDummy,
output: "hello \nworld",
err: errDummy,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
mockCmd := NewMockexecCmd(ctrl)
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
output, err := run(mockCmd)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.output, output)
})
}
}

View File

@@ -1,100 +0,0 @@
package command
import (
"bufio"
"errors"
"io"
"os"
"os/exec"
)
// Start launches a command and streams stdout and stderr to channels.
// All the channels returned are ready only and won't be closed
// if the command fails later.
func (c *Cmder) Start(cmd *exec.Cmd) (
stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
return start(cmd)
}
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
waitError <-chan error, startErr error,
) {
stop := make(chan struct{})
stdoutReady := make(chan struct{})
stdoutLinesCh := make(chan string)
stdoutDone := make(chan struct{})
stderrReady := make(chan struct{})
stderrLinesCh := make(chan string)
stderrDone := make(chan struct{})
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, nil, err
}
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
stderr, err := cmd.StderrPipe()
if err != nil {
_ = stdout.Close()
close(stop)
<-stdoutDone
return nil, nil, nil, err
}
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
err = cmd.Start()
if err != nil {
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
return nil, nil, nil, err
}
waitErrorCh := make(chan error)
go func() {
err := cmd.Wait()
_ = stdout.Close()
_ = stderr.Close()
close(stop)
<-stdoutDone
<-stderrDone
waitErrorCh <- err
}()
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
}
func streamToChannel(ready chan<- struct{},
stop <-chan struct{}, done chan<- struct{},
stream io.Reader, lines chan<- string,
) {
defer close(done)
close(ready)
scanner := bufio.NewScanner(stream)
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
const maxCapacity = 20 * 1024 * 1024 // 20MB
scanner.Buffer(lineBuffer, maxCapacity)
for scanner.Scan() {
// scanner is closed if the context is canceled
// or if the command failed starting because the
// stream is closed (io.EOF error).
lines <- scanner.Text()
}
err := scanner.Err()
if err == nil || errors.Is(err, os.ErrClosed) {
return
}
// ignore the error if it is stopped.
select {
case <-stop:
return
default:
lines <- "stream error: " + err.Error()
}
}

View File

@@ -1,118 +0,0 @@
package command
import (
"bytes"
"errors"
"io"
"strings"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func linesToReadCloser(lines []string) io.ReadCloser {
s := strings.Join(lines, "\n")
return io.NopCloser(bytes.NewBufferString(s))
}
func Test_start(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
testCases := map[string]struct {
stdout []string
stdoutPipeErr error
stderr []string
stderrPipeErr error
startErr error
waitErr error
err error
}{
"no output": {},
"success": {
stdout: []string{"hello", "world"},
stderr: []string{"some", "error"},
},
"stdout pipe error": {
stdoutPipeErr: errDummy,
err: errDummy,
},
"stderr pipe error": {
stderrPipeErr: errDummy,
err: errDummy,
},
"start error": {
startErr: errDummy,
err: errDummy,
},
"wait error": {
waitErr: errDummy,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdout := linesToReadCloser(testCase.stdout)
stderr := linesToReadCloser(testCase.stderr)
mockCmd := NewMockexecCmd(ctrl)
mockCmd.EXPECT().StdoutPipe().
Return(stdout, testCase.stdoutPipeErr)
if testCase.stdoutPipeErr == nil {
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
if testCase.stderrPipeErr == nil {
mockCmd.EXPECT().Start().Return(testCase.startErr)
if testCase.startErr == nil {
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
}
}
}
stdoutLines, stderrLines, waitError, err := start(mockCmd)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
assert.Nil(t, stdoutLines)
assert.Nil(t, stderrLines)
assert.Nil(t, waitError)
return
}
require.NoError(t, err)
var stdoutIndex, stderrIndex int
done := false
for !done {
select {
case line := <-stdoutLines:
assert.Equal(t, testCase.stdout[stdoutIndex], line)
stdoutIndex++
case line := <-stderrLines:
assert.Equal(t, testCase.stderr[stderrIndex], line)
stderrIndex++
case err := <-waitError:
if testCase.waitErr != nil {
require.Error(t, err)
assert.Equal(t, testCase.waitErr.Error(), err.Error())
} else {
assert.NoError(t, err)
}
done = true
}
}
assert.Equal(t, len(testCase.stdout), stdoutIndex)
assert.Equal(t, len(testCase.stderr), stderrIndex)
})
}
}

View File

@@ -1,25 +0,0 @@
package settings
import (
"slices"
"github.com/qdm12/gosettings/reader"
"golang.org/x/exp/maps"
)
func readObsolete(r *reader.Reader) (warnings []string) {
keyToMessage := map[string]string{
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
}
sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys)
warnings = make([]string, 0, len(keyToMessage))
for _, key := range sortedKeys {
if r.Get(key) != nil {
warnings = append(warnings, keyToMessage[key])
}
}
return warnings
}

View File

@@ -1,101 +0,0 @@
package settings
import (
"fmt"
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"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 the zero value in the internal
// state.
ServerAddress netip.Addr
// KeepNameserver is true if the existing DNS server
// found in /etc/resolv.conf should be used
// Note setting this to true will likely DNS traffic
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS over TLS server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// 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("validating DoT settings: %w", err)
}
return nil
}
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
DoT: d.DoT.copy(),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DNS) overrideWith(other DNS) {
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
d.KeepNameserver = gosettings.DefaultPointer(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("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.AppendNode(d.DoT.toLinesNode())
return node
}
func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return err
}
err = d.DoT.read(r)
if err != nil {
return fmt.Errorf("DNS over TLS settings: %w", err)
}
return nil
}

View File

@@ -1,195 +0,0 @@
package settings
import (
"errors"
"fmt"
"net/http"
"net/netip"
"regexp"
"github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// DNSBlacklist is settings for the DNS blacklist building.
type DNSBlacklist struct {
BlockMalicious *bool
BlockAds *bool
BlockSurveillance *bool
AllowedHosts []string
AddBlockedHosts []string
AddBlockedIPs []netip.Addr
AddBlockedIPPrefixes []netip.Prefix
}
func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
b.BlockSurveillance = gosettings.DefaultPointer(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: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
}
}
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
}
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
settings blockbuilder.Settings,
) {
return blockbuilder.Settings{
Client: client,
BlockMalicious: b.BlockMalicious,
BlockAds: b.BlockAds,
BlockSurveillance: b.BlockSurveillance,
AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs,
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
}
}
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", gosettings.BoolToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Append("Allowed hosts:")
for _, host := range b.AllowedHosts {
allowedHostsNode.Append(host)
}
}
if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Append("Blocked hosts:")
for _, host := range b.AddBlockedHosts {
blockedHostsNode.Append(host)
}
}
if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Append("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Append(ip.String())
}
}
if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Append(ipNetwork.String())
}
}
return node
}
func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
b.BlockMalicious, err = r.BoolPtr("BLOCK_MALICIOUS")
if err != nil {
return err
}
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
reader.RetroKeys("BLOCK_NSA"))
if err != nil {
return err
}
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
if err != nil {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
if err != nil {
return err
}
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
return nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
if len(privateAddresses) == 0 {
return nil, nil, nil
}
ips = make([]netip.Addr, 0, len(privateAddresses))
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses))
for _, privateAddress := range privateAddresses {
ip, err := netip.ParseAddr(privateAddress)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netip.ParsePrefix(privateAddress)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
}
return ips, ipPrefixes, nil
}

View File

@@ -1,170 +0,0 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"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.
// It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in
// the internal state.
UpdatePeriod *time.Duration
// Providers is a list of DNS over TLS providers
Providers []string `json:"providers"`
// Caching is true if the DoT server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the DoT server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// 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)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
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: gosettings.CopyPointer(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
}
func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
}
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.DoT.IPv4[0].Addr()
}
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", gosettings.BoolToYesNo(d.Enabled))
if !*d.Enabled {
return node
}
update := "disabled" //nolint:goconst
if *d.UpdatePeriod > 0 {
update = "every " + d.UpdatePeriod.String()
}
node.Appendf("Update period: %s", update)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
node.AppendNode(d.Blacklist.toLinesNode())
return node
}
func (d *DoT) read(reader *reader.Reader) (err error) {
d.Enabled, err = reader.BoolPtr("DOT")
if err != nil {
return err
}
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return err
}
d.Providers = reader.CSV("DOT_PROVIDERS")
d.Caching, err = reader.BoolPtr("DOT_CACHING")
if err != nil {
return err
}
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
if err != nil {
return err
}
err = d.Blacklist.read(reader)
if err != nil {
return err
}
return nil
}

View File

@@ -1,56 +0,0 @@
package settings
import "errors"
var (
ErrValueUnknown = errors.New("value is unknown")
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port")
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
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")
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
ErrRegionNotValid = errors.New("the region specified is not valid")
ErrServerAddressNotValid = errors.New("server listening address is not valid")
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
)

View File

@@ -1,142 +0,0 @@
package settings
import (
"fmt"
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// Firewall contains settings to customize the firewall operation.
type Firewall struct {
VPNInputPorts []uint16
InputPorts []uint16
OutboundSubnets []netip.Prefix
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)
}
for _, subnet := range f.OutboundSubnets {
if subnet.Addr().IsUnspecified() {
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
}
}
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: gosettings.CopySlice(f.VPNInputPorts),
InputPorts: gosettings.CopySlice(f.InputPorts),
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
Enabled: gosettings.CopyPointer(f.Enabled),
Debug: gosettings.CopyPointer(f.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 = gosettings.OverrideWithSlice(f.VPNInputPorts, other.VPNInputPorts)
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
}
func (f *Firewall) setDefaults() {
f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
f.Debug = gosettings.DefaultPointer(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", gosettings.BoolToYesNo(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
}
func (f *Firewall) read(r *reader.Reader) (err error) {
f.VPNInputPorts, err = r.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return err
}
f.InputPorts, err = r.CSVUint16("FIREWALL_INPUT_PORTS")
if err != nil {
return err
}
f.OutboundSubnets, err = r.CSVNetipPrefixes(
"FIREWALL_OUTBOUND_SUBNETS", reader.RetroKeys("EXTRA_SUBNETS"))
if err != nil {
return err
}
f.Enabled, err = r.BoolPtr("FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT")
if err != nil {
return err
}
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return err
}
return nil
}

View File

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

View File

@@ -1,119 +0,0 @@
package settings
import (
"fmt"
"os"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
)
// 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
// ReadHeaderTimeout is the HTTP server header read timeout
// duration of the HTTP server. It defaults to 100 milliseconds.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration of the
// HTTP server. It defaults to 500 milliseconds.
ReadTimeout time.Duration
// TargetAddress is the address (host or host:port)
// to TCP dial to periodically for the health check.
// It cannot be the empty string in the internal state.
TargetAddress string
// SuccessWait is the duration to wait to re-run the
// healthcheck after a successful healthcheck.
// It defaults to 5 seconds and cannot be zero in
// the internal state.
SuccessWait time.Duration
// VPN has health settings specific to the VPN loop.
VPN HealthyWait
}
func (h Health) Validate() (err error) {
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
}
err = h.VPN.validate()
if err != nil {
return fmt.Errorf("health VPN settings: %w", err)
}
return nil
}
func (h *Health) copy() (copied Health) {
return Health{
ServerAddress: h.ServerAddress,
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
TargetAddress: h.TargetAddress,
SuccessWait: h.SuccessWait,
VPN: h.VPN.copy(),
}
}
// OverrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *Health) OverrideWith(other Health) {
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
h.VPN.overrideWith(other.VPN)
}
func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
const defaultReadHeaderTimeout = 100 * time.Millisecond
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 500 * time.Millisecond
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
const defaultSuccessWait = 5 * time.Second
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
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.Appendf("Duration to wait after success: %s", h.SuccessWait)
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
node.AppendNode(h.VPN.toLinesNode("VPN"))
return node
}
func (h *Health) Read(r *reader.Reader) (err error) {
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return err
}
err = h.VPN.read(r)
if err != nil {
return fmt.Errorf("VPN health settings: %w", err)
}
return nil
}

View File

@@ -1,76 +0,0 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"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
}
func (h *HealthyWait) copy() (copied HealthyWait) {
return HealthyWait{
Initial: gosettings.CopyPointer(h.Initial),
Addition: gosettings.CopyPointer(h.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 = gosettings.OverrideWithPointer(h.Initial, other.Initial)
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
}
func (h *HealthyWait) setDefaults() {
const initialDurationDefault = 6 * time.Second
const additionDurationDefault = 5 * time.Second
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
h.Addition = gosettings.DefaultPointer(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
}
func (h *HealthyWait) read(r *reader.Reader) (err error) {
h.Initial, err = r.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return err
}
h.Addition, err = r.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return err
}
return nil
}

View File

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

View File

@@ -1,10 +0,0 @@
package helpers
func IsOneOf[T comparable](value T, choices ...T) (ok bool) {
for _, choice := range choices {
if value == choice {
return true
}
}
return false
}

View File

@@ -1,30 +0,0 @@
package settings
import gomock "github.com/golang/mock/gomock"
type sourceKeyValue struct {
key string
value string
}
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
source := NewMockSource(ctrl)
var previousCall *gomock.Call
for _, keyValue := range keyValues {
transformedKey := keyValue.key
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
if previousCall != nil {
keyTransformCall.After(previousCall)
}
isSet := keyValue.value != ""
previousCall = source.EXPECT().Get(transformedKey).
Return(keyValue.value, isSet).After(keyTransformCall)
if isSet {
previousCall = source.EXPECT().KeyTransform(keyValue.key).
Return(transformedKey).After(previousCall)
previousCall = source.EXPECT().String().
Return("mock source").After(previousCall)
}
}
return source
}

View File

@@ -1,182 +0,0 @@
package settings
import (
"fmt"
"os"
"strings"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
)
// 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
// ReadHeaderTimeout is the HTTP header read timeout duration
// of the HTTP server. It defaults to 1 second if left unset.
ReadHeaderTimeout time.Duration
// ReadTimeout is the HTTP read timeout duration
// of the HTTP server. It defaults to 3 seconds if left unset.
ReadTimeout time.Duration
}
func (h HTTPProxy) validate() (err error) {
// Do not validate user and password
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
}
return nil
}
func (h *HTTPProxy) copy() (copied HTTPProxy) {
return HTTPProxy{
User: gosettings.CopyPointer(h.User),
Password: gosettings.CopyPointer(h.Password),
ListeningAddress: h.ListeningAddress,
Enabled: gosettings.CopyPointer(h.Enabled),
Stealth: gosettings.CopyPointer(h.Stealth),
Log: gosettings.CopyPointer(h.Log),
ReadHeaderTimeout: h.ReadHeaderTimeout,
ReadTimeout: h.ReadTimeout,
}
}
// overrideWith overrides fields of the receiver
// settings object with any field set in the other
// settings.
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
h.User = gosettings.OverrideWithPointer(h.User, other.User)
h.Password = gosettings.OverrideWithPointer(h.Password, other.Password)
h.ListeningAddress = gosettings.OverrideWithComparable(h.ListeningAddress, other.ListeningAddress)
h.Enabled = gosettings.OverrideWithPointer(h.Enabled, other.Enabled)
h.Stealth = gosettings.OverrideWithPointer(h.Stealth, other.Stealth)
h.Log = gosettings.OverrideWithPointer(h.Log, other.Log)
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
}
func (h *HTTPProxy) setDefaults() {
h.User = gosettings.DefaultPointer(h.User, "")
h.Password = gosettings.DefaultPointer(h.Password, "")
h.ListeningAddress = gosettings.DefaultComparable(h.ListeningAddress, ":8888")
h.Enabled = gosettings.DefaultPointer(h.Enabled, false)
h.Stealth = gosettings.DefaultPointer(h.Stealth, false)
h.Log = gosettings.DefaultPointer(h.Log, false)
const defaultReadHeaderTimeout = time.Second
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
const defaultReadTimeout = 3 * time.Second
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
}
func (h HTTPProxy) String() string {
return h.toLinesNode().String()
}
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
node = gotree.New("HTTP proxy settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(h.Enabled))
if !*h.Enabled {
return node
}
node.Appendf("Listening address: %s", h.ListeningAddress)
node.Appendf("User: %s", *h.User)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*h.Password))
node.Appendf("Stealth mode: %s", gosettings.BoolToYesNo(h.Stealth))
node.Appendf("Log: %s", gosettings.BoolToYesNo(h.Log))
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
node.Appendf("Read timeout: %s", h.ReadTimeout)
return node
}
func (h *HTTPProxy) read(r *reader.Reader) (err error) {
h.User = r.Get("HTTPPROXY_USER",
reader.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
reader.ForceLowercase(false))
h.Password = r.Get("HTTPPROXY_PASSWORD",
reader.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
reader.ForceLowercase(false))
h.ListeningAddress, err = readHTTProxyListeningAddress(r)
if err != nil {
return err
}
h.Enabled, err = r.BoolPtr("HTTPPROXY", reader.RetroKeys("PROXY", "TINYPROXY"))
if err != nil {
return err
}
h.Stealth, err = r.BoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return err
}
h.Log, err = readHTTProxyLog(r)
if err != nil {
return err
}
return nil
}
func readHTTProxyListeningAddress(r *reader.Reader) (listeningAddress string, err error) {
// Retro-compatible keys using a port only
port, err := r.Uint16Ptr("",
reader.RetroKeys("HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT"),
reader.IsRetro("HTTPPROXY_LISTENING_ADDRESS"))
if err != nil {
return "", err
} else if port != nil {
return fmt.Sprintf(":%d", *port), nil
}
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
return r.String(currentKey), nil
}
func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
const currentKey = "HTTPPROXY_LOG"
// Retro-compatible keys using different boolean verbs
value := r.String("",
reader.RetroKeys("PROXY_LOG", "TINYPROXY_LOG"),
reader.IsRetro(currentKey))
switch strings.ToLower(value) {
case "":
return r.BoolPtr(currentKey)
case "on", "info", "connect", "notice":
return ptrTo(true), nil
case "disabled", "no", "off":
return ptrTo(false), nil
default:
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
ErrValueUnknown, value)
}
}

View File

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

View File

@@ -1,57 +0,0 @@
package settings
import (
"fmt"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
)
// Log contains settings to configure the logger.
type Log struct {
// Level is the log level of the logger.
// It cannot be empty in the internal state.
Level string
}
func (l Log) validate() (err error) {
_, err = log.ParseLevel(l.Level)
if err != nil {
return fmt.Errorf("level: %w", err)
}
return nil
}
func (l *Log) copy() (copied Log) {
return Log{
Level: l.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 = gosettings.OverrideWithComparable(l.Level, other.Level)
}
func (l *Log) setDefaults() {
l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String())
}
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)
return node
}
func (l *Log) read(r *reader.Reader) (err error) {
l.Level = r.String("LOG_LEVEL")
return nil
}

View File

@@ -1,4 +0,0 @@
package settings
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source

View File

@@ -1,77 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
// Package settings is a generated GoMock package.
package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockSource is a mock of Source interface.
type MockSource struct {
ctrl *gomock.Controller
recorder *MockSourceMockRecorder
}
// MockSourceMockRecorder is the mock recorder for MockSource.
type MockSourceMockRecorder struct {
mock *MockSource
}
// NewMockSource creates a new mock instance.
func NewMockSource(ctrl *gomock.Controller) *MockSource {
mock := &MockSource{ctrl: ctrl}
mock.recorder = &MockSourceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockSource) Get(arg0 string) (string, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(bool)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
}
// KeyTransform mocks base method.
func (m *MockSource) KeyTransform(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyTransform", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// KeyTransform indicates an expected call of KeyTransform.
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
}
// String mocks base method.
func (m *MockSource) String() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "String")
ret0, _ := ret[0].(string)
return ret0
}
// String indicates an expected call of String.
func (mr *MockSourceMockRecorder) String() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
}

View File

@@ -1,46 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
// Package settings is a generated GoMock package.
package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockWarner is a mock of Warner interface.
type MockWarner struct {
ctrl *gomock.Controller
recorder *MockWarnerMockRecorder
}
// MockWarnerMockRecorder is the mock recorder for MockWarner.
type MockWarnerMockRecorder struct {
mock *MockWarner
}
// NewMockWarner creates a new mock instance.
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
mock := &MockWarner{ctrl: ctrl}
mock.recorder = &MockWarnerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
return m.recorder
}
// Warn mocks base method.
func (m *MockWarner) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
}

View File

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

View File

@@ -1,436 +0,0 @@
package settings
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
"github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"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".
Version string `json:"version"`
// User is the OpenVPN authentication username.
// It cannot be nil in the internal state if OpenVPN is used.
// It is usually required but in some cases can be the empty string
// to indicate no user+password authentication is needed.
User *string `json:"user"`
// Password is the OpenVPN authentication password.
// It cannot be nil in the internal state if OpenVPN is used.
// It is usually required but in some cases can be the empty string
// to indicate no user+password authentication is needed.
Password *string `json:"password"`
// 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 `json:"config_file_path"`
// Ciphers is a list of ciphers to use for OpenVPN,
// different from the ones specified by the VPN
// service provider configuration files.
Ciphers []string `json:"ciphers"`
// 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 `json:"auth"`
// Cert is the base64 encoded DER of an OpenVPN certificate for the <cert> block.
// This is notably used by Cyberghost and VPN secure.
// It can be set to the empty string to be ignored.
// It cannot be nil in the internal state.
Cert *string `json:"cert"`
// Key is the base64 encoded DER of an OpenVPN 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.
Key *string `json:"key"`
// EncryptedKey is the base64 encoded DER of an encrypted key for OpenVPN.
// It is used by VPN secure.
// It defaults to the empty string meaning it is not
// to be used. KeyPassphrase must be set if this one is set.
EncryptedKey *string `json:"encrypted_key"`
// KeyPassphrase is the key passphrase to be used by OpenVPN
// to decrypt the EncryptedPrivateKey. It defaults to the
// empty string and must be set if EncryptedPrivateKey is set.
KeyPassphrase *string `json:"key_passphrase"`
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
PIAEncPreset *string `json:"pia_encryption_preset"`
// 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 `json:"mssfix"`
// Interface is the OpenVPN device interface name.
// It cannot be an empty string in the internal state.
Interface string `json:"interface"`
// ProcessUser is the OpenVPN process OS username
// to use. It cannot be empty in the internal state.
// It defaults to 'root'.
ProcessUser string `json:"process_user"`
// Verbosity is the OpenVPN verbosity level from 0 to 6.
// It cannot be nil in the internal state.
Verbosity *int `json:"verbosity"`
// Flags is a slice of additional flags to be passed
// to the OpenVPN program.
Flags []string `json:"flags"`
}
var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}$`)
func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{openvpn.Openvpn24}
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
}
isCustom := vpnProvider == providers.Custom
isUserRequired := !isCustom &&
vpnProvider != providers.Airvpn &&
vpnProvider != providers.VPNSecure
if isUserRequired && *o.User == "" {
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
}
passwordRequired := isUserRequired &&
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
if passwordRequired && *o.Password == "" {
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
}
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
if err != nil {
return fmt.Errorf("custom configuration file: %w", err)
}
err = validateOpenVPNClientCertificate(vpnProvider, *o.Cert)
if err != nil {
return fmt.Errorf("client certificate: %w", err)
}
err = validateOpenVPNClientKey(vpnProvider, *o.Key)
if err != nil {
return fmt.Errorf("client key: %w", err)
}
err = validateOpenVPNEncryptedKey(vpnProvider, *o.EncryptedKey)
if err != nil {
return fmt.Errorf("encrypted key: %w", err)
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
}
const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
}
if !regexpInterfaceName.MatchString(o.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
}
if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
}
return nil
}
func validateOpenVPNConfigFilepath(isCustom bool,
confFile string,
) (err error) {
if !isCustom {
return nil
}
if confFile == "" {
return fmt.Errorf("%w", ErrFilepathMissing)
}
err = validate.FileExists(confFile)
if err != nil {
return err
}
extractor := extract.New()
_, _, err = extractor.Data(confFile)
if err != nil {
return fmt.Errorf("extracting information from custom configuration file: %w", err)
}
return nil
}
func validateOpenVPNClientCertificate(vpnProvider,
clientCert string,
) (err error) {
switch vpnProvider {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
}
if clientCert == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(clientCert)
if err != nil {
return err
}
return nil
}
func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
switch vpnProvider {
case
providers.Airvpn,
providers.Cyberghost,
providers.VPNUnlimited,
providers.Wevpn:
if clientKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
}
if clientKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(clientKey)
if err != nil {
return err
}
return nil
}
func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
}
if encryptedPrivateKey == "" {
return nil
}
_, err = base64.StdEncoding.DecodeString(encryptedPrivateKey)
if err != nil {
return err
}
return nil
}
func (o *OpenVPN) copy() (copied OpenVPN) {
return OpenVPN{
Version: o.Version,
User: gosettings.CopyPointer(o.User),
Password: gosettings.CopyPointer(o.Password),
ConfFile: gosettings.CopyPointer(o.ConfFile),
Ciphers: gosettings.CopySlice(o.Ciphers),
Auth: gosettings.CopyPointer(o.Auth),
Cert: gosettings.CopyPointer(o.Cert),
Key: gosettings.CopyPointer(o.Key),
EncryptedKey: gosettings.CopyPointer(o.EncryptedKey),
KeyPassphrase: gosettings.CopyPointer(o.KeyPassphrase),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
MSSFix: gosettings.CopyPointer(o.MSSFix),
Interface: o.Interface,
ProcessUser: o.ProcessUser,
Verbosity: gosettings.CopyPointer(o.Verbosity),
Flags: gosettings.CopySlice(o.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 = gosettings.OverrideWithComparable(o.Version, other.Version)
o.User = gosettings.OverrideWithPointer(o.User, other.User)
o.Password = gosettings.OverrideWithPointer(o.Password, other.Password)
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.Ciphers = gosettings.OverrideWithSlice(o.Ciphers, other.Ciphers)
o.Auth = gosettings.OverrideWithPointer(o.Auth, other.Auth)
o.Cert = gosettings.OverrideWithPointer(o.Cert, other.Cert)
o.Key = gosettings.OverrideWithPointer(o.Key, other.Key)
o.EncryptedKey = gosettings.OverrideWithPointer(o.EncryptedKey, other.EncryptedKey)
o.KeyPassphrase = gosettings.OverrideWithPointer(o.KeyPassphrase, other.KeyPassphrase)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
o.MSSFix = gosettings.OverrideWithPointer(o.MSSFix, other.MSSFix)
o.Interface = gosettings.OverrideWithComparable(o.Interface, other.Interface)
o.ProcessUser = gosettings.OverrideWithComparable(o.ProcessUser, other.ProcessUser)
o.Verbosity = gosettings.OverrideWithPointer(o.Verbosity, other.Verbosity)
o.Flags = gosettings.OverrideWithSlice(o.Flags, other.Flags)
}
func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn24)
o.User = gosettings.DefaultPointer(o.User, "")
if vpnProvider == providers.Mullvad {
o.Password = gosettings.DefaultPointer(o.Password, "m")
} else {
o.Password = gosettings.DefaultPointer(o.Password, "")
}
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.Auth = gosettings.DefaultPointer(o.Auth, "")
o.Cert = gosettings.DefaultPointer(o.Cert, "")
o.Key = gosettings.DefaultPointer(o.Key, "")
o.EncryptedKey = gosettings.DefaultPointer(o.EncryptedKey, "")
o.KeyPassphrase = gosettings.DefaultPointer(o.KeyPassphrase, "")
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = gosettings.DefaultPointer(o.PIAEncPreset, defaultEncPreset)
o.MSSFix = gosettings.DefaultPointer(o.MSSFix, 0)
o.Interface = gosettings.DefaultComparable(o.Interface, "tun0")
o.ProcessUser = gosettings.DefaultComparable(o.ProcessUser, "root")
o.Verbosity = gosettings.DefaultPointer(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", gosettings.ObfuscateKey(*o.User))
node.Appendf("Password: %s", gosettings.ObfuscateKey(*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.Cert != "" {
node.Appendf("Client crt: %s", gosettings.ObfuscateKey(*o.Cert))
}
if *o.Key != "" {
node.Appendf("Client key: %s", gosettings.ObfuscateKey(*o.Key))
}
if *o.EncryptedKey != "" {
node.Appendf("Encrypted key: %s (key passhrapse %s)",
gosettings.ObfuscateKey(*o.EncryptedKey), gosettings.ObfuscateKey(*o.KeyPassphrase))
}
if *o.PIAEncPreset != "" {
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
}
if *o.MSSFix > 0 {
node.Appendf("MSS Fix: %d", *o.MSSFix)
}
if o.Interface != "" {
node.Appendf("Network interface: %s", o.Interface)
}
node.Appendf("Run OpenVPN as: %s", o.ProcessUser)
node.Appendf("Verbosity level: %d", *o.Verbosity)
if len(o.Flags) > 0 {
node.Appendf("Flags: %s", o.Flags)
}
return node
}
// WithDefaults is a shorthand using setDefaults.
// It's used in unit tests in other packages.
func (o OpenVPN) WithDefaults(provider string) OpenVPN {
o.setDefaults(provider)
return o
}
func (o *OpenVPN) read(r *reader.Reader) (err error) {
o.Version = r.String("OPENVPN_VERSION")
o.User = r.Get("OPENVPN_USER", reader.RetroKeys("USER"), reader.ForceLowercase(false))
o.Password = r.Get("OPENVPN_PASSWORD", reader.RetroKeys("PASSWORD"), reader.ForceLowercase(false))
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
o.Ciphers = r.CSV("OPENVPN_CIPHERS", reader.RetroKeys("OPENVPN_CIPHER"))
o.Auth = r.Get("OPENVPN_AUTH")
o.Cert = r.Get("OPENVPN_CERT", reader.ForceLowercase(false))
o.Key = r.Get("OPENVPN_KEY", reader.ForceLowercase(false))
o.EncryptedKey = r.Get("OPENVPN_ENCRYPTED_KEY", reader.ForceLowercase(false))
o.KeyPassphrase = r.Get("OPENVPN_KEY_PASSPHRASE", reader.ForceLowercase(false))
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
o.MSSFix, err = r.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return err
}
o.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys("OPENVPN_INTERFACE"), reader.ForceLowercase(false))
o.ProcessUser, err = readOpenVPNProcessUser(r)
if err != nil {
return err
}
o.Verbosity, err = r.IntPtr("OPENVPN_VERBOSITY")
if err != nil {
return err
}
flagsPtr := r.Get("OPENVPN_FLAGS", reader.ForceLowercase(false))
if flagsPtr != nil {
o.Flags = strings.Fields(*flagsPtr)
}
return nil
}
func readOpenVPNProcessUser(r *reader.Reader) (processUser string, err error) {
value, err := r.BoolPtr("OPENVPN_ROOT") // Retro-compatibility
if err != nil {
return "", err
} else if value != nil {
if *value {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
}
return r.String("OPENVPN_PROCESS_USER"), nil
}

View File

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

View File

@@ -1,207 +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/constants/providers"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess/presets"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
)
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 `json:"config_file_path"`
// Protocol is the OpenVPN network protocol to use,
// and can be udp or tcp. It cannot be the empty string
// in the internal state.
Protocol string `json:"protocol"`
// 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 `json:"custom_port"`
// PIAEncPreset is the encryption preset for
// Private Internet Access. It can be set to an
// empty string for other providers.
PIAEncPreset *string `json:"pia_encryption_preset"`
}
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate ConfFile
if confFile := *o.ConfFile; confFile != "" {
err := validate.FileExists(confFile)
if err != nil {
return fmt.Errorf("configuration file: %w", err)
}
}
err = validate.IsOneOf(o.Protocol, constants.UDP, constants.TCP)
if err != nil {
return fmt.Errorf("network protocol: %w", err)
}
// Validate TCP
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
providers.Giganews,
providers.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider)
}
// Validate CustomPort
if *o.CustomPort != 0 {
switch vpnProvider {
// no restriction on port
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
case providers.Airvpn:
allowedTCP = []uint16{
53, 80, 443, // IP in 1, 3
1194, 2018, 41185, // IP in 1, 2, 3, 4
}
allowedUDP = []uint16{53, 80, 443, 1194, 2018, 41185}
case providers.Ivpn:
allowedTCP = []uint16{80, 443, 1143}
allowedUDP = []uint16{53, 1194, 2049, 2050}
case providers.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
case providers.Wevpn:
allowedTCP = []uint16{53, 1195, 1199, 2018}
allowedUDP = []uint16{80, 1194, 1198}
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
default:
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
}
allowedPorts := allowedUDP
if o.Protocol == constants.TCP {
allowedPorts = allowedTCP
}
err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
if err != nil {
return fmt.Errorf("%w: for VPN service provider %s: %w",
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
}
}
}
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
}
}
return nil
}
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
return OpenVPNSelection{
ConfFile: gosettings.CopyPointer(o.ConfFile),
Protocol: o.Protocol,
CustomPort: gosettings.CopyPointer(o.CustomPort),
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
}
}
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
}
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
var defaultEncPreset string
if vpnProvider == providers.PrivateInternetAccess {
defaultEncPreset = presets.Strong
}
o.PIAEncPreset = gosettings.DefaultPointer(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", strings.ToUpper(o.Protocol))
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
}
func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
if err != nil {
return err
}
o.PIAEncPreset = r.Get("PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
reader.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
return nil
}

View File

@@ -1,188 +0,0 @@
package settings
import (
"fmt"
"path/filepath"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"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 `json:"enabled"`
// Provider is set to specify which custom port forwarding code
// should be used. This is especially necessary for the custom
// provider using Wireguard for a provider where Wireguard is not
// natively supported but custom port forwarding code is available.
// It defaults to the empty string, meaning the current provider
// should be the one used for port forwarding.
// It cannot be nil for the internal state.
Provider *string `json:"provider"`
// Filepath is the port forwarding status file path
// 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 `json:"status_file_path"`
// ListeningPort is the port traffic would be redirected to from the
// forwarded port. The redirection is disabled if it is set to 0, which
// is its default as well.
ListeningPort *uint16 `json:"listening_port"`
// Username is only used for Private Internet Access port forwarding.
Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding.
Password string `json:"password"`
}
func (p PortForwarding) Validate(vpnProvider string) (err error) {
if !*p.Enabled {
return nil
}
// Validate current provider or custom provider specified
providerSelected := vpnProvider
if *p.Provider != "" {
providerSelected = *p.Provider
}
validProviders := []string{
providers.Perfectprivacy,
providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
}
// Validate Filepath
if *p.Filepath != "" { // optional
_, err := filepath.Abs(*p.Filepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
if providerSelected == providers.PrivateInternetAccess {
switch {
case p.Username == "":
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
case p.Password == "":
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
}
}
return nil
}
func (p *PortForwarding) Copy() (copied PortForwarding) {
return PortForwarding{
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
}
}
func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
}
func (p *PortForwarding) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
}
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:")
listeningPort := "disabled"
if *p.ListeningPort != 0 {
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
}
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider")
} else {
node.Appendf("Use code for provider: %s", *p.Provider)
}
filepath := *p.Filepath
if filepath == "" {
filepath = "[not set]"
}
node.Appendf("Forwarded port file path: %s", filepath)
if p.Username != "" {
credentialsNode := node.Appendf("Credentials:")
credentialsNode.Appendf("Username: %s", p.Username)
credentialsNode.Appendf("Password: %s", gosettings.ObfuscateKey(p.Password))
}
return node
}
func (p *PortForwarding) read(r *reader.Reader) (err error) {
p.Enabled, err = r.BoolPtr("VPN_PORT_FORWARDING",
reader.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil {
return err
}
p.Provider = r.Get("VPN_PORT_FORWARDING_PROVIDER")
p.Filepath = r.Get("VPN_PORT_FORWARDING_STATUS_FILE",
reader.ForceLowercase(false),
reader.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return err
}
usernameKeys := []string{"VPN_PORT_FORWARDING_USERNAME", "OPENVPN_USER", "USER"}
for _, key := range usernameKeys {
p.Username = r.String(key, reader.ForceLowercase(false))
if p.Username != "" {
break
}
}
passwordKeys := []string{"VPN_PORT_FORWARDING_PASSWORD", "OPENVPN_PASSWORD", "PASSWORD"}
for _, key := range passwordKeys {
p.Password = r.String(key, reader.ForceLowercase(false))
if p.Password != "" {
break
}
}
return nil
}

View File

@@ -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: ptrTo(false),
}
s := settings.String()
assert.Empty(t, s)
}

View File

@@ -1,128 +0,0 @@
package settings
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"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 the empty string in the internal state.
Name string `json:"name"`
// ServerSelection is the settings to
// select the VPN server.
ServerSelection ServerSelection `json:"server_selection"`
// PortForwarding is the settings about port forwarding.
PortForwarding PortForwarding `json:"port_forwarding"`
}
// TODO v4 remove pointer for receiver (because of Surfshark).
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
// Validate Name
var validNames []string
if vpnType == vpn.OpenVPN {
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
} else { // Wireguard
validNames = []string{
providers.Airvpn,
providers.Custom,
providers.Fastestvpn,
providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark,
providers.Windscribe,
}
}
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
}
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.Validate(p.Name)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func (p *Provider) copy() (copied Provider) {
return Provider{
Name: p.Name,
ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.Copy(),
}
}
func (p *Provider) overrideWith(other Provider) {
p.Name = gosettings.OverrideWithComparable(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.OverrideWith(other.PortForwarding)
}
func (p *Provider) setDefaults() {
p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess)
p.PortForwarding.setDefaults()
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
}
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
}
func (p *Provider) read(r *reader.Reader, vpnType string) (err error) {
p.Name = readVPNServiceProvider(r, vpnType)
err = p.ServerSelection.read(r, p.Name, vpnType)
if err != nil {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.read(r)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
return nil
}
func readVPNServiceProvider(r *reader.Reader, vpnType string) (vpnProvider string) {
vpnProvider = r.String("VPN_SERVICE_PROVIDER", reader.RetroKeys("VPNSP"))
if vpnProvider == "" {
if vpnType != vpn.Wireguard && r.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return providers.Custom
}
return ""
}
vpnProvider = strings.ToLower(vpnProvider)
if vpnProvider == "pia" { // retro compatibility
return providers.PrivateInternetAccess
}
return vpnProvider
}

View File

@@ -1,175 +0,0 @@
package settings
import (
"fmt"
"path/filepath"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// PublicIP contains settings for port forwarding.
type PublicIP struct {
// Enabled is set to true to fetch the public ip address
// information on VPN connection. It defaults to true.
Enabled *bool
// 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
// APIs is the list of public ip APIs to use to fetch public IP information.
// If there is more than one API, the first one is used
// by default and the others are used as fallbacks in case of
// the service rate limiting us. It defaults to use all services,
// with the first one being ipinfo.io for historical reasons.
APIs []PublicIPAPI
}
type PublicIPAPI struct {
// Name is the name of the public ip API service.
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
Name string
// Token is the token to use for the public ip API service.
Token string
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
updatedSettings = p.copy()
updatedSettings.overrideWith(partialUpdate)
err = updatedSettings.validate()
if err != nil {
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
}
return updatedSettings, nil
}
func (p PublicIP) validate() (err error) {
if *p.IPFilepath != "" { // optional
_, err := filepath.Abs(*p.IPFilepath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
}
}
for _, publicIPAPI := range p.APIs {
_, err = api.ParseProvider(publicIPAPI.Name)
if err != nil {
return fmt.Errorf("API name: %w", err)
}
}
return nil
}
func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{
Enabled: gosettings.CopyPointer(p.Enabled),
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
APIs: gosettings.CopySlice(p.APIs),
}
}
func (p *PublicIP) overrideWith(other PublicIP) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
}
func (p *PublicIP) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
{Name: string(api.IPInfo)},
{Name: string(api.Cloudflare)},
{Name: string(api.IfConfigCo)},
{Name: string(api.IP2Location)},
})
}
func (p PublicIP) String() string {
return p.toLinesNode().String()
}
func (p PublicIP) toLinesNode() (node *gotree.Node) {
if !*p.Enabled {
return gotree.New("Public IP settings: disabled")
}
node = gotree.New("Public IP settings:")
if *p.IPFilepath != "" {
node.Appendf("IP file path: %s", *p.IPFilepath)
}
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
if p.APIs[0].Token != "" {
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
}
node.Append(baseAPIString)
if len(p.APIs) > 1 {
backupAPIsNode := node.Append("Public IP data backup APIs:")
for i := 1; i < len(p.APIs); i++ {
message := p.APIs[i].Name
if p.APIs[i].Token != "" {
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
}
backupAPIsNode.Append(message)
}
}
return node
}
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
p.Enabled, err = readPublicIPEnabled(r, warner)
if err != nil {
return err
}
p.IPFilepath = r.Get("PUBLICIP_FILE",
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
apiNames := r.CSV("PUBLICIP_API")
if len(apiNames) > 0 {
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
p.APIs = make([]PublicIPAPI, len(apiNames))
for i := range apiNames {
p.APIs[i].Name = apiNames[i]
var token string
if i < len(apiTokens) { // only set token if it exists
token = apiTokens[i]
}
p.APIs[i].Token = token
}
}
return nil
}
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
enabled *bool, err error,
) {
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
if err != nil {
return nil, err
} else if periodPtr == nil {
return r.BoolPtr("PUBLICIP_ENABLED")
}
if *periodPtr == 0 {
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
return ptrTo(false), nil
}
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
"Please migrate to use PUBLICIP_ENABLED only in the future.")
return ptrTo(true), nil
}

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