Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f52df9747 | ||
|
|
f04fd845bb | ||
|
|
5dcbe79fa8 | ||
|
|
574ac9a603 | ||
|
|
6871444728 | ||
|
|
f4db7e3e53 | ||
|
|
da92b6bfb9 | ||
|
|
d713782fe1 | ||
|
|
02cde5f50b | ||
|
|
c5a7a83d3a | ||
|
|
6655a1a5e6 | ||
|
|
b8cb181070 | ||
|
|
a56471fe73 | ||
|
|
8c769812ae | ||
|
|
f7a842e4ee | ||
|
|
23c0334f68 | ||
|
|
e2ee7a0408 | ||
|
|
8f862b3df7 | ||
|
|
ae1f91a997 | ||
|
|
d4fb76770f | ||
|
|
ea28c791e6 | ||
|
|
251555f859 | ||
|
|
fa7bda7ee4 | ||
|
|
f385c4203a | ||
|
|
1e4243dedb | ||
|
|
5f78ee7b79 | ||
|
|
c6eb5c1785 | ||
|
|
11338b6382 | ||
|
|
6f3a074e00 | ||
|
|
e827079604 | ||
|
|
cf66db8d4b | ||
|
|
25acbf8501 | ||
|
|
e4c7a887d2 | ||
|
|
fb8a615660 | ||
|
|
1d9d49f406 | ||
|
|
0069b59ffe | ||
|
|
d4ba1b1e09 | ||
|
|
3a20b84f3a | ||
|
|
d52fc777ac | ||
|
|
5753a428d8 | ||
|
|
85afef5775 | ||
|
|
b4fc24995c | ||
|
|
5917bb10e4 | ||
|
|
258e150ebf | ||
|
|
96f2b2b617 | ||
|
|
d556db079b | ||
|
|
a811a82329 | ||
|
|
d17a0dae1f | ||
|
|
ef40f2f91b | ||
|
|
a921f9848c | ||
|
|
95ba3261fd | ||
|
|
fe81eb65c2 | ||
|
|
8428714cf5 | ||
|
|
bedf613cff | ||
|
|
e643ce5b99 | ||
|
|
cb64302294 | ||
|
|
8d5f2fec09 | ||
|
|
60e98235ca | ||
|
|
f55fb4055f | ||
|
|
da4e410bb7 | ||
|
|
cdd1f87437 | ||
|
|
7058373916 | ||
|
|
8dd38fd182 | ||
|
|
73479bab26 | ||
|
|
f5366c33bc | ||
|
|
db886163c2 | ||
|
|
91f5338db0 | ||
|
|
82a02287ac | ||
|
|
2dc674559e | ||
|
|
38e713fea2 | ||
|
|
2cbb14c36c | ||
|
|
610e88958e | ||
|
|
bb76477467 | ||
|
|
433a799759 | ||
|
|
22965ccce3 | ||
|
|
4257581f55 | ||
|
|
d60d629105 | ||
|
|
3f721b1717 | ||
|
|
97049bfab4 | ||
|
|
84944a87d3 | ||
|
|
fb62910b17 |
5
.devcontainer/.dockerignore
Normal file
5
.devcontainer/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1 @@
|
||||
FROM qmcgaw/godevcontainer
|
||||
68
.devcontainer/README.md
Normal file
68
.devcontainer/README.md
Normal 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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug.md
vendored
2
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Report a bug
|
||||
title: 'Bug: ...'
|
||||
title: 'Bug: FILL THIS TEXT!'
|
||||
labels: ":bug: bug"
|
||||
assignees: qdm12
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/help.md
vendored
2
.github/ISSUE_TEMPLATE/help.md
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Help
|
||||
about: Ask for help
|
||||
title: 'Help: ...'
|
||||
title: 'Help: FILL THIS TEXT!'
|
||||
labels: ":pray: help wanted"
|
||||
assignees:
|
||||
|
||||
|
||||
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
43
.github/workflows/buildx-branch.yml
vendored
43
.github/workflows/buildx-branch.yml
vendored
@@ -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
|
||||
|
||||
37
.github/workflows/buildx-latest.yml
vendored
37
.github/workflows/buildx-latest.yml
vendored
@@ -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
|
||||
|
||||
37
.github/workflows/buildx-release.yml
vendored
37
.github/workflows/buildx-release.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/dockerhub-description.yml
vendored
12
.github/workflows/dockerhub-description.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
35
Dockerfile
35
Dockerfile
@@ -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
405
README.md
@@ -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">
|
||||
|
||||
[](https://github.com/qdm12/gluetun/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
|
||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags)
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||
|
||||
[](https://github.com/qdm12/gluetun/issues)
|
||||
[](https://github.com/qdm12/gluetun/issues)
|
||||
[](https://github.com/qdm12/gluetun/issues)
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|
||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|
||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
||||

|
||||
[](https://github.com/qdm12/gluetun/commits)
|
||||
|
||||
## Videos
|
||||
[](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)
|
||||
[](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://www.paypal.me/qmcgaw)
|
||||
[](https://github.com/sponsors/qdm12)
|
||||
[](https://www.paypal.me/qmcgaw)
|
||||
|
||||
[](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 🥇👍
|
||||
[](https://github.com/qdm12/gluetun/commits)
|
||||
[](https://github.com/qdm12/gluetun/pulls?q=is%3Apr+is%3Aclosed)
|
||||
|
||||
[](https://github.com/qdm12/gluetun/issues)
|
||||
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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(httpClient, logger, allSettings.HTTPProxy)
|
||||
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
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
BIN
doc/logo_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -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
6
go.mod
@@ -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
17
go.sum
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
41
internal/cli/clientkey.go
Normal 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
|
||||
}
|
||||
20
internal/cli/healthcheck.go
Normal file
20
internal/cli/healthcheck.go
Normal 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)
|
||||
}
|
||||
48
internal/cli/openvpnconfig.go
Normal file
48
internal/cli/openvpnconfig.go
Normal 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
64
internal/cli/update.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
UserReadPermission os.FileMode = 0400
|
||||
AllReadWritePermissions os.FileMode = 0666
|
||||
)
|
||||
@@ -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}}}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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}}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
14
internal/constants/status.go
Normal file
14
internal/constants/status.go
Normal 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"
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
8
internal/dns/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package dns
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrBadStatusCode = errors.New("bad HTTP status")
|
||||
ErrCannotReadBody = errors.New("cannot read response body")
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,10 +25,16 @@ 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_RDWR|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
s := strings.TrimSuffix(string(data), "\n")
|
||||
lines := strings.Split(s, "\n")
|
||||
if len(lines) == 1 && lines[0] == "" {
|
||||
@@ -44,6 +52,11 @@ func (c *configurator) UseDNSSystemWide(ip net.IP, keepNameserver bool) error {
|
||||
if !found {
|
||||
lines = append(lines, "nameserver "+ip.String())
|
||||
}
|
||||
data = []byte(strings.Join(lines, "\n"))
|
||||
return c.fileManager.WriteToFile(string(constants.ResolvConf), data)
|
||||
s = strings.Join(lines, "\n") + "\n"
|
||||
_, err = file.WriteString(s)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ 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"
|
||||
)
|
||||
@@ -17,30 +19,36 @@ func Test_UseDNSSystemWide(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
data []byte
|
||||
writtenData []byte
|
||||
writtenData string
|
||||
openErr error
|
||||
readErr error
|
||||
writeErr error
|
||||
closeErr error
|
||||
err error
|
||||
}{
|
||||
"no data": {
|
||||
writtenData: []byte("nameserver 127.0.0.1"),
|
||||
writtenData: "nameserver 127.0.0.1\n",
|
||||
},
|
||||
"open error": {
|
||||
openErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
"read error": {
|
||||
readErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
"write error": {
|
||||
writtenData: []byte("nameserver 127.0.0.1"),
|
||||
writtenData: "nameserver 127.0.0.1\n",
|
||||
writeErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("error"),
|
||||
},
|
||||
"lines without nameserver": {
|
||||
data: []byte("abc\ndef\n"),
|
||||
writtenData: []byte("abc\ndef\nnameserver 127.0.0.1"),
|
||||
writtenData: "abc\ndef\nnameserver 127.0.0.1\n",
|
||||
},
|
||||
"lines with nameserver": {
|
||||
data: []byte("abc\nnameserver abc def\ndef\n"),
|
||||
writtenData: []byte("abc\nnameserver 127.0.0.1\ndef"),
|
||||
writtenData: "abc\nnameserver 127.0.0.1\ndef\n",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
@@ -48,19 +56,43 @@ 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)
|
||||
|
||||
file := mock_os.NewMockFile(mockCtrl)
|
||||
if tc.openErr == nil {
|
||||
firstReadCall := file.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 := file.EXPECT().
|
||||
Read(gomock.AssignableToTypeOf([]byte{})).
|
||||
Return(0, readErr).After(firstReadCall)
|
||||
if tc.readErr == nil {
|
||||
writeCall := file.EXPECT().WriteString(tc.writtenData).
|
||||
Return(0, tc.writeErr).After(finalReadCall)
|
||||
file.EXPECT().Close().Return(tc.closeErr).After(writeCall)
|
||||
} else {
|
||||
file.EXPECT().Close().Return(tc.closeErr).After(finalReadCall)
|
||||
}
|
||||
}
|
||||
|
||||
openFile := func(name string, flag int, perm os.FileMode) (os.File, error) {
|
||||
assert.Equal(t, string(constants.ResolvConf), name)
|
||||
assert.Equal(t, os.O_RDWR|os.O_TRUNC, flag)
|
||||
assert.Equal(t, os.FileMode(0644), perm)
|
||||
return file, tc.openErr
|
||||
}
|
||||
|
||||
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", "127.0.0.1")
|
||||
c := &configurator{
|
||||
fileManager: fileManager,
|
||||
logger: logger,
|
||||
openFile: openFile,
|
||||
logger: logger,
|
||||
}
|
||||
err := c.UseDNSSystemWide(net.IP{127, 0, 0, 1}, false)
|
||||
if tc.err != nil {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
9
internal/dns/roundtrip_test.go
Normal file
9
internal/dns/roundtrip_test.go
Normal 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
99
internal/dns/state.go
Normal 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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
24
internal/httpproxy/accept.go
Normal file
24
internal/httpproxy/accept.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package httpproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handler) isAccepted(responseWriter http.ResponseWriter, request *http.Request) bool {
|
||||
// Not compatible with HTTP < 1.0 or HTTP >= 2.0 (see https://github.com/golang/go/issues/14797#issuecomment-196103814)
|
||||
const (
|
||||
minimalMajorVersion = 1
|
||||
minimalMinorVersion = 0
|
||||
maximumMajorVersion = 2
|
||||
maximumMinorVersion = 0
|
||||
)
|
||||
if !request.ProtoAtLeast(minimalMajorVersion, minimalMinorVersion) ||
|
||||
request.ProtoAtLeast(maximumMajorVersion, maximumMinorVersion) {
|
||||
message := fmt.Sprintf("http version not supported: %s", request.Proto)
|
||||
h.logger.Info("%s, from %s", message, request.RemoteAddr)
|
||||
http.Error(responseWriter, message, http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -6,10 +6,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isAuthorized(responseWriter http.ResponseWriter, request *http.Request,
|
||||
username, password string) (authorized bool) {
|
||||
func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http.Request) (authorized bool) {
|
||||
if len(h.username) == 0 || (request.Method != "CONNECT" && !request.URL.IsAbs()) {
|
||||
return true
|
||||
}
|
||||
basicAuth := request.Header.Get("Proxy-Authorization")
|
||||
if len(basicAuth) == 0 {
|
||||
h.logger.Info("Proxy-Authorization header not found from %s", request.RemoteAddr)
|
||||
responseWriter.Header().Set("Proxy-Authenticate", `Basic realm="Access to Gluetun over HTTP"`)
|
||||
responseWriter.WriteHeader(http.StatusProxyAuthRequired)
|
||||
return false
|
||||
@@ -17,6 +20,8 @@ func isAuthorized(responseWriter http.ResponseWriter, request *http.Request,
|
||||
b64UsernamePassword := strings.TrimPrefix(basicAuth, "Basic ")
|
||||
b, err := base64.StdEncoding.DecodeString(b64UsernamePassword)
|
||||
if err != nil {
|
||||
h.logger.Info("Cannot decode Proxy-Authorization header value from %s: %s",
|
||||
request.RemoteAddr, err.Error())
|
||||
responseWriter.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
@@ -26,7 +31,9 @@ func isAuthorized(responseWriter http.ResponseWriter, request *http.Request,
|
||||
responseWriter.WriteHeader(http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if username != usernamePassword[0] && password != usernamePassword[1] {
|
||||
if h.username != usernamePassword[0] || h.password != usernamePassword[1] {
|
||||
h.logger.Info("Username or password mismatch from %s", request.RemoteAddr)
|
||||
h.logger.Debug("username provided %q and password provided %q", usernamePassword[0], usernamePassword[1])
|
||||
responseWriter.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,20 +9,18 @@ import (
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func newHandler(ctx context.Context, wg *sync.WaitGroup,
|
||||
client *http.Client, logger logging.Logger,
|
||||
func newHandler(ctx context.Context, wg *sync.WaitGroup, logger logging.Logger,
|
||||
stealth, verbose bool, username, password string) http.Handler {
|
||||
const relayTimeout = 10 * time.Second
|
||||
const httpTimeout = 24 * time.Hour
|
||||
return &handler{
|
||||
ctx: ctx,
|
||||
wg: wg,
|
||||
client: client,
|
||||
logger: logger,
|
||||
relayTimeout: relayTimeout,
|
||||
verbose: verbose,
|
||||
stealth: stealth,
|
||||
username: username,
|
||||
password: password,
|
||||
ctx: ctx,
|
||||
wg: wg,
|
||||
client: &http.Client{Timeout: httpTimeout},
|
||||
logger: logger,
|
||||
verbose: verbose,
|
||||
stealth: stealth,
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,16 +29,20 @@ type handler struct {
|
||||
wg *sync.WaitGroup
|
||||
client *http.Client
|
||||
logger logging.Logger
|
||||
relayTimeout time.Duration
|
||||
verbose, stealth bool
|
||||
username, password string
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
if len(h.username) > 0 && !isAuthorized(responseWriter, request, h.username, h.password) {
|
||||
h.logger.Info("%s unauthorized", request.RemoteAddr)
|
||||
if !h.isAccepted(responseWriter, request) {
|
||||
return
|
||||
}
|
||||
if !h.isAuthorized(responseWriter, request) {
|
||||
return
|
||||
}
|
||||
request.Header.Del("Proxy-Connection")
|
||||
request.Header.Del("Proxy-Authenticate")
|
||||
request.Header.Del("Proxy-Authorization")
|
||||
switch request.Method {
|
||||
case http.MethodConnect:
|
||||
h.handleHTTPS(responseWriter, request)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package httpproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -18,9 +17,7 @@ func (h *handler) handleHTTP(responseWriter http.ResponseWriter, request *http.R
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(h.ctx, h.relayTimeout)
|
||||
defer cancel()
|
||||
request = request.WithContext(ctx)
|
||||
request = request.WithContext(h.ctx)
|
||||
|
||||
request.RequestURI = ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func (h *handler) handleHTTPS(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
dialer := net.Dialer{Timeout: h.relayTimeout}
|
||||
dialer := net.Dialer{}
|
||||
destinationConn, err := dialer.DialContext(h.ctx, "tcp", request.Host)
|
||||
if err != nil {
|
||||
http.Error(responseWriter, err.Error(), http.StatusServiceUnavailable)
|
||||
|
||||
@@ -3,113 +3,91 @@ package httpproxy
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 {
|
||||
client *http.Client
|
||||
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
|
||||
}
|
||||
|
||||
func NewLooper(client *http.Client, logger logging.Logger,
|
||||
settings settings.HTTPProxy) Looper {
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
|
||||
func NewLooper(logger logging.Logger, settings settings.HTTPProxy) Looper {
|
||||
return &looper{
|
||||
client: client,
|
||||
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, l.client, 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 {
|
||||
@@ -119,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -20,25 +20,24 @@ type server struct {
|
||||
internalWG *sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(ctx context.Context, address string,
|
||||
logger logging.Logger, client *http.Client,
|
||||
func New(ctx context.Context, address string, logger logging.Logger,
|
||||
stealth, verbose bool, username, password string) Server {
|
||||
wg := &sync.WaitGroup{}
|
||||
return &server{
|
||||
address: address,
|
||||
handler: newHandler(ctx, wg, client, logger, stealth, verbose, username, password),
|
||||
handler: newHandler(ctx, wg, logger, stealth, verbose, username, password),
|
||||
logger: logger,
|
||||
internalWG: wg,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -48,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
101
internal/httpproxy/state.go
Normal 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"
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
var regularExpressions = struct { //nolint:gochecknoglobals
|
||||
unboundPrefix *regexp.Regexp
|
||||
}{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type OpenVPNConnection struct {
|
||||
IP net.IP
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
121
internal/openvpn/state.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
109
internal/params/secrets.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -78,7 +78,7 @@ func (n *nordvpn) GetOpenVPNConnection(selection models.ServerSelection) (
|
||||
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 +115,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 +142,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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -72,7 +72,7 @@ func (w *windscribe) GetOpenVPNConnection(selection models.ServerSelection) (con
|
||||
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")
|
||||
}
|
||||
|
||||
8
internal/publicip/errors.go
Normal file
8
internal/publicip/errors.go
Normal 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
27
internal/publicip/fs.go
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user