Compare commits

..

82 Commits

Author SHA1 Message Date
Quentin McGaw
f0db0f0780 Fix: Empty connections for NordVPN and Windscribe 2021-01-31 18:53:25 +00:00
Quentin McGaw
5bd99b9f35 Fix: DNS_KEEP_NAMESERVER 2021-01-06 21:57:16 +00:00
Quentin McGaw
89bd10fc33 Fix DNS_KEEP_NAMESERVER behavior 2021-01-03 16:38:46 +00:00
Quentin McGaw
1f52df9747 DNS ready signaling fixed 2021-01-02 23:55:53 +00:00
Quentin McGaw
f04fd845bb Bug fix: DNS setup failure loop behavior 2021-01-02 23:55:29 +00:00
Quentin McGaw
5dcbe79fa8 Move OS package to golibs 2021-01-02 01:57:00 +00:00
Quentin McGaw
574ac9a603 Maintenance: update buildx Github workflow to v3 2021-01-01 20:46:52 +00:00
Quentin McGaw
6871444728 Change: remove decomissioned SecureDNS option 2021-01-01 20:45:11 +00:00
Quentin McGaw
f4db7e3e53 Change: remove LibreDNS, it does'nt support DNSSEC 2021-01-01 20:44:01 +00:00
Quentin McGaw
da92b6bfb9 Bug fix: Privado server selection 2020-12-31 21:57:26 +00:00
Quentin McGaw
d713782fe1 Change: Use SERVER_HOSTNAME instead of HOSTNAME 2020-12-31 21:50:28 +00:00
Quentin McGaw
02cde5f50b Code maintenance: consistent proto type conversion 2020-12-31 21:39:34 +00:00
Quentin McGaw
c5a7a83d3a Bug fix: do not fail if servers.json is empty 2020-12-31 21:19:29 +00:00
Quentin McGaw
6655a1a5e6 Bug fix: Update hardcoded Purevpn server data
- Refers to #320
2020-12-31 21:07:49 +00:00
Quentin McGaw
b8cb181070 Bug fix: PureVPN updater from ZIP files
- Fix #317
- Refers to #320
2020-12-31 21:07:30 +00:00
Quentin McGaw
a56471fe73 Code maintenance: rework ovpn host extraction 2020-12-31 20:35:49 +00:00
Quentin McGaw
8c769812ae Documentation: minor readme improvements 2020-12-31 04:49:18 +00:00
Quentin McGaw
f7a842e4ee Documentation: readme sections moved to Wiki 2020-12-31 04:40:04 +00:00
Quentin McGaw
23c0334f68 Documentation: Add visitors count to readme 2020-12-31 03:49:01 +00:00
Quentin McGaw
e2ee7a0408 Documentation: minor issue template update 2020-12-31 03:00:15 +00:00
Quentin McGaw
8f862b3df7 Bug fix: Remove trail newline from secrets
- Fix #330
2020-12-31 02:03:51 +00:00
Quentin McGaw
ae1f91a997 Documentation: Update Docker image labels 2020-12-30 22:30:59 +00:00
Quentin McGaw
d4fb76770f Documentation: Moare badges and metadata 2020-12-30 22:29:18 +00:00
Quentin McGaw
ea28c791e6 Code maintenance: http proxy starts from Run func 2020-12-30 22:02:47 +00:00
Quentin McGaw
251555f859 Code maintenance: Shadowsocks loop refactor 2020-12-30 22:01:08 +00:00
Quentin McGaw
fa7bda7ee4 Code maintenance: remove unneeded defaultInterface in Shadowsocks 2020-12-30 21:43:45 +00:00
Quentin McGaw
f385c4203a Bug fix: truncate /etc/resolv.conf, fixing DNS
- Refers to #326
- Refers to #329
2020-12-30 20:48:41 +00:00
Quentin McGaw
1e4243dedb Bug fix: Stop DOT if disabled by new settings 2020-12-30 20:38:59 +00:00
Quentin McGaw
5f78ee7b79 Bug fix: missing mutex Unlock in DNS set settings 2020-12-30 20:37:14 +00:00
Quentin McGaw
c6eb5c1785 Bug fix: Plaintext DNS fix (#326, #329) 2020-12-30 20:36:19 +00:00
Quentin McGaw
11338b6382 Feature: faster healthcheck, fix #283 2020-12-30 19:34:11 +00:00
Quentin McGaw
6f3a074e00 Code maintenance: HTTP proxy loop reworked
- Blocking method calls on loop
- Restart proxy when settings change
- Detect server crash error and restart it
2020-12-30 18:44:46 +00:00
Quentin McGaw
e827079604 Code maintenance: updater loop waitgroup 2020-12-30 18:32:58 +00:00
Quentin McGaw
cf66db8d4b Bug fix: Stopping updater loop deadlock 2020-12-30 18:29:28 +00:00
Quentin McGaw
25acbf8501 Feature: Increasing backoff time for crashes
- Fix #247
2020-12-30 17:22:54 +00:00
Quentin McGaw
e4c7a887d2 Bug fix: healthcheck uses DOT via default resolver 2020-12-30 16:43:08 +00:00
Quentin McGaw
fb8a615660 Feature: Update PIA servers using v5 PIA API 2020-12-30 15:54:13 +00:00
Quentin McGaw
1d9d49f406 Bug fix: Privado settings log 2020-12-30 15:34:07 +00:00
Quentin McGaw
0069b59ffe Change: remove redundant dns over tls log 2020-12-30 15:29:40 +00:00
Quentin McGaw
d4ba1b1e09 Bug fix: larger timeout for healtcheck 2020-12-30 15:24:46 +00:00
Quentin McGaw
3a20b84f3a Documentation: readme changes
- Remove videos section (outdated)
- Add quick links section for help and support
- Simplify support section
2020-12-29 23:00:55 +00:00
Quentin McGaw
d52fc777ac Code maintenance: update dockerhub readme workflow 2020-12-29 22:46:44 +00:00
Quentin McGaw
5753a428d8 Documentation: announcement on newer image name 2020-12-29 22:46:18 +00:00
Quentin McGaw
85afef5775 Change: gluetun docker image name 2020-12-29 22:10:44 +00:00
Quentin McGaw
b4fc24995c Code maintenance: Microbadger hook uses continue-on-error 2020-12-29 21:35:09 +00:00
Quentin McGaw
5917bb10e4 Feature: Docker secrets, refers to #306 2020-12-29 20:47:56 +00:00
Quentin McGaw
258e150ebf Code maintenance: GetPassword signature changed 2020-12-29 20:06:24 +00:00
Quentin McGaw
96f2b2b617 Change: PASSWORD changed to OPENVPN_PASSWORD 2020-12-29 20:05:17 +00:00
Quentin McGaw
d556db079b Change: USER changed to OPENVPN_USER 2020-12-29 20:02:58 +00:00
Quentin McGaw
a811a82329 Change: Remove CLIENT_KEY variable 2020-12-29 19:54:58 +00:00
Quentin McGaw
d17a0dae1f Documentation: Missing PUID and PGID update 2020-12-29 19:46:41 +00:00
Quentin McGaw
ef40f2f91b Code maintenance: Use Unset() option for params 2020-12-29 18:29:21 +00:00
Quentin McGaw
a921f9848c Code maintenance: CLI interface abstraction 2020-12-29 18:24:03 +00:00
Quentin McGaw
95ba3261fd Code maintenance: lint bug fix for armv7 2020-12-29 18:16:29 +00:00
Quentin McGaw
fe81eb65c2 Bug fix: Program exit on Openvpn fatal error 2020-12-29 17:50:36 +00:00
Quentin McGaw
8428714cf5 Code maintenance: upgrade golangci-lint to 1.34.1 2020-12-29 17:50:12 +00:00
Quentin McGaw
bedf613cff Code maintenance: storage merging reworked 2020-12-29 17:49:38 +00:00
Quentin McGaw
e643ce5b99 Fix publicip and updater loops exit bugs 2020-12-29 16:44:55 +00:00
Quentin McGaw
cb64302294 Rename UID and GID to PUID and PGID 2020-12-29 16:44:35 +00:00
Quentin McGaw
8d5f2fec09 Code maintenance: use native Go HTTP client 2020-12-29 02:55:34 +00:00
Quentin McGaw
60e98235ca Code maintenance: Better deps injection in main.go 2020-12-29 01:21:54 +00:00
Quentin McGaw
f55fb4055f Code maintenance: OS user abstraction interface 2020-12-29 01:16:53 +00:00
Quentin McGaw
da4e410bb7 Upgrade direct dependencies 2020-12-29 01:06:08 +00:00
Quentin McGaw
cdd1f87437 Code maintenance: Remove unneeded ctrl.Finish() 2020-12-29 01:04:07 +00:00
Quentin McGaw
7058373916 Code maintenance: Unix abstraction interface
- Used for creating the tun device if it does not exist
- Mocks generated for testing
2020-12-29 01:02:47 +00:00
Quentin McGaw
8dd38fd182 Code maintenance: better JSON decoding for HTTP 2020-12-29 00:56:51 +00:00
Quentin McGaw
73479bab26 Code maintenance: OS package for file system
- OS custom internal package for file system interaction
- Remove fileManager external dependency
- Closer API to Go's native API on the OS
- Create directories at startup
- Better testability
- Move Unsetenv to os interface
2020-12-29 00:55:31 +00:00
Quentin McGaw
f5366c33bc Remove unneeded .Times(1) for unit tests mocks 2020-12-28 01:52:30 +00:00
Quentin McGaw
db886163c2 Public IP getter loop refactored 2020-12-28 01:51:55 +00:00
Quentin McGaw
91f5338db0 Fix updater loop bug 2020-12-28 01:50:13 +00:00
Quentin McGaw
82a02287ac Public IP endpoint with GET /ip fixing #319 2020-12-27 21:06:00 +00:00
Quentin McGaw
2dc674559e Re-use username for UID if it exists 2020-12-27 00:36:39 +00:00
Quentin McGaw
38e713fea2 Fix Block-outside-dns #316 2020-12-23 06:46:54 +00:00
Quentin McGaw
2cbb14c36c Fix Purevpn settings display, refers to #317 2020-12-22 14:08:12 +00:00
Quentin McGaw
610e88958e Upgrade golangci-lint to v1.33.0 2020-12-22 13:52:37 +00:00
Quentin McGaw
bb76477467 Fix #316 2020-12-22 13:49:49 +00:00
Quentin McGaw
433a799759 Fix environment variables table for Purevpn 2020-12-22 13:46:52 +00:00
Quentin McGaw
22965ccce3 Fix #315 2020-12-22 06:21:25 +00:00
Quentin McGaw
4257581f55 Loops and HTTP control server rework (#308)
- CRUD REST HTTP server
- `/v1` HTTP server prefix
- Retrocompatible with older routes (redirects to v1 or handles the requests directly)
- DNS, Updater and Openvpn refactored to have a REST-like state with new methods to change their states synchronously
- Openvpn, Unbound and Updater status, see #287
2020-12-19 20:10:34 -05:00
Quentin McGaw
d60d629105 Dev container documentation and cleanup 2020-12-08 06:24:46 +00:00
Quentin McGaw
3f721b1717 Simplify Github workflows triggers 2020-12-07 02:15:50 +00:00
Quentin McGaw
97049bfab4 Add 256x256 png logo for Unraid 2020-12-07 02:11:23 +00:00
135 changed files with 6080 additions and 2533 deletions

View File

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

1
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM qmcgaw/godevcontainer

68
.devcontainer/README.md Normal file
View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "pia-dev",
"name": "gluetun-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
@@ -12,27 +12,25 @@
"workspaceFolder": "/workspace",
"extensions": [
"golang.go",
"IBM.output-colorizer",
"eamodio.gitlens",
"mhutchie.git-graph",
"eamodio.gitlens", // IDE Git information
"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"
"ms-azuretools.vscode-docker", // Docker integration and linting
"shardulm94.trailing-spaces", // Show trailing spaces
"Gruntfuggly.todo-tree", // Highlights TODO comments
"bierner.emojisense", // Emoji sense for markdown
"stkb.rewrap", // rewrap comments after n characters on one line
"vscode-icons-team.vscode-icons", // Better file extension icons
"github.vscode-pull-request-github", // Github interaction
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"IBM.output-colorizer", // Colorize your output/test logs
"mohsen1.prettify-json", // Prettify JSON data
],
"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,
@@ -43,7 +41,6 @@
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
@@ -53,20 +50,21 @@
"source.organizeImports": true
}
},
// Golang testing
"go.toolsEnvVars": {
"GOFLAGS": "-tags=integration"
"GOFLAGS": "-tags=",
// "CGO_ENABLED": 1 // for the race detector
},
"gopls.env": {
"GOFLAGS": "-tags=integration"
"GOFLAGS": "-tags="
},
"go.testEnvVars": {},
"go.testFlags": [
"-v",
// "-race"
],
"go.testTimeout": "600s",
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnSingleTest": true
"go.coverOnTestPackage": true
}
}

View File

@@ -2,14 +2,24 @@ version: "3.7"
services:
vscode:
image: qmcgaw/godevcontainer
build: .
image: godevcontainer
volumes:
- ../:/workspace
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# SSH directory
- ~/.ssh:/home/vscode/.ssh
- ~/.ssh:/root/.ssh
- /var/run/docker.sock:/var/run/docker.sock
# Git config
- ~/.gitconfig:/home/districter/.gitconfig
- ~/.gitconfig:/root/.gitconfig
environment:
- TZ=
cap_add:
# For debugging with dlv
- SYS_PTRACE
security_opt:
# For debugging with dlv
- seccomp:unconfined
entrypoint: zsh -c "while sleep 1000; do :; done"

View File

@@ -1,7 +1,7 @@
---
name: Bug
about: Report a bug
title: 'Bug: ...'
title: 'Bug: FILL THIS TEXT!'
labels: ":bug: bug"
assignees: qdm12

View File

@@ -1,7 +1,7 @@
---
name: Help
about: Ask for help
title: 'Help: ...'
title: 'Help: FILL THIS TEXT!'
labels: ":pray: help wanted"
assignees:

View File

@@ -2,28 +2,15 @@ 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
paths:
- .github/workflows/build.yml
- cmd/**
- internal/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -2,38 +2,25 @@ 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
- "*"
- "*/*"
- "!master"
paths:
- .github/workflows/buildx-branch.yml
- cmd/**
- internal/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
uses: crazy-max/ghaction-docker-buildx@v3
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
@@ -45,6 +32,8 @@ jobs:
--build-arg COMMIT=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
-t qmcgaw/gluetun:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I=
continue-on-error: true

View File

@@ -2,35 +2,22 @@ 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
paths:
- .github/workflows/buildx-latest.yml
- cmd/**
- internal/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
uses: crazy-max/ghaction-docker-buildx@v3
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
@@ -42,6 +29,8 @@ jobs:
--build-arg COMMIT=`git rev-parse --short HEAD` \
--build-arg VERSION=latest \
-t qmcgaw/private-internet-access:latest \
-t qmcgaw/gluetun:latest \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I=
continue-on-error: true

View File

@@ -2,35 +2,22 @@ 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
paths:
- .github/workflows/buildx-release.yml
- cmd/**
- internal/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
uses: crazy-max/ghaction-docker-buildx@v3
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
@@ -42,6 +29,8 @@ jobs:
--build-arg COMMIT=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/private-internet-access:${GITHUB_REF##*/} \
-t qmcgaw/gluetun:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/private-internet-access/tQFy7AxtSUNANPe6aoVChYdsI_I=
continue-on-error: true

View File

@@ -12,8 +12,10 @@ jobs:
- 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
uses: peter-evans/dockerhub-description@v2.4.1
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

View File

@@ -1,10 +1,10 @@
name: labels
on:
push:
branches: ["master"]
branches: [master]
paths:
- '.github/labels.yml'
- '.github/workflows/labels.yml'
- .github/labels.yml
- .github/workflows/labels.yml
jobs:
labeler:
runs-on: ubuntu-latest

View File

@@ -10,7 +10,10 @@ issues:
linters:
- dupl
- maligned
- path: internal/unix/constants\.go
linters:
- golint
text: don't use ALL_CAPS in Go names; use CamelCase
linters:
disable-all: true
enable:

View File

@@ -4,7 +4,7 @@ ARG GO_VERSION=1.15
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
RUN apk --update add git
ENV CGO_ENABLED=0
ARG GOLANGCI_LINT_VERSION=v1.31.0
ARG GOLANGCI_LINT_VERSION=v1.34.1
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 .
@@ -18,10 +18,10 @@ COPY internal/ ./internal/
RUN go test ./...
RUN golangci-lint run --timeout=10m
RUN go build -trimpath -ldflags="-s -w \
-X 'main.version=$VERSION' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o entrypoint main.go
-X 'main.version=$VERSION' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o entrypoint main.go
FROM alpine:${ALPINE_VERSION}
ARG VERSION=unknown
@@ -35,8 +35,8 @@ LABEL \
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
org.opencontainers.image.documentation="https://github.com/qdm12/gluetun" \
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \
org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux"
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
ENV VPNSP=pia \
VERSION_INFORMATION=on \
PROTOCOL=udp \
@@ -45,12 +45,14 @@ ENV VPNSP=pia \
OPENVPN_TARGET_IP= \
OPENVPN_IPV6=off \
TZ= \
UID=1000 \
GID=1000 \
IP_STATUS_FILE="/tmp/gluetun/ip" \
PUID= \
PGID= \
PUBLICIP_FILE="/tmp/gluetun/ip" \
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN, PureVPN only
USER= \
PASSWORD= \
OPENVPN_USER= \
OPENVPN_PASSWORD= \
USER_SECRETFILE=/run/secrets/openvpn_user \
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
REGION= \
# PIA only
PIA_ENCRYPTION=strong \
@@ -61,7 +63,7 @@ ENV VPNSP=pia \
# Mullvad, PureVPN, Windscribe only
CITY= \
# Windscribe only
HOSTNAME= \
SERVER_HOSTNAME= \
# Mullvad only
ISP= \
OWNED=no \
@@ -69,6 +71,8 @@ ENV VPNSP=pia \
PORT= \
# Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
# NordVPN only
SERVER_NUMBER= \
# Openvpn
@@ -102,16 +106,19 @@ ENV VPNSP=pia \
HTTPPROXY_PORT=8888 \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password \
# Shadowsocks
SHADOWSOCKS=off \
SHADOWSOCKS_LOG=off \
SHADOWSOCKS_PORT=8388 \
SHADOWSOCKS_PASSWORD= \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \
UPDATER_PERIOD=0
ENTRYPOINT ["/entrypoint"]
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=10m --timeout=10s --start-period=30s --retries=2 CMD /entrypoint healthcheck
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /entrypoint healthcheck
RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* && \
deluser openvpn && \

405
README.md
View File

@@ -3,27 +3,36 @@
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
**ANNOUNCEMENT**: *Github Wiki reworked*
**ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`*
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
[![Build status](https://github.com/qdm12/gluetun/workflows/Buildx%20latest/badge.svg)](https://github.com/qdm12/gluetun/actions?query=workflow%3A%22Buildx+latest%22)
[![Size](https://img.shields.io/docker/image-size/qmcgaw/gluetun?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
[![Size](https://img.shields.io/docker/image-size/qmcgaw/gluetun/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/gluetun/tags)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/private-internet-access)
[![Docker Stars](https://img.shields.io/docker/stars/qmcgaw/private-internet-access.svg)](https://hub.docker.com/r/qmcgaw/private-internet-access)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/gluetun.svg)](https://hub.docker.com/r/qmcgaw/gluetun)
[![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)
![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)
![GitHub Release Date](https://img.shields.io/github/release-date/qdm12/gluetun?label=Last%20release%20date)
[![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)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/gluetun/latest?sort=semver)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits)
## Videos
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/gluetun)](https://github.com/qdm12/gluetun)
1. [**Introduction**](https://youtu.be/3jIbU6J2Hs0)
1. [**Connect a container**](https://youtu.be/mH7J_2JKNK0)
1. [**Connect LAN devices**](https://youtu.be/qvjrM15Y0uk)
## Quick links
- Problem or suggestion?
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
## Features
@@ -36,8 +45,8 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun#connect-to-it)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun#connect-to-it)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆
- VPN server side port forwarding for Private Internet Access and Vyprvpn
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
@@ -54,352 +63,70 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado
```bash
docker run -d --name gluetun --cap-add=NET_ADMIN \
-e VPNSP="private internet access" -e REGION="CA Montreal" \
-e USER=js89ds7 -e PASSWORD=8fd9s239G \
-e OPENVPN_USER=js89ds7 -e OPENVPN_PASSWORD=8fd9s239G \
-v /yourpath:/gluetun \
qmcgaw/private-internet-access
qmcgaw/gluetun
```
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
```bash
echo "your openvpn username" > openvpn_user
echo "your openvpn password" > openvpn_password
docker-compose up -d
```
Note that you can:
You should probably check the many [environment variables](https://github.com/qdm12/gluetun/wiki/Environment-variables) available to adapt the container to your needs.
- Change the many [environment variables](#environment-variables) available
- Use `-p 8888:8888/tcp` to access the HTTP web proxy
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the Shadowsocks proxy
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
## Further setup
**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)**
The following points are all optional but should give you insights on all the possibilities with this container.
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.
- Use [Docker secrets](https://github.com/qdm12/gluetun/wiki/Docker-secrets) to read your credentials instead of environment variables
- [Test your setup](https://github.com/qdm12/gluetun/wiki/Test-your-setup)
- [How to connect other containers and devices to Gluetun](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
- [VPN server side port forwarding](https://github.com/qdm12/gluetun/wiki/Port-forwarding)
- [HTTP control server](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server) to automate things, restart Openvpn etc.
- Update the image with `docker pull qmcgaw/gluetun:latest`. See this [Wiki document](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) for Docker tags available.
## Testing
## Development
Check the VPN IP address matches your expectations
```sh
docker run --rm --network=container:gluetun alpine:3.12 wget -qO- https://ipinfo.io
```
▶ [Testing Wiki page](https://github.com/qdm12/gluetun/wiki/Testing-the-setup)
## Environment variables
**TLDR**; only set the 🏁 marked environment variables to get started.
💡 For all server filtering options such as `REGION`, you can have multiple values separated by a comma, i.e. `Germany,Singapore`
### VPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `VPNSP` | `private internet access` | `private internet access`, `mullvad`, `windscribe`, `surfshark`, `vyprvpn`, `nordvpn`, `purevpn`, `privado` | VPN Service Provider |
| `IP_STATUS_FILE` | `/tmp/gluetun/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 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 |
| `OPENVPN_IPV6` | `off` | `on`, `off` | Enable tunneling of IPv6 (only for Mullvad) |
*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` | `/tmp/gluetun/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`, `443` or `1401` for TCP; `53`, `1194`, `1195`, `1196`, `1197`, `1300`, `1301`, `1302`, `1303` or `1400` for UDP. Defaults to TCP `443` and UDP `1194` | Custom VPN port to use |
| `OWNED` | `no` | `yes` or `no` | If the VPN server is owned by Mullvad |
💡 [Mullvad IPv6 Wiki page](https://github.com/qdm12/gluetun/wiki/Mullvad-IPv6)
For **port forwarding**, obtain a port from [here](https://mullvad.net/en/account/#/ports) and add it to `FIREWALL_VPN_INPUT_PORTS`
- Windscribe (see [this](https://github.com/qdm12/gluetun/blob/master/internal/constants/windscribe.go#L43) for the choices of regions, cities and hostnames)
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | | | Comma separated list of regions to choose the VPN server |
| `CITY` | | | Comma separated list of cities to choose the VPN server |
| `HOSTNAME` | | | Comma separated list of hostnames to choose the VPN server |
| `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-Servers) | VPN server region |
- Cyberghost
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| 🏁 | | | **See additional setup steps below** |
| `REGION` | | One of the Cyberghost regions, [Wiki page](https://github.com/qdm12/gluetun/wiki/Cyberghost-Servers) | VPN server country |
| `CYBERGHOST_GROUP` | `Premium UDP Europe` | One of the server groups (see above Wiki page) | Server group |
**Additional setup steps**: Bind mount your `client.key` file to `/gluetun/client.key` and your `client.crt` file to `/gluetun/client.crt`. For example, you can use with your `docker run` command:
```sh
-v /yourpath/client.key:/gluetun/client.key:ro -v /yourpath/client.crt:/gluetun/client.crt:ro
```
- 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 |
For **port forwarding**, add a port you want to be accessible to `FIREWALL_VPN_INPUT_PORTS`
- 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 |
- Privado
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `HOSTNAME` | | [One of the Privado hostname](internal/constants/privado.go#L26), i.e. `ams-001.vpn.privado.io` | VPN server hostname |
### 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 and routing
| 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. |
| `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_INPUT_PORTS` | | i.e. `1000,8000` | Comma separated list of ports to allow through the default interface. This seems needed for Kubernetes sidecars. |
| `FIREWALL_DEBUG` | `off` | `on` or `off` | Prints every firewall related command. You should use it for **debugging purposes** only. |
| `FIREWALL_OUTBOUND_SUBNETS` | | i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28` | Comma separated subnets that Gluetun and the containers sharing its network stack are allowed to access. This involves firewall and routing modifications. |
### Shadowsocks
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `SHADOWSOCKS` | `off` | `on`, `off` | Enable the internal Shadowsocks proxy |
| `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 |
### HTTP proxy
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `HTTPPROXY` | `off` | `on`, `off` | Enable the internal HTTP proxy |
| `HTTPPROXY_LOG` | `off` | `on` or `off` | Logs every tunnel requests |
| `HTTPPROXY_PORT` | `8888` | `1024` to `65535` | Internal port number for the HTTP proxy to listen on |
| `HTTPPROXY_USER` | | | Username to use to connect to the HTTP proxy |
| `HTTPPROXY_PASSWORD` | | | Password to use to connect to the HTTP proxy |
| `HTTPPROXY_STEALTH` | `off` | `on` or `off` | Stealth mode means HTTP proxy headers are not added to your requests |
### 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 |
### HTTP Control server
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `HTTP_CONTROL_SERVER_PORT` | `8000` | `1` to `65535` | Listening port for the HTTP control server |
| `HTTP_CONTROL_SERVER_LOG` | `on` | `on` or `off` | Enable logging of HTTP requests |
### Other
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. |
| `VERSION_INFORMATION` | `on` | `on`, `off` | Logs a message indicating if a newer version is available once the VPN is connected |
| `UPDATER_PERIOD` | `0` | Valid duration string such as `24h` | Period to update all VPN servers information in memory and to /gluetun/servers.json. Set to `0` to disable. This does a burst of DNS over TLS requests, which may be blocked if you set `BLOCK_MALICIOUS=on` for example. |
## 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 (i.e. with Chrome, Kodi, etc.)</summary><p>
⚠️ You might want to use Shadowsocks instead which tunnels UDP as well as TCP and does not leak your credentials.
The HTTP proxy will not encrypt your username and password every time you send a request to the HTTP proxy server.
1. Setup an 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`
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 `HTTPPROXY_USER` and `HTTPPROXY_PASSWORD`. Note that Chrome does not support authentication.
1. If you set `HTTPPROXY_LOG` to `on`, more information will be logged in the Docker logs
</p></details>
- <details><summary>Connect LAN devices through the built-in *Shadowsocks* proxy (per app, system wide, etc.)</summary><p>
1. Setup a Shadowsocks 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`
1. With your Shadowsocks 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
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=/tmp/gluetun/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.
For `VPNSP=private internet access` (default), you will keep the same forwarded port for 60 days as long as you bind mount the `/gluetun` directory.
You can also use the HTTP control server (see below) to get the port forwarded.
## HTTP control server
[Wiki page](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server)
## Development and contributing
- Contribute with code: start with [this Wiki page](https://github.com/qdm12/gluetun/wiki/Developement-setup)
- [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
- 💻 [Contribute with code](https://github.com/qdm12/gluetun/wiki/Development) ([existing contributors 👍](https://github.com/qdm12/gluetun/blob/master/.github/CONTRIBUTING.md#Contributors))
- [List of issues and feature requests](https://github.com/qdm12/gluetun/issues)
- [Kanban board](https://github.com/qdm12/gluetun/projects/1)
## License
This repository is under an [MIT license](https://github.com/qdm12/gluetun/master/license)
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](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:
- Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
[![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://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)
- Contribute to the issues and discussions on Github
- Many thanks to @Frepke, @Ralph521, G. Mendez, M. Otmar Weber, J. Perez, A. Cooper and **others** for supporting me financially 🥇👍
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.
## Metadata
Many thanks to @Frepke, @Ralph521, G. Mendez, M. Otmar Weber, J. Perez and A. Cooper for supporting me financially 🥇👍
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/commits)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/gluetun.svg)](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=gluetun.readme)
![GitHub stars](https://img.shields.io/github/stars/qdm12/gluetun?style=social)
![GitHub watchers](https://img.shields.io/github/watchers/qdm12/gluetun?style=social)
![Contributors](https://img.shields.io/github/contributors/qdm12/gluetun)
![GitHub forks](https://img.shields.io/github/forks/qdm12/gluetun?style=social)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/gluetun)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/gluetun)
[![dockeri.co](https://dockeri.co/image/qmcgaw/gluetun)](https://hub.docker.com/r/qmcgaw/gluetun)
![Docker Layers for latest](https://img.shields.io/microbadger/layers/qmcgaw/gluetun/latest?label=Docker%20image%20layers)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/gluetun)

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"net"
"net/http"
"os"
nativeos "os"
"os/signal"
"strings"
"sync"
@@ -29,42 +29,52 @@ import (
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/gluetun/internal/updater"
versionpkg "github.com/qdm12/gluetun/internal/version"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user"
)
//nolint:gochecknoglobals
var (
buildInfo models.BuildInformation
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
)
func main() {
buildInfo.Version = version
buildInfo.Commit = commit
buildInfo.BuildDate = buildDate
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
BuildDate: buildDate,
}
ctx := context.Background()
os.Exit(_main(ctx, os.Args))
args := nativeos.Args
os := os.New()
osUser := user.New()
unix := unix.New()
cli := cli.New()
nativeos.Exit(_main(ctx, buildInfo, args, os, osUser, unix, cli))
}
func _main(background context.Context, args []string) int { //nolint:gocognit,gocyclo
//nolint:gocognit,gocyclo
func _main(background context.Context, buildInfo models.BuildInformation,
args []string, os os.OS, osUser user.OSUser, unix unix.Unix,
cli cli.CLI) int {
if len(args) > 1 { // cli operation
var err error
switch args[1] {
case "healthcheck":
err = cli.HealthCheck(background)
case "clientkey":
err = cli.ClientKey(args[2:])
err = cli.ClientKey(args[2:], os.OpenFile)
case "openvpnconfig":
err = cli.OpenvpnConfig()
err = cli.OpenvpnConfig(os)
case "update":
err = cli.Update(args[2:])
err = cli.Update(args[2:], os)
default:
err = fmt.Errorf("command %q is unknown", args[1])
}
@@ -80,17 +90,15 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
const clientTimeout = 15 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
client := network.NewClient(clientTimeout)
// Create configurators
fileManager := files.NewFileManager()
alpineConf := alpine.NewConfigurator(fileManager)
ovpnConf := openvpn.NewConfigurator(logger, fileManager)
dnsConf := dns.NewConfigurator(logger, client, fileManager)
alpineConf := alpine.NewConfigurator(os.OpenFile, osUser)
ovpnConf := openvpn.NewConfigurator(logger, os, unix)
dnsConf := dns.NewConfigurator(logger, httpClient, os.OpenFile)
routingConf := routing.NewRouting(logger)
firewallConf := firewall.NewConfigurator(logger, routingConf, fileManager)
firewallConf := firewall.NewConfigurator(logger, routingConf, os.OpenFile)
streamMerger := command.NewStreamMerger()
paramsReader := params.NewReader(logger, fileManager)
paramsReader := params.NewReader(logger, os)
fmt.Println(gluetunLogging.Splash(buildInfo))
printVersions(ctx, logger, map[string]func(ctx context.Context) (string, error){
@@ -106,25 +114,37 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
}
logger.Info(allSettings.String())
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
logger.Error(err)
return 1
}
if err := os.MkdirAll("/gluetun", 0644); err != nil {
logger.Error(err)
return 1
}
// TODO run this in a loop or in openvpn to reload from file without restarting
storage := storage.New(logger)
const updateServerFile = true
allServers, err := storage.SyncServers(constants.GetAllServers(), updateServerFile)
storage := storage.New(logger, os, constants.ServersData)
allServers, err := storage.SyncServers(constants.GetAllServers())
if err != nil {
logger.Error(err)
return 1
}
// Should never change
uid, gid := allSettings.System.UID, allSettings.System.GID
puid, pgid := allSettings.System.PUID, allSettings.System.PGID
err = alpineConf.CreateUser("nonrootuser", uid)
const defaultUsername = "nonrootuser"
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
if err != nil {
logger.Error(err)
return 1
}
err = fileManager.SetOwnership("/etc/unbound", uid, gid)
if err != nil {
if nonRootUsername != defaultUsername {
logger.Info("using existing username %s corresponding to user id %d", nonRootUsername, puid)
}
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
logger.Error(err)
return 1
}
@@ -217,57 +237,47 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
go collectStreamLines(ctx, streamMerger, logger, signalTunnelReady)
openvpnLooper := openvpn.NewLooper(allSettings.VPNSP, allSettings.OpenVPN, uid, gid, allServers,
ovpnConf, firewallConf, routingConf, logger, httpClient, fileManager, streamMerger, cancel)
openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers,
ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, streamMerger, cancel)
wg.Add(1)
// wait for restartOpenvpn
go openvpnLooper.Run(ctx, wg)
updaterOptions := updater.NewOptions("127.0.0.1")
updaterLooper := updater.NewLooper(updaterOptions, allSettings.UpdaterPeriod,
allServers, storage, openvpnLooper.SetAllServers, httpClient, logger)
updaterLooper := updater.NewLooper(allSettings.Updater,
allServers, storage, openvpnLooper.SetServers, httpClient, logger)
wg.Add(1)
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
go updaterLooper.Run(ctx, wg)
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, logger, streamMerger, uid, gid)
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, logger, streamMerger, nonRootUsername, puid, pgid)
wg.Add(1)
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go unboundLooper.Run(ctx, wg, signalDNSReady)
publicIPLooper := publicip.NewLooper(client, logger, fileManager,
allSettings.System.IPStatusFilepath, allSettings.PublicIPPeriod, uid, gid)
publicIPLooper := publicip.NewLooper(
httpClient, logger, allSettings.PublicIP, puid, pgid, os)
wg.Add(1)
go publicIPLooper.Run(ctx, wg)
wg.Add(1)
go publicIPLooper.RunRestartTicker(ctx, wg)
publicIPLooper.SetPeriod(allSettings.PublicIPPeriod) // call after RunRestartTicker
httpProxyLooper := httpproxy.NewLooper(logger, allSettings.HTTPProxy)
wg.Add(1)
go httpProxyLooper.Run(ctx, wg)
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks, logger, defaultInterface)
restartShadowsocks := shadowsocksLooper.Restart
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks, logger)
wg.Add(1)
go shadowsocksLooper.Run(ctx, wg)
if allSettings.HTTPProxy.Enabled {
httpProxyLooper.Restart()
}
if allSettings.ShadowSocks.Enabled {
restartShadowsocks()
}
wg.Add(1)
go routeReadyEvents(ctx, wg, tunnelReadyCh, dnsReadyCh,
go routeReadyEvents(ctx, wg, buildInfo, tunnelReadyCh, dnsReadyCh,
unboundLooper, updaterLooper, publicIPLooper, routingConf, logger, httpClient,
allSettings.VersionInformation, allSettings.OpenVPN.Provider.PortForwarding.Enabled, openvpnLooper.PortForward,
)
controlServerAddress := fmt.Sprintf("0.0.0.0:%d", allSettings.ControlServer.Port)
controlServerLogging := allSettings.ControlServer.Log
httpServer := server.New(controlServerAddress, controlServerLogging,
logger, buildInfo, openvpnLooper, unboundLooper, updaterLooper)
logger, buildInfo, openvpnLooper, unboundLooper, updaterLooper, publicIPLooper)
wg.Add(1)
go httpServer.Run(ctx, wg)
@@ -276,14 +286,15 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
wg.Add(1)
go healthcheckServer.Run(ctx, wg)
// Start openvpn for the first time
openvpnLooper.Restart()
// Start openvpn for the first time in a blocking call
// until openvpn is launched
_, _ = openvpnLooper.SetStatus(constants.Running) // TODO option to disable with variable
signalsCh := make(chan os.Signal, 1)
signalsCh := make(chan nativeos.Signal, 1)
signal.Notify(signalsCh,
syscall.SIGINT,
syscall.SIGTERM,
os.Interrupt,
nativeos.Interrupt,
)
shutdownErrorsCount := 0
select {
@@ -293,14 +304,9 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go
case <-ctx.Done():
logger.Warn("context canceled, shutting down")
}
logger.Info("Clearing ip status file %s", allSettings.System.IPStatusFilepath)
if err := fileManager.Remove(string(allSettings.System.IPStatusFilepath)); err != nil {
logger.Error(err)
shutdownErrorsCount++
}
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
logger.Info("Clearing forwarded port status file %s", allSettings.OpenVPN.Provider.PortForwarding.Filepath)
if err := fileManager.Remove(string(allSettings.OpenVPN.Provider.PortForwarding.Filepath)); err != nil {
if err := os.Remove(string(allSettings.OpenVPN.Provider.PortForwarding.Filepath)); err != nil {
logger.Error(err)
shutdownErrorsCount++
}
@@ -385,7 +391,8 @@ func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger,
})
}
func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, tunnelReadyCh, dnsReadyCh <-chan struct{},
func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, buildInfo models.BuildInformation,
tunnelReadyCh, dnsReadyCh <-chan struct{},
unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper,
routing routing.Routing, logger logging.Logger, httpClient *http.Client,
versionInformation, portForwardingEnabled bool, startPortForward func(vpnGateway net.IP)) {
@@ -401,7 +408,9 @@ func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, tunnelReadyCh, dn
tickerWg.Wait()
return
case <-tunnelReadyCh: // blocks until openvpn is connected
unboundLooper.Restart()
if unboundLooper.GetSettings().Enabled {
_, _ = unboundLooper.SetStatus(constants.Running)
}
restartTickerCancel() // stop previous restart tickers
tickerWg.Wait()
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
@@ -424,7 +433,8 @@ func routeReadyEvents(ctx context.Context, wg *sync.WaitGroup, tunnelReadyCh, dn
startPortForward(vpnGateway)
}
case <-dnsReadyCh:
publicIPLooper.Restart() // TODO do not restart if disabled
// Runs the Public IP getter job once
_, _ = publicIPLooper.SetStatus(constants.Running)
if !versionInformation {
break
}

1720
doc/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 KiB

BIN
doc/logo_256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,7 +1,7 @@
version: "3.7"
services:
gluetun:
image: qmcgaw/private-internet-access
image: qmcgaw/gluetun
container_name: gluetun
cap_add:
- NET_ADMIN
@@ -14,25 +14,18 @@ services:
# command:
volumes:
- /yourpath:/gluetun
secrets:
- openvpn_user
- openvpn_password
environment:
# More variables are available, see the readme table
- VPNSP=private internet access
# Timezone for accurate logs times
- TZ=
# 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
restart: always
secrets:
openvpn_user:
file: ./openvpn_user
openvpn_password:
file: ./openvpn_password

6
go.mod
View File

@@ -3,12 +3,12 @@ module github.com/qdm12/gluetun
go 1.15
require (
github.com/fatih/color v1.9.0
github.com/fatih/color v1.10.0
github.com/golang/mock v1.4.4
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/qdm12/golibs v0.0.0-20201025221346-fe352060c25a
github.com/qdm12/golibs v0.0.0-20210102015428-6e1d159e61a3
github.com/qdm12/ss-server v0.1.0
github.com/stretchr/testify v1.6.1
github.com/vishvananda/netlink v1.1.0
golang.org/x/sys v0.0.0-20201018121011-98379d014ca7
golang.org/x/sys v0.0.0-20201223074533-0d417f636930
)

17
go.sum
View File

@@ -12,6 +12,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/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=
@@ -58,9 +60,13 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/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=
@@ -72,13 +78,14 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/golibs v0.0.0-20201025221346-fe352060c25a h1:v0zUA1FWeVkTEd9KyxfehbRVJeFGOqyMY6FHO/Q9ITU=
github.com/qdm12/golibs v0.0.0-20201025221346-fe352060c25a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/golibs v0.0.0-20210102015428-6e1d159e61a3 h1:tnkjZkYZuAFNga7Wd/j3z3gPJLkv0OXow4q/YTkRdmE=
github.com/qdm12/golibs v0.0.0-20210102015428-6e1d159e61a3/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/qdm12/ss-server v0.1.0 h1:WV9MkHCDEWRwe4WpnYFeR/zcZAxYoTbfntLDnw9AQ50=
github.com/qdm12/ss-server v0.1.0/go.mod h1:ABVUkxubboL3vqBkOwDV9glX1/x7SnYrckBe5d+M/zw=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -115,8 +122,10 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018121011-98379d014ca7 h1:CNOpL+H7PSxBI7dF/EIUsfOguRSzWp6CQ91yxZE6PG4=
golang.org/x/sys v0.0.0-20201018121011-98379d014ca7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

View File

@@ -1,25 +1,22 @@
package alpine
import (
"os/user"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/user"
)
type Configurator interface {
CreateUser(username string, uid int) error
CreateUser(username string, uid int) (createdUsername string, err error)
}
type configurator struct {
fileManager files.FileManager
lookupUID func(uid string) (*user.User, error)
lookupUser func(username string) (*user.User, error)
openFile os.OpenFileFunc
osUser user.OSUser
}
func NewConfigurator(fileManager files.FileManager) Configurator {
func NewConfigurator(openFile os.OpenFileFunc, osUser user.OSUser) Configurator {
return &configurator{
fileManager: fileManager,
lookupUID: user.LookupId,
lookupUser: user.Lookup,
openFile: openFile,
osUser: osUser,
}
}

View File

@@ -2,38 +2,40 @@ package alpine
import (
"fmt"
"os"
"os/user"
)
// CreateUser creates a user in Alpine with the given UID.
func (c *configurator) CreateUser(username string, uid int) error {
func (c *configurator) CreateUser(username string, uid int) (createdUsername string, err error) {
UIDStr := fmt.Sprintf("%d", uid)
u, err := c.lookupUID(UIDStr)
u, err := c.osUser.LookupID(UIDStr)
_, unknownUID := err.(user.UnknownUserIdError)
if err != nil && !unknownUID {
return fmt.Errorf("cannot create user: %w", err)
return "", fmt.Errorf("cannot create user: %w", err)
} else if u != nil {
if u.Username == username {
return nil
return "", nil
}
return fmt.Errorf("user with ID %d exists with username %q instead of %q", uid, u.Username, username)
return u.Username, nil
}
u, err = c.lookupUser(username)
u, err = c.osUser.Lookup(username)
_, unknownUsername := err.(user.UnknownUserError)
if err != nil && !unknownUsername {
return fmt.Errorf("cannot create user: %w", 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",
return "", fmt.Errorf("cannot create user: user with name %s already exists for ID %s instead of %d",
username, u.Uid, uid)
}
passwd, err := c.fileManager.ReadFile("/etc/passwd")
file, err := c.openFile("/etc/passwd", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("cannot create user: %w", 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))...)
if err := c.fileManager.WriteToFile("/etc/passwd", passwd); err != nil {
return fmt.Errorf("cannot create user: %w", err)
s := fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid)
_, err = file.WriteString(s)
if err != nil {
_ = file.Close()
return "", err
}
return nil
return username, file.Close()
}

View File

@@ -2,131 +2,19 @@ package cli
import (
"context"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/healthcheck"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func ClientKey(args []string) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", string(constants.ClientKey), "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
type CLI interface {
ClientKey(args []string, openFile os.OpenFileFunc) error
HealthCheck(ctx context.Context) error
OpenvpnConfig(os os.OS) error
Update(args []string, os os.OS) error
}
func HealthCheck(ctx context.Context) error {
const timeout = 3 * time.Second
httpClient := &http.Client{Timeout: timeout}
healthchecker := healthcheck.NewChecker(httpClient)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
const url = "http://" + constants.HealthcheckAddress
return healthchecker.Check(ctx, url)
}
type cli struct{}
func OpenvpnConfig() error {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return err
}
paramsReader := params.NewReader(logger, files.NewFileManager())
allSettings, err := settings.GetAllSettings(paramsReader)
if err != nil {
return err
}
allServers, err := storage.New(logger).SyncServers(constants.GetAllServers(), false)
if err != nil {
return err
}
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetOpenVPNConnection(allSettings.OpenVPN.Provider.ServerSelection)
if err != nil {
return err
}
lines := providerConf.BuildConf(
connection,
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
}
func Update(args []string) error {
options := updater.Options{CLI: true}
var flushToFile bool
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&flushToFile, "file", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&options.Stdout, "stdout", false, "Write results to console to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "1.1.1.1", "DNS resolver address to use")
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
if err := flagSet.Parse(args); err != nil {
return err
}
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return err
}
if !flushToFile && !options.Stdout {
return fmt.Errorf("at least one of -file or -stdout must be specified")
}
ctx := context.Background()
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
storage := storage.New(logger)
const writeSync = false
currentServers, err := storage.SyncServers(constants.GetAllServers(), writeSync)
if err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return err
}
if flushToFile {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
}
return nil
func New() CLI {
return &cli{}
}

41
internal/cli/clientkey.go Normal file
View File

@@ -0,0 +1,41 @@
package cli
import (
"flag"
"fmt"
"io/ioutil"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/os"
)
func (c *cli) ClientKey(args []string, openFile os.OpenFileFunc) error {
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
filepath := flagSet.String("path", string(constants.ClientKey), "file path to the client.key file")
if err := flagSet.Parse(args); err != nil {
return err
}
file, err := openFile(*filepath, os.O_RDONLY, 0)
if err != nil {
return err
}
data, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
if err != nil {
return err
}
s := string(data)
s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "")
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
fmt.Println(s)
return nil
}

View File

@@ -0,0 +1,20 @@
package cli
import (
"context"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/healthcheck"
)
func (c *cli) HealthCheck(ctx context.Context) error {
const timeout = 10 * time.Second
httpClient := &http.Client{Timeout: timeout}
healthchecker := healthcheck.NewChecker(httpClient)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
const url = "http://" + constants.HealthcheckAddress
return healthchecker.Check(ctx, url)
}

View File

@@ -0,0 +1,48 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/params"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func (c *cli) OpenvpnConfig(os os.OS) error {
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return err
}
paramsReader := params.NewReader(logger, os)
allSettings, err := settings.GetAllSettings(paramsReader)
if err != nil {
return err
}
allServers, err := storage.New(logger, os, constants.ServersData).
SyncServers(constants.GetAllServers())
if err != nil {
return err
}
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetOpenVPNConnection(allSettings.OpenVPN.Provider.ServerSelection)
if err != nil {
return err
}
lines := providerConf.BuildConf(
connection,
allSettings.OpenVPN.Verbosity,
"nonroortuser",
allSettings.OpenVPN.Root,
allSettings.OpenVPN.Cipher,
allSettings.OpenVPN.Auth,
allSettings.OpenVPN.Provider.ExtraConfigOptions,
)
fmt.Println(strings.Join(lines, "\n"))
return nil
}

64
internal/cli/update.go Normal file
View File

@@ -0,0 +1,64 @@
package cli
import (
"context"
"flag"
"fmt"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/gluetun/internal/storage"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func (c *cli) Update(args []string, os os.OS) error {
options := settings.Updater{CLI: true}
var flushToFile bool
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
flagSet.BoolVar(&flushToFile, "file", false, "Write results to /gluetun/servers.json (for end users)")
flagSet.BoolVar(&options.Stdout, "stdout", false, "Write results to console to modify the program (for maintainers)")
flagSet.StringVar(&options.DNSAddress, "dns", "1.1.1.1", "DNS resolver address to use")
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
if err := flagSet.Parse(args); err != nil {
return err
}
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel)
if err != nil {
return err
}
if !flushToFile && !options.Stdout {
return fmt.Errorf("at least one of -file or -stdout must be specified")
}
ctx := context.Background()
const clientTimeout = 10 * time.Second
httpClient := &http.Client{Timeout: clientTimeout}
storage := storage.New(logger, os, constants.ServersData)
currentServers, err := storage.SyncServers(constants.GetAllServers())
if err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
updater := updater.New(options, httpClient, currentServers, logger)
allServers, err := updater.UpdateServers(ctx)
if err != nil {
return err
}
if flushToFile {
if err := storage.FlushToFile(allServers); err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
}
return nil
}

View File

@@ -17,10 +17,6 @@ const (
Quadrant models.DNSProvider = "quadrant"
// CleanBrowsing is a DNS over TLS provider.
CleanBrowsing models.DNSProvider = "cleanbrowsing"
// SecureDNS is a DNS over TLS provider.
SecureDNS models.DNSProvider = "securedns"
// LibreDNS is a DNS over TLS provider.
LibreDNS models.DNSProvider = "libredns"
)
// DNSProviderMapping returns a constant mapping of dns provider name
@@ -80,24 +76,10 @@ func DNSProviderMapping() map[models.DNSProvider]models.DNSProviderData {
SupportsIPv6: true,
Host: models.DNSHost("security-filter-dns.cleanbrowsing.org"),
},
SecureDNS: {
IPs: []net.IP{
{146, 185, 167, 43},
{0x2a, 0x3, 0xb0, 0xc0, 0x0, 0x0, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0xe, 0x9a, 0x30, 0x1},
},
SupportsTLS: true,
SupportsIPv6: true,
Host: models.DNSHost("dot.securedns.eu"),
},
LibreDNS: {
IPs: []net.IP{{116, 203, 115, 192}},
SupportsTLS: true,
Host: models.DNSHost("dot.libredns.gr"),
},
}
}
// Block lists URLs
// Block lists URLs.
//nolint:lll
const (
AdsBlockListHostnamesURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/ads-hostnames.updated"

View File

@@ -29,4 +29,6 @@ const (
ClientKey models.Filepath = "/gluetun/client.key"
// Client certificate filepath, used by Cyberghost.
ClientCertificate models.Filepath = "/gluetun/client.crt"
// Servers information filepath.
ServersData = "/gluetun/servers.json"
)

View File

@@ -1,8 +0,0 @@
package constants
import "os"
const (
UserReadPermission os.FileMode = 0400
AllReadWritePermissions os.FileMode = 0666
)

View File

@@ -28,99 +28,104 @@ func PIAGeoChoices() (choices []string) {
//nolint:lll
func PIAServers() []models.PIAServer {
return []models.PIAServer{
{Region: "AU Melbourne", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "melbourne406", IPs: []net.IP{{27, 50, 74, 148}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "melbourne406", IPs: []net.IP{{27, 50, 74, 156}}}},
{Region: "AU Perth", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "perth404", IPs: []net.IP{{43, 250, 205, 186}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "perth404", IPs: []net.IP{{43, 250, 205, 190}}}},
{Region: "AU Sydney", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "sydney403", IPs: []net.IP{{180, 92, 192, 156}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "sydney403", IPs: []net.IP{{117, 120, 9, 7}}}},
{Region: "Albania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "tirana401", IPs: []net.IP{{31, 171, 154, 134}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "tirana401", IPs: []net.IP{{31, 171, 154, 136}}}},
{Region: "Algeria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "algiers401", IPs: []net.IP{{45, 133, 91, 248}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "algiers401", IPs: []net.IP{{45, 133, 91, 234}}}},
{Region: "Andorra", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "andorra403", IPs: []net.IP{{188, 241, 82, 4}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "andorra403", IPs: []net.IP{{188, 241, 82, 7}}}},
{Region: "Argentina", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "buenosaires401", IPs: []net.IP{{190, 106, 134, 86}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "buenosaires401", IPs: []net.IP{{190, 106, 134, 87}}}},
{Region: "Armenia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "armenia401", IPs: []net.IP{{45, 139, 50, 244}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "armenia401", IPs: []net.IP{{45, 139, 50, 249}}}},
{Region: "Austria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vienna402", IPs: []net.IP{{156, 146, 60, 46}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vienna402", IPs: []net.IP{{156, 146, 60, 53}}}},
{Region: "Bahamas", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "bahamas402", IPs: []net.IP{{45, 132, 143, 226}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "bahamas402", IPs: []net.IP{{45, 132, 143, 208}}}},
{Region: "Belgium", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "brussels408", IPs: []net.IP{{91, 90, 123, 46}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "brussels408", IPs: []net.IP{{91, 90, 123, 46}}}},
{Region: "Brazil", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "saopaolo404", IPs: []net.IP{{45, 133, 180, 248}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "saopaolo404", IPs: []net.IP{{45, 133, 180, 244}}}},
{Region: "Bulgaria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "sofia403", IPs: []net.IP{{217, 138, 221, 70}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "sofia403", IPs: []net.IP{{217, 138, 221, 70}}}},
{Region: "CA Montreal", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "montreal410", IPs: []net.IP{{199, 36, 223, 235}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "montreal401", IPs: []net.IP{{176, 113, 74, 45}}}},
{Region: "CA Ontario", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "ontario403", IPs: []net.IP{{172, 98, 92, 5}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "ontario403", IPs: []net.IP{{172, 98, 92, 45}}}},
{Region: "CA Toronto", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "toronto405", IPs: []net.IP{{172, 83, 47, 226}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "toronto405", IPs: []net.IP{{172, 83, 47, 234}}}},
{Region: "CA Vancouver", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vancouver407", IPs: []net.IP{{172, 98, 89, 18}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vancouver407", IPs: []net.IP{{172, 98, 89, 43}}}},
{Region: "Cambodia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cambodia402", IPs: []net.IP{{188, 215, 235, 120}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cambodia401", IPs: []net.IP{{188, 215, 235, 100}}}},
{Region: "China", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "china404", IPs: []net.IP{{188, 241, 80, 10}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "china404", IPs: []net.IP{{188, 241, 80, 10}}}},
{Region: "Cyprus", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cyprus401", IPs: []net.IP{{45, 132, 137, 253}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cyprus401", IPs: []net.IP{{45, 132, 137, 234}}}},
{Region: "Czech Republic", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "prague402", IPs: []net.IP{{212, 102, 39, 163}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "prague402", IPs: []net.IP{{212, 102, 39, 147}}}},
{Region: "DE Berlin", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "berlin421", IPs: []net.IP{{154, 13, 1, 87}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "berlin421", IPs: []net.IP{{154, 13, 1, 80}}}},
{Region: "DE Frankfurt", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "frankfurt406", IPs: []net.IP{{212, 102, 57, 115}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "frankfurt406", IPs: []net.IP{{212, 102, 57, 67}}}},
{Region: "Denmark", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "copenhagen402", IPs: []net.IP{{188, 126, 94, 72}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "copenhagen402", IPs: []net.IP{{188, 126, 94, 72}}}},
{Region: "Egypt", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cairo402", IPs: []net.IP{{188, 214, 122, 119}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cairo402", IPs: []net.IP{{188, 214, 122, 123}}}},
{Region: "Estonia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "talinn402", IPs: []net.IP{{95, 153, 31, 77}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "talinn402", IPs: []net.IP{{95, 153, 31, 73}}}},
{Region: "Finland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "helsinki402", IPs: []net.IP{{188, 126, 89, 51}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "helsinki402", IPs: []net.IP{{188, 126, 89, 53}}}},
{Region: "France", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "paris406", IPs: []net.IP{{143, 244, 57, 136}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "paris406", IPs: []net.IP{{143, 244, 57, 184}}}},
{Region: "Georgia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "georgia402", IPs: []net.IP{{45, 132, 138, 208}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "georgia402", IPs: []net.IP{{45, 132, 138, 228}}}},
{Region: "Greece", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "athens403", IPs: []net.IP{{154, 57, 3, 108}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "athens403", IPs: []net.IP{{154, 57, 3, 118}}}},
{Region: "Greenland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "greenland402", IPs: []net.IP{{45, 131, 209, 206}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "greenland402", IPs: []net.IP{{45, 131, 209, 226}}}},
{Region: "Hong Kong", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "china403", IPs: []net.IP{{86, 107, 104, 217}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "china403", IPs: []net.IP{{86, 107, 104, 216}}}},
{Region: "Hungary", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "budapest401", IPs: []net.IP{{217, 138, 192, 218}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "budapest401", IPs: []net.IP{{217, 138, 192, 220}}}},
{Region: "Iceland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "reykjavik402", IPs: []net.IP{{45, 133, 193, 93}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "reykjavik402", IPs: []net.IP{{45, 133, 193, 86}}}},
{Region: "India", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "mumbai402", IPs: []net.IP{{45, 120, 139, 128}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "mumbai402", IPs: []net.IP{{45, 120, 139, 128}}}},
{Region: "Ireland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dublin409", IPs: []net.IP{{188, 241, 178, 35}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dublin409", IPs: []net.IP{{188, 241, 178, 40}}}},
{Region: "Isle of Man", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "douglas402", IPs: []net.IP{{45, 132, 140, 220}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "douglas402", IPs: []net.IP{{45, 132, 140, 214}}}},
{Region: "Israel", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "jerusalem402", IPs: []net.IP{{185, 77, 248, 25}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "jerusalem402", IPs: []net.IP{{185, 77, 248, 27}}}},
{Region: "Italy", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "milano403", IPs: []net.IP{{156, 146, 41, 115}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "milano403", IPs: []net.IP{{156, 146, 41, 115}}}},
{Region: "Japan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "tokyo404", IPs: []net.IP{{156, 146, 34, 227}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "tokyo402", IPs: []net.IP{{156, 146, 34, 126}}}},
{Region: "Kazakhstan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "kazakhstan402", IPs: []net.IP{{45, 133, 88, 227}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "kazakhstan402", IPs: []net.IP{{45, 133, 88, 227}}}},
{Region: "Latvia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "riga401", IPs: []net.IP{{109, 248, 149, 4}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "riga401", IPs: []net.IP{{109, 248, 149, 14}}}},
{Region: "Liechtenstein", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "liechtenstein402", IPs: []net.IP{{45, 139, 48, 227}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "liechtenstein402", IPs: []net.IP{{45, 139, 48, 207}}}},
{Region: "Lithuania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vilnius401", IPs: []net.IP{{85, 206, 165, 167}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vilnius401", IPs: []net.IP{{85, 206, 165, 167}}}},
{Region: "Luxembourg", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "luxembourg403", IPs: []net.IP{{92, 223, 89, 245}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "luxembourg403", IPs: []net.IP{{92, 223, 89, 103}}}},
{Region: "Macedonia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "macedonia401", IPs: []net.IP{{185, 225, 28, 122}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "macedonia401", IPs: []net.IP{{185, 225, 28, 126}}}},
{Region: "Malta", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "malta402", IPs: []net.IP{{45, 137, 198, 219}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "malta402", IPs: []net.IP{{45, 137, 198, 208}}}},
{Region: "Mexico", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "mexico410", IPs: []net.IP{{77, 81, 142, 114}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "mexico410", IPs: []net.IP{{77, 81, 142, 113}}}},
{Region: "Moldova", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "chisinau401", IPs: []net.IP{{178, 175, 129, 38}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "chisinau401", IPs: []net.IP{{178, 175, 129, 37}}}},
{Region: "Monaco", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "monaco402", IPs: []net.IP{{45, 137, 199, 208}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "monaco402", IPs: []net.IP{{45, 137, 199, 206}}}},
{Region: "Montenegro", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "montenegro401", IPs: []net.IP{{45, 131, 208, 250}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "montenegro401", IPs: []net.IP{{45, 131, 208, 249}}}},
{Region: "Morocco", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "morocco401", IPs: []net.IP{{45, 131, 211, 233}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "morocco401", IPs: []net.IP{{45, 131, 211, 243}}}},
{Region: "Netherlands", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "amsterdam402", IPs: []net.IP{{212, 102, 34, 144}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "amsterdam402", IPs: []net.IP{{212, 102, 35, 84}}}},
{Region: "New Zealand", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newzealand404", IPs: []net.IP{{43, 250, 207, 25}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newzealand403", IPs: []net.IP{{43, 250, 207, 94}}}},
{Region: "Nigeria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "nigeria401", IPs: []net.IP{{45, 137, 196, 243}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "nigeria402", IPs: []net.IP{{45, 137, 196, 217}}}},
{Region: "Norway", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "oslo402", IPs: []net.IP{{46, 246, 122, 75}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "oslo402", IPs: []net.IP{{46, 246, 122, 88}}}},
{Region: "Panama", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "panama402", IPs: []net.IP{{45, 131, 210, 220}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "panama402", IPs: []net.IP{{45, 131, 210, 220}}}},
{Region: "Philippines", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "philippines402", IPs: []net.IP{{188, 214, 125, 153}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "philippines401", IPs: []net.IP{{188, 214, 125, 136}}}},
{Region: "Poland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "warsaw406", IPs: []net.IP{{194, 110, 114, 74}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "warsaw406", IPs: []net.IP{{194, 110, 114, 68}}}},
{Region: "Portugal", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "lisbon403", IPs: []net.IP{{89, 26, 241, 132}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "lisbon403", IPs: []net.IP{{89, 26, 241, 132}}}},
{Region: "Qatar", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "qatar402", IPs: []net.IP{{45, 131, 7, 208}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "qatar402", IPs: []net.IP{{45, 131, 7, 210}}}},
{Region: "Romania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "romania407", IPs: []net.IP{{143, 244, 54, 136}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "romania407", IPs: []net.IP{{143, 244, 54, 170}}}},
{Region: "Saudi Arabia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "saudiarabia401", IPs: []net.IP{{45, 131, 6, 236}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "saudiarabia401", IPs: []net.IP{{45, 131, 6, 236}}}},
{Region: "Serbia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "belgrade401", IPs: []net.IP{{37, 120, 193, 246}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "belgrade401", IPs: []net.IP{{37, 120, 193, 254}}}},
{Region: "Singapore", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "singapore401", IPs: []net.IP{{156, 146, 57, 177}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "singapore401", IPs: []net.IP{{156, 146, 57, 223}}}},
{Region: "Slovakia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "bratislava402", IPs: []net.IP{{37, 120, 221, 216}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "bratislava402", IPs: []net.IP{{37, 120, 221, 218}}}},
{Region: "South Africa", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "johannesburg401", IPs: []net.IP{{154, 16, 93, 39}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "johannesburg401", IPs: []net.IP{{154, 16, 93, 45}}}},
{Region: "Spain", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "madrid402", IPs: []net.IP{{212, 102, 49, 25}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "madrid402", IPs: []net.IP{{212, 102, 49, 17}}}},
{Region: "Sri Lanka", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "srilanka402", IPs: []net.IP{{45, 132, 136, 210}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "srilanka402", IPs: []net.IP{{45, 132, 136, 214}}}},
{Region: "Sweden", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "stockholm403", IPs: []net.IP{{195, 246, 120, 94}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "stockholm401", IPs: []net.IP{{195, 246, 120, 14}}}},
{Region: "Switzerland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "zurich404", IPs: []net.IP{{212, 102, 37, 73}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "zurich408", IPs: []net.IP{{212, 102, 37, 6}}}},
{Region: "Taiwan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "taiwan402", IPs: []net.IP{{188, 214, 106, 93}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "taiwan402", IPs: []net.IP{{188, 214, 106, 94}}}},
{Region: "Turkey", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "istanbul401", IPs: []net.IP{{188, 213, 34, 70}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "istanbul401", IPs: []net.IP{{188, 213, 34, 73}}}},
{Region: "UK London", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "london402", IPs: []net.IP{{212, 102, 63, 137}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "london402", IPs: []net.IP{{212, 102, 63, 154}}}},
{Region: "UK Manchester", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "manchester410", IPs: []net.IP{{194, 37, 96, 40}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "manchester410", IPs: []net.IP{{194, 37, 96, 43}}}},
{Region: "UK Southampton", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "southampton403", IPs: []net.IP{{143, 244, 37, 113}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "southampton403", IPs: []net.IP{{143, 244, 37, 99}}}},
{Region: "US Atlanta", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "atlanta424", IPs: []net.IP{{154, 21, 21, 193}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "atlanta424", IPs: []net.IP{{154, 21, 21, 175}}}},
{Region: "US California", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "losangeles409", IPs: []net.IP{{143, 244, 49, 144}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "losangeles409", IPs: []net.IP{{143, 244, 49, 186}}}},
{Region: "US Chicago", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "chicago410", IPs: []net.IP{{154, 21, 28, 239}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "chicago410", IPs: []net.IP{{154, 21, 28, 247}}}},
{Region: "US Denver", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "denver404", IPs: []net.IP{{70, 39, 111, 204}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "denver404", IPs: []net.IP{{70, 39, 111, 236}}}},
{Region: "US East", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newjersey405", IPs: []net.IP{{37, 235, 104, 13}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newjersey405", IPs: []net.IP{{37, 235, 104, 17}}}},
{Region: "US Florida", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "miami403", IPs: []net.IP{{37, 235, 98, 3}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "miami403", IPs: []net.IP{{37, 235, 98, 64}}}},
{Region: "US Houston", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "houston406", IPs: []net.IP{{205, 251, 138, 134}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "houston406", IPs: []net.IP{{205, 251, 138, 137}}}},
{Region: "US Las Vegas", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "lasvegas406", IPs: []net.IP{{82, 102, 31, 187}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "lasvegas406", IPs: []net.IP{{82, 102, 31, 180}}}},
{Region: "US New York", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newyork420", IPs: []net.IP{{138, 199, 10, 115}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newyork418", IPs: []net.IP{{156, 146, 58, 217}}}},
{Region: "US Seattle", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "seattle413", IPs: []net.IP{{154, 21, 20, 44}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "seattle413", IPs: []net.IP{{154, 21, 20, 55}}}},
{Region: "US Silicon Valley", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "siliconvalley407", IPs: []net.IP{{154, 21, 212, 209}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "siliconvalley407", IPs: []net.IP{{154, 21, 212, 197}}}},
{Region: "US Texas", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dallas418", IPs: []net.IP{{154, 3, 250, 180}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dallas418", IPs: []net.IP{{154, 3, 250, 179}}}},
{Region: "US Washington DC", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "washington420", IPs: []net.IP{{70, 32, 6, 69}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "washington420", IPs: []net.IP{{70, 32, 6, 68}}}},
{Region: "US West", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "phoenix407", IPs: []net.IP{{184, 170, 241, 93}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "phoenix407", IPs: []net.IP{{184, 170, 241, 69}}}},
{Region: "Ukraine", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "kiev401", IPs: []net.IP{{62, 149, 20, 61}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "kiev401", IPs: []net.IP{{62, 149, 20, 58}}}},
{Region: "United Arab Emirates", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dubai403", IPs: []net.IP{{217, 138, 193, 156}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dubai403", IPs: []net.IP{{217, 138, 193, 148}}}},
{Region: "Venezuela", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "venezuela401", IPs: []net.IP{{45, 133, 89, 239}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "venezuela401", IPs: []net.IP{{45, 133, 89, 246}}}},
{Region: "Vietnam", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vietnam402", IPs: []net.IP{{188, 214, 152, 83}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vietnam402", IPs: []net.IP{{188, 214, 152, 83}}}},
{Region: "AU Melbourne", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "melbourne404", IPs: []net.IP{{103, 2, 198, 93}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "melbourne404", IPs: []net.IP{{103, 2, 198, 85}}}},
{Region: "AU Perth", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "perth403", IPs: []net.IP{{43, 250, 205, 156}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "perth403", IPs: []net.IP{{43, 250, 205, 153}}}},
{Region: "AU Sydney", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "sydney408", IPs: []net.IP{{117, 120, 10, 56}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "sydney408", IPs: []net.IP{{117, 120, 10, 48}}}},
{Region: "Albania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "tirana402", IPs: []net.IP{{31, 171, 154, 115}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "tirana402", IPs: []net.IP{{31, 171, 154, 118}}}},
{Region: "Algeria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "algiers404", IPs: []net.IP{{176, 125, 228, 19}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "algiers404", IPs: []net.IP{{176, 125, 228, 26}}}},
{Region: "Andorra", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "andorra405", IPs: []net.IP{{188, 241, 82, 40}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "andorra405", IPs: []net.IP{{188, 241, 82, 37}}}},
{Region: "Argentina", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "buenosaires401", IPs: []net.IP{{190, 106, 134, 95}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "buenosaires401", IPs: []net.IP{{190, 106, 134, 85}}}},
{Region: "Armenia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "armenia403", IPs: []net.IP{{185, 253, 160, 3}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "armenia403", IPs: []net.IP{{185, 253, 160, 9}}}},
{Region: "Austria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vienna403", IPs: []net.IP{{156, 146, 60, 108}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vienna403", IPs: []net.IP{{156, 146, 60, 110}}}},
{Region: "Bahamas", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "bahamas404", IPs: []net.IP{{95, 181, 238, 9}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "bahamas404", IPs: []net.IP{{95, 181, 238, 9}}}},
{Region: "Bangladesh", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "bangladesh403", IPs: []net.IP{{84, 252, 93, 7}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "bangladesh403", IPs: []net.IP{{84, 252, 93, 7}}}},
{Region: "Belgium", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "brussels404", IPs: []net.IP{{37, 120, 143, 156}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "brussels404", IPs: []net.IP{{37, 120, 143, 156}}}},
{Region: "Bosnia and Herzegovina", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "sarajevo402", IPs: []net.IP{{185, 212, 111, 4}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "sarajevo402", IPs: []net.IP{{185, 212, 111, 71}}}},
{Region: "Brazil", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "saopaolo401", IPs: []net.IP{{45, 133, 180, 236}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "saopaolo401", IPs: []net.IP{{45, 133, 180, 227}}}},
{Region: "Bulgaria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "sofia402", IPs: []net.IP{{217, 138, 221, 89}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "sofia402", IPs: []net.IP{{217, 138, 221, 89}}}},
{Region: "CA Montreal", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "montreal410", IPs: []net.IP{{199, 36, 223, 210}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "montreal410", IPs: []net.IP{{199, 36, 223, 210}}}},
{Region: "CA Ontario", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "ontario408", IPs: []net.IP{{172, 98, 92, 87}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "ontario408", IPs: []net.IP{{172, 98, 92, 80}}}},
{Region: "CA Toronto", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "toronto402", IPs: []net.IP{{66, 115, 142, 58}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "toronto402", IPs: []net.IP{{66, 115, 142, 58}}}},
{Region: "CA Vancouver", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vancouver411", IPs: []net.IP{{208, 78, 42, 168}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vancouver411", IPs: []net.IP{{208, 78, 42, 161}}}},
{Region: "Cambodia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cambodia401", IPs: []net.IP{{188, 215, 235, 109}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cambodia401", IPs: []net.IP{{188, 215, 235, 110}}}},
{Region: "China", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "china404", IPs: []net.IP{{188, 241, 80, 9}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "china404", IPs: []net.IP{{188, 241, 80, 4}}}},
{Region: "Cyprus", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cyprus403", IPs: []net.IP{{185, 253, 162, 8}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cyprus403", IPs: []net.IP{{185, 253, 162, 14}}}},
{Region: "Czech Republic", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "prague405", IPs: []net.IP{{143, 244, 59, 168}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "prague405", IPs: []net.IP{{143, 244, 59, 154}}}},
{Region: "DE Berlin", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "berlin425", IPs: []net.IP{{154, 13, 1, 148}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "berlin425", IPs: []net.IP{{154, 13, 1, 146}}}},
{Region: "DE Frankfurt", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "frankfurt440", IPs: []net.IP{{185, 216, 33, 165}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "frankfurt440", IPs: []net.IP{{185, 216, 33, 166}}}},
{Region: "Denmark", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "copenhagen404", IPs: []net.IP{{188, 126, 94, 190}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "copenhagen404", IPs: []net.IP{{188, 126, 94, 165}}}},
{Region: "Egypt", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "cairo402", IPs: []net.IP{{188, 214, 122, 126}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "cairo402", IPs: []net.IP{{188, 214, 122, 123}}}},
{Region: "Estonia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "talinn402", IPs: []net.IP{{95, 153, 31, 68}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "talinn402", IPs: []net.IP{{95, 153, 31, 78}}}},
{Region: "Finland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "helsinki402", IPs: []net.IP{{188, 126, 89, 45}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "helsinki402", IPs: []net.IP{{188, 126, 89, 35}}}},
{Region: "France", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "paris406", IPs: []net.IP{{143, 244, 57, 169}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "paris406", IPs: []net.IP{{143, 244, 57, 169}}}},
{Region: "Georgia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "georgia403", IPs: []net.IP{{95, 181, 236, 8}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "georgia403", IPs: []net.IP{{95, 181, 236, 10}}}},
{Region: "Greece", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "athens401", IPs: []net.IP{{154, 57, 3, 87}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "athens401", IPs: []net.IP{{154, 57, 3, 85}}}},
{Region: "Greenland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "greenland404", IPs: []net.IP{{91, 90, 120, 149}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "greenland404", IPs: []net.IP{{91, 90, 120, 147}}}},
{Region: "Hong Kong", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "china403", IPs: []net.IP{{86, 107, 104, 213}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "china403", IPs: []net.IP{{86, 107, 104, 219}}}},
{Region: "Hungary", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "budapest402", IPs: []net.IP{{86, 106, 74, 115}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "budapest402", IPs: []net.IP{{86, 106, 74, 117}}}},
{Region: "Iceland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "reykjavik402", IPs: []net.IP{{45, 133, 193, 83}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "reykjavik402", IPs: []net.IP{{45, 133, 193, 88}}}},
{Region: "India", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "mumbai402", IPs: []net.IP{{45, 120, 139, 138}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "mumbai402", IPs: []net.IP{{45, 120, 139, 127}}}},
{Region: "Ireland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dublin411", IPs: []net.IP{{188, 241, 178, 23}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dublin411", IPs: []net.IP{{188, 241, 178, 30}}}},
{Region: "Isle of Man", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "douglas403", IPs: []net.IP{{91, 90, 124, 7}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "douglas403", IPs: []net.IP{{91, 90, 124, 18}}}},
{Region: "Israel", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "jerusalem407", IPs: []net.IP{{185, 77, 248, 91}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "jerusalem407", IPs: []net.IP{{185, 77, 248, 91}}}},
{Region: "Italy", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "milano403", IPs: []net.IP{{156, 146, 41, 74}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "milano403", IPs: []net.IP{{156, 146, 41, 84}}}},
{Region: "Japan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "tokyo401", IPs: []net.IP{{156, 146, 34, 130}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "tokyo401", IPs: []net.IP{{156, 146, 34, 130}}}},
{Region: "Kazakhstan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "kazakhstan403", IPs: []net.IP{{62, 133, 47, 13}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "kazakhstan403", IPs: []net.IP{{62, 133, 47, 5}}}},
{Region: "Latvia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "riga401", IPs: []net.IP{{109, 248, 149, 5}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "riga401", IPs: []net.IP{{109, 248, 149, 8}}}},
{Region: "Liechtenstein", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "liechtenstein403", IPs: []net.IP{{91, 90, 122, 7}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "liechtenstein403", IPs: []net.IP{{91, 90, 122, 18}}}},
{Region: "Lithuania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vilnius403", IPs: []net.IP{{85, 206, 165, 118}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vilnius403", IPs: []net.IP{{85, 206, 165, 116}}}},
{Region: "Luxembourg", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "luxembourg407", IPs: []net.IP{{5, 253, 204, 150}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "luxembourg407", IPs: []net.IP{{5, 253, 204, 155}}}},
{Region: "Macao", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "macau403", IPs: []net.IP{{84, 252, 92, 6}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "macau403", IPs: []net.IP{{84, 252, 92, 15}}}},
{Region: "Macedonia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "macedonia401", IPs: []net.IP{{185, 225, 28, 120}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "macedonia401", IPs: []net.IP{{185, 225, 28, 120}}}},
{Region: "Malta", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "malta403", IPs: []net.IP{{176, 125, 230, 13}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "malta403", IPs: []net.IP{{176, 125, 230, 7}}}},
{Region: "Mexico", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "mexico403", IPs: []net.IP{{77, 81, 142, 14}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "mexico403", IPs: []net.IP{{77, 81, 142, 13}}}},
{Region: "Moldova", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "chisinau401", IPs: []net.IP{{178, 175, 129, 44}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "chisinau401", IPs: []net.IP{{178, 175, 129, 46}}}},
{Region: "Monaco", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "monaco403", IPs: []net.IP{{95, 181, 233, 8}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "monaco403", IPs: []net.IP{{95, 181, 233, 5}}}},
{Region: "Mongolia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "mongolia403", IPs: []net.IP{{185, 253, 163, 15}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "mongolia403", IPs: []net.IP{{185, 253, 163, 5}}}},
{Region: "Montenegro", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "montenegro403", IPs: []net.IP{{176, 125, 229, 14}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "montenegro403", IPs: []net.IP{{176, 125, 229, 4}}}},
{Region: "Morocco", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "morocco403", IPs: []net.IP{{95, 181, 232, 4}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "morocco403", IPs: []net.IP{{95, 181, 232, 8}}}},
{Region: "Netherlands", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "amsterdam412", IPs: []net.IP{{143, 244, 41, 196}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "amsterdam412", IPs: []net.IP{{143, 244, 41, 196}}}},
{Region: "New Zealand", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newzealand403", IPs: []net.IP{{43, 250, 207, 90}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newzealand403", IPs: []net.IP{{43, 250, 207, 84}}}},
{Region: "Nigeria", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "nigeria403", IPs: []net.IP{{102, 165, 25, 86}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "nigeria403", IPs: []net.IP{{102, 165, 25, 85}}}},
{Region: "Norway", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "oslo401", IPs: []net.IP{{46, 246, 122, 37}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "oslo401", IPs: []net.IP{{46, 246, 122, 60}}}},
{Region: "Panama", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "panama404", IPs: []net.IP{{91, 90, 126, 25}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "panama404", IPs: []net.IP{{91, 90, 126, 28}}}},
{Region: "Philippines", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "philippines401", IPs: []net.IP{{188, 214, 125, 140}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "philippines401", IPs: []net.IP{{188, 214, 125, 137}}}},
{Region: "Poland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "warsaw409", IPs: []net.IP{{194, 110, 114, 119}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "warsaw409", IPs: []net.IP{{194, 110, 114, 118}}}},
{Region: "Portugal", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "lisbon402", IPs: []net.IP{{89, 26, 241, 87}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "lisbon402", IPs: []net.IP{{89, 26, 241, 88}}}},
{Region: "Qatar", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "qatar403", IPs: []net.IP{{95, 181, 234, 9}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "qatar403", IPs: []net.IP{{95, 181, 234, 8}}}},
{Region: "Romania", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "romania408", IPs: []net.IP{{143, 244, 54, 117}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "romania408", IPs: []net.IP{{143, 244, 54, 116}}}},
{Region: "Saudi Arabia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "saudiarabia403", IPs: []net.IP{{95, 181, 235, 8}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "saudiarabia403", IPs: []net.IP{{95, 181, 235, 4}}}},
{Region: "Serbia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "belgrade401", IPs: []net.IP{{37, 120, 193, 250}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "belgrade401", IPs: []net.IP{{37, 120, 193, 249}}}},
{Region: "Singapore", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "singapore401", IPs: []net.IP{{156, 146, 57, 213}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "singapore401", IPs: []net.IP{{156, 146, 57, 197}}}},
{Region: "Slovakia", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "bratislava402", IPs: []net.IP{{37, 120, 221, 213}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "bratislava402", IPs: []net.IP{{37, 120, 221, 218}}}},
{Region: "South Africa", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "johannesburg401", IPs: []net.IP{{154, 16, 93, 33}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "johannesburg401", IPs: []net.IP{{154, 16, 93, 46}}}},
{Region: "Spain", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "madrid401", IPs: []net.IP{{212, 102, 49, 68}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "madrid401", IPs: []net.IP{{212, 102, 49, 68}}}},
{Region: "Sri Lanka", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "srilanka403", IPs: []net.IP{{95, 181, 239, 8}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "srilanka403", IPs: []net.IP{{95, 181, 239, 13}}}},
{Region: "Sweden", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "stockholm401", IPs: []net.IP{{195, 246, 120, 4}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "stockholm401", IPs: []net.IP{{195, 246, 120, 39}}}},
{Region: "Switzerland", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "zurich407", IPs: []net.IP{{156, 146, 62, 194}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "zurich407", IPs: []net.IP{{156, 146, 62, 194}}}},
{Region: "Taiwan", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "taiwan401", IPs: []net.IP{{188, 214, 106, 74}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "taiwan401", IPs: []net.IP{{188, 214, 106, 69}}}},
{Region: "Turkey", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "istanbul402", IPs: []net.IP{{188, 213, 34, 88}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "istanbul402", IPs: []net.IP{{188, 213, 34, 83}}}},
{Region: "UK London", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "london405", IPs: []net.IP{{212, 102, 53, 15}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "london405", IPs: []net.IP{{212, 102, 53, 60}}}},
{Region: "UK London-2", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "london420", IPs: []net.IP{{37, 235, 96, 200}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "london420", IPs: []net.IP{{37, 235, 96, 206}}}},
{Region: "UK Manchester", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "manchester414", IPs: []net.IP{{194, 37, 96, 194}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "manchester414", IPs: []net.IP{{194, 37, 96, 197}}}},
{Region: "UK Southampton", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "southampton401", IPs: []net.IP{{143, 244, 37, 244}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "southampton401", IPs: []net.IP{{143, 244, 37, 194}}}},
{Region: "US Atlanta", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "atlanta417", IPs: []net.IP{{154, 21, 22, 216}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "atlanta417", IPs: []net.IP{{154, 21, 22, 216}}}},
{Region: "US California", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "losangeles401", IPs: []net.IP{{143, 244, 48, 15}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "losangeles401", IPs: []net.IP{{143, 244, 48, 17}}}},
{Region: "US Chicago", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "chicago413", IPs: []net.IP{{154, 21, 23, 125}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "chicago413", IPs: []net.IP{{154, 21, 23, 137}}}},
{Region: "US Denver", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "denver410", IPs: []net.IP{{174, 128, 227, 24}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "denver410", IPs: []net.IP{{174, 128, 227, 24}}}},
{Region: "US East", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newjersey405", IPs: []net.IP{{143, 244, 46, 65}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newjersey405", IPs: []net.IP{{143, 244, 46, 115}}}},
{Region: "US Florida", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "miami407", IPs: []net.IP{{156, 146, 42, 78}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "miami407", IPs: []net.IP{{156, 146, 42, 113}}}},
{Region: "US Houston", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "houston413", IPs: []net.IP{{205, 251, 142, 20}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "houston413", IPs: []net.IP{{205, 251, 142, 20}}}},
{Region: "US Las Vegas", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "lasvegas426", IPs: []net.IP{{196, 53, 64, 175}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "lasvegas426", IPs: []net.IP{{196, 53, 64, 189}}}},
{Region: "US New York", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "newyork404", IPs: []net.IP{{143, 244, 44, 227}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "newyork404", IPs: []net.IP{{143, 244, 44, 208}}}},
{Region: "US Seattle", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "seattle422", IPs: []net.IP{{156, 146, 48, 204}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "seattle422", IPs: []net.IP{{156, 146, 48, 239}}}},
{Region: "US Silicon Valley", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "siliconvalley420", IPs: []net.IP{{66, 115, 165, 93}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "siliconvalley420", IPs: []net.IP{{66, 115, 165, 80}}}},
{Region: "US Texas", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dallas415", IPs: []net.IP{{154, 3, 250, 16}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dallas415", IPs: []net.IP{{154, 3, 250, 24}}}},
{Region: "US Washington DC", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "washington452", IPs: []net.IP{{91, 149, 244, 110}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "washington452", IPs: []net.IP{{91, 149, 244, 114}}}},
{Region: "US West", PortForward: false, OpenvpnUDP: models.PIAServerOpenvpn{CN: "phoenix413", IPs: []net.IP{{184, 170, 241, 169}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "phoenix413", IPs: []net.IP{{184, 170, 241, 169}}}},
{Region: "Ukraine", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "kiev403", IPs: []net.IP{{62, 149, 20, 7}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "kiev403", IPs: []net.IP{{62, 149, 20, 4}}}},
{Region: "United Arab Emirates", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "dubai403", IPs: []net.IP{{217, 138, 193, 153}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "dubai403", IPs: []net.IP{{217, 138, 193, 150}}}},
{Region: "Venezuela", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "venezuela403", IPs: []net.IP{{95, 181, 237, 3}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "venezuela403", IPs: []net.IP{{95, 181, 237, 9}}}},
{Region: "Vietnam", PortForward: true, OpenvpnUDP: models.PIAServerOpenvpn{CN: "vietnam401", IPs: []net.IP{{188, 214, 152, 67}}}, OpenvpnTCP: models.PIAServerOpenvpn{CN: "vietnam401", IPs: []net.IP{{188, 214, 152, 67}}}},
}
}

View File

@@ -20,7 +20,6 @@ func PrivadoHostnameChoices() (choices []string) {
return choices
}
//nolint:gomnd
func PrivadoServers() []models.PrivadoServer {
return []models.PrivadoServer{
{Hostname: "akl-001.vpn.privado.io", IP: net.IP{23, 254, 104, 114}},

View File

@@ -44,155 +44,46 @@ func PurevpnCityChoices() (choices []string) {
//nolint:lll
func PurevpnServers() []models.PurevpnServer {
return []models.PurevpnServer{
{Region: "Africa", Country: "Algeria", City: "Algiers", IPs: []net.IP{{172, 94, 64, 2}}},
{Region: "Africa", Country: "Angola", City: "Benguela", IPs: []net.IP{{45, 115, 26, 2}}},
{Region: "Africa", Country: "Cape Verde", City: "Praia", IPs: []net.IP{{45, 74, 25, 2}}},
{Region: "Africa", Country: "Egypt", City: "Cairo", IPs: []net.IP{{192, 198, 120, 122}}},
{Region: "Africa", Country: "Ethiopia", City: "Addis Ababa", IPs: []net.IP{{104, 250, 178, 4}}},
{Region: "Africa", Country: "Ghana", City: "Accra", IPs: []net.IP{{196, 251, 67, 4}}},
{Region: "Africa", Country: "Kenya", City: "Mombasa", IPs: []net.IP{{102, 135, 0, 2}}},
{Region: "Africa", Country: "Madagascar", City: "Antananarivo", IPs: []net.IP{{206, 123, 156, 131}}},
{Region: "Africa", Country: "Mauritania", City: "Nouakchott", IPs: []net.IP{{206, 123, 158, 63}}},
{Region: "Africa", Country: "Mauritius", City: "Port Louis", IPs: []net.IP{{104, 250, 181, 4}}},
{Region: "Africa", Country: "Morocco", City: "Rabat", IPs: []net.IP{{104, 243, 250, 126}}},
{Region: "Africa", Country: "Niger", City: "Niamey", IPs: []net.IP{{206, 123, 157, 131}}},
{Region: "Africa", Country: "Nigeria", City: "Suleja", IPs: []net.IP{{102, 165, 25, 38}}},
{Region: "Africa", Country: "Senegal", City: "Dakar", IPs: []net.IP{{206, 123, 158, 131}}},
{Region: "Africa", Country: "Seychelles", City: "Victoria", IPs: []net.IP{{172, 111, 128, 126}}},
{Region: "Africa", Country: "South Africa", City: "Johannesburg", IPs: []net.IP{{45, 74, 45, 2}}},
{Region: "Africa", Country: "Tanzania", City: "Dar Es Salaam", IPs: []net.IP{{102, 135, 0, 2}}},
{Region: "Africa", Country: "Tunisia", City: "Tunis", IPs: []net.IP{{206, 123, 159, 4}}},
{Region: "Asia", Country: "Afghanistan", City: "Kabul", IPs: []net.IP{{172, 111, 208, 2}}},
{Region: "Asia", Country: "Armenia", City: "Singapore", IPs: []net.IP{{37, 120, 208, 147}}},
{Region: "Asia", Country: "Azerbaijan", City: "Baku", IPs: []net.IP{{104, 250, 177, 4}}},
{Region: "Asia", Country: "Bangladesh", City: "Dhaka", IPs: []net.IP{{206, 123, 154, 190}}},
{Region: "Asia", Country: "Brunei Darussalam", City: "Bandar Seri Begawan", IPs: []net.IP{{36, 255, 98, 2}}},
{Region: "Asia", Country: "Cambodia", City: "Phnom Penh", IPs: []net.IP{{104, 250, 176, 122}}},
{Region: "Asia", Country: "India", City: "Chennai", IPs: []net.IP{{129, 227, 107, 242}}},
{Region: "Asia", Country: "Indonesia", City: "Jakarta", IPs: []net.IP{{103, 55, 9, 2}}},
{Region: "Asia", Country: "Japan", City: "Tokyo", IPs: []net.IP{{172, 94, 56, 2}}},
{Region: "Asia", Country: "Kazakhstan", City: "Almaty", IPs: []net.IP{{206, 123, 152, 4}}},
{Region: "Asia", Country: "Korea, South", City: "Seoul", IPs: []net.IP{{45, 115, 25, 1}}},
{Region: "Asia", Country: "Kyrgyzstan", City: "Bishkek", IPs: []net.IP{{206, 123, 151, 131}}},
{Region: "Asia", Country: "Laos", City: "Vientiane", IPs: []net.IP{{206, 123, 153, 4}}},
{Region: "Asia", Country: "Macao", City: "Beyrouth", IPs: []net.IP{{104, 243, 240, 121}}},
{Region: "Asia", Country: "Malaysia", City: "Johor Baharu", IPs: []net.IP{{103, 28, 90, 54}, {103, 28, 90, 55}, {103, 28, 90, 71}, {103, 28, 90, 72}, {103, 117, 20, 21}, {103, 117, 20, 163}, {103, 117, 20, 164}, {103, 117, 20, 201}}},
{Region: "Asia", Country: "Malaysia", City: "Kuala Lumpur", IPs: []net.IP{{104, 250, 160, 4}}},
{Region: "Asia", Country: "Mongolia", City: "Ulaanbaatar", IPs: []net.IP{{206, 123, 153, 131}}},
{Region: "Asia", Country: "Pakistan", City: "Islamabad", IPs: []net.IP{{104, 250, 187, 3}}},
{Region: "Asia", Country: "Papua New Guinea", City: "Port Moresby", IPs: []net.IP{{206, 123, 155, 131}}},
{Region: "Asia", Country: "Philippines", City: "Manila", IPs: []net.IP{{129, 227, 119, 84}}},
{Region: "Asia", Country: "Sri Lanka", City: "Colombo", IPs: []net.IP{{206, 123, 154, 4}}},
{Region: "Asia", Country: "Taiwan", City: "Taipei", IPs: []net.IP{{128, 1, 155, 178}}},
{Region: "Asia", Country: "Tajikistan", City: "Dushanbe", IPs: []net.IP{{206, 123, 151, 4}}},
{Region: "Asia", Country: "Thailand", City: "Bangkok", IPs: []net.IP{{104, 37, 6, 4}}},
{Region: "Asia", Country: "Turkey", City: "Istanbul", IPs: []net.IP{{185, 220, 58, 3}}},
{Region: "Asia", Country: "Turkmenistan", City: "Ashgabat", IPs: []net.IP{{206, 123, 152, 131}}},
{Region: "Asia", Country: "Uzbekistan", City: "Tashkent", IPs: []net.IP{{206, 123, 150, 131}}},
{Region: "Asia", Country: "Vietnam", City: "Hanoi", IPs: []net.IP{{192, 253, 249, 132}}},
{Region: "Europe", Country: "Albania", City: "Tirane", IPs: []net.IP{{46, 243, 224, 2}}},
{Region: "Europe", Country: "Armenia", City: "Yerevan", IPs: []net.IP{{172, 94, 35, 4}}},
{Region: "Europe", Country: "Austria", City: "Vienna", IPs: []net.IP{{172, 94, 109, 2}}},
{Region: "Europe", Country: "Belgium", City: "Brussels", IPs: []net.IP{{185, 210, 217, 147}}},
{Region: "Europe", Country: "Bosnia and Herzegovina", City: "Sarajevo", IPs: []net.IP{{104, 250, 169, 122}}},
{Region: "Europe", Country: "Bulgaria", City: "Sofia", IPs: []net.IP{{217, 138, 221, 114}}},
{Region: "Europe", Country: "Croatia", City: "Zagreb", IPs: []net.IP{{104, 250, 163, 2}}},
{Region: "Europe", Country: "Cyprus", City: "Nicosia", IPs: []net.IP{{188, 72, 119, 4}}},
{Region: "Europe", Country: "Denmark", City: "Copenhagen", IPs: []net.IP{{89, 45, 7, 5}}},
{Region: "Europe", Country: "Estonia", City: "Tallinn", IPs: []net.IP{{185, 166, 87, 2}, {188, 72, 111, 4}}},
{Region: "Europe", Country: "France", City: "Paris", IPs: []net.IP{{172, 94, 53, 2}, {172, 111, 219, 2}}},
{Region: "Europe", Country: "Georgia", City: "Tbilisi", IPs: []net.IP{{141, 101, 156, 2}}},
{Region: "Europe", Country: "Germany", City: "Frankfurt", IPs: []net.IP{{172, 94, 8, 4}}},
{Region: "Europe", Country: "Germany", City: "Munich", IPs: []net.IP{{172, 94, 8, 4}}},
{Region: "Europe", Country: "Germany", City: "Nuremberg", IPs: []net.IP{{172, 94, 125, 2}}},
{Region: "Europe", Country: "Greece", City: "Thessaloniki", IPs: []net.IP{{172, 94, 109, 2}}},
{Region: "Europe", Country: "Hungary", City: "Budapest", IPs: []net.IP{{172, 111, 129, 2}, {188, 72, 125, 126}}},
{Region: "Europe", Country: "Iceland", City: "Reykjavik", IPs: []net.IP{{192, 253, 250, 1}}},
{Region: "Europe", Country: "Ireland", City: "Dublin", IPs: []net.IP{{185, 210, 217, 147}}},
{Region: "Europe", Country: "Isle of Man", City: "Onchan", IPs: []net.IP{{46, 243, 144, 2}}},
{Region: "Europe", Country: "Italy", City: "Milano", IPs: []net.IP{{45, 9, 251, 2}}},
{Region: "Europe", Country: "Latvia", City: "RIGA", IPs: []net.IP{{185, 118, 76, 5}}},
{Region: "Europe", Country: "Liechtenstein", City: "Vaduz", IPs: []net.IP{{104, 250, 164, 4}}},
{Region: "Europe", Country: "Lithuania", City: "Vilnius", IPs: []net.IP{{188, 72, 116, 3}}},
{Region: "Europe", Country: "Luxembourg", City: "Luxembourg", IPs: []net.IP{{188, 72, 114, 2}}},
{Region: "Europe", Country: "Malta", City: "Sliema", IPs: []net.IP{{46, 243, 241, 4}}},
{Region: "Europe", Country: "Monaco", City: "Monaco", IPs: []net.IP{{104, 250, 168, 132}}},
{Region: "Europe", Country: "Montenegro", City: "Podgorica", IPs: []net.IP{{104, 250, 165, 121}}},
{Region: "Europe", Country: "Netherlands", City: "Amsterdam", IPs: []net.IP{{92, 119, 179, 195}}},
{Region: "Europe", Country: "Norway", City: "Oslo", IPs: []net.IP{{82, 102, 22, 211}}},
{Region: "Europe", Country: "Poland", City: "Warsaw", IPs: []net.IP{{5, 253, 206, 251}}},
{Region: "Europe", Country: "Portugal", City: "Lisbon", IPs: []net.IP{{45, 74, 10, 1}}},
{Region: "Europe", Country: "Romania", City: "Bucharest", IPs: []net.IP{{192, 253, 253, 2}}},
{Region: "Europe", Country: "Serbia", City: "Niš", IPs: []net.IP{{104, 250, 166, 2}}},
{Region: "Europe", Country: "Slovakia", City: "Bratislava", IPs: []net.IP{{188, 72, 112, 3}}},
{Region: "Europe", Country: "Slovenia", City: "Ljubljana", IPs: []net.IP{{104, 243, 246, 129}}},
{Region: "Europe", Country: "Spain", City: "Barcelona", IPs: []net.IP{{185, 230, 124, 147}}},
{Region: "Europe", Country: "Sweden", City: "Stockholm", IPs: []net.IP{{45, 74, 46, 2}}},
{Region: "Europe", Country: "Switzerland", City: "Zurich", IPs: []net.IP{{172, 111, 217, 2}}},
{Region: "Europe", Country: "United Kingdom", City: "Gosport", IPs: []net.IP{{45, 74, 0, 2}, {45, 74, 62, 2}}},
{Region: "Europe", Country: "United Kingdom", City: "London", IPs: []net.IP{{45, 74, 0, 2}, {45, 74, 62, 2}}},
{Region: "Europe", Country: "United Kingdom", City: "Maidenhead", IPs: []net.IP{{172, 111, 183, 2}}},
{Region: "Europe", Country: "United Kingdom", City: "Manchester", IPs: []net.IP{{172, 111, 183, 2}}},
{Region: "Middle East", Country: "Bahrain", City: "Manama", IPs: []net.IP{{46, 243, 150, 4}}},
{Region: "Middle East", Country: "Jordan", City: "Amman", IPs: []net.IP{{172, 111, 152, 3}}},
{Region: "Middle East", Country: "Kuwait", City: "Kuwait", IPs: []net.IP{{206, 123, 146, 4}}},
{Region: "Middle East", Country: "Oman", City: "Salalah", IPs: []net.IP{{46, 243, 148, 125}}},
{Region: "Middle East", Country: "Qatar", City: "Doha", IPs: []net.IP{{46, 243, 147, 2}}},
{Region: "Middle East", Country: "Saudi Arabia", City: "Jeddah", IPs: []net.IP{{45, 74, 1, 4}}},
{Region: "Middle East", Country: "United Arab Emirates", City: "Dubai", IPs: []net.IP{{104, 37, 6, 4}}},
{Region: "North America", Country: "Aruba", City: "Oranjestad", IPs: []net.IP{{104, 243, 246, 129}}},
{Region: "North America", Country: "Barbados", City: "Bridgetown", IPs: []net.IP{{172, 94, 97, 2}}},
{Region: "North America", Country: "Belize", City: "Belmopan", IPs: []net.IP{{104, 243, 241, 4}}},
{Region: "North America", Country: "Bermuda", City: "Hamilton", IPs: []net.IP{{172, 94, 76, 2}}},
{Region: "North America", Country: "Canada", City: "Montreal", IPs: []net.IP{{172, 94, 7, 2}}},
{Region: "North America", Country: "Canada", City: "Toronto", IPs: []net.IP{{172, 94, 7, 2}}},
{Region: "North America", Country: "Canada", City: "Vancouver", IPs: []net.IP{{107, 181, 177, 42}}},
{Region: "North America", Country: "Cayman Islands", City: "George Town", IPs: []net.IP{{172, 94, 113, 2}}},
{Region: "North America", Country: "Costa Rica", City: "San Jose", IPs: []net.IP{{104, 243, 245, 1}}},
{Region: "North America", Country: "Dominica", City: "Roseau", IPs: []net.IP{{45, 74, 22, 2}}},
{Region: "North America", Country: "Dominican Republic", City: "Santo Domingo", IPs: []net.IP{{45, 74, 23, 129}}},
{Region: "North America", Country: "El Salvador", City: "San Salvador", IPs: []net.IP{{45, 74, 17, 129}}},
{Region: "North America", Country: "Grenada", City: "St George's", IPs: []net.IP{{45, 74, 21, 129}}},
{Region: "North America", Country: "Guatemala", City: "Guatemala", IPs: []net.IP{{45, 74, 17, 2}}},
{Region: "North America", Country: "Haiti", City: "PORT-AU-PRINCE", IPs: []net.IP{{45, 74, 24, 2}}},
{Region: "North America", Country: "Honduras", City: "TEGUCIGALPA", IPs: []net.IP{{45, 74, 18, 2}}},
{Region: "North America", Country: "Jamaica", City: "Kingston", IPs: []net.IP{{104, 250, 182, 126}}},
{Region: "North America", Country: "Mexico", City: "Mexico City", IPs: []net.IP{{104, 243, 243, 131}}},
{Region: "North America", Country: "Montserrat", City: "plymouth", IPs: []net.IP{{45, 74, 26, 190}}},
{Region: "North America", Country: "Puerto Rico", City: "San Juan", IPs: []net.IP{{104, 37, 2, 2}}},
{Region: "North America", Country: "Saint Lucia", City: "Castries", IPs: []net.IP{{45, 74, 23, 2}}},
{Region: "North America", Country: "The Bahamas", City: "Freeport", IPs: []net.IP{{104, 243, 242, 2}}},
{Region: "North America", Country: "Trinidad and Tobago", City: "Port of Spain", IPs: []net.IP{{45, 74, 21, 2}}},
{Region: "North America", Country: "Turks and Caicos Islands", City: "Balfour Town", IPs: []net.IP{{45, 74, 24, 129}}},
{Region: "North America", Country: "United States", City: "Ashburn", IPs: []net.IP{{46, 243, 249, 2}}},
{Region: "North America", Country: "United States", City: "Chicago", IPs: []net.IP{{46, 243, 249, 4}}},
{Region: "North America", Country: "United States", City: "Columbus", IPs: []net.IP{{172, 94, 115, 2}}},
{Region: "North America", Country: "United States", City: "Georgia", IPs: []net.IP{{141, 101, 168, 4}}},
{Region: "North America", Country: "United States", City: "Houston", IPs: []net.IP{{172, 94, 1, 4}}},
{Region: "North America", Country: "United States", City: "Los Angeles", IPs: []net.IP{{141, 101, 169, 4}}},
{Region: "North America", Country: "United States", City: "Miami", IPs: []net.IP{{5, 254, 79, 114}}},
{Region: "North America", Country: "United States", City: "New Jersey", IPs: []net.IP{{172, 94, 1, 4}}},
{Region: "North America", Country: "United States", City: "New York", IPs: []net.IP{{172, 94, 1, 4}}},
{Region: "North America", Country: "United States", City: "Phoenix", IPs: []net.IP{{172, 94, 26, 4}}},
{Region: "North America", Country: "United States", City: "Salt Lake City", IPs: []net.IP{{141, 101, 168, 4}}},
{Region: "North America", Country: "United States", City: "San Francisco", IPs: []net.IP{{172, 94, 1, 4}}},
{Region: "North America", Country: "United States", City: "Seattle", IPs: []net.IP{{172, 94, 86, 2}}},
{Region: "North America", Country: "United States", City: "Washington, D.C.", IPs: []net.IP{{141, 101, 169, 4}}},
{Region: "Oceania", Country: "Australia", City: "Brisbane", IPs: []net.IP{{172, 111, 236, 2}}},
{Region: "Oceania", Country: "Australia", City: "Melbourne", IPs: []net.IP{{118, 127, 62, 2}}},
{Region: "Oceania", Country: "Australia", City: "Sydney", IPs: []net.IP{{192, 253, 241, 2}}},
{Region: "Oceania", Country: "New Zealand", City: "Auckland", IPs: []net.IP{{43, 228, 156, 4}}},
{Region: "South America", Country: "Argentina", City: "Buenos Aires", IPs: []net.IP{{104, 243, 244, 1}}},
{Region: "South America", Country: "Bolivia", City: "Sucre", IPs: []net.IP{{172, 94, 77, 2}}},
{Region: "South America", Country: "Brazil", City: "Sao Paulo", IPs: []net.IP{{104, 243, 244, 2}}},
{Region: "South America", Country: "British Virgin Island", City: "Road Town", IPs: []net.IP{{104, 250, 184, 130}}},
{Region: "South America", Country: "Chile", City: "Santiago", IPs: []net.IP{{191, 96, 183, 251}}},
{Region: "South America", Country: "Colombia", City: "Bogota", IPs: []net.IP{{172, 111, 132, 1}}},
{Region: "South America", Country: "Ecuador", City: "Quito", IPs: []net.IP{{104, 250, 180, 126}}},
{Region: "South America", Country: "Guyana", City: "Georgetown", IPs: []net.IP{{45, 74, 20, 129}}},
{Region: "South America", Country: "Panama", City: "Panama City", IPs: []net.IP{{104, 243, 243, 131}}},
{Region: "South America", Country: "Paraguay", City: "Asuncion", IPs: []net.IP{{45, 74, 19, 129}}},
{Region: "South America", Country: "Peru", City: "Lima", IPs: []net.IP{{172, 111, 131, 1}}},
{Region: "South America", Country: "Suriname", City: "Paramaribo", IPs: []net.IP{{45, 74, 20, 4}}},
{Country: "Australia", Region: "New South Wales", City: "Sydney", IPs: []net.IP{{192, 253, 241, 4}, {43, 245, 161, 85}}},
{Country: "Australia", Region: "Western Australia", City: "Perth", IPs: []net.IP{{172, 94, 123, 4}}},
{Country: "Austria", Region: "Lower Austria", City: "Langenzersdorf", IPs: []net.IP{{172, 94, 109, 4}}},
{Country: "Austria", Region: "Vienna", City: "Vienna", IPs: []net.IP{{217, 64, 127, 252}}},
{Country: "Belgium", Region: "Flanders", City: "Zaventem", IPs: []net.IP{{172, 111, 223, 4}}},
{Country: "Bulgaria", Region: "Sofia-Capital", City: "Sofia", IPs: []net.IP{{217, 138, 221, 120}}},
{Country: "Canada", Region: "Alberta", City: "Calgary", IPs: []net.IP{{172, 94, 34, 4}}},
{Country: "Canada", Region: "Ontario", City: "Toronto", IPs: []net.IP{{104, 200, 138, 196}}},
{Country: "France", Region: "Île-de-France", City: "Paris", IPs: []net.IP{{89, 40, 183, 178}}},
{Country: "Germany", Region: "Hesse", City: "Frankfurt am Main", IPs: []net.IP{{188, 72, 84, 4}}},
{Country: "Greece", Region: "Central Macedonia", City: "Thessaloníki", IPs: []net.IP{{178, 21, 169, 244}}},
{Country: "Hong Kong", Region: "Central and Western", City: "Central", IPs: []net.IP{{141, 101, 168, 4}, {141, 101, 168, 4}}},
{Country: "Hong Kong", Region: "Central and Western", City: "Hong Kong", IPs: []net.IP{{43, 226, 231, 6}, {172, 111, 168, 4}}},
{Country: "India", Region: "Tamil Nadu", City: "Chennai", IPs: []net.IP{{129, 227, 107, 242}}},
{Country: "Italy", Region: "Trentino-Alto Adige", City: "Trento", IPs: []net.IP{{172, 111, 173, 3}}},
{Country: "Japan", Region: "Ōsaka", City: "Osaka", IPs: []net.IP{{172, 94, 56, 4}}},
{Country: "Malaysia", Region: "Kuala Lumpur", City: "Kuala Lumpur", IPs: []net.IP{{103, 55, 10, 133}}},
{Country: "Netherlands", Region: "North Holland", City: "Amsterdam", IPs: []net.IP{{5, 254, 73, 172}}},
{Country: "Norway", Region: "Oslo", City: "Oslo", IPs: []net.IP{{82, 102, 22, 212}}},
{Country: "Panama", Region: "Panamá", City: "Panamá", IPs: []net.IP{{104, 243, 243, 131}}},
{Country: "Philippines", Region: "Metro Manila", City: "Quezon City", IPs: []net.IP{{129, 227, 119, 84}}},
{Country: "Poland", Region: "Mazovia", City: "Warsaw", IPs: []net.IP{{5, 253, 206, 251}}},
{Country: "Portugal", Region: "Lisbon", City: "Lisbon", IPs: []net.IP{{5, 154, 174, 3}}},
{Country: "Russian Federation", Region: "Moscow", City: "Moscow", IPs: []net.IP{{46, 243, 220, 2}, {206, 123, 139, 4}}},
{Country: "Singapore", Region: "Singapore", City: "Singapore", IPs: []net.IP{{37, 120, 208, 147}}},
{Country: "South Africa", Region: "Gauteng", City: "Johannesburg", IPs: []net.IP{{102, 165, 3, 33}}},
{Country: "Spain", Region: "Madrid", City: "Madrid", IPs: []net.IP{{217, 138, 218, 210}}},
{Country: "Sweden", Region: "Stockholm", City: "Stockholm", IPs: []net.IP{{86, 106, 103, 139}}},
{Country: "Switzerland", Region: "Zurich", City: "Zürich", IPs: []net.IP{{45, 12, 222, 103}}},
{Country: "Taiwan", Region: "Taiwan", City: "Taipei", IPs: []net.IP{{128, 1, 155, 178}}},
{Country: "United Arab Emirates", Region: "Dubai", City: "Dubai", IPs: []net.IP{{104, 37, 6, 4}}},
{Country: "United Kingdom", Region: "England", City: "Birmingham", IPs: []net.IP{{188, 72, 89, 4}}},
{Country: "United Kingdom", Region: "England", City: "London", IPs: []net.IP{{45, 141, 154, 71}, {45, 141, 154, 71}}},
{Country: "United States", Region: "California", City: "Los Angeles", IPs: []net.IP{{172, 111, 147, 4}}},
{Country: "United States", Region: "California", City: "South San Francisco", IPs: []net.IP{{141, 101, 166, 4}, {141, 101, 166, 4}, {141, 101, 166, 4}}},
{Country: "United States", Region: "Florida", City: "Miami", IPs: []net.IP{{5, 254, 79, 115}}},
{Country: "United States", Region: "Massachusetts", City: "Newton", IPs: []net.IP{{104, 243, 244, 2}}},
{Country: "United States", Region: "Michigan", City: "Ypsilanti", IPs: []net.IP{{172, 111, 149, 4}}},
{Country: "United States", Region: "Texas", City: "Dallas", IPs: []net.IP{{208, 84, 155, 100}}},
{Country: "United States", Region: "Utah", City: "Salt Lake City", IPs: []net.IP{{45, 74, 52, 4}, {45, 74, 52, 4}, {45, 74, 52, 4}, {45, 74, 52, 4}, {45, 74, 52, 4}, {45, 74, 52, 4}}},
{Country: "Vietnam", Region: "Hanoi", City: "Cầu Giấy", IPs: []net.IP{{192, 253, 249, 132}}},
}
}

View File

@@ -23,12 +23,12 @@ func GetAllServers() (allServers models.AllServers) {
},
Pia: models.PiaServers{
Version: 2,
Timestamp: 1605392393,
Timestamp: 1609343591,
Servers: PIAServers(),
},
Purevpn: models.PurevpnServers{
Version: 1,
Timestamp: 1599323261,
Timestamp: 1609448478,
Servers: PurevpnServers(),
},
Privado: models.PrivadoServers{

View File

@@ -62,7 +62,7 @@ func Test_versions(t *testing.T) {
"Purevpn": {
model: models.PurevpnServer{},
version: allServers.Purevpn.Version,
digest: "cc1a2219",
digest: "ada45379",
},
"Surfshark": {
model: models.SurfsharkServer{},
@@ -133,12 +133,12 @@ func Test_timestamps(t *testing.T) {
"Private Internet Access": {
servers: allServers.Pia.Servers,
timestamp: allServers.Pia.Timestamp,
digest: "4a172b0a",
digest: "d797f112",
},
"Purevpn": {
servers: allServers.Purevpn.Servers,
timestamp: allServers.Purevpn.Timestamp,
digest: "cdf9b708",
digest: "8abe18d4",
},
"Privado": {
servers: allServers.Privado.Servers,

View File

@@ -2,9 +2,9 @@ package constants
const (
// Announcement is a message announcement.
Announcement = "Support for Privado"
Announcement = "New Docker image qmcgaw/gluetun"
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd.
AnnouncementExpiration = "2020-11-25"
AnnouncementExpiration = "2021-01-20"
)
const (

View File

@@ -0,0 +1,14 @@
package constants
import (
"github.com/qdm12/gluetun/internal/models"
)
const (
Starting models.LoopStatus = "starting"
Running models.LoopStatus = "running"
Stopping models.LoopStatus = "stopping"
Stopped models.LoopStatus = "stopped"
Crashed models.LoopStatus = "crashed"
Completed models.LoopStatus = "completed"
)

View File

@@ -16,12 +16,11 @@ import (
func Test_Start(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("starting unbound").Times(1)
logger.EXPECT().Info("starting unbound")
commander := mock_command.NewMockCommander(mockCtrl)
commander.EXPECT().Start(context.Background(), "unbound", "-d", "-c", string(constants.UnboundConf), "-vv").
Return(nil, nil, nil, nil).Times(1)
Return(nil, nil, nil, nil)
c := &configurator{commander: commander, logger: logger}
stdout, waitFn, err := c.Start(context.Background(), 2)
assert.Nil(t, stdout)
@@ -54,10 +53,9 @@ func Test_Version(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
commander := mock_command.NewMockCommander(mockCtrl)
commander.EXPECT().Run(context.Background(), "unbound", "-V").
Return(tc.runOutput, tc.runErr).Times(1)
Return(tc.runOutput, tc.runErr)
c := &configurator{commander: commander}
version, err := c.Version(context.Background())
if tc.err != nil {

View File

@@ -3,33 +3,52 @@ package dns
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/os"
)
func (c *configurator) MakeUnboundConf(ctx context.Context, settings settings.DNS, uid, gid int) (err error) {
func (c *configurator) MakeUnboundConf(ctx context.Context, settings settings.DNS,
username string, puid, pgid int) (err error) {
c.logger.Info("generating Unbound configuration")
lines, warnings := generateUnboundConf(ctx, settings, c.client, c.logger)
lines, warnings := generateUnboundConf(ctx, settings, username, c.client, c.logger)
for _, warning := range warnings {
c.logger.Warn(warning)
}
return c.fileManager.WriteLinesToFile(
string(constants.UnboundConf),
lines,
files.Ownership(uid, gid),
files.Permissions(constants.UserReadPermission))
const filepath = string(constants.UnboundConf)
file, err := c.openFile(filepath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0400)
if err != nil {
return err
}
_, err = file.WriteString(strings.Join(lines, "\n"))
if err != nil {
_ = file.Close()
return err
}
if err := file.Chown(puid, pgid); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return nil
}
// MakeUnboundConf generates an Unbound configuration from the user provided settings.
func generateUnboundConf(ctx context.Context, settings settings.DNS,
client network.Client, logger logging.Logger) (
func generateUnboundConf(ctx context.Context, settings settings.DNS, username string,
client *http.Client, logger logging.Logger) (
lines []string, warnings []error) {
doIPv6 := "no"
if settings.IPv6 {
@@ -69,7 +88,7 @@ func generateUnboundConf(ctx context.Context, settings settings.DNS,
"interface": "0.0.0.0",
"port": "53",
// Other
"username": "\"nonrootuser\"",
"username": fmt.Sprintf("%q", username),
}
// Block lists
@@ -132,7 +151,7 @@ func generateUnboundConf(ctx context.Context, settings settings.DNS,
return lines, warnings
}
func buildBlocked(ctx context.Context, client network.Client, blockMalicious, blockAds, blockSurveillance bool,
func buildBlocked(ctx context.Context, client *http.Client, blockMalicious, blockAds, blockSurveillance bool,
allowedHostnames, privateAddresses []string) (hostnamesLines, ipsLines []string, errs []error) {
chHostnames := make(chan []string)
chIPs := make(chan []string)
@@ -162,13 +181,27 @@ func buildBlocked(ctx context.Context, client network.Client, blockMalicious, bl
return hostnamesLines, ipsLines, errs
}
func getList(ctx context.Context, client network.Client, url string) (results []string, err error) {
content, status, err := client.Get(ctx, url)
func getList(ctx context.Context, client *http.Client, url string) (results []string, err error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
} else if status != http.StatusOK {
return nil, fmt.Errorf("HTTP status code is %d and not 200", status)
}
response, err := client.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w from %s: %s", ErrBadStatusCode, url, response.Status)
}
content, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrCannotReadBody, err)
}
results = strings.Split(string(content), "\n")
// remove empty lines
@@ -187,7 +220,7 @@ func getList(ctx context.Context, client network.Client, url string) (results []
return results, nil
}
func buildBlockedHostnames(ctx context.Context, client network.Client, blockMalicious, blockAds, blockSurveillance bool,
func buildBlockedHostnames(ctx context.Context, client *http.Client, blockMalicious, blockAds, blockSurveillance bool,
allowedHostnames []string) (lines []string, errs []error) {
chResults := make(chan []string)
chError := make(chan error)
@@ -239,7 +272,7 @@ func buildBlockedHostnames(ctx context.Context, client network.Client, blockMali
return lines, errs
}
func buildBlockedIPs(ctx context.Context, client network.Client, blockMalicious, blockAds, blockSurveillance bool,
func buildBlockedIPs(ctx context.Context, client *http.Client, blockMalicious, blockAds, blockSurveillance bool,
privateAddresses []string) (lines []string, errs []error) {
chResults := make(chan []string)
chError := make(chan error)

View File

@@ -1,8 +1,11 @@
package dns
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
@@ -11,7 +14,6 @@ import (
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/logging/mock_logging"
"github.com/qdm12/golibs/network/mock_network"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -31,19 +33,46 @@ func Test_generateUnboundConf(t *testing.T) {
IPv6: true,
}
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
client := mock_network.NewMockClient(mockCtrl)
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListHostnamesURL)).
Return([]byte("b\na\nc"), 200, nil).Times(1)
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListIPsURL)).
Return([]byte("c\nd\n"), 200, nil).Times(1)
clientCalls := map[models.URL]int{
constants.MaliciousBlockListIPsURL: 0,
constants.MaliciousBlockListHostnamesURL: 0,
}
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
url := models.URL(r.URL.String())
if _, ok := clientCalls[url]; !ok {
t.Errorf("unknown URL %q", url)
return nil, nil
}
clientCalls[url]++
var body string
switch url {
case constants.MaliciousBlockListIPsURL:
body = "c\nd"
case constants.MaliciousBlockListHostnamesURL:
body = "b\na\nc"
default:
t.Errorf("unknown URL %q", url)
return nil, nil
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(body)),
}, nil
}),
}
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("%d hostnames blocked overall", 2).Times(1)
logger.EXPECT().Info("%d IP addresses blocked overall", 3).Times(1)
lines, warnings := generateUnboundConf(ctx, settings, client, logger)
logger.EXPECT().Info("%d hostnames blocked overall", 2)
logger.EXPECT().Info("%d IP addresses blocked overall", 3)
lines, warnings := generateUnboundConf(ctx, settings, "nonrootuser", client, logger)
require.Len(t, warnings, 0)
expected := `
for url, count := range clientCalls {
assert.Equalf(t, 1, count, "for url %q", url)
}
const expected = `
server:
cache-max-ttl: 9000
cache-min-ttl: 3600
@@ -210,7 +239,10 @@ func Test_buildBlocked(t *testing.T) {
ipsLines: []string{
" private-address: malicious",
" private-address: surveillance"},
errsString: []string{"ads error", "ads error"},
errsString: []string{
`Get "https://raw.githubusercontent.com/qdm12/files/master/ads-ips.updated": ads error`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/ads-hostnames.updated": ads error`,
},
},
"all blocked with errors": {
malicious: blockParams{
@@ -225,38 +257,74 @@ func Test_buildBlocked(t *testing.T) {
blocked: true,
clientErr: fmt.Errorf("surveillance"),
},
errsString: []string{"malicious", "malicious", "ads", "ads", "surveillance", "surveillance"},
errsString: []string{
`Get "https://raw.githubusercontent.com/qdm12/files/master/malicious-ips.updated": malicious`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/malicious-hostnames.updated": malicious`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/ads-ips.updated": ads`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/ads-hostnames.updated": ads`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/surveillance-ips.updated": surveillance`,
`Get "https://raw.githubusercontent.com/qdm12/files/master/surveillance-hostnames.updated": surveillance`,
},
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
client := mock_network.NewMockClient(mockCtrl)
clientCalls := map[models.URL]int{}
if tc.malicious.blocked {
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListHostnamesURL)).
Return(tc.malicious.content, 200, tc.malicious.clientErr).Times(1)
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListIPsURL)).
Return(tc.malicious.content, 200, tc.malicious.clientErr).Times(1)
clientCalls[constants.MaliciousBlockListIPsURL] = 0
clientCalls[constants.MaliciousBlockListHostnamesURL] = 0
}
if tc.ads.blocked {
client.EXPECT().Get(ctx, string(constants.AdsBlockListHostnamesURL)).
Return(tc.ads.content, 200, tc.ads.clientErr).Times(1)
client.EXPECT().Get(ctx, string(constants.AdsBlockListIPsURL)).
Return(tc.ads.content, 200, tc.ads.clientErr).Times(1)
clientCalls[constants.AdsBlockListIPsURL] = 0
clientCalls[constants.AdsBlockListHostnamesURL] = 0
}
if tc.surveillance.blocked {
client.EXPECT().Get(ctx, string(constants.SurveillanceBlockListHostnamesURL)).
Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Times(1)
client.EXPECT().Get(ctx, string(constants.SurveillanceBlockListIPsURL)).
Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Times(1)
clientCalls[constants.SurveillanceBlockListIPsURL] = 0
clientCalls[constants.SurveillanceBlockListHostnamesURL] = 0
}
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
url := models.URL(r.URL.String())
if _, ok := clientCalls[url]; !ok {
t.Errorf("unknown URL %q", url)
return nil, nil
}
clientCalls[url]++
var body []byte
var err error
switch url {
case constants.MaliciousBlockListIPsURL, constants.MaliciousBlockListHostnamesURL:
body = tc.malicious.content
err = tc.malicious.clientErr
case constants.AdsBlockListIPsURL, constants.AdsBlockListHostnamesURL:
body = tc.ads.content
err = tc.ads.clientErr
case constants.SurveillanceBlockListIPsURL, constants.SurveillanceBlockListHostnamesURL:
body = tc.surveillance.content
err = tc.surveillance.clientErr
default: // just in case if the test is badly written
t.Errorf("unknown URL %q", url)
return nil, nil
}
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(body)),
}, nil
}),
}
hostnamesLines, ipsLines, errs := buildBlocked(ctx, client,
tc.malicious.blocked, tc.ads.blocked, tc.surveillance.blocked,
tc.allowedHostnames, tc.privateAddresses)
var errsString []string
for _, err := range errs {
errsString = append(errsString, err.Error())
@@ -264,6 +332,10 @@ func Test_buildBlocked(t *testing.T) {
assert.ElementsMatch(t, tc.errsString, errsString)
assert.ElementsMatch(t, tc.hostnamesLines, hostnamesLines)
assert.ElementsMatch(t, tc.ipsLines, ipsLines)
for url, count := range clientCalls {
assert.Equalf(t, 1, count, "for url %q", url)
}
})
}
}
@@ -277,21 +349,45 @@ func Test_getList(t *testing.T) {
results []string
err error
}{
"no result": {nil, 200, nil, nil, nil},
"bad status": {nil, 500, nil, nil, fmt.Errorf("HTTP status code is 500 and not 200")},
"network error": {nil, 200, fmt.Errorf("error"), nil, fmt.Errorf("error")},
"results": {[]byte("a\nb\nc\n"), 200, nil, []string{"a", "b", "c"}, nil},
"no result": {
status: http.StatusOK,
},
"bad status": {
status: http.StatusInternalServerError,
err: fmt.Errorf("bad HTTP status from irrelevant_url: Internal Server Error"),
},
"network error": {
status: http.StatusOK,
clientErr: fmt.Errorf("error"),
err: fmt.Errorf(`Get "irrelevant_url": error`),
},
"results": {
content: []byte("a\nb\nc\n"),
status: http.StatusOK,
results: []string{"a", "b", "c"},
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
client := mock_network.NewMockClient(mockCtrl)
client.EXPECT().Get(ctx, "irrelevant_url").
Return(tc.content, tc.status, tc.clientErr).Times(1)
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, "irrelevant_url", r.URL.String())
if tc.clientErr != nil {
return nil, tc.clientErr
}
return &http.Response{
StatusCode: tc.status,
Status: http.StatusText(tc.status),
Body: ioutil.NopCloser(bytes.NewReader(tc.content)),
}, nil
}),
}
results, err := getList(ctx, client, "irrelevant_url")
if tc.err != nil {
require.Error(t, err)
@@ -319,10 +415,7 @@ func Test_buildBlockedHostnames(t *testing.T) {
lines []string
errsString []string
}{
"nothing blocked": {
lines: nil,
errsString: nil,
},
"nothing blocked": {},
"only malicious blocked": {
malicious: blockParams{
blocked: true,
@@ -332,7 +425,6 @@ func Test_buildBlockedHostnames(t *testing.T) {
lines: []string{
" local-zone: \"site_a\" static",
" local-zone: \"site_b\" static"},
errsString: nil,
},
"all blocked with some duplicates": {
malicious: blockParams{
@@ -351,7 +443,6 @@ func Test_buildBlockedHostnames(t *testing.T) {
" local-zone: \"site_a\" static",
" local-zone: \"site_b\" static",
" local-zone: \"site_c\" static"},
errsString: nil,
},
"all blocked with one errored": {
malicious: blockParams{
@@ -370,7 +461,9 @@ func Test_buildBlockedHostnames(t *testing.T) {
" local-zone: \"site_a\" static",
" local-zone: \"site_b\" static",
" local-zone: \"site_c\" static"},
errsString: []string{"surveillance error"},
errsString: []string{
`Get "https://raw.githubusercontent.com/qdm12/files/master/surveillance-hostnames.updated": surveillance error`,
},
},
"blocked with allowed hostnames": {
malicious: blockParams{
@@ -387,35 +480,71 @@ func Test_buildBlockedHostnames(t *testing.T) {
" local-zone: \"site_d\" static"},
},
}
for name, tc := range tests { //nolint:dupl
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
client := mock_network.NewMockClient(mockCtrl)
clientCalls := map[models.URL]int{}
if tc.malicious.blocked {
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListHostnamesURL)).
Return(tc.malicious.content, 200, tc.malicious.clientErr).Times(1)
clientCalls[constants.MaliciousBlockListHostnamesURL] = 0
}
if tc.ads.blocked {
client.EXPECT().Get(ctx, string(constants.AdsBlockListHostnamesURL)).
Return(tc.ads.content, 200, tc.ads.clientErr).Times(1)
clientCalls[constants.AdsBlockListHostnamesURL] = 0
}
if tc.surveillance.blocked {
client.EXPECT().Get(ctx, string(constants.SurveillanceBlockListHostnamesURL)).
Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Times(1)
clientCalls[constants.SurveillanceBlockListHostnamesURL] = 0
}
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
url := models.URL(r.URL.String())
if _, ok := clientCalls[url]; !ok {
t.Errorf("unknown URL %q", url)
return nil, nil
}
clientCalls[url]++
var body []byte
var err error
switch url {
case constants.MaliciousBlockListHostnamesURL:
body = tc.malicious.content
err = tc.malicious.clientErr
case constants.AdsBlockListHostnamesURL:
body = tc.ads.content
err = tc.ads.clientErr
case constants.SurveillanceBlockListHostnamesURL:
body = tc.surveillance.content
err = tc.surveillance.clientErr
default: // just in case if the test is badly written
t.Errorf("unknown URL %q", url)
return nil, nil
}
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(body)),
}, nil
}),
}
lines, errs := buildBlockedHostnames(ctx, client,
tc.malicious.blocked, tc.ads.blocked,
tc.surveillance.blocked, tc.allowedHostnames)
var errsString []string
for _, err := range errs {
errsString = append(errsString, err.Error())
}
assert.ElementsMatch(t, tc.errsString, errsString)
assert.ElementsMatch(t, tc.lines, lines)
for url, count := range clientCalls {
assert.Equalf(t, 1, count, "for url %q", url)
}
})
}
}
@@ -435,10 +564,7 @@ func Test_buildBlockedIPs(t *testing.T) {
lines []string
errsString []string
}{
"nothing blocked": {
lines: nil,
errsString: nil,
},
"nothing blocked": {},
"only malicious blocked": {
malicious: blockParams{
blocked: true,
@@ -448,7 +574,6 @@ func Test_buildBlockedIPs(t *testing.T) {
lines: []string{
" private-address: site_a",
" private-address: site_b"},
errsString: nil,
},
"all blocked with some duplicates": {
malicious: blockParams{
@@ -467,7 +592,6 @@ func Test_buildBlockedIPs(t *testing.T) {
" private-address: site_a",
" private-address: site_b",
" private-address: site_c"},
errsString: nil,
},
"all blocked with one errored": {
malicious: blockParams{
@@ -486,7 +610,9 @@ func Test_buildBlockedIPs(t *testing.T) {
" private-address: site_a",
" private-address: site_b",
" private-address: site_c"},
errsString: []string{"surveillance error"},
errsString: []string{
`Get "https://raw.githubusercontent.com/qdm12/files/master/surveillance-ips.updated": surveillance error`,
},
},
"blocked with private addresses": {
malicious: blockParams{
@@ -505,35 +631,72 @@ func Test_buildBlockedIPs(t *testing.T) {
" private-address: site_d"},
},
}
for name, tc := range tests { //nolint:dupl
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
client := mock_network.NewMockClient(mockCtrl)
clientCalls := map[models.URL]int{}
if tc.malicious.blocked {
client.EXPECT().Get(ctx, string(constants.MaliciousBlockListIPsURL)).
Return(tc.malicious.content, 200, tc.malicious.clientErr).Times(1)
clientCalls[constants.MaliciousBlockListIPsURL] = 0
}
if tc.ads.blocked {
client.EXPECT().Get(ctx, string(constants.AdsBlockListIPsURL)).
Return(tc.ads.content, 200, tc.ads.clientErr).Times(1)
clientCalls[constants.AdsBlockListIPsURL] = 0
}
if tc.surveillance.blocked {
client.EXPECT().Get(ctx, string(constants.SurveillanceBlockListIPsURL)).
Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Times(1)
clientCalls[constants.SurveillanceBlockListIPsURL] = 0
}
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
url := models.URL(r.URL.String())
if _, ok := clientCalls[url]; !ok {
t.Errorf("unknown URL %q", url)
return nil, nil
}
clientCalls[url]++
var body []byte
var err error
switch url {
case constants.MaliciousBlockListIPsURL:
body = tc.malicious.content
err = tc.malicious.clientErr
case constants.AdsBlockListIPsURL:
body = tc.ads.content
err = tc.ads.clientErr
case constants.SurveillanceBlockListIPsURL:
body = tc.surveillance.content
err = tc.surveillance.clientErr
default: // just in case if the test is badly written
t.Errorf("unknown URL %q", url)
return nil, nil
}
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(body)),
}, nil
}),
}
lines, errs := buildBlockedIPs(ctx, client,
tc.malicious.blocked, tc.ads.blocked,
tc.surveillance.blocked, tc.privateAddresses)
var errsString []string
for _, err := range errs {
errsString = append(errsString, err.Error())
}
assert.ElementsMatch(t, tc.errsString, errsString)
assert.ElementsMatch(t, tc.lines, lines)
for url, count := range clientCalls {
assert.Equalf(t, 1, count, "for url %q", url)
}
})
}
}

View File

@@ -4,18 +4,18 @@ import (
"context"
"io"
"net"
"net/http"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/os"
)
type Configurator interface {
DownloadRootHints(ctx context.Context, uid, gid int) error
DownloadRootKey(ctx context.Context, uid, gid int) error
MakeUnboundConf(ctx context.Context, settings settings.DNS, uid, gid int) (err error)
DownloadRootHints(ctx context.Context, puid, pgid int) error
DownloadRootKey(ctx context.Context, puid, pgid int) error
MakeUnboundConf(ctx context.Context, settings settings.DNS, username string, puid, pgid int) (err error)
UseDNSInternally(IP net.IP)
UseDNSSystemWide(ip net.IP, keepNameserver bool) error
Start(ctx context.Context, logLevel uint8) (stdout io.ReadCloser, waitFn func() error, err error)
@@ -24,19 +24,20 @@ type Configurator interface {
}
type configurator struct {
logger logging.Logger
client network.Client
fileManager files.FileManager
commander command.Commander
lookupIP func(host string) ([]net.IP, error)
logger logging.Logger
client *http.Client
openFile os.OpenFileFunc
commander command.Commander
lookupIP func(host string) ([]net.IP, error)
}
func NewConfigurator(logger logging.Logger, client network.Client, fileManager files.FileManager) Configurator {
func NewConfigurator(logger logging.Logger, httpClient *http.Client,
openFile os.OpenFileFunc) Configurator {
return &configurator{
logger: logger.WithPrefix("dns configurator: "),
client: client,
fileManager: fileManager,
commander: command.NewCommander(),
lookupIP: net.LookupIP,
logger: logger.WithPrefix("dns configurator: "),
client: httpClient,
openFile: openFile,
commander: command.NewCommander(),
lookupIP: net.LookupIP,
}
}

8
internal/dns/errors.go Normal file
View File

@@ -0,0 +1,8 @@
package dns
import "errors"
var (
ErrBadStatusCode = errors.New("bad HTTP status")
ErrCannotReadBody = errors.New("cannot read response body")
)

View File

@@ -2,11 +2,13 @@ package dns
import (
"context"
"errors"
"net"
"sync"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging"
@@ -15,85 +17,62 @@ import (
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup, signalDNSReady func())
RunRestartTicker(ctx context.Context, wg *sync.WaitGroup)
Restart()
Start()
Stop()
GetStatus() (status models.LoopStatus)
SetStatus(status models.LoopStatus) (outcome string, err error)
GetSettings() (settings settings.DNS)
SetSettings(settings settings.DNS)
SetSettings(settings settings.DNS) (outcome string)
}
type looper struct {
conf Configurator
settings settings.DNS
settingsMutex sync.RWMutex
logger logging.Logger
streamMerger command.StreamMerger
uid int
gid int
restart chan struct{}
start chan struct{}
stop chan struct{}
updateTicker chan struct{}
timeNow func() time.Time
timeSince func(time.Time) time.Duration
state state
conf Configurator
logger logging.Logger
streamMerger command.StreamMerger
username string
puid int
pgid int
loopLock sync.Mutex
start chan struct{}
running chan models.LoopStatus
stop chan struct{}
stopped chan struct{}
updateTicker chan struct{}
backoffTime time.Duration
timeNow func() time.Time
timeSince func(time.Time) time.Duration
}
const defaultBackoffTime = 10 * time.Second
func NewLooper(conf Configurator, settings settings.DNS, logger logging.Logger,
streamMerger command.StreamMerger, uid, gid int) Looper {
streamMerger command.StreamMerger, username string, puid, pgid int) Looper {
return &looper{
state: state{
status: constants.Stopped,
settings: settings,
},
conf: conf,
settings: settings,
logger: logger.WithPrefix("dns over tls: "),
uid: uid,
gid: gid,
username: username,
puid: puid,
pgid: pgid,
streamMerger: streamMerger,
restart: make(chan struct{}),
start: make(chan struct{}),
running: make(chan models.LoopStatus),
stop: make(chan struct{}),
stopped: make(chan struct{}),
updateTicker: make(chan struct{}),
backoffTime: defaultBackoffTime,
timeNow: time.Now,
timeSince: time.Since,
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Start() { l.start <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) GetSettings() (settings settings.DNS) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.DNS) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
updatePeriodDiffers := l.settings.UpdatePeriod != settings.UpdatePeriod
l.settings = settings
l.settingsMutex.Unlock()
if updatePeriodDiffers {
l.updateTicker <- struct{}{}
}
}
func (l *looper) isEnabled() bool {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings.Enabled
}
func (l *looper) setEnabled(enabled bool) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings.Enabled = enabled
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Warn(err)
l.logger.Info("attempting restart in 10 seconds")
const waitDuration = 10 * time.Second
timer := time.NewTimer(waitDuration)
l.logger.Info("attempting restart in %s", l.backoffTime)
timer := time.NewTimer(l.backoffTime)
l.backoffTime *= 2
select {
case <-timer.C:
case <-ctx.Done():
@@ -103,121 +82,50 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
}
}
func (l *looper) waitForFirstStart(ctx context.Context, signalDNSReady func()) {
for {
select {
case <-l.stop:
l.setEnabled(false)
l.logger.Info("not started yet")
case <-l.restart:
if l.isEnabled() {
return
}
signalDNSReady()
l.logger.Info("not restarting because disabled")
case <-l.start:
l.setEnabled(true)
return
case <-ctx.Done():
return
}
}
}
func (l *looper) waitForSubsequentStart(ctx context.Context, unboundCancel context.CancelFunc) {
if l.isEnabled() {
return
}
for {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
if !l.isEnabled() {
l.logger.Info("not restarting because disabled")
} else {
return
}
case <-l.start:
l.setEnabled(true)
return
case <-ctx.Done():
unboundCancel()
return
}
}
}
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, signalDNSReady func()) {
defer wg.Done()
const fallback = false
l.useUnencryptedDNS(fallback)
l.waitForFirstStart(ctx, signalDNSReady)
if ctx.Err() != nil {
l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution?
select {
case <-l.start:
case <-ctx.Done():
return
}
defer l.logger.Warn("loop exited")
var unboundCtx context.Context
var unboundCancel context.CancelFunc = func() {}
var waitError chan error
triggeredRestart := false
l.setEnabled(true)
crashed := false
l.backoffTime = defaultBackoffTime
for ctx.Err() == nil {
l.waitForSubsequentStart(ctx, unboundCancel)
// Upper scope variables for Unbound only
var unboundCancel context.CancelFunc = func() {}
waitError := make(chan error)
settings := l.GetSettings()
// Setup
if err := l.conf.DownloadRootHints(ctx, l.uid, l.gid); err != nil {
l.logAndWait(ctx, err)
continue
for l.GetSettings().Enabled {
if ctx.Err() != nil {
l.logger.Warn("context canceled: exiting loop")
return
}
var err error
unboundCancel, err = l.setupUnbound(ctx, crashed, waitError)
if err != nil {
if !errors.Is(err, errUpdateFiles) {
const fallback = true
l.useUnencryptedDNS(fallback)
}
l.logAndWait(ctx, err)
continue
}
break
}
if err := l.conf.DownloadRootKey(ctx, l.uid, l.gid); err != nil {
l.logAndWait(ctx, err)
continue
}
if err := l.conf.MakeUnboundConf(ctx, settings, l.uid, l.gid); err != nil {
l.logAndWait(ctx, err)
continue
}
if triggeredRestart {
triggeredRestart = false
unboundCancel()
<-waitError
close(waitError)
}
unboundCtx, unboundCancel = context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(unboundCtx, settings.VerbosityDetailsLevel)
if err != nil {
unboundCancel()
const fallback = true
if !l.GetSettings().Enabled {
const fallback = false
l.useUnencryptedDNS(fallback)
l.logAndWait(ctx, err)
continue
}
// Started successfully
go l.streamMerger.Merge(unboundCtx, stream, command.MergeName("unbound"))
l.conf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound
if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, settings.KeepNameserver); err != nil { // use Unbound
l.logger.Error(err)
}
if err := l.conf.WaitForUnbound(); err != nil {
unboundCancel()
const fallback = true
l.useUnencryptedDNS(fallback)
l.logAndWait(ctx, err)
continue
}
waitError = make(chan error)
go func() {
err := waitFn() // blocking
waitError <- err
}()
l.logger.Info("DNS over TLS is ready")
signalDNSReady()
stayHere := true
@@ -229,31 +137,84 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup, signalDNSReady fun
<-waitError
close(waitError)
return
case <-l.restart: // triggered restart
l.logger.Info("restarting")
// unboundCancel occurs next loop run when the setup is complete
triggeredRestart = true
stayHere = false
case <-l.start:
l.logger.Info("already started")
case <-l.stop:
l.logger.Info("stopping")
const fallback = false
l.useUnencryptedDNS(fallback)
unboundCancel()
<-waitError
close(waitError)
l.setEnabled(false)
l.stopped <- struct{}{}
case <-l.start:
l.logger.Info("starting")
stayHere = false
case err := <-waitError: // unexpected error
close(waitError)
unboundCancel()
l.state.setStatusWithLock(constants.Crashed)
const fallback = true
l.useUnencryptedDNS(fallback)
l.logAndWait(ctx, err)
stayHere = false
}
}
close(waitError)
unboundCancel()
}
unboundCancel()
}
var errUpdateFiles = errors.New("cannot update files")
// Returning cancel == nil signals we want to re-run setupUnbound
// Returning err == errUpdateFiles signals we should not fall back
// on the plaintext DNS as DOT is still up and running.
func (l *looper) setupUnbound(ctx context.Context,
previousCrashed bool, waitError chan<- error) (cancel context.CancelFunc, err error) {
err = l.updateFiles(ctx)
if err != nil {
l.state.setStatusWithLock(constants.Crashed)
return nil, errUpdateFiles
}
settings := l.GetSettings()
unboundCtx, cancel := context.WithCancel(context.Background())
stream, waitFn, err := l.conf.Start(unboundCtx, settings.VerbosityDetailsLevel)
if err != nil {
cancel()
if !previousCrashed {
l.running <- constants.Crashed
}
return nil, err
}
// Started successfully
go l.streamMerger.Merge(unboundCtx, stream, command.MergeName("unbound"))
l.conf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound
if err := l.conf.UseDNSSystemWide(net.IP{127, 0, 0, 1}, settings.KeepNameserver); err != nil { // use Unbound
l.logger.Error(err)
}
if err := l.conf.WaitForUnbound(); err != nil {
if !previousCrashed {
l.running <- constants.Crashed
}
cancel()
return nil, err
}
go func() {
err := waitFn() // blocking
waitError <- err
}()
l.logger.Info("ready")
if !previousCrashed {
l.running <- constants.Running
} else {
l.backoffTime = defaultBackoffTime
l.state.setStatusWithLock(constants.Running)
}
return cancel, nil
}
func (l *looper) useUnencryptedDNS(fallback bool) {
@@ -279,7 +240,11 @@ func (l *looper) useUnencryptedDNS(fallback bool) {
data := constants.DNSProviderMapping()[provider]
for _, targetIP = range data.IPs {
if targetIP.To4() != nil {
l.logger.Info("falling back on plaintext DNS at address %s", targetIP)
if fallback {
l.logger.Info("falling back on plaintext DNS at address %s", targetIP)
} else {
l.logger.Info("using plaintext DNS at address %s", targetIP)
}
l.conf.UseDNSInternally(targetIP)
if err := l.conf.UseDNSSystemWide(targetIP, settings.KeepNameserver); err != nil {
l.logger.Error(err)
@@ -314,7 +279,20 @@ func (l *looper) RunRestartTicker(ctx context.Context, wg *sync.WaitGroup) {
return
case <-timer.C:
lastTick = l.timeNow()
l.restart <- struct{}{}
status := l.GetStatus()
if status == constants.Running {
if err := l.updateFiles(ctx); err != nil {
l.state.setStatusWithLock(constants.Crashed)
l.logger.Error(err)
l.logger.Warn("skipping Unbound restart due to failed files update")
continue
}
}
_, _ = l.SetStatus(constants.Stopped)
_, _ = l.SetStatus(constants.Running)
settings := l.GetSettings()
timer.Reset(settings.UpdatePeriod)
case <-l.updateTicker:
@@ -337,3 +315,17 @@ func (l *looper) RunRestartTicker(ctx context.Context, wg *sync.WaitGroup) {
}
}
}
func (l *looper) updateFiles(ctx context.Context) (err error) {
if err := l.conf.DownloadRootHints(ctx, l.puid, l.pgid); err != nil {
return err
}
if err := l.conf.DownloadRootKey(ctx, l.puid, l.pgid); err != nil {
return err
}
settings := l.GetSettings()
if err := l.conf.MakeUnboundConf(ctx, settings, l.username, l.puid, l.pgid); err != nil {
return err
}
return nil
}

View File

@@ -2,10 +2,12 @@ package dns
import (
"context"
"io/ioutil"
"net"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/os"
)
// UseDNSInternally is to change the Go program DNS only.
@@ -23,27 +25,43 @@ func (c *configurator) UseDNSInternally(ip net.IP) {
// UseDNSSystemWide changes the nameserver to use for DNS system wide.
func (c *configurator) UseDNSSystemWide(ip net.IP, keepNameserver bool) error {
c.logger.Info("using DNS address %s system wide", ip.String())
data, err := c.fileManager.ReadFile(string(constants.ResolvConf))
const filepath = string(constants.ResolvConf)
file, err := c.openFile(filepath, os.O_RDONLY, 0)
if err != nil {
return err
}
data, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
s := strings.TrimSuffix(string(data), "\n")
lines := strings.Split(s, "\n")
if len(lines) == 1 && lines[0] == "" {
lines = nil
lines := []string{
"nameserver " + ip.String(),
}
found := false
if !keepNameserver { // default
for i := range lines {
if strings.HasPrefix(lines[i], "nameserver ") {
lines[i] = "nameserver " + ip.String()
found = true
}
for _, line := range strings.Split(s, "\n") {
if line == "" ||
(!keepNameserver && strings.HasPrefix(line, "nameserver ")) {
continue
}
lines = append(lines, line)
}
if !found {
lines = append(lines, "nameserver "+ip.String())
s = strings.Join(lines, "\n") + "\n"
file, err = c.openFile(filepath, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
data = []byte(strings.Join(lines, "\n"))
return c.fileManager.WriteToFile(string(constants.ResolvConf), data)
_, err = file.WriteString(s)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -2,45 +2,84 @@ package dns
import (
"fmt"
"io"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files/mock_files"
"github.com/qdm12/golibs/logging/mock_logging"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/mock_os"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_UseDNSSystemWide(t *testing.T) {
t.Parallel()
tests := map[string]struct {
data []byte
writtenData []byte
readErr error
writeErr error
err error
ip net.IP
keepNameserver bool
data []byte
firstOpenErr error
readErr error
firstCloseErr error
secondOpenErr error
writtenData string
writeErr error
secondCloseErr error
err error
}{
"no data": {
writtenData: []byte("nameserver 127.0.0.1"),
ip: net.IP{127, 0, 0, 1},
writtenData: "nameserver 127.0.0.1\n",
},
"first open error": {
ip: net.IP{127, 0, 0, 1},
firstOpenErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"read error": {
readErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"first close error": {
firstCloseErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"second open error": {
ip: net.IP{127, 0, 0, 1},
secondOpenErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"write error": {
writtenData: []byte("nameserver 127.0.0.1"),
ip: net.IP{127, 0, 0, 1},
writtenData: "nameserver 127.0.0.1\n",
writeErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"second close error": {
ip: net.IP{127, 0, 0, 1},
writtenData: "nameserver 127.0.0.1\n",
secondCloseErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"lines without nameserver": {
ip: net.IP{127, 0, 0, 1},
data: []byte("abc\ndef\n"),
writtenData: []byte("abc\ndef\nnameserver 127.0.0.1"),
writtenData: "nameserver 127.0.0.1\nabc\ndef\n",
},
"lines with nameserver": {
ip: net.IP{127, 0, 0, 1},
data: []byte("abc\nnameserver abc def\ndef\n"),
writtenData: []byte("abc\nnameserver 127.0.0.1\ndef"),
writtenData: "nameserver 127.0.0.1\nabc\ndef\n",
},
"keep nameserver": {
ip: net.IP{127, 0, 0, 1},
keepNameserver: true,
data: []byte("abc\nnameserver abc def\ndef\n"),
writtenData: "nameserver 127.0.0.1\nabc\nnameserver abc def\ndef\n",
},
}
for name, tc := range tests {
@@ -48,21 +87,84 @@ func Test_UseDNSSystemWide(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fileManager := mock_files.NewMockFileManager(mockCtrl)
fileManager.EXPECT().ReadFile(string(constants.ResolvConf)).
Return(tc.data, tc.readErr).Times(1)
if tc.readErr == nil {
fileManager.EXPECT().WriteToFile(string(constants.ResolvConf), tc.writtenData).
Return(tc.writeErr).Times(1)
type fileCall struct {
path string
flag int
perm os.FileMode
file os.File
err error
}
var fileCalls []fileCall
readOnlyFile := mock_os.NewMockFile(mockCtrl)
if tc.firstOpenErr == nil {
firstReadCall := readOnlyFile.EXPECT().
Read(gomock.AssignableToTypeOf([]byte{})).
DoAndReturn(func(b []byte) (int, error) {
copy(b, tc.data)
return len(tc.data), nil
})
readErr := tc.readErr
if readErr == nil {
readErr = io.EOF
}
finalReadCall := readOnlyFile.EXPECT().
Read(gomock.AssignableToTypeOf([]byte{})).
Return(0, readErr).After(firstReadCall)
readOnlyFile.EXPECT().Close().
Return(tc.firstCloseErr).
After(finalReadCall)
}
fileCalls = append(fileCalls, fileCall{
path: string(constants.ResolvConf),
flag: os.O_RDONLY,
perm: 0,
file: readOnlyFile,
err: tc.firstOpenErr,
}) // always return readOnlyFile
if tc.firstOpenErr == nil && tc.readErr == nil && tc.firstCloseErr == nil {
writeOnlyFile := mock_os.NewMockFile(mockCtrl)
if tc.secondOpenErr == nil {
writeCall := writeOnlyFile.EXPECT().
WriteString(tc.writtenData).
Return(0, tc.writeErr)
writeOnlyFile.EXPECT().
Close().
Return(tc.secondCloseErr).
After(writeCall)
}
fileCalls = append(fileCalls, fileCall{
path: string(constants.ResolvConf),
flag: os.O_WRONLY | os.O_TRUNC,
perm: os.FileMode(0644),
file: writeOnlyFile,
err: tc.secondOpenErr,
})
}
fileCallIndex := 0
openFile := func(name string, flag int, perm os.FileMode) (os.File, error) {
fileCall := fileCalls[fileCallIndex]
fileCallIndex++
assert.Equal(t, fileCall.path, name)
assert.Equal(t, fileCall.flag, flag)
assert.Equal(t, fileCall.perm, perm)
return fileCall.file, fileCall.err
}
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("using DNS address %s system wide", "127.0.0.1").Times(1)
logger.EXPECT().Info("using DNS address %s system wide", tc.ip.String())
c := &configurator{
fileManager: fileManager,
logger: logger,
openFile: openFile,
logger: logger,
}
err := c.UseDNSSystemWide(net.IP{127, 0, 0, 1}, false)
err := c.UseDNSSystemWide(tc.ip, tc.keepNameserver)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())

View File

@@ -3,38 +3,56 @@ package dns
import (
"context"
"fmt"
"io"
"net/http"
"os"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files"
)
func (c *configurator) DownloadRootHints(ctx context.Context, uid, gid int) error {
c.logger.Info("downloading root hints from %s", constants.NamedRootURL)
content, status, err := c.client.Get(ctx, string(constants.NamedRootURL))
if err != nil {
return err
} else if status != http.StatusOK {
return fmt.Errorf("HTTP status code is %d for %s", status, constants.NamedRootURL)
}
return c.fileManager.WriteToFile(
string(constants.RootHints),
content,
files.Ownership(uid, gid),
files.Permissions(constants.UserReadPermission))
func (c *configurator) DownloadRootHints(ctx context.Context, puid, pgid int) error {
return c.downloadAndSave(ctx, "root hints",
string(constants.NamedRootURL), string(constants.RootHints), puid, pgid)
}
func (c *configurator) DownloadRootKey(ctx context.Context, uid, gid int) error {
c.logger.Info("downloading root key from %s", constants.RootKeyURL)
content, status, err := c.client.Get(ctx, string(constants.RootKeyURL))
func (c *configurator) DownloadRootKey(ctx context.Context, puid, pgid int) error {
return c.downloadAndSave(ctx, "root key",
string(constants.RootKeyURL), string(constants.RootKey), puid, pgid)
}
func (c *configurator) downloadAndSave(ctx context.Context, logName, url, filepath string, puid, pgid int) error {
c.logger.Info("downloading %s from %s", logName, url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
} else if status != http.StatusOK {
return fmt.Errorf("HTTP status code is %d for %s", status, constants.RootKeyURL)
}
return c.fileManager.WriteToFile(
string(constants.RootKey),
content,
files.Ownership(uid, gid),
files.Permissions(constants.UserReadPermission))
response, err := c.client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("%w from %s: %s", ErrBadStatusCode, url, response.Status)
}
file, err := c.openFile(filepath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0400)
if err != nil {
return err
}
_, err = io.Copy(file, response.Body)
if err != nil {
_ = file.Close()
return err
}
err = file.Chown(puid, pgid)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -1,47 +1,71 @@
package dns
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/files/mock_files"
"github.com/qdm12/golibs/logging/mock_logging"
"github.com/qdm12/golibs/network/mock_network"
"github.com/qdm12/golibs/os"
"github.com/qdm12/golibs/os/mock_os"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_DownloadRootHints(t *testing.T) { //nolint:dupl
func Test_downloadAndSave(t *testing.T) {
t.Parallel()
const defaultURL = "https://test.com"
tests := map[string]struct {
url string // to trigger a new request error
content []byte
status int
clientErr error
openErr error
writeErr error
chownErr error
closeErr error
err error
}{
"no data": {
url: defaultURL,
status: http.StatusOK,
},
"bad status": {
url: defaultURL,
status: http.StatusBadRequest,
err: fmt.Errorf("HTTP status code is 400 for https://raw.githubusercontent.com/qdm12/files/master/named.root.updated"), //nolint:lll
err: fmt.Errorf("bad HTTP status from %s: Bad Request", defaultURL),
},
"client error": {
url: defaultURL,
clientErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
err: fmt.Errorf("Get %q: error", defaultURL),
},
"write error": {
"open error": {
url: defaultURL,
status: http.StatusOK,
openErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"chown error": {
url: defaultURL,
status: http.StatusOK,
writeErr: fmt.Errorf("error"),
chownErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"close error": {
url: defaultURL,
status: http.StatusOK,
closeErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"data": {
url: defaultURL,
content: []byte("content"),
status: http.StatusOK,
},
@@ -51,24 +75,65 @@ func Test_DownloadRootHints(t *testing.T) { //nolint:dupl
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("downloading root hints from %s", constants.NamedRootURL).Times(1)
client := mock_network.NewMockClient(mockCtrl)
client.EXPECT().Get(ctx, string(constants.NamedRootURL)).
Return(tc.content, tc.status, tc.clientErr).Times(1)
fileManager := mock_files.NewMockFileManager(mockCtrl)
if tc.clientErr == nil && tc.status == http.StatusOK {
fileManager.EXPECT().WriteToFile(
string(constants.RootHints),
tc.content,
gomock.AssignableToTypeOf(files.Ownership(0, 0)),
gomock.AssignableToTypeOf(files.Ownership(0, 0))).
Return(tc.writeErr).Times(1)
logger.EXPECT().Info("downloading %s from %s", "root hints", tc.url)
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, tc.url, r.URL.String())
if tc.clientErr != nil {
return nil, tc.clientErr
}
return &http.Response{
StatusCode: tc.status,
Status: http.StatusText(tc.status),
Body: ioutil.NopCloser(bytes.NewReader(tc.content)),
}, nil
}),
}
c := &configurator{logger: logger, client: client, fileManager: fileManager}
err := c.DownloadRootHints(ctx, 1000, 1000)
openFile := func(name string, flag int, perm os.FileMode) (os.File, error) {
return nil, nil
}
const filepath = "/test"
if tc.clientErr == nil && tc.status == http.StatusOK {
file := mock_os.NewMockFile(mockCtrl)
if tc.openErr == nil {
if len(tc.content) > 0 {
file.EXPECT().
Write(tc.content).
Return(len(tc.content), tc.writeErr)
}
file.EXPECT().
Close().
Return(tc.closeErr)
file.EXPECT().
Chown(1000, 1000).
Return(tc.chownErr)
}
openFile = func(name string, flag int, perm os.FileMode) (os.File, error) {
assert.Equal(t, filepath, name)
assert.Equal(t, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, flag)
assert.Equal(t, os.FileMode(0400), perm)
return file, tc.openErr
}
}
c := &configurator{
logger: logger,
client: client,
openFile: openFile,
}
err := c.downloadAndSave(ctx, "root hints",
tc.url, filepath,
1000, 1000)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())
@@ -79,65 +144,52 @@ func Test_DownloadRootHints(t *testing.T) { //nolint:dupl
}
}
func Test_DownloadRootKey(t *testing.T) { //nolint:dupl
func Test_DownloadRootHints(t *testing.T) {
t.Parallel()
tests := map[string]struct {
content []byte
status int
clientErr error
writeErr error
err error
}{
"no data": {
status: http.StatusOK,
},
"bad status": {
status: http.StatusBadRequest,
err: fmt.Errorf("HTTP status code is 400 for https://raw.githubusercontent.com/qdm12/files/master/root.key.updated"), //nolint:lll
},
"client error": {
clientErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"write error": {
status: http.StatusOK,
writeErr: fmt.Errorf("error"),
err: fmt.Errorf("error"),
},
"data": {
content: []byte("content"),
status: http.StatusOK,
},
mockCtrl := gomock.NewController(t)
ctx := context.Background()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("downloading %s from %s", "root hints", string(constants.NamedRootURL))
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, string(constants.NamedRootURL), r.URL.String())
return nil, errors.New("test")
}),
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("downloading root key from %s", constants.RootKeyURL).Times(1)
client := mock_network.NewMockClient(mockCtrl)
client.EXPECT().Get(ctx, string(constants.RootKeyURL)).
Return(tc.content, tc.status, tc.clientErr).Times(1)
fileManager := mock_files.NewMockFileManager(mockCtrl)
if tc.clientErr == nil && tc.status == http.StatusOK {
fileManager.EXPECT().WriteToFile(
string(constants.RootKey),
tc.content,
gomock.AssignableToTypeOf(files.Ownership(0, 0)),
gomock.AssignableToTypeOf(files.Ownership(0, 0)),
).Return(tc.writeErr).Times(1)
}
c := &configurator{logger: logger, client: client, fileManager: fileManager}
err := c.DownloadRootKey(ctx, 1000, 1001)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
c := &configurator{
logger: logger,
client: client,
}
err := c.DownloadRootHints(ctx, 1000, 1000)
require.Error(t, err)
assert.Equal(t, `Get "https://raw.githubusercontent.com/qdm12/files/master/named.root.updated": test`, err.Error())
}
func Test_DownloadRootKey(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
ctx := context.Background()
logger := mock_logging.NewMockLogger(mockCtrl)
logger.EXPECT().Info("downloading %s from %s", "root key", string(constants.RootKeyURL))
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, string(constants.RootKeyURL), r.URL.String())
return nil, errors.New("test")
}),
}
c := &configurator{
logger: logger,
client: client,
}
err := c.DownloadRootKey(ctx, 1000, 1000)
require.Error(t, err)
assert.Equal(t, `Get "https://raw.githubusercontent.com/qdm12/files/master/root.key.updated": test`, err.Error())
}

View File

@@ -0,0 +1,9 @@
package dns
import "net/http"
type roundTripFunc func(r *http.Request) (*http.Response, error)
func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return s(r)
}

99
internal/dns/state.go Normal file
View File

@@ -0,0 +1,99 @@
package dns
import (
"fmt"
"reflect"
"sync"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
)
type state struct {
status models.LoopStatus
settings settings.DNS
statusMu sync.RWMutex
settingsMu sync.RWMutex
}
func (s *state) setStatusWithLock(status models.LoopStatus) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
s.status = status
}
func (l *looper) GetStatus() (status models.LoopStatus) {
l.state.statusMu.RLock()
defer l.state.statusMu.RUnlock()
return l.state.status
}
func (l *looper) SetStatus(status models.LoopStatus) (outcome string, err error) {
l.state.statusMu.Lock()
defer l.state.statusMu.Unlock()
existingStatus := l.state.status
switch status {
case constants.Running:
switch existingStatus {
case constants.Starting, constants.Running, constants.Stopping, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Starting
l.state.statusMu.Unlock()
l.start <- struct{}{}
newStatus := <-l.running
l.state.statusMu.Lock()
l.state.status = newStatus
return newStatus.String(), nil
case constants.Stopped:
switch existingStatus {
case constants.Starting, constants.Stopping, constants.Stopped, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Stopping
l.state.statusMu.Unlock()
l.stop <- struct{}{}
<-l.stopped
l.state.statusMu.Lock()
l.state.status = constants.Stopped
return status.String(), nil
default:
return "", fmt.Errorf("status %q can only be %q or %q",
status, constants.Running, constants.Stopped)
}
}
func (l *looper) GetSettings() (settings settings.DNS) {
l.state.settingsMu.RLock()
defer l.state.settingsMu.RUnlock()
return l.state.settings
}
func (l *looper) SetSettings(settings settings.DNS) (outcome string) {
l.state.settingsMu.Lock()
settingsUnchanged := reflect.DeepEqual(l.state.settings, settings)
if settingsUnchanged {
l.state.settingsMu.Unlock()
return "settings left unchanged"
}
tempSettings := l.state.settings
tempSettings.UpdatePeriod = settings.UpdatePeriod
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
l.state.settings = settings
l.state.settingsMu.Unlock()
if onlyUpdatePeriodChanged {
l.updateTicker <- struct{}{}
return "update period changed"
}
_, _ = l.SetStatus(constants.Stopped)
if settings.Enabled {
outcome, _ = l.SetStatus(constants.Running)
}
return outcome
}

View File

@@ -8,8 +8,8 @@ import (
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
// Configurator allows to change firewall rules and modify network routes.
@@ -29,7 +29,7 @@ type configurator struct { //nolint:maligned
commander command.Commander
logger logging.Logger
routing routing.Routing
fileManager files.FileManager // for custom iptables rules
openFile os.OpenFileFunc // for custom iptables rules
iptablesMutex sync.Mutex
debug bool
defaultInterface string
@@ -47,12 +47,12 @@ type configurator struct { //nolint:maligned
}
// NewConfigurator creates a new Configurator instance.
func NewConfigurator(logger logging.Logger, routing routing.Routing, fileManager files.FileManager) Configurator {
func NewConfigurator(logger logging.Logger, routing routing.Routing, openFile os.OpenFileFunc) Configurator {
return &configurator{
commander: command.NewCommander(),
logger: logger.WithPrefix("firewall: "),
routing: routing,
fileManager: fileManager,
openFile: openFile,
allowedInputPorts: make(map[uint16]string),
}
}

View File

@@ -3,7 +3,9 @@ package firewall
import (
"context"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"github.com/qdm12/gluetun/internal/models"
@@ -150,14 +152,18 @@ func (c *configurator) acceptInputToPort(ctx context.Context, intf string, port
}
func (c *configurator) runUserPostRules(ctx context.Context, filepath string, remove bool) error {
exists, err := c.fileManager.FileExists(filepath)
if err != nil {
return err
} else if !exists {
file, err := c.openFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return err
}
b, err := c.fileManager.ReadFile(filepath)
b, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
lines := strings.Split(string(b), "\n")

View File

@@ -1,21 +1,25 @@
package healthcheck
import (
"net"
"errors"
"net/http"
"sync"
"github.com/qdm12/golibs/logging"
)
type handler struct {
logger logging.Logger
resolver *net.Resolver
logger logging.Logger
healthErr error
healthErrMu sync.RWMutex
}
func newHandler(logger logging.Logger, resolver *net.Resolver) http.Handler {
var errHealthcheckNotRunYet = errors.New("healthcheck did not run yet")
func newHandler(logger logging.Logger) *handler {
return &handler{
logger: logger,
resolver: resolver,
logger: logger,
healthErr: errHealthcheckNotRunYet,
}
}
@@ -24,11 +28,22 @@ func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re
http.Error(responseWriter, "method not supported for healthcheck", http.StatusBadRequest)
return
}
err := healthCheck(request.Context(), h.resolver)
if err != nil {
if err := h.getErr(); err != nil {
h.logger.Error(err)
http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
return
}
responseWriter.WriteHeader(http.StatusOK)
}
func (h *handler) setErr(err error) {
h.healthErrMu.Lock()
defer h.healthErrMu.Unlock()
h.healthErr = err
}
func (h *handler) getErr() (err error) {
h.healthErrMu.RLock()
defer h.healthErrMu.RUnlock()
return h.healthErr
}

View File

@@ -2,8 +2,46 @@ package healthcheck
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
)
func (s *server) runHealthcheckLoop(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
err := healthCheck(ctx, s.resolver)
s.handler.setErr(err)
if err != nil { // try again after 1 second
timer := time.NewTimer(time.Second)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
case <-timer.C:
}
continue
}
// Success, check again in 10 minutes
const period = 10 * time.Minute
timer := time.NewTimer(period)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
case <-timer.C:
}
}
}
var (
errNoIPResolved = errors.New("no IP address resolved")
)
func healthCheck(ctx context.Context, resolver *net.Resolver) (err error) {
@@ -12,9 +50,9 @@ func healthCheck(ctx context.Context, resolver *net.Resolver) (err error) {
ips, err := resolver.LookupIP(ctx, "ip", domainToResolve)
switch {
case err != nil:
return fmt.Errorf("cannot resolve github.com: %s", err)
return err
case len(ips) == 0:
return fmt.Errorf("resolved no IP addresses for %s", domainToResolve)
return fmt.Errorf("%w for %s", errNoIPResolved, domainToResolve)
default:
return nil
}

View File

@@ -16,26 +16,36 @@ type Server interface {
}
type server struct {
address string
logger logging.Logger
handler http.Handler
address string
logger logging.Logger
handler *handler
resolver *net.Resolver
}
func NewServer(address string, logger logging.Logger) Server {
healthcheckLogger := logger.WithPrefix("healthcheck: ")
return &server{
address: address,
logger: logger.WithPrefix("healthcheck: "),
handler: newHandler(logger, &net.Resolver{}),
address: address,
logger: healthcheckLogger,
handler: newHandler(healthcheckLogger),
resolver: net.DefaultResolver,
}
}
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
internalWg := &sync.WaitGroup{}
internalWg.Add(1)
go s.runHealthcheckLoop(ctx, internalWg)
server := http.Server{
Addr: s.address,
Handler: s.handler,
}
internalWg.Add(1)
go func() {
defer wg.Done()
defer internalWg.Done()
<-ctx.Done()
s.logger.Warn("context canceled: shutting down server")
defer s.logger.Warn("server shut down")
@@ -46,9 +56,12 @@ func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
s.logger.Error("failed shutting down: %s", err)
}
}()
s.logger.Info("listening on %s", s.address)
err := server.ListenAndServe()
if err != nil && !errors.Is(ctx.Err(), context.Canceled) {
s.logger.Error(err)
}
internalWg.Wait()
}

View File

@@ -4,108 +4,90 @@ import (
"context"
"fmt"
"sync"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/logging"
)
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Restart()
Start()
Stop()
SetStatus(status models.LoopStatus) (outcome string, err error)
GetStatus() (status models.LoopStatus)
GetSettings() (settings settings.HTTPProxy)
SetSettings(settings settings.HTTPProxy)
SetSettings(settings settings.HTTPProxy) (outcome string)
}
type looper struct {
settings settings.HTTPProxy
settingsMutex sync.RWMutex
logger logging.Logger
restart chan struct{}
state state
// Other objects
logger logging.Logger
// Internal channels and locks
loopLock sync.Mutex
running chan models.LoopStatus
stop, stopped chan struct{}
start chan struct{}
stop chan struct{}
backoffTime time.Duration
}
const defaultBackoffTime = 10 * time.Second
func NewLooper(logger logging.Logger, settings settings.HTTPProxy) Looper {
return &looper{
settings: settings,
logger: logger.WithPrefix("http proxy: "),
restart: make(chan struct{}),
start: make(chan struct{}),
stop: make(chan struct{}),
state: state{
status: constants.Stopped,
settings: settings,
},
logger: logger.WithPrefix("http proxy: "),
start: make(chan struct{}),
running: make(chan models.LoopStatus),
stop: make(chan struct{}),
stopped: make(chan struct{}),
backoffTime: defaultBackoffTime,
}
}
func (l *looper) GetSettings() (settings settings.HTTPProxy) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.HTTPProxy) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings = settings
}
func (l *looper) isEnabled() bool {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings.Enabled
}
func (l *looper) setEnabled(enabled bool) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings.Enabled = enabled
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Start() { l.start <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
waitForStart := true
for waitForStart {
select {
case <-l.stop:
l.logger.Info("not started yet")
case <-l.start:
waitForStart = false
case <-l.restart:
waitForStart = false
case <-ctx.Done():
return
}
crashed := false
if l.GetSettings().Enabled {
go func() {
_, _ = l.SetStatus(constants.Running)
}()
}
select {
case <-l.start:
case <-ctx.Done():
return
}
defer l.logger.Warn("loop exited")
for ctx.Err() == nil {
for !l.isEnabled() {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
l.setEnabled(true)
case <-l.start:
l.setEnabled(true)
case <-ctx.Done():
return
}
}
runCtx, runCancel := context.WithCancel(ctx)
settings := l.GetSettings()
address := fmt.Sprintf("0.0.0.0:%d", settings.Port)
address := fmt.Sprintf(":%d", settings.Port)
server := New(runCtx, address, l.logger, settings.Stealth, settings.Log, settings.User, settings.Password)
server := New(ctx, address, l.logger, settings.Stealth, settings.Log, settings.User, settings.Password)
runCtx, runCancel := context.WithCancel(context.Background())
runWg := &sync.WaitGroup{}
runWg.Add(1)
go server.Run(runCtx, runWg)
errorCh := make(chan error)
go server.Run(runCtx, runWg, errorCh)
// TODO stable timer, check Shadowsocks
if !crashed {
l.running <- constants.Running
crashed = false
} else {
l.backoffTime = defaultBackoffTime
l.state.setStatusWithLock(constants.Running)
}
stayHere := true
for stayHere {
@@ -115,21 +97,38 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
runCancel()
runWg.Wait()
return
case <-l.restart: // triggered restart
l.logger.Info("restarting")
case <-l.start:
l.logger.Info("starting")
runCancel()
runWg.Wait()
stayHere = false
case <-l.start:
l.logger.Info("already started")
case <-l.stop:
l.logger.Info("stopping")
runCancel()
runWg.Wait()
l.setEnabled(false)
l.stopped <- struct{}{}
case err := <-errorCh:
runWg.Wait()
l.state.setStatusWithLock(constants.Crashed)
l.logAndWait(ctx, err)
crashed = true
stayHere = false
}
}
runCancel() // repetition for linter only
}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Error(err)
l.logger.Info("retrying in %s", l.backoffTime)
timer := time.NewTimer(l.backoffTime)
l.backoffTime *= 2
select {
case <-timer.C:
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
}
}

View File

@@ -10,7 +10,7 @@ import (
)
type Server interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Run(ctx context.Context, wg *sync.WaitGroup, errorCh chan<- error)
}
type server struct {
@@ -31,13 +31,13 @@ func New(ctx context.Context, address string, logger logging.Logger,
}
}
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup, errorCh chan<- error) {
defer wg.Done()
server := http.Server{Addr: s.address, Handler: s.handler}
go func() {
<-ctx.Done()
s.logger.Warn("context canceled: exiting loop")
defer s.logger.Warn("loop exited")
s.logger.Warn("shutting down server")
defer s.logger.Warn("server shut down")
const shutdownGraceDuration = 2 * time.Second
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
defer cancel()
@@ -47,8 +47,8 @@ func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
}()
s.logger.Info("listening on %s", s.address)
err := server.ListenAndServe()
if err != nil && ctx.Err() != context.Canceled {
s.logger.Error(err)
if err != nil && ctx.Err() == nil {
errorCh <- err
}
s.internalWG.Wait()
}

101
internal/httpproxy/state.go Normal file
View File

@@ -0,0 +1,101 @@
package httpproxy
import (
"fmt"
"reflect"
"sync"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
)
type state struct {
status models.LoopStatus
settings settings.HTTPProxy
statusMu sync.RWMutex
settingsMu sync.RWMutex
}
func (s *state) setStatusWithLock(status models.LoopStatus) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
s.status = status
}
func (l *looper) GetStatus() (status models.LoopStatus) {
l.state.statusMu.RLock()
defer l.state.statusMu.RUnlock()
return l.state.status
}
func (l *looper) SetStatus(status models.LoopStatus) (outcome string, err error) {
l.state.statusMu.Lock()
defer l.state.statusMu.Unlock()
existingStatus := l.state.status
switch status {
case constants.Running:
switch existingStatus {
case constants.Starting, constants.Running, constants.Stopping, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Starting
l.state.statusMu.Unlock()
l.start <- struct{}{}
newStatus := <-l.running
l.state.statusMu.Lock()
l.state.status = newStatus
return newStatus.String(), nil
case constants.Stopped:
switch existingStatus {
case constants.Stopped, constants.Stopping, constants.Starting, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Stopping
l.state.statusMu.Unlock()
l.stop <- struct{}{}
<-l.stopped
l.state.statusMu.Lock()
l.state.status = status
return status.String(), nil
default:
return "", fmt.Errorf("status %q can only be %q or %q",
status, constants.Running, constants.Stopped)
}
}
func (l *looper) GetSettings() (settings settings.HTTPProxy) {
l.state.settingsMu.RLock()
defer l.state.settingsMu.RUnlock()
return l.state.settings
}
func (l *looper) SetSettings(settings settings.HTTPProxy) (outcome string) {
l.state.settingsMu.Lock()
settingsUnchanged := reflect.DeepEqual(settings, l.state.settings)
if settingsUnchanged {
l.state.settingsMu.Unlock()
return "settings left unchanged"
}
newEnabled := settings.Enabled
previousEnabled := l.state.settings.Enabled
l.state.settings = settings
l.state.settingsMu.Unlock()
// Either restart or set changed status
switch {
case !newEnabled && !previousEnabled:
case newEnabled && previousEnabled:
_, _ = l.SetStatus(constants.Stopped)
_, _ = l.SetStatus(constants.Running)
case newEnabled && !previousEnabled:
_, _ = l.SetStatus(constants.Running)
case !newEnabled && previousEnabled:
_, _ = l.SetStatus(constants.Stopped)
}
return "settings updated"
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/qdm12/golibs/logging"
)
//nolint:lll
var regularExpressions = struct { //nolint:gochecknoglobals
unboundPrefix *regexp.Regexp
}{

View File

@@ -20,8 +20,14 @@ type (
VPNProvider string
// NetworkProtocol contains the network protocol to be used to communicate with the VPN servers.
NetworkProtocol string
// Loop status such as stopped or running.
LoopStatus string
)
func (ls LoopStatus) String() string {
return string(ls)
}
func marshalJSONString(s string) (data []byte, err error) {
return []byte(fmt.Sprintf("%q", s)), nil
}

View File

@@ -3,5 +3,5 @@ package models
type BuildInformation struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildDate string `json:"buildDate"`
BuildDate string `json:"build_date"`
}

View File

@@ -1,6 +1,8 @@
package models
import "net"
import (
"net"
)
type OpenVPNConnection struct {
IP net.IP

View File

@@ -9,15 +9,15 @@ import (
// ProviderSettings contains settings specific to a VPN provider.
type ProviderSettings struct {
Name VPNProvider `json:"name"`
ServerSelection ServerSelection `json:"serverSelection"`
ExtraConfigOptions ExtraConfigOptions `json:"extraConfig"`
PortForwarding PortForwarding `json:"portForwarding"`
ServerSelection ServerSelection `json:"server_selection"`
ExtraConfigOptions ExtraConfigOptions `json:"extra_config"`
PortForwarding PortForwarding `json:"port_forwarding"`
}
type ServerSelection struct {
// Common
Protocol NetworkProtocol `json:"networkProtocol"`
TargetIP net.IP `json:"targetIP,omitempty"`
Protocol NetworkProtocol `json:"network_protocol"`
TargetIP net.IP `json:"target_ip,omitempty"`
// Cyberghost, PIA, Surfshark, Windscribe, Vyprvpn, NordVPN
Regions []string `json:"regions"`
@@ -34,20 +34,20 @@ type ServerSelection struct {
Owned bool `json:"owned"`
// Mullvad, Windscribe
CustomPort uint16 `json:"customPort"`
CustomPort uint16 `json:"custom_port"`
// NordVPN
Numbers []uint16 `json:"numbers"`
// PIA
EncryptionPreset string `json:"encryptionPreset"`
EncryptionPreset string `json:"encryption_preset"`
}
type ExtraConfigOptions struct {
ClientCertificate string `json:"-"` // Cyberghost
ClientKey string `json:"-"` // Cyberghost
EncryptionPreset string `json:"encryptionPreset"` // PIA
OpenVPNIPv6 bool `json:"openvpnIPv6"` // Mullvad
ClientCertificate string `json:"-"` // Cyberghost
ClientKey string `json:"-"` // Cyberghost
EncryptionPreset string `json:"encryption_preset"` // PIA
OpenVPNIPv6 bool `json:"openvpn_ipv6"` // Mullvad
}
// PortForwarding contains settings for port forwarding.
@@ -134,8 +134,7 @@ func (p *ProviderSettings) String() string {
)
case "privado":
settingsList = append(settingsList,
"Cities: "+commaJoin(p.ServerSelection.Cities),
"Server numbers: "+commaJoin(numbers),
"Hostnames: "+commaJoin(p.ServerSelection.Hostnames),
)
default:
settingsList = append(settingsList,

View File

@@ -96,15 +96,15 @@ func (s *NordvpnServer) String() string {
}
type PurevpnServer struct {
Region string `json:"region"`
Country string `json:"country"`
Region string `json:"region"`
City string `json:"city"`
IPs []net.IP `json:"ips"`
}
func (s *PurevpnServer) String() string {
return fmt.Sprintf("{Region: %q, Country: %q, City: %q, IPs: %s}",
s.Region, s.Country, s.City, goStringifyIPs(s.IPs))
return fmt.Sprintf("{Country: %q, Region: %q, City: %q, IPs: %s}",
s.Country, s.Region, s.City, goStringifyIPs(s.IPs))
}
type PrivadoServer struct {

View File

@@ -1,31 +1,68 @@
package openvpn
import (
"io/ioutil"
"os"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/files"
)
// WriteAuthFile writes the OpenVPN auth file to disk with the right permissions.
func (c *configurator) WriteAuthFile(user, password string, uid, gid int) error {
exists, err := c.fileManager.FileExists(string(constants.OpenVPNAuthConf))
if err != nil {
func (c *configurator) WriteAuthFile(user, password string, puid, pgid int) error {
const filepath = string(constants.OpenVPNAuthConf)
file, err := c.os.OpenFile(filepath, os.O_RDONLY, 0)
if err != nil && !os.IsNotExist(err) {
return err
} else if exists {
data, err := c.fileManager.ReadFile(string(constants.OpenVPNAuthConf))
}
if os.IsNotExist(err) {
file, err = c.os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0400)
if err != nil {
return err
}
lines := strings.Split(string(data), "\n")
if len(lines) > 1 && lines[0] == user && lines[1] == password {
return nil
_, err = file.WriteString(user + "\n" + password)
if err != nil {
_ = file.Close()
return err
}
c.logger.Info("username and password changed", constants.OpenVPNAuthConf)
err = file.Chown(puid, pgid)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}
return c.fileManager.WriteLinesToFile(
string(constants.OpenVPNAuthConf),
[]string{user, password},
files.Ownership(uid, gid),
files.Permissions(constants.UserReadPermission))
data, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
lines := strings.Split(string(data), "\n")
if len(lines) > 1 && lines[0] == user && lines[1] == password {
return nil
}
c.logger.Info("username and password changed in %s", constants.OpenVPNAuthConf)
file, err = c.os.OpenFile(filepath, os.O_TRUNC|os.O_WRONLY, 0400)
if err != nil {
return err
}
_, err = file.WriteString(user + "\n" + password)
if err != nil {
_ = file.Close()
return err
}
err = file.Chown(puid, pgid)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"net"
"net/http"
"strings"
"sync"
"time"
@@ -14,32 +15,28 @@ import (
"github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
Restart()
PortForward(vpnGatewayIP net.IP)
GetStatus() (status models.LoopStatus)
SetStatus(status models.LoopStatus) (outcome string, err error)
GetSettings() (settings settings.OpenVPN)
SetSettings(settings settings.OpenVPN)
GetPortForwarded() (portForwarded uint16)
SetAllServers(allServers models.AllServers)
SetSettings(settings settings.OpenVPN) (outcome string)
GetServers() (servers models.AllServers)
SetServers(servers models.AllServers)
GetPortForwarded() (port uint16)
PortForward(vpnGatewayIP net.IP)
}
type looper struct {
// Variable parameters
provider models.VPNProvider
settings settings.OpenVPN
settingsMutex sync.RWMutex
portForwarded uint16
portForwardedMutex sync.RWMutex
allServers models.AllServers
allServersMutex sync.RWMutex
state state
// Fixed parameters
uid int
gid int
username string
puid int
pgid int
// Configurators
conf Configurator
fw firewall.Configurator
@@ -47,105 +44,108 @@ type looper struct {
// Other objects
logger, pfLogger logging.Logger
client *http.Client
fileManager files.FileManager
openFile os.OpenFileFunc
streamMerger command.StreamMerger
cancel context.CancelFunc
// Internal channels
restart chan struct{}
// Internal channels and locks
loopLock sync.Mutex
running chan models.LoopStatus
stop, stopped chan struct{}
start chan struct{}
portForwardSignals chan net.IP
crashed bool
backoffTime time.Duration
}
func NewLooper(provider models.VPNProvider, settings settings.OpenVPN,
uid, gid int, allServers models.AllServers,
const defaultBackoffTime = 15 * time.Second
func NewLooper(settings settings.OpenVPN,
username string, puid, pgid int, allServers models.AllServers,
conf Configurator, fw firewall.Configurator, routing routing.Routing,
logger logging.Logger, client *http.Client, fileManager files.FileManager,
logger logging.Logger, client *http.Client, openFile os.OpenFileFunc,
streamMerger command.StreamMerger, cancel context.CancelFunc) Looper {
return &looper{
provider: provider,
settings: settings,
uid: uid,
gid: gid,
allServers: allServers,
state: state{
status: constants.Stopped,
settings: settings,
allServers: allServers,
},
username: username,
puid: puid,
pgid: pgid,
conf: conf,
fw: fw,
routing: routing,
logger: logger.WithPrefix("openvpn: "),
pfLogger: logger.WithPrefix("port forwarding: "),
client: client,
fileManager: fileManager,
openFile: openFile,
streamMerger: streamMerger,
cancel: cancel,
restart: make(chan struct{}),
start: make(chan struct{}),
running: make(chan models.LoopStatus),
stop: make(chan struct{}),
stopped: make(chan struct{}),
portForwardSignals: make(chan net.IP),
backoffTime: defaultBackoffTime,
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) PortForward(vpnGateway net.IP) { l.portForwardSignals <- vpnGateway }
func (l *looper) GetSettings() (settings settings.OpenVPN) {
l.settingsMutex.RLock()
defer l.settingsMutex.RUnlock()
return l.settings
}
func (l *looper) SetSettings(settings settings.OpenVPN) {
l.settingsMutex.Lock()
defer l.settingsMutex.Unlock()
l.settings = settings
}
func (l *looper) SetAllServers(allServers models.AllServers) {
l.allServersMutex.Lock()
defer l.allServersMutex.Unlock()
l.allServers = allServers
func (l *looper) signalCrashedStatus() {
if !l.crashed {
l.crashed = true
l.running <- constants.Crashed
}
}
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-l.restart:
case <-l.start:
case <-ctx.Done():
return
}
defer l.logger.Warn("loop exited")
for ctx.Err() == nil {
settings := l.GetSettings()
l.allServersMutex.RLock()
providerConf := provider.New(l.provider, l.allServers, time.Now)
l.allServersMutex.RUnlock()
settings, allServers := l.state.getSettingsAndServers()
providerConf := provider.New(settings.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetOpenVPNConnection(settings.Provider.ServerSelection)
if err != nil {
l.logger.Error(err)
l.signalCrashedStatus()
l.cancel()
return
}
lines := providerConf.BuildConf(
connection,
settings.Verbosity,
l.uid,
l.gid,
l.username,
settings.Root,
settings.Cipher,
settings.Auth,
settings.Provider.ExtraConfigOptions,
)
if err := l.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines,
files.Ownership(l.uid, l.gid), files.Permissions(constants.UserReadPermission)); err != nil {
if err := writeOpenvpnConf(lines, l.openFile); err != nil {
l.logger.Error(err)
l.signalCrashedStatus()
l.cancel()
return
}
if err := l.conf.WriteAuthFile(settings.User, settings.Password, l.uid, l.gid); err != nil {
if err := l.conf.WriteAuthFile(settings.User, settings.Password, l.puid, l.pgid); err != nil {
l.logger.Error(err)
l.signalCrashedStatus()
l.cancel()
return
}
if err := l.fw.SetVPNConnection(ctx, connection); err != nil {
l.logger.Error(err)
l.signalCrashedStatus()
l.cancel()
return
}
@@ -155,6 +155,7 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
stream, waitFn, err := l.conf.Start(openvpnCtx)
if err != nil {
openvpnCancel()
l.signalCrashedStatus()
l.logAndWait(ctx, err)
continue
}
@@ -179,31 +180,50 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
err := waitFn() // blocking
waitError <- err
}()
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
openvpnCancel()
<-waitError
close(waitError)
return
case <-l.restart: // triggered restart
l.logger.Info("restarting")
openvpnCancel()
<-waitError
close(waitError)
case err := <-waitError: // unexpected error
openvpnCancel()
close(waitError)
l.logAndWait(ctx, err)
if l.crashed {
l.crashed = false
l.backoffTime = defaultBackoffTime
l.state.setStatusWithLock(constants.Running)
} else {
l.running <- constants.Running
}
stayHere := true
for stayHere {
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
openvpnCancel()
<-waitError
close(waitError)
return
case <-l.stop:
l.logger.Info("stopping")
openvpnCancel()
<-waitError
l.stopped <- struct{}{}
case <-l.start:
l.logger.Info("starting")
stayHere = false
case err := <-waitError: // unexpected error
openvpnCancel()
l.state.setStatusWithLock(constants.Crashed)
l.logAndWait(ctx, err)
l.crashed = true
stayHere = false
}
}
close(waitError)
openvpnCancel() // just for the linter
}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Error(err)
const waitTime = 30 * time.Second
l.logger.Info("retrying in %s", waitTime)
timer := time.NewTimer(waitTime)
l.logger.Info("retrying in %s", l.backoffTime)
timer := time.NewTimer(l.backoffTime)
l.backoffTime *= 2
select {
case <-timer.C:
case <-ctx.Done():
@@ -218,24 +238,37 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
func (l *looper) portForward(ctx context.Context, wg *sync.WaitGroup,
providerConf provider.Provider, client *http.Client, gateway net.IP) {
defer wg.Done()
settings := l.GetSettings()
l.state.portForwardedMu.RLock()
settings := l.state.settings
l.state.portForwardedMu.RUnlock()
if !settings.Provider.PortForwarding.Enabled {
return
}
syncState := func(port uint16) (pfFilepath models.Filepath) {
l.portForwardedMutex.Lock()
l.portForwarded = port
l.portForwardedMutex.Unlock()
settings := l.GetSettings()
l.state.portForwardedMu.Lock()
defer l.state.portForwardedMu.Unlock()
l.state.portForwarded = port
l.state.settingsMu.RLock()
defer l.state.settingsMu.RUnlock()
return settings.Provider.PortForwarding.Filepath
}
providerConf.PortForward(ctx,
client, l.fileManager, l.pfLogger,
client, l.openFile, l.pfLogger,
gateway, l.fw, syncState)
}
func (l *looper) GetPortForwarded() (portForwarded uint16) {
l.portForwardedMutex.RLock()
defer l.portForwardedMutex.RUnlock()
return l.portForwarded
func writeOpenvpnConf(lines []string, openFile os.OpenFileFunc) error {
const filepath = string(constants.OpenVPNConf)
file, err := openFile(filepath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
_, err = file.WriteString(strings.Join(lines, "\n"))
if err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
return nil
}

View File

@@ -3,38 +3,33 @@ package openvpn
import (
"context"
"io"
"os"
"github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"golang.org/x/sys/unix"
"github.com/qdm12/golibs/os"
)
type Configurator interface {
Version(ctx context.Context) (string, error)
WriteAuthFile(user, password string, uid, gid int) error
WriteAuthFile(user, password string, puid, pgid int) error
CheckTUN() error
CreateTUN() error
Start(ctx context.Context) (stdout io.ReadCloser, waitFn func() error, err error)
}
type configurator struct {
fileManager files.FileManager
logger logging.Logger
commander command.Commander
openFile func(name string, flag int, perm os.FileMode) (*os.File, error)
mkDev func(major uint32, minor uint32) uint64
mkNod func(path string, mode uint32, dev int) error
logger logging.Logger
commander command.Commander
os os.OS
unix unix.Unix
}
func NewConfigurator(logger logging.Logger, fileManager files.FileManager) Configurator {
func NewConfigurator(logger logging.Logger, os os.OS, unix unix.Unix) Configurator {
return &configurator{
fileManager: fileManager,
logger: logger.WithPrefix("openvpn configurator: "),
commander: command.NewCommander(),
openFile: os.OpenFile,
mkDev: unix.Mkdev,
mkNod: unix.Mknod,
logger: logger.WithPrefix("openvpn configurator: "),
commander: command.NewCommander(),
os: os,
unix: unix,
}
}

121
internal/openvpn/state.go Normal file
View File

@@ -0,0 +1,121 @@
package openvpn
import (
"fmt"
"reflect"
"sync"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
)
type state struct {
status models.LoopStatus
settings settings.OpenVPN
allServers models.AllServers
portForwarded uint16
statusMu sync.RWMutex
settingsMu sync.RWMutex
allServersMu sync.RWMutex
portForwardedMu sync.RWMutex
}
func (s *state) setStatusWithLock(status models.LoopStatus) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
s.status = status
}
func (s *state) getSettingsAndServers() (settings settings.OpenVPN, allServers models.AllServers) {
s.settingsMu.RLock()
s.allServersMu.RLock()
settings = s.settings
allServers = s.allServers
s.settingsMu.RLock()
s.allServersMu.RLock()
return settings, allServers
}
func (l *looper) GetStatus() (status models.LoopStatus) {
l.state.statusMu.RLock()
defer l.state.statusMu.RUnlock()
return l.state.status
}
func (l *looper) SetStatus(status models.LoopStatus) (outcome string, err error) {
l.state.statusMu.Lock()
defer l.state.statusMu.Unlock()
existingStatus := l.state.status
switch status {
case constants.Running:
switch existingStatus {
case constants.Starting, constants.Running, constants.Stopping, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Starting
l.state.statusMu.Unlock()
l.start <- struct{}{}
newStatus := <-l.running
l.state.statusMu.Lock()
l.state.status = newStatus
return newStatus.String(), nil
case constants.Stopped:
switch existingStatus {
case constants.Starting, constants.Stopping, constants.Stopped, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Stopping
l.state.statusMu.Unlock()
l.stop <- struct{}{}
<-l.stopped
l.state.statusMu.Lock()
l.state.status = constants.Stopped
return status.String(), nil
default:
return "", fmt.Errorf("status %q can only be %q or %q",
status, constants.Running, constants.Stopped)
}
}
func (l *looper) GetSettings() (settings settings.OpenVPN) {
l.state.settingsMu.RLock()
defer l.state.settingsMu.RUnlock()
return l.state.settings
}
func (l *looper) SetSettings(settings settings.OpenVPN) (outcome string) {
l.state.settingsMu.Lock()
settingsUnchanged := reflect.DeepEqual(l.state.settings, settings)
if settingsUnchanged {
l.state.settingsMu.Unlock()
return "settings left unchanged"
}
l.state.settings = settings
_, _ = l.SetStatus(constants.Stopped)
outcome, _ = l.SetStatus(constants.Running)
return outcome
}
func (l *looper) GetServers() (servers models.AllServers) {
l.state.allServersMu.RLock()
defer l.state.allServersMu.RUnlock()
return l.state.allServers
}
func (l *looper) SetServers(servers models.AllServers) {
l.state.allServersMu.Lock()
defer l.state.allServersMu.Unlock()
l.state.allServers = servers
}
func (l *looper) GetPortForwarded() (port uint16) {
l.state.portForwardedMu.RLock()
defer l.state.portForwardedMu.RUnlock()
return l.state.portForwarded
}

View File

@@ -5,13 +5,13 @@ import (
"os"
"github.com/qdm12/gluetun/internal/constants"
"golang.org/x/sys/unix"
"github.com/qdm12/gluetun/internal/unix"
)
// CheckTUN checks the tunnel device is present and accessible.
func (c *configurator) CheckTUN() error {
c.logger.Info("checking for device %s", constants.TunnelDevice)
f, err := c.openFile(string(constants.TunnelDevice), os.O_RDWR, 0)
f, err := c.os.OpenFile(string(constants.TunnelDevice), os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("TUN device is not available: %w", err)
}
@@ -23,19 +23,29 @@ func (c *configurator) CheckTUN() error {
func (c *configurator) CreateTUN() error {
c.logger.Info("creating %s", constants.TunnelDevice)
if err := c.fileManager.CreateDir("/dev/net"); err != nil {
if err := c.os.MkdirAll("/dev/net", 0751); err != nil {
return err
}
const (
major = 10
minor = 200
)
dev := c.mkDev(major, minor)
if err := c.mkNod(string(constants.TunnelDevice), unix.S_IFCHR, int(dev)); err != nil {
dev := c.unix.Mkdev(major, minor)
if err := c.unix.Mknod(string(constants.TunnelDevice), unix.S_IFCHR, int(dev)); err != nil {
return err
}
if err := c.fileManager.SetUserPermissions(string(constants.TunnelDevice), 0666); err != nil {
const filepath = string(constants.TunnelDevice)
file, err := c.os.OpenFile(filepath, os.O_WRONLY, 0666)
if err != nil {
return err
}
return nil
const readWriteAllPerms os.FileMode = 0666
if err := file.Chmod(readWriteAllPerms); err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -23,20 +23,15 @@ func (p *reader) GetCyberghostRegions() (regions []string, err error) {
return p.envParams.GetCSVInPossibilities("REGION", constants.CyberghostRegionChoices())
}
// GetCyberghostClientKey obtains the one line client key to use for openvpn from the
// environment variable CLIENT_KEY or from the file at /gluetun/client.key.
// GetCyberghostClientKey obtains the client key to use for openvpn
// from the secret file /run/secrets/openvpn_clientkey or from the file
// /gluetun/client.key.
func (p *reader) GetCyberghostClientKey() (clientKey string, err error) {
clientKey, err = p.envParams.GetEnv("CLIENT_KEY", libparams.CaseSensitiveValue())
if err != nil {
return "", err
} else if len(clientKey) > 0 {
return clientKey, nil
}
content, err := p.fileManager.ReadFile(string(constants.ClientKey))
b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", string(constants.ClientKey))
if err != nil {
return "", err
}
return extractClientKey(content)
return extractClientKey(b)
}
func extractClientKey(b []byte) (key string, err error) {
@@ -52,14 +47,15 @@ func extractClientKey(b []byte) (key string, err error) {
return s, nil
}
// GetCyberghostClientCertificate obtains the client certificate to use for openvpn from the
// file at /gluetun/client.crt.
// GetCyberghostClientCertificate obtains the client certificate to use for openvpn
// from the secret file /run/secrets/openvpn_clientcrt or from the file
// /gluetun/client.crt.
func (p *reader) GetCyberghostClientCertificate() (clientCertificate string, err error) {
content, err := p.fileManager.ReadFile(string(constants.ClientCertificate))
b, err := p.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", string(constants.ClientCertificate))
if err != nil {
return "", err
}
return extractClientCertificate(content)
return extractClientCertificate(b)
}
func extractClientCertificate(b []byte) (certificate string, err error) {

View File

@@ -28,8 +28,7 @@ func (r *reader) GetDNSOverTLSProviders() (providers []models.DNSProvider, err e
provider := models.DNSProvider(word)
switch provider {
case constants.Cloudflare, constants.Google, constants.Quad9,
constants.Quadrant, constants.CleanBrowsing, constants.SecureDNS,
constants.LibreDNS:
constants.Quadrant, constants.CleanBrowsing:
providers = append(providers, provider)
default:
return nil, fmt.Errorf("DNS over TLS provider %q is not valid", provider)
@@ -130,8 +129,8 @@ func (r *reader) GetDNSOverTLSPrivateAddresses() (privateAddresses []string, err
return privateAddresses, nil
}
// GetDNSOverTLSIPv6 obtains if Unbound should resolve ipv6 addresses using ipv6 DNS over TLS
// servers from the environment variable DOT_IPV6.
// GetDNSOverTLSIPv6 obtains if Unbound should resolve ipv6 addresses using
// ipv6 DNS over TLS from the environment variable DOT_IPV6.
func (r *reader) GetDNSOverTLSIPv6() (ipv6 bool, err error) {
return r.envParams.GetOnOff("DOT_IPV6", libparams.Default("off"))
}

View File

@@ -49,26 +49,28 @@ func (r *reader) GetHTTPProxyPort() (port uint16, err error) {
return r.envParams.GetPort("HTTPPROXY_PORT", retroKeysOption, libparams.Default("8888"))
}
// GetHTTPProxyUser obtains the HTTP proxy server user from the environment variable
// HTTPPROXY_USER, and using TINYPROXY_USER and PROXY_USER as retro-compatibility names.
// GetHTTPProxyUser obtains the HTTP proxy server user.
// It first tries to use the HTTPPROXY_USER environment variable (easier for the end user)
// and then tries to read from the secret file httpproxy_user if nothing was found.
func (r *reader) GetHTTPProxyUser() (user string, err error) {
retroKeysOption := libparams.RetroKeys(
const compulsory = false
return r.getFromEnvOrSecretFile(
"HTTPPROXY_USER",
compulsory,
[]string{"TINYPROXY_USER", "PROXY_USER"},
r.onRetroActive,
)
return r.envParams.GetEnv("HTTPPROXY_USER",
retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset())
}
// GetHTTPProxyPassword obtains the HTTP proxy server password from the environment variable
// HTTPPROXY_PASSWORD, and using TINYPROXY_PASSWORD and PROXY_PASSWORD as retro-compatibility names.
// GetHTTPProxyPassword obtains the HTTP proxy server password.
// It first tries to use the HTTPPROXY_PASSWORD environment variable (easier for the end user)
// and then tries to read from the secret file httpproxy_password if nothing was found.
func (r *reader) GetHTTPProxyPassword() (password string, err error) {
retroKeysOption := libparams.RetroKeys(
const compulsory = false
return r.getFromEnvOrSecretFile(
"HTTPPROXY_USER",
compulsory,
[]string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"},
r.onRetroActive,
)
return r.envParams.GetEnv("HTTPPROXY_PASSWORD",
retroKeysOption, libparams.CaseSensitiveValue(), libparams.Unset())
}
// GetHTTPProxyStealth obtains the HTTP proxy server stealth mode

View File

@@ -9,29 +9,19 @@ import (
)
// GetUser obtains the user to use to connect to the VPN servers.
func (r *reader) GetUser() (s string, err error) {
defer func() {
unsetenvErr := r.unsetEnv("USER")
if err == nil {
err = unsetenvErr
}
}()
return r.envParams.GetEnv("USER", libparams.CaseSensitiveValue(), libparams.Compulsory())
// It first tries to use the OPENVPN_USER environment variable (easier for the end user)
// and then tries to read from the secret file openvpn_user if nothing was found.
func (r *reader) GetUser() (user string, err error) {
const compulsory = true
return r.getFromEnvOrSecretFile("OPENVPN_USER", compulsory, []string{"USER"})
}
// GetPassword obtains the password to use to connect to the VPN servers.
func (r *reader) GetPassword(required bool) (s string, err error) {
defer func() {
unsetenvErr := r.unsetEnv("PASSWORD")
if err == nil {
err = unsetenvErr
}
}()
options := []libparams.GetEnvSetter{libparams.CaseSensitiveValue()}
if required {
options = append(options, libparams.Compulsory())
}
return r.envParams.GetEnv("PASSWORD", options...)
// It first tries to use the OPENVPN_PASSWORD environment variable (easier for the end user)
// and then tries to read from the secret file openvpn_password if nothing was found.
func (r *reader) GetPassword() (s string, err error) {
const compulsory = true
return r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", compulsory, []string{"PASSWORD"})
}
// GetNetworkProtocol obtains the network protocol to use to connect to the

View File

@@ -2,12 +2,11 @@ package params
import (
"net"
"os"
"time"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
libparams "github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification"
)
@@ -34,10 +33,10 @@ type Reader interface {
GetDNSKeepNameserver() (on bool, err error)
// System
GetUID() (uid int, err error)
GetGID() (gid int, err error)
GetPUID() (puid int, err error)
GetPGID() (pgid int, err error)
GetTimezone() (timezone string, err error)
GetIPStatusFilepath() (filepath models.Filepath, err error)
GetPublicIPFilepath() (filepath models.Filepath, err error)
// Firewall getters
GetFirewall() (enabled bool, err error)
@@ -48,7 +47,7 @@ type Reader interface {
// VPN getters
GetUser() (s string, err error)
GetPassword(required bool) (s string, err error)
GetPassword() (s string, err error)
GetNetworkProtocol() (protocol models.NetworkProtocol, err error)
GetOpenVPNVerbosity() (verbosity int, err error)
GetOpenVPNRoot() (root bool, err error)
@@ -128,22 +127,20 @@ type Reader interface {
}
type reader struct {
envParams libparams.EnvParams
logger logging.Logger
verifier verification.Verifier
unsetEnv func(key string) error
fileManager files.FileManager
envParams libparams.EnvParams
logger logging.Logger
verifier verification.Verifier
os os.OS
}
// Newreader returns a paramsReadeer object to read parameters from
// environment variables.
func NewReader(logger logging.Logger, fileManager files.FileManager) Reader {
func NewReader(logger logging.Logger, os os.OS) Reader {
return &reader{
envParams: libparams.NewEnvParams(),
logger: logger,
verifier: verification.NewVerifier(),
unsetEnv: os.Unsetenv,
fileManager: fileManager,
envParams: libparams.NewEnvParams(),
logger: logger,
verifier: verification.NewVerifier(),
os: os,
}
}

View File

@@ -2,10 +2,13 @@ package params
import (
"github.com/qdm12/gluetun/internal/constants"
libparams "github.com/qdm12/golibs/params"
)
// GetPrivadoHostnames obtains the hostnames for the Privado server from the
// environment variable HOSTNAME.
// environment variable SERVER_HOSTNAME.
func (r *reader) GetPrivadoHostnames() (hosts []string, err error) {
return r.envParams.GetCSVInPossibilities("HOSTNAME", constants.PrivadoHostnameChoices())
return r.envParams.GetCSVInPossibilities("SERVER_HOSTNAME",
constants.PrivadoHostnameChoices(),
libparams.RetroKeys([]string{"HOSTNAME"}, r.onRetroActive))
}

View File

@@ -3,6 +3,7 @@ package params
import (
"time"
"github.com/qdm12/gluetun/internal/models"
libparams "github.com/qdm12/golibs/params"
)
@@ -15,3 +16,13 @@ func (r *reader) GetPublicIPPeriod() (period time.Duration, err error) {
}
return time.ParseDuration(s)
}
// GetPublicIPFilepath obtains the public IP filepath
// from the environment variable PUBLICIP_FILE with retro-compatible
// environment variable IP_STATUS_FILE.
func (r *reader) GetPublicIPFilepath() (filepath models.Filepath, err error) {
filepathStr, err := r.envParams.GetPath("PUBLICIP_FILE",
libparams.RetroKeys([]string{"IP_STATUS_FILE"}, r.onRetroActive),
libparams.Default("/tmp/gluetun/ip"), libparams.CaseSensitiveValue())
return models.Filepath(filepathStr), err
}

109
internal/params/secrets.go Normal file
View File

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

View File

@@ -32,16 +32,12 @@ func (r *reader) GetShadowSocksPort() (port uint16, err error) {
return uint16(portUint64), err
}
// GetShadowSocksPassword obtains the ShadowSocks server password from the environment variable
// SHADOWSOCKS_PASSWORD.
// GetShadowSocksPassword obtains the ShadowSocks server password.
// It first tries to use the SHADOWSOCKS_PASSWORD environment variable (easier for the end user)
// and then tries to read from the secret file shadowsocks_password if nothing was found.
func (r *reader) GetShadowSocksPassword() (password string, err error) {
defer func() {
unsetErr := r.unsetEnv("SHADOWSOCKS_PASSWORD")
if err == nil {
err = unsetErr
}
}()
return r.envParams.GetEnv("SHADOWSOCKS_PASSWORD", libparams.CaseSensitiveValue())
const compulsory = false
return r.getFromEnvOrSecretFile("SHADOWSOCKS_PASSWORD", compulsory, nil)
}
// GetShadowSocksMethod obtains the ShadowSocks method to use from the environment variable

View File

@@ -1,29 +1,26 @@
package params
import (
"github.com/qdm12/gluetun/internal/models"
libparams "github.com/qdm12/golibs/params"
)
// GetUID obtains the user ID to use from the environment variable UID.
func (r *reader) GetUID() (uid int, err error) {
return r.envParams.GetEnvIntRange("UID", 0, 65535, libparams.Default("1000"))
// GetPUID obtains the user ID to use from the environment variable PUID
// with retro compatible variable UID.
func (r *reader) GetPUID() (ppuid int, err error) {
return r.envParams.GetEnvIntRange("PUID", 0, 65535,
libparams.Default("1000"),
libparams.RetroKeys([]string{"UID"}, r.onRetroActive))
}
// GetGID obtains the group ID to use from the environment variable GID.
func (r *reader) GetGID() (gid int, err error) {
return r.envParams.GetEnvIntRange("GID", 0, 65535, libparams.Default("1000"))
// GetGID obtains the group ID to use from the environment variable PGID
// with retro compatible variable PGID.
func (r *reader) GetPGID() (pgid int, err error) {
return r.envParams.GetEnvIntRange("PGID", 0, 65535,
libparams.Default("1000"),
libparams.RetroKeys([]string{"GID"}, r.onRetroActive))
}
// GetTZ obtains the timezone from the environment variable TZ.
func (r *reader) GetTimezone() (timezone string, err error) {
return r.envParams.GetEnv("TZ")
}
// GetIPStatusFilepath obtains the IP status file path
// from the environment variable IP_STATUS_FILE.
func (r *reader) GetIPStatusFilepath() (filepath models.Filepath, err error) {
filepathStr, err := r.envParams.GetPath("IP_STATUS_FILE",
libparams.Default("/tmp/gluetun/ip"), libparams.CaseSensitiveValue())
return models.Filepath(filepathStr), err
}

View File

@@ -21,9 +21,12 @@ func (r *reader) GetWindscribeCities() (cities []string, err error) {
}
// GetWindscribeHostnames obtains the hostnames for the Windscribe servers from the
// environment variable HOSTNAME.
// environment variable SERVER_HOSTNAME.
func (r *reader) GetWindscribeHostnames() (hostnames []string, err error) {
return r.envParams.GetCSVInPossibilities("HOSTNAME", constants.WindscribeHostnameChoices())
return r.envParams.GetCSVInPossibilities("SERVER_HOSTNAME",
constants.WindscribeHostnameChoices(),
libparams.RetroKeys([]string{"HOSTNAME"}, r.onRetroActive),
)
}
// GetWindscribePort obtains the port to reach the Windscribe server on from the

View File

@@ -11,8 +11,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type cyberghost struct {
@@ -62,8 +62,8 @@ func (c *cyberghost) GetOpenVPNConnection(selection models.ServerSelection) (
return pickRandomConnection(connections, c.randSource), nil
}
func (c *cyberghost) BuildConf(connection models.OpenVPNConnection, verbosity,
uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
func (c *cyberghost) BuildConf(connection models.OpenVPNConnection, verbosity int,
username string, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
}
@@ -105,7 +105,7 @@ func (c *cyberghost) BuildConf(connection models.OpenVPNConnection, verbosity,
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -133,7 +133,7 @@ func (c *cyberghost) BuildConf(connection models.OpenVPNConnection, verbosity,
}
func (c *cyberghost) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for cyberghost")
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type mullvad struct {
@@ -73,7 +73,7 @@ func (m *mullvad) GetOpenVPNConnection(selection models.ServerSelection) (
}
func (m *mullvad) BuildConf(connection models.OpenVPNConnection,
verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
verbosity int, username string, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
}
@@ -114,7 +114,7 @@ func (m *mullvad) BuildConf(connection models.OpenVPNConnection,
lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`)
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -128,7 +128,7 @@ func (m *mullvad) BuildConf(connection models.OpenVPNConnection,
}
func (m *mullvad) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for mullvad")
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type nordvpn struct {
@@ -71,14 +71,13 @@ func (n *nordvpn) GetOpenVPNConnection(selection models.ServerSelection) (
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connection := models.OpenVPNConnection{IP: servers[i].IP, Port: port, Protocol: selection.Protocol}
connections = append(connections, connection)
connections[i] = models.OpenVPNConnection{IP: servers[i].IP, Port: port, Protocol: selection.Protocol}
}
return pickRandomConnection(connections, n.randSource), nil
}
func (n *nordvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int, root bool,
func (n *nordvpn) BuildConf(connection models.OpenVPNConnection, verbosity int, username string, root bool,
cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -115,13 +114,13 @@ func (n *nordvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connection.Protocol)),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -142,7 +141,7 @@ func (n *nordvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
}
func (n *nordvpn) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for nordvpn")
}

View File

@@ -19,8 +19,8 @@ import (
"github.com/qdm12/gluetun/internal/firewall"
gluetunLog "github.com/qdm12/gluetun/internal/logging"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type pia struct {
@@ -109,7 +109,7 @@ func (p *pia) GetOpenVPNConnection(selection models.ServerSelection) (
return connection, nil
}
func (p *pia) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int, root bool,
func (p *pia) BuildConf(connection models.OpenVPNConnection, verbosity int, username string, root bool,
cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
var X509CRL, certificate string
var defaultCipher, defaultAuth string
@@ -161,7 +161,7 @@ func (p *pia) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid
lines = append(lines, "ncp-disable")
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<crl-verify>",
@@ -183,7 +183,7 @@ func (p *pia) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid
//nolint:gocognit
func (p *pia) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
if !p.activeServer.PortForward {
pfLogger.Error("The server %s does not support port forwarding", p.activeServer.Region)
@@ -203,7 +203,7 @@ func (p *pia) PortForward(ctx context.Context, client *http.Client,
return
}
defer pfLogger.Warn("loop exited")
data, err := readPIAPortForwardData(fileManager)
data, err := readPIAPortForwardData(openFile)
if err != nil {
pfLogger.Error(err)
}
@@ -222,7 +222,7 @@ func (p *pia) PortForward(ctx context.Context, client *http.Client,
if !dataFound || expired {
tryUntilSuccessful(ctx, pfLogger, func() error {
data, err = refreshPIAPortForwardData(ctx, client, gateway, fileManager)
data, err = refreshPIAPortForwardData(ctx, client, gateway, openFile)
return err
})
if ctx.Err() != nil {
@@ -240,12 +240,9 @@ func (p *pia) PortForward(ctx context.Context, client *http.Client,
return
}
filepath := syncState(data.Port)
filepath := string(syncState(data.Port))
pfLogger.Info("Writing port to %s", filepath)
if err := fileManager.WriteToFile(
string(filepath), []byte(fmt.Sprintf("%d", data.Port)),
files.Permissions(constants.AllReadWritePermissions),
); err != nil {
if err := writePortForwardedToFile(openFile, filepath, data.Port); err != nil {
pfLogger.Error(err)
}
@@ -281,7 +278,7 @@ func (p *pia) PortForward(ctx context.Context, client *http.Client,
pfLogger.Warn("Forward port has expired on %s, getting another one", data.Expiration.Format(time.RFC1123))
oldPort := data.Port
for {
data, err = refreshPIAPortForwardData(ctx, client, gateway, fileManager)
data, err = refreshPIAPortForwardData(ctx, client, gateway, openFile)
if err != nil {
pfLogger.Error(err)
continue
@@ -298,10 +295,7 @@ func (p *pia) PortForward(ctx context.Context, client *http.Client,
}
filepath := syncState(data.Port)
pfLogger.Info("Writing port to %s", filepath)
if err := fileManager.WriteToFile(
string(filepath), []byte(fmt.Sprintf("%d", data.Port)),
files.Permissions(constants.AllReadWritePermissions),
); err != nil {
if err := writePortForwardedToFile(openFile, string(filepath), data.Port); err != nil {
pfLogger.Error(err)
}
if err := bindPIAPort(ctx, client, gateway, data); err != nil {
@@ -365,8 +359,8 @@ func newPIAHTTPClient(serverName string) (client *http.Client, err error) {
}
func refreshPIAPortForwardData(ctx context.Context, client *http.Client,
gateway net.IP, fileManager files.FileManager) (data piaPortForwardData, err error) {
data.Token, err = fetchPIAToken(ctx, fileManager, client)
gateway net.IP, openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
data.Token, err = fetchPIAToken(ctx, openFile, client)
if err != nil {
return data, fmt.Errorf("cannot obtain token: %w", err)
}
@@ -374,7 +368,7 @@ func refreshPIAPortForwardData(ctx context.Context, client *http.Client,
if err != nil {
return data, fmt.Errorf("cannot obtain port forwarding data: %w", err)
}
if err := writePIAPortForwardData(fileManager, data); err != nil {
if err := writePIAPortForwardData(openFile, data); err != nil {
return data, fmt.Errorf("cannot persist port forwarding information to file: %w", err)
}
return data, nil
@@ -393,34 +387,39 @@ type piaPortForwardData struct {
Expiration time.Time `json:"expires_at"`
}
func readPIAPortForwardData(fileManager files.FileManager) (data piaPortForwardData, err error) {
func readPIAPortForwardData(openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
const filepath = string(constants.PIAPortForward)
exists, err := fileManager.FileExists(filepath)
if err != nil {
return data, err
} else if !exists {
file, err := openFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(err) {
return data, nil
} else if err != nil {
return data, err
}
b, err := fileManager.ReadFile(filepath)
decoder := json.NewDecoder(file)
err = decoder.Decode(&data)
if err != nil {
_ = file.Close()
return data, err
}
if err := json.Unmarshal(b, &data); err != nil {
return data, err
}
return data, nil
return data, file.Close()
}
func writePIAPortForwardData(fileManager files.FileManager, data piaPortForwardData) (err error) {
b, err := json.Marshal(&data)
if err != nil {
return fmt.Errorf("cannot encode data: %w", err)
}
err = fileManager.WriteToFile(string(constants.PIAPortForward), b)
func writePIAPortForwardData(openFile os.OpenFileFunc, data piaPortForwardData) (err error) {
const filepath = string(constants.PIAPortForward)
file, err := openFile(filepath,
os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
0644)
if err != nil {
return err
}
return nil
encoder := json.NewEncoder(file)
err = encoder.Encode(data)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}
func unpackPIAPayload(payload string) (port uint16, token string, expiration time.Time, err error) {
@@ -449,8 +448,9 @@ func packPIAPayload(port uint16, token string, expiration time.Time) (payload st
return payload, nil
}
func fetchPIAToken(ctx context.Context, fileManager files.FileManager, client *http.Client) (token string, err error) {
username, password, err := getOpenvpnCredentials(fileManager)
func fetchPIAToken(ctx context.Context, openFile os.OpenFileFunc,
client *http.Client) (token string, err error) {
username, password, err := getOpenvpnCredentials(openFile)
if err != nil {
return "", fmt.Errorf("cannot get Openvpn credentials: %w", err)
}
@@ -469,19 +469,18 @@ func fetchPIAToken(ctx context.Context, fileManager files.FileManager, client *h
return "", err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if response.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(response.Body)
shortenMessage := string(b)
shortenMessage = strings.ReplaceAll(shortenMessage, "\n", "")
shortenMessage = strings.ReplaceAll(shortenMessage, " ", " ")
return "", fmt.Errorf("%s: response received: %q", response.Status, shortenMessage)
} else if err != nil {
return "", err
}
decoder := json.NewDecoder(response.Body)
var result struct {
Token string `json:"token"`
}
if err := json.Unmarshal(b, &result); err != nil {
if err := decoder.Decode(&result); err != nil {
return "", err
} else if len(result.Token) == 0 {
return "", fmt.Errorf("token is empty")
@@ -489,10 +488,19 @@ func fetchPIAToken(ctx context.Context, fileManager files.FileManager, client *h
return result.Token, nil
}
func getOpenvpnCredentials(fileManager files.FileManager) (username, password string, err error) {
authData, err := fileManager.ReadFile(string(constants.OpenVPNAuthConf))
func getOpenvpnCredentials(openFile os.OpenFileFunc) (username, password string, err error) {
const filepath = string(constants.OpenVPNAuthConf)
file, err := openFile(filepath, os.O_RDONLY, 0)
if err != nil {
return "", "", fmt.Errorf("cannot read openvpn auth file: %w", err)
return "", "", fmt.Errorf("cannot read openvpn auth file: %s", err)
}
authData, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return "", "", fmt.Errorf("cannot read openvpn auth file: %s", err)
}
if err := file.Close(); err != nil {
return "", "", err
}
lines := strings.Split(string(authData), "\n")
const minLines = 2
@@ -525,16 +533,13 @@ func fetchPIAPortForwardData(ctx context.Context, client *http.Client, gateway n
if response.StatusCode != http.StatusOK {
return 0, "", expiration, fmt.Errorf("cannot obtain signature: %s", response.Status)
}
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return 0, "", expiration, fmt.Errorf("cannot obtain signature: %w", err)
}
decoder := json.NewDecoder(response.Body)
var data struct {
Status string `json:"status"`
Payload string `json:"payload"`
Signature string `json:"signature"`
}
if err := json.Unmarshal(b, &data); err != nil {
if err := decoder.Decode(&data); err != nil {
return 0, "", expiration, fmt.Errorf("cannot decode received data: %w", err)
} else if data.Status != "OK" {
return 0, "", expiration, fmt.Errorf("response received from PIA has status %s", data.Status)
@@ -571,18 +576,30 @@ func bindPIAPort(ctx context.Context, client *http.Client, gateway net.IP, data
if response.StatusCode != http.StatusOK {
return fmt.Errorf("cannot bind port: %s", response.Status)
}
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("cannot bind port: %w", err)
}
decoder := json.NewDecoder(response.Body)
var responseData struct {
Status string `json:"status"`
Message string `json:"message"`
}
if err := json.Unmarshal(b, &responseData); err != nil {
if err := decoder.Decode(&responseData); err != nil {
return fmt.Errorf("cannot bind port: %w", err)
} else if responseData.Status != "OK" {
return fmt.Errorf("response received from PIA: %s (%s)", responseData.Status, responseData.Message)
}
return nil
}
func writePortForwardedToFile(openFile os.OpenFileFunc,
filepath string, port uint16) (err error) {
file, err := openFile(filepath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
_, err = file.Write([]byte(fmt.Sprintf("%d", port)))
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type privado struct {
@@ -64,13 +64,13 @@ func (s *privado) GetOpenVPNConnection(selection models.ServerSelection) (
Protocol: selection.Protocol,
Hostname: servers[i].Hostname,
}
connections = append(connections, connection)
connections[i] = connection
}
return pickRandomConnection(connections, s.randSource), nil
}
func (s *privado) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int, root bool,
func (s *privado) BuildConf(connection models.OpenVPNConnection, verbosity int, username string, root bool,
cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -104,7 +104,7 @@ func (s *privado) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -117,7 +117,7 @@ func (s *privado) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
}
func (s *privado) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for privado")
}

View File

@@ -8,17 +8,17 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
// Provider contains methods to read and modify the openvpn configuration to connect as a client.
type Provider interface {
GetOpenVPNConnection(selection models.ServerSelection) (connection models.OpenVPNConnection, err error)
BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int,
BuildConf(connection models.OpenVPNConnection, verbosity int, username string,
root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string)
PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath))
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type purevpn struct {
@@ -72,7 +72,7 @@ func (p *purevpn) GetOpenVPNConnection(selection models.ServerSelection) (
return pickRandomConnection(connections, p.randSource), nil
}
func (p *purevpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int, root bool,
func (p *purevpn) BuildConf(connection models.OpenVPNConnection, verbosity int, username string, root bool,
cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -103,12 +103,12 @@ func (p *purevpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connection.Protocol)),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port),
fmt.Sprintf("cipher %s", cipher),
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -150,7 +150,7 @@ func (p *purevpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
}
func (p *purevpn) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for purevpn")
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type surfshark struct {
@@ -73,7 +73,7 @@ func (s *surfshark) GetOpenVPNConnection(selection models.ServerSelection) (
return pickRandomConnection(connections, s.randSource), nil
}
func (s *surfshark) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int, root bool,
func (s *surfshark) BuildConf(connection models.OpenVPNConnection, verbosity int, username string, root bool,
cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -104,6 +104,7 @@ func (s *surfshark) BuildConf(connection models.OpenVPNConnection, verbosity, ui
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
"auth-retry nointeract",
"suppress-timestamps",
@@ -116,7 +117,7 @@ func (s *surfshark) BuildConf(connection models.OpenVPNConnection, verbosity, ui
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -137,7 +138,7 @@ func (s *surfshark) BuildConf(connection models.OpenVPNConnection, verbosity, ui
}
func (s *surfshark) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for surfshark")
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type vyprvpn struct {
@@ -69,7 +69,7 @@ func (v *vyprvpn) GetOpenVPNConnection(selection models.ServerSelection) (
return pickRandomConnection(connections, v.randSource), nil
}
func (v *vyprvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int,
func (v *vyprvpn) BuildConf(connection models.OpenVPNConnection, verbosity int, username string,
root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -106,7 +106,7 @@ func (v *vyprvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -119,7 +119,7 @@ func (v *vyprvpn) BuildConf(connection models.OpenVPNConnection, verbosity, uid,
}
func (v *vyprvpn) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for vyprvpn")
}

View File

@@ -11,8 +11,8 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type windscribe struct {
@@ -65,14 +65,14 @@ func (w *windscribe) GetOpenVPNConnection(selection models.ServerSelection) (con
}
connections := make([]models.OpenVPNConnection, len(servers))
for _, server := range servers {
connections = append(connections, models.OpenVPNConnection{IP: server.IP, Port: port, Protocol: selection.Protocol})
for i := range servers {
connections[i] = models.OpenVPNConnection{IP: servers[i].IP, Port: port, Protocol: selection.Protocol}
}
return pickRandomConnection(connections, w.randSource), nil
}
func (w *windscribe) BuildConf(connection models.OpenVPNConnection, verbosity, uid, gid int,
func (w *windscribe) BuildConf(connection models.OpenVPNConnection, verbosity int, username string,
root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
if len(cipher) == 0 {
cipher = aes256cbc
@@ -111,7 +111,7 @@ func (w *windscribe) BuildConf(connection models.OpenVPNConnection, verbosity, u
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !root {
lines = append(lines, "user nonrootuser")
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
@@ -132,7 +132,7 @@ func (w *windscribe) BuildConf(connection models.OpenVPNConnection, verbosity, u
}
func (w *windscribe) PortForward(ctx context.Context, client *http.Client,
fileManager files.FileManager, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath models.Filepath)) {
panic("port forwarding is not supported for windscribe")
}

View File

@@ -0,0 +1,8 @@
package publicip
import "errors"
var (
ErrBadStatusCode = errors.New("bad HTTP status")
ErrCannotReadBody = errors.New("cannot read response body")
)

27
internal/publicip/fs.go Normal file
View File

@@ -0,0 +1,27 @@
package publicip
import "github.com/qdm12/golibs/os"
func persistPublicIP(openFile os.OpenFileFunc,
filepath string, content string, puid, pgid int) error {
file, err := openFile(
filepath,
os.O_TRUNC|os.O_WRONLY|os.O_CREATE,
0644)
if err != nil {
return err
}
_, err = file.WriteString(content)
if err != nil {
_ = file.Close()
return err
}
if err := file.Chown(puid, pgid); err != nil {
_ = file.Close()
return err
}
return file.Close()
}

View File

@@ -2,79 +2,82 @@ package publicip
import (
"context"
"net"
"net/http"
"sync"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/files"
"github.com/qdm12/gluetun/internal/settings"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/os"
)
type Looper interface {
Run(ctx context.Context, wg *sync.WaitGroup)
RunRestartTicker(ctx context.Context, wg *sync.WaitGroup)
Restart()
Stop()
GetPeriod() (period time.Duration)
SetPeriod(period time.Duration)
GetStatus() (status models.LoopStatus)
SetStatus(status models.LoopStatus) (outcome string, err error)
GetSettings() (settings settings.PublicIP)
SetSettings(settings settings.PublicIP) (outcome string)
GetPublicIP() (publicIP net.IP)
}
type looper struct {
period time.Duration
periodMutex sync.RWMutex
getter IPGetter
logger logging.Logger
fileManager files.FileManager
ipStatusFilepath models.Filepath
uid int
gid int
restart chan struct{}
stop chan struct{}
updateTicker chan struct{}
timeNow func() time.Time
timeSince func(time.Time) time.Duration
state state
// Objects
getter IPGetter
logger logging.Logger
os os.OS
// Fixed settings
puid int
pgid int
// Internal channels and locks
loopLock sync.Mutex
start chan struct{}
running chan models.LoopStatus
stop chan struct{}
stopped chan struct{}
updateTicker chan struct{}
backoffTime time.Duration
// Mock functions
timeNow func() time.Time
timeSince func(time.Time) time.Duration
}
func NewLooper(client network.Client, logger logging.Logger, fileManager files.FileManager,
ipStatusFilepath models.Filepath, period time.Duration, uid, gid int) Looper {
const defaultBackoffTime = 5 * time.Second
func NewLooper(client *http.Client, logger logging.Logger,
settings settings.PublicIP, puid, pgid int,
os os.OS) Looper {
return &looper{
period: period,
getter: NewIPGetter(client),
logger: logger.WithPrefix("ip getter: "),
fileManager: fileManager,
ipStatusFilepath: ipStatusFilepath,
uid: uid,
gid: gid,
restart: make(chan struct{}),
stop: make(chan struct{}),
updateTicker: make(chan struct{}),
timeNow: time.Now,
timeSince: time.Since,
state: state{
status: constants.Stopped,
settings: settings,
},
// Objects
getter: NewIPGetter(client),
logger: logger.WithPrefix("ip getter: "),
os: os,
puid: puid,
pgid: pgid,
start: make(chan struct{}),
running: make(chan models.LoopStatus),
stop: make(chan struct{}),
stopped: make(chan struct{}),
updateTicker: make(chan struct{}),
backoffTime: defaultBackoffTime,
timeNow: time.Now,
timeSince: time.Since,
}
}
func (l *looper) Restart() { l.restart <- struct{}{} }
func (l *looper) Stop() { l.stop <- struct{}{} }
func (l *looper) GetPeriod() (period time.Duration) {
l.periodMutex.RLock()
defer l.periodMutex.RUnlock()
return l.period
}
func (l *looper) SetPeriod(period time.Duration) {
l.periodMutex.Lock()
l.period = period
l.periodMutex.Unlock()
l.updateTicker <- struct{}{}
}
func (l *looper) logAndWait(ctx context.Context, err error) {
l.logger.Error(err)
const waitTime = 5 * time.Second
l.logger.Info("retrying in %s", waitTime)
timer := time.NewTimer(waitTime)
l.logger.Info("retrying in %s", l.backoffTime)
timer := time.NewTimer(l.backoffTime)
l.backoffTime *= 2
select {
case <-timer.C:
case <-ctx.Done():
@@ -86,53 +89,83 @@ func (l *looper) logAndWait(ctx context.Context, err error) {
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
crashed := false
select {
case <-l.restart:
case <-l.start:
case <-ctx.Done():
return
}
defer l.logger.Warn("loop exited")
enabled := true
for ctx.Err() == nil {
for !enabled {
// wait for a signal to re-enable
select {
case <-l.stop:
l.logger.Info("already disabled")
case <-l.restart:
enabled = true
case <-ctx.Done():
getCtx, getCancel := context.WithCancel(ctx)
defer getCancel()
ipCh := make(chan net.IP)
errorCh := make(chan error)
go func() {
ip, err := l.getter.Get(getCtx)
if err != nil {
if getCtx.Err() == nil {
errorCh <- err
}
return
}
ipCh <- ip
}()
if !crashed {
l.running <- constants.Running
crashed = false
} else {
l.backoffTime = defaultBackoffTime
l.state.setStatusWithLock(constants.Running)
}
// Enabled and has a period set
ip, err := l.getter.Get(ctx)
if err != nil {
l.logAndWait(ctx, err)
continue
}
l.logger.Info("Public IP address is %s", ip)
const userReadWritePermissions = 0600
err = l.fileManager.WriteLinesToFile(
string(l.ipStatusFilepath),
[]string{ip.String()},
files.Ownership(l.uid, l.gid),
files.Permissions(userReadWritePermissions))
if err != nil {
l.logAndWait(ctx, err)
continue
}
select {
case <-l.restart: // triggered restart
case <-l.stop:
enabled = false
case <-ctx.Done():
return
stayHere := true
for stayHere {
select {
case <-ctx.Done():
l.logger.Warn("context canceled: exiting loop")
getCancel()
close(errorCh)
filepath := l.GetSettings().IPFilepath
l.logger.Info("Removing ip file %s", filepath)
if err := l.os.Remove(string(filepath)); err != nil {
l.logger.Error(err)
}
return
case <-l.start:
l.logger.Info("starting")
getCancel()
stayHere = false
case <-l.stop:
l.logger.Info("stopping")
getCancel()
<-errorCh
l.stopped <- struct{}{}
case ip := <-ipCh:
getCancel()
l.state.setPublicIP(ip)
l.logger.Info("Public IP address is %s", ip)
filepath := string(l.state.settings.IPFilepath)
err := persistPublicIP(l.os.OpenFile, filepath, ip.String(), l.puid, l.pgid)
if err != nil {
l.logger.Error(err)
}
l.state.setStatusWithLock(constants.Completed)
case err := <-errorCh:
getCancel()
close(ipCh)
l.state.setStatusWithLock(constants.Crashed)
l.logAndWait(ctx, err)
crashed = true
stayHere = false
}
}
close(errorCh)
}
}
@@ -141,10 +174,9 @@ func (l *looper) RunRestartTicker(ctx context.Context, wg *sync.WaitGroup) {
timer := time.NewTimer(time.Hour)
timer.Stop() // 1 hour, cannot be a race condition
timerIsStopped := true
period := l.GetPeriod()
if period > 0 {
timer.Reset(period)
if period := l.GetSettings().Period; period > 0 {
timerIsStopped = false
timer.Reset(period)
}
lastTick := time.Unix(0, 0)
for {
@@ -156,14 +188,14 @@ func (l *looper) RunRestartTicker(ctx context.Context, wg *sync.WaitGroup) {
return
case <-timer.C:
lastTick = l.timeNow()
l.restart <- struct{}{}
timer.Reset(l.GetPeriod())
l.start <- struct{}{}
timer.Reset(l.GetSettings().Period)
case <-l.updateTicker:
if !timer.Stop() {
if !timerIsStopped && !timer.Stop() {
<-timer.C
}
timerIsStopped = true
period := l.GetPeriod()
period := l.GetSettings().Period
if period == 0 {
continue
}

View File

@@ -3,12 +3,11 @@ package publicip
import (
"context"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"strings"
"github.com/qdm12/golibs/network"
)
type IPGetter interface {
@@ -16,11 +15,11 @@ type IPGetter interface {
}
type ipGetter struct {
client network.Client
client *http.Client
randIntn func(n int) int
}
func NewIPGetter(client network.Client) IPGetter {
func NewIPGetter(client *http.Client) IPGetter {
return &ipGetter{
client: client,
randIntn: rand.Intn,
@@ -39,12 +38,27 @@ func (i *ipGetter) Get(ctx context.Context) (ip net.IP, err error) {
"https://ipinfo.io/ip",
}
url := urls[i.randIntn(len(urls))]
content, status, err := i.client.Get(ctx, url, network.UseRandomUserAgent())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
} else if status != http.StatusOK {
return nil, fmt.Errorf("received unexpected status code %d from %s", status, url)
}
response, err := i.client.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w from %s: %s", ErrBadStatusCode, url, response.Status)
}
content, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrCannotReadBody, err)
}
s := strings.ReplaceAll(string(content), "\n", "")
ip = net.ParseIP(s)
if ip == nil {

110
internal/publicip/state.go Normal file
View File

@@ -0,0 +1,110 @@
package publicip
import (
"fmt"
"net"
"reflect"
"sync"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/settings"
)
type state struct {
status models.LoopStatus
settings settings.PublicIP
ip net.IP
statusMu sync.RWMutex
settingsMu sync.RWMutex
ipMu sync.RWMutex
}
func (s *state) setStatusWithLock(status models.LoopStatus) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
s.status = status
}
func (l *looper) GetStatus() (status models.LoopStatus) {
l.state.statusMu.RLock()
defer l.state.statusMu.RUnlock()
return l.state.status
}
func (l *looper) SetStatus(status models.LoopStatus) (outcome string, err error) {
l.state.statusMu.Lock()
defer l.state.statusMu.Unlock()
existingStatus := l.state.status
switch status {
case constants.Running:
switch existingStatus {
case constants.Starting, constants.Running, constants.Stopping, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Starting
l.state.statusMu.Unlock()
l.start <- struct{}{}
newStatus := <-l.running
l.state.statusMu.Lock()
l.state.status = newStatus
return newStatus.String(), nil
case constants.Stopped:
switch existingStatus {
case constants.Stopped, constants.Stopping, constants.Starting, constants.Crashed:
return fmt.Sprintf("already %s", existingStatus), nil
}
l.loopLock.Lock()
defer l.loopLock.Unlock()
l.state.status = constants.Stopping
l.state.statusMu.Unlock()
l.stop <- struct{}{}
<-l.stopped
l.state.statusMu.Lock()
l.state.status = status
return status.String(), nil
default:
return "", fmt.Errorf("status %q can only be %q or %q",
status, constants.Running, constants.Stopped)
}
}
func (l *looper) GetSettings() (settings settings.PublicIP) {
l.state.settingsMu.RLock()
defer l.state.settingsMu.RUnlock()
return l.state.settings
}
func (l *looper) SetSettings(settings settings.PublicIP) (outcome string) {
l.state.settingsMu.Lock()
defer l.state.settingsMu.Unlock()
settingsUnchanged := reflect.DeepEqual(settings, l.state.settings)
if settingsUnchanged {
return "settings left unchanged"
}
periodChanged := l.state.settings.Period != settings.Period
l.state.settings = settings
if periodChanged {
l.updateTicker <- struct{}{}
// TODO blocking
}
return "settings updated"
}
func (l *looper) GetPublicIP() (publicIP net.IP) {
l.state.ipMu.RLock()
defer l.state.ipMu.RUnlock()
publicIP = make(net.IP, len(l.state.ip))
copy(publicIP, l.state.ip)
return publicIP
}
func (s *state) setPublicIP(publicIP net.IP) {
s.ipMu.Lock()
defer s.ipMu.Unlock()
s.ip = make(net.IP, len(publicIP))
copy(s.ip, publicIP)
}

76
internal/server/dns.go Normal file
View File

@@ -0,0 +1,76 @@
//nolint:dupl
package server
import (
"encoding/json"
"net/http"
"strings"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/golibs/logging"
)
func newDNSHandler(looper dns.Looper, logger logging.Logger) http.Handler {
return &dnsHandler{
looper: looper,
logger: logger,
}
}
type dnsHandler struct {
looper dns.Looper
logger logging.Logger
}
func (h *dnsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/dns")
switch r.RequestURI {
case "/status": //nolint:goconst
switch r.Method {
case http.MethodGet:
h.getStatus(w)
case http.MethodPut:
h.setStatus(w, r)
default:
http.Error(w, "", http.StatusNotFound)
}
default:
http.Error(w, "", http.StatusNotFound)
}
}
func (h *dnsHandler) getStatus(w http.ResponseWriter) {
status := h.looper.GetStatus()
encoder := json.NewEncoder(w)
data := statusWrapper{Status: string(status)}
if err := encoder.Encode(data); err != nil {
h.logger.Warn(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (h *dnsHandler) setStatus(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var data statusWrapper
if err := decoder.Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
status, err := data.getStatus()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
outcome, err := h.looper.SetStatus(status)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
encoder := json.NewEncoder(w)
if err := encoder.Encode(outcomeWrapper{Outcome: outcome}); err != nil {
h.logger.Warn(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}

View File

@@ -1,12 +1,13 @@
package server
import (
"fmt"
"net/http"
"strings"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/publicip"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/golibs/logging"
)
@@ -16,55 +17,36 @@ func newHandler(logger logging.Logger, logging bool,
openvpnLooper openvpn.Looper,
unboundLooper dns.Looper,
updaterLooper updater.Looper,
publicIPLooper publicip.Looper,
) http.Handler {
return &handler{
logger: logger,
logging: logging,
buildInfo: buildInfo,
openvpnLooper: openvpnLooper,
unboundLooper: unboundLooper,
updaterLooper: updaterLooper,
}
handler := &handler{}
openvpn := newOpenvpnHandler(openvpnLooper, logger)
dns := newDNSHandler(unboundLooper, logger)
updater := newUpdaterHandler(updaterLooper, logger)
publicip := newPublicIPHandler(publicIPLooper, logger)
handler.v0 = newHandlerV0(logger, openvpnLooper, unboundLooper, updaterLooper)
handler.v1 = newHandlerV1(logger, buildInfo, openvpn, dns, updater, publicip)
handlerWithLog := withLogMiddleware(handler, logger, logging)
handler.setLogEnabled = handlerWithLog.setEnabled
return handlerWithLog
}
type handler struct {
logger logging.Logger
logging bool
buildInfo models.BuildInformation
openvpnLooper openvpn.Looper
unboundLooper dns.Looper
updaterLooper updater.Looper
v0 http.Handler
v1 http.Handler
setLogEnabled func(enabled bool)
}
func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
if h.logging {
h.logger.Info("HTTP %s %s", request.Method, request.RequestURI)
}
switch request.Method {
case http.MethodGet:
switch request.RequestURI {
case "/version":
h.getVersion(responseWriter)
responseWriter.WriteHeader(http.StatusOK)
case "/openvpn/actions/restart":
h.openvpnLooper.Restart()
responseWriter.WriteHeader(http.StatusOK)
case "/unbound/actions/restart":
h.unboundLooper.Restart()
responseWriter.WriteHeader(http.StatusOK)
case "/openvpn/portforwarded":
h.getPortForwarded(responseWriter)
case "/openvpn/settings":
h.getOpenvpnSettings(responseWriter)
case "/updater/restart":
h.updaterLooper.Restart()
responseWriter.WriteHeader(http.StatusOK)
default:
errString := fmt.Sprintf("Nothing here for %s %s", request.Method, request.RequestURI)
http.Error(responseWriter, errString, http.StatusBadRequest)
}
default:
errString := fmt.Sprintf("Nothing here for %s %s", request.Method, request.RequestURI)
http.Error(responseWriter, errString, http.StatusBadRequest)
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.RequestURI = strings.TrimSuffix(r.RequestURI, "/")
if !strings.HasPrefix(r.RequestURI, "/v1/") && r.RequestURI != "/v1" {
h.v0.ServeHTTP(w, r)
return
}
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/v1")
h.v1.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,69 @@
package server
import (
"net/http"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/dns"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/golibs/logging"
)
func newHandlerV0(logger logging.Logger,
openvpn openvpn.Looper, dns dns.Looper, updater updater.Looper) http.Handler {
return &handlerV0{
logger: logger,
openvpn: openvpn,
dns: dns,
updater: updater,
}
}
type handlerV0 struct {
logger logging.Logger
openvpn openvpn.Looper
dns dns.Looper
updater updater.Looper
}
func (h *handlerV0) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "unversioned API: only supports GET method", http.StatusNotFound)
return
}
switch r.RequestURI {
case "/version":
http.Redirect(w, r, "/v1/version", http.StatusPermanentRedirect)
case "/openvpn/actions/restart":
outcome, _ := h.openvpn.SetStatus(constants.Stopped)
h.logger.Info("openvpn: %s", outcome)
outcome, _ = h.openvpn.SetStatus(constants.Running)
h.logger.Info("openvpn: %s", outcome)
if _, err := w.Write([]byte("openvpn restarted, please consider using the /v1/ API in the future.")); err != nil {
h.logger.Warn(err)
}
case "/unbound/actions/restart":
outcome, _ := h.dns.SetStatus(constants.Stopped)
h.logger.Info("dns: %s", outcome)
outcome, _ = h.dns.SetStatus(constants.Running)
h.logger.Info("dns: %s", outcome)
if _, err := w.Write([]byte("dns restarted, please consider using the /v1/ API in the future.")); err != nil {
h.logger.Warn(err)
}
case "/openvpn/portforwarded":
http.Redirect(w, r, "/v1/openvpn/portforwarded", http.StatusPermanentRedirect)
case "/openvpn/settings":
http.Redirect(w, r, "/v1/openvpn/settings", http.StatusPermanentRedirect)
case "/updater/restart":
outcome, _ := h.updater.SetStatus(constants.Stopped)
h.logger.Info("updater: %s", outcome)
outcome, _ = h.updater.SetStatus(constants.Running)
h.logger.Info("updater: %s", outcome)
if _, err := w.Write([]byte("updater restarted, please consider using the /v1/ API in the future.")); err != nil {
h.logger.Warn(err)
}
default:
http.Error(w, "unversioned API: requested URI not found", http.StatusNotFound)
}
}

View File

@@ -0,0 +1,58 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
)
func newHandlerV1(logger logging.Logger, buildInfo models.BuildInformation,
openvpn, dns, updater, publicip http.Handler) http.Handler {
return &handlerV1{
logger: logger,
buildInfo: buildInfo,
openvpn: openvpn,
dns: dns,
updater: updater,
publicip: publicip,
}
}
type handlerV1 struct {
logger logging.Logger
buildInfo models.BuildInformation
openvpn http.Handler
dns http.Handler
updater http.Handler
publicip http.Handler
}
func (h *handlerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case r.RequestURI == "/version" && r.Method == http.MethodGet:
h.getVersion(w)
case strings.HasPrefix(r.RequestURI, "/openvpn"):
h.openvpn.ServeHTTP(w, r)
case strings.HasPrefix(r.RequestURI, "/dns"):
h.dns.ServeHTTP(w, r)
case strings.HasPrefix(r.RequestURI, "/updater"):
h.updater.ServeHTTP(w, r)
case strings.HasPrefix(r.RequestURI, "/publicip"):
h.publicip.ServeHTTP(w, r)
default:
errString := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
http.Error(w, errString, http.StatusNotFound)
}
}
func (h *handlerV1) getVersion(w http.ResponseWriter) {
encoder := json.NewEncoder(w)
if err := encoder.Encode(h.buildInfo); err != nil {
h.logger.Warn(err)
w.WriteHeader(http.StatusInternalServerError)
}
}

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