Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4ec59f9bd |
@@ -1,4 +1,5 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
devcontainer.json
|
devcontainer.json
|
||||||
|
docker-compose.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
README.md
|
README.md
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
FROM qmcgaw/godevcontainer
|
||||||
RUN apk add wireguard-tools htop openssl
|
|
||||||
|
|||||||
@@ -2,47 +2,68 @@
|
|||||||
|
|
||||||
Development container that can be used with VSCode.
|
Development container that can be used with VSCode.
|
||||||
|
|
||||||
It works on Linux, Windows (WSL2) and OSX.
|
It works on Linux, Windows and OSX.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [VS code](https://code.visualstudio.com/download) installed
|
- [VS code](https://code.visualstudio.com/download) installed
|
||||||
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
- [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
|
- [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
|
## Setup
|
||||||
|
|
||||||
1. Create the following files and directory on your host if you don't have them:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
touch ~/.gitconfig ~/.zsh_history
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
|
||||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||||
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||||
|
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
For any customization to take effect, you should "rebuild and reopen":
|
### Customize the image
|
||||||
|
|
||||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||||
2. Select `Dev-Containers: Rebuild Container`
|
|
||||||
|
|
||||||
Changes you can make are notably:
|
```Dockerfile
|
||||||
|
FROM qmcgaw/godevcontainer
|
||||||
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
USER root
|
||||||
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
RUN apk add curl
|
||||||
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
USER vscode
|
||||||
|
|
||||||
```json
|
|
||||||
// Welcome script
|
|
||||||
{
|
|
||||||
"source": "/yourpath/.welcome.sh",
|
|
||||||
"target": "/root/.welcome.sh",
|
|
||||||
"type": "bind"
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
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,52 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "gluetun-dev",
|
"name": "gluetun-dev",
|
||||||
// User defined settings
|
"dockerComposeFile": [
|
||||||
"containerEnv": {
|
"docker-compose.yml"
|
||||||
"TZ": ""
|
|
||||||
},
|
|
||||||
// Fixed settings
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "./Dockerfile"
|
|
||||||
},
|
|
||||||
"postCreateCommand": "~/.windows.sh && go mod download",
|
|
||||||
"capAdd": [
|
|
||||||
"NET_ADMIN", // Gluetun specific
|
|
||||||
"SYS_PTRACE" // for dlv Go debugging
|
|
||||||
],
|
],
|
||||||
"securityOpt": [
|
"service": "vscode",
|
||||||
"seccomp=unconfined" // for dlv Go debugging
|
"runServices": [
|
||||||
|
"vscode"
|
||||||
],
|
],
|
||||||
"mounts": [
|
"shutdownAction": "stopCompose",
|
||||||
// Zsh commands history persistence
|
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
|
||||||
{
|
"workspaceFolder": "/workspace",
|
||||||
"source": "${localEnv:HOME}/.zsh_history",
|
|
||||||
"target": "/root/.zsh_history",
|
|
||||||
"type": "bind"
|
|
||||||
},
|
|
||||||
// Git configuration file
|
|
||||||
{
|
|
||||||
"source": "${localEnv:HOME}/.gitconfig",
|
|
||||||
"target": "/root/.gitconfig",
|
|
||||||
"type": "bind"
|
|
||||||
},
|
|
||||||
// SSH directory for Linux, OSX and WSL
|
|
||||||
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
|
||||||
// created in the container. On Windows, files are copied
|
|
||||||
// from /mnt/ssh to ~/.ssh to fix permissions.
|
|
||||||
{
|
|
||||||
"source": "${localEnv:HOME}/.ssh",
|
|
||||||
"target": "/mnt/ssh",
|
|
||||||
"type": "bind"
|
|
||||||
},
|
|
||||||
// Docker socket to access the host Docker server
|
|
||||||
{
|
|
||||||
"source": "/var/run/docker.sock",
|
|
||||||
"target": "/var/run/docker.sock",
|
|
||||||
"type": "bind"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"golang.go",
|
"golang.go",
|
||||||
"eamodio.gitlens", // IDE Git information
|
"eamodio.gitlens", // IDE Git information
|
||||||
@@ -61,45 +24,54 @@
|
|||||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||||
"IBM.output-colorizer", // Colorize your output/test logs
|
"IBM.output-colorizer", // Colorize your output/test logs
|
||||||
"github.copilot" // AI code completion
|
"mohsen1.prettify-json", // Prettify JSON data
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"remote.extensionKind": {
|
"remote.extensionKind": {
|
||||||
"ms-azuretools.vscode-docker": "workspace"
|
"ms-azuretools.vscode-docker": "workspace"
|
||||||
},
|
},
|
||||||
|
"editor.codeActionsOnSaveTimeout": 3000,
|
||||||
"go.useLanguageServer": true,
|
"go.useLanguageServer": true,
|
||||||
"[go]": {
|
"[go]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": true,
|
||||||
}
|
},
|
||||||
|
// Optional: Disable snippets, as they conflict with completion ranking.
|
||||||
|
"editor.snippetSuggestions": "none"
|
||||||
},
|
},
|
||||||
"[go.mod]": {
|
"[go.mod]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"gopls": {
|
"gopls": {
|
||||||
"usePlaceholders": false,
|
"usePlaceholders": false,
|
||||||
"staticcheck": true,
|
"staticcheck": true
|
||||||
"ui.diagnostic.analyses": {
|
|
||||||
"ST1000": false
|
|
||||||
},
|
|
||||||
"formatting.gofumpt": true,
|
|
||||||
},
|
},
|
||||||
|
"go.autocompleteUnimportedPackages": true,
|
||||||
|
"go.gotoSymbol.includeImports": true,
|
||||||
|
"go.gotoSymbol.includeGoroot": true,
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
"go.lintOnSave": "package",
|
"go.buildOnSave": "workspace",
|
||||||
|
"go.lintOnSave": "workspace",
|
||||||
|
"go.vetOnSave": "workspace",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"go.buildTags": "linux",
|
|
||||||
"go.toolsEnvVars": {
|
"go.toolsEnvVars": {
|
||||||
"CGO_ENABLED": "0"
|
"GOFLAGS": "-tags=",
|
||||||
|
// "CGO_ENABLED": 1 // for the race detector
|
||||||
|
},
|
||||||
|
"gopls.env": {
|
||||||
|
"GOFLAGS": "-tags="
|
||||||
},
|
},
|
||||||
"go.testEnvVars": {
|
"go.testEnvVars": {
|
||||||
"CGO_ENABLED": "1"
|
"": ""
|
||||||
},
|
},
|
||||||
"go.testFlags": [
|
"go.testFlags": [
|
||||||
"-v",
|
"-v",
|
||||||
"-race"
|
// "-race"
|
||||||
],
|
],
|
||||||
"go.testTimeout": "10s",
|
"go.testTimeout": "10s",
|
||||||
"go.coverOnSingleTest": true,
|
"go.coverOnSingleTest": true,
|
||||||
@@ -107,5 +79,3 @@
|
|||||||
"go.coverOnTestPackage": true
|
"go.coverOnTestPackage": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
30
.devcontainer/docker-compose.yml
Normal file
30
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
vscode:
|
||||||
|
build: .
|
||||||
|
image: godevcontainer
|
||||||
|
volumes:
|
||||||
|
- ../:/workspace
|
||||||
|
# Docker socket to access Docker server
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
# Docker configuration
|
||||||
|
- ~/.docker:/root/.docker:z
|
||||||
|
# SSH directory for Linux, OSX and WSL
|
||||||
|
- ~/.ssh:/root/.ssh:z
|
||||||
|
# For Windows without WSL, a copy will be made
|
||||||
|
# from /tmp/.ssh to ~/.ssh to fix permissions
|
||||||
|
#- ~/.ssh:/tmp/.ssh:ro
|
||||||
|
# Shell history persistence
|
||||||
|
- ~/.zsh_history:/root/.zsh_history:z
|
||||||
|
# Git config
|
||||||
|
- ~/.gitconfig:/root/.gitconfig:z
|
||||||
|
environment:
|
||||||
|
- TZ=
|
||||||
|
cap_add:
|
||||||
|
# For debugging with dlv
|
||||||
|
- SYS_PTRACE
|
||||||
|
security_opt:
|
||||||
|
# For debugging with dlv
|
||||||
|
- seccomp:unconfined
|
||||||
|
entrypoint: zsh -c "while sleep 1000; do :; done"
|
||||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
|
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
|
||||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||||
|
|||||||
45
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Bug
|
||||||
|
about: Report a bug
|
||||||
|
title: 'Bug: FILL THIS TEXT!'
|
||||||
|
labels: ":bug: bug"
|
||||||
|
assignees: qdm12
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!---
|
||||||
|
⚠️ Answer the following or I'll insta-close your issue
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Is this urgent?**: No
|
||||||
|
|
||||||
|
**Host OS** (approximate answer is fine too): Ubuntu 18
|
||||||
|
|
||||||
|
**CPU arch** or **device name**: amd64
|
||||||
|
|
||||||
|
**What VPN provider are you using**:
|
||||||
|
|
||||||
|
**What are you using to run your container?**: Docker Compose
|
||||||
|
|
||||||
|
**What is the version of the program** (See the line at the top of your logs)
|
||||||
|
|
||||||
|
```
|
||||||
|
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
|
||||||
|
```
|
||||||
|
|
||||||
|
**What's the problem** 🤔
|
||||||
|
|
||||||
|
That feature doesn't work
|
||||||
|
|
||||||
|
**Share your logs... (careful to remove in example tokens)**
|
||||||
|
|
||||||
|
```log
|
||||||
|
|
||||||
|
PASTE YOUR LOGS
|
||||||
|
IN THERE
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
<!---
|
||||||
|
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
|
||||||
|
-->
|
||||||
117
.github/ISSUE_TEMPLATE/bug.yml
vendored
117
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,117 +0,0 @@
|
|||||||
name: Bug
|
|
||||||
description: Report a bug
|
|
||||||
title: "Bug: "
|
|
||||||
labels: [":bug: bug"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this bug report!
|
|
||||||
|
|
||||||
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
|
|
||||||
- you do not fill out **the title of the issue** ☝️
|
|
||||||
- you do not provide the **Gluetun version** as requested below
|
|
||||||
- you provide **less than 10 lines of logs** as requested below
|
|
||||||
- type: dropdown
|
|
||||||
id: urgent
|
|
||||||
attributes:
|
|
||||||
label: Is this urgent?
|
|
||||||
description: |
|
|
||||||
Is this a critical bug, or do you need this fixed urgently?
|
|
||||||
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
|
|
||||||
options:
|
|
||||||
- "No"
|
|
||||||
- "Yes"
|
|
||||||
- type: input
|
|
||||||
id: host-os
|
|
||||||
attributes:
|
|
||||||
label: Host OS
|
|
||||||
description: What is your host OS?
|
|
||||||
placeholder: "Debian Buster"
|
|
||||||
- type: dropdown
|
|
||||||
id: cpu-arch
|
|
||||||
attributes:
|
|
||||||
label: CPU arch
|
|
||||||
description: You can find it on Linux with `uname -m`.
|
|
||||||
options:
|
|
||||||
- x86_64
|
|
||||||
- aarch64
|
|
||||||
- armv7l
|
|
||||||
- "386"
|
|
||||||
- s390x
|
|
||||||
- ppc64le
|
|
||||||
- type: dropdown
|
|
||||||
id: vpn-service-provider
|
|
||||||
attributes:
|
|
||||||
label: VPN service provider
|
|
||||||
options:
|
|
||||||
- AirVPN
|
|
||||||
- Custom
|
|
||||||
- Cyberghost
|
|
||||||
- ExpressVPN
|
|
||||||
- FastestVPN
|
|
||||||
- Giganews
|
|
||||||
- HideMyAss
|
|
||||||
- IPVanish
|
|
||||||
- IVPN
|
|
||||||
- Mullvad
|
|
||||||
- NordVPN
|
|
||||||
- Privado
|
|
||||||
- Private Internet Access
|
|
||||||
- PrivateVPN
|
|
||||||
- ProtonVPN
|
|
||||||
- PureVPN
|
|
||||||
- SlickVPN
|
|
||||||
- Surfshark
|
|
||||||
- TorGuard
|
|
||||||
- VPNSecure.me
|
|
||||||
- VPNUnlimited
|
|
||||||
- VyprVPN
|
|
||||||
- WeVPN
|
|
||||||
- Windscribe
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: docker
|
|
||||||
attributes:
|
|
||||||
label: What are you using to run the container
|
|
||||||
options:
|
|
||||||
- docker run
|
|
||||||
- docker-compose
|
|
||||||
- Portainer
|
|
||||||
- Kubernetes
|
|
||||||
- Podman
|
|
||||||
- Unraid
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: What is the version of Gluetun
|
|
||||||
description: |
|
|
||||||
Copy paste the version line at the top of your logs.
|
|
||||||
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: "What's the problem 🤔"
|
|
||||||
placeholder: "That feature does not work..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Share your logs (at least 10 lines)
|
|
||||||
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
|
||||||
render: plain text
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: config
|
|
||||||
attributes:
|
|
||||||
label: Share your configuration
|
|
||||||
description: Share your configuration such as `docker-compose.yml`. Ensure to remove credentials.
|
|
||||||
render: yml
|
|
||||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Report a Wiki issue
|
|
||||||
url: https://github.com/qdm12/gluetun-wiki/issues/new/choose
|
|
||||||
about: Please create an issue on the gluetun-wiki repository.
|
|
||||||
- name: Configuration help?
|
|
||||||
url: https://github.com/qdm12/gluetun/discussions/new/choose
|
|
||||||
about: Please create a Github discussion.
|
|
||||||
- name: Unraid template issue
|
|
||||||
url: https://github.com/qdm12/gluetun/discussions/550
|
|
||||||
about: Please read the relevant Github discussion.
|
|
||||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a feature to add to this project
|
||||||
|
title: 'Feature request: FILL THIS TEXT!'
|
||||||
|
labels: ":bulb: feature request"
|
||||||
|
assignees: qdm12
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**What's the feature?** 🧐
|
||||||
|
|
||||||
|
- Support this new feature because that and that
|
||||||
|
|
||||||
|
**Optional extra information** 🚀
|
||||||
|
|
||||||
|
- I tried `docker run something` and it doesn't work
|
||||||
|
- That [url](https://github.com/qdm12/gluetun) is interesting
|
||||||
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
19
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Suggest a feature to add to Gluetun
|
|
||||||
title: "Feature request: "
|
|
||||||
labels: [":bulb: feature request"]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: "What's the feature 🧐"
|
|
||||||
placeholder: "Make the tunnel resistant to earth quakes"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: extra
|
|
||||||
attributes:
|
|
||||||
label: "Extra information and references"
|
|
||||||
placeholder: |
|
|
||||||
- I tried `docker run something` and it doesn't work
|
|
||||||
- That [url](https://github.com/qdm12/gluetun) is interesting
|
|
||||||
67
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
67
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: Help
|
||||||
|
about: Ask for help
|
||||||
|
title: 'Help: FILL THIS TEXT!'
|
||||||
|
labels: ":pray: help wanted"
|
||||||
|
assignees:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!---
|
||||||
|
⚠️ If this about a Docker configuration problem or another service:
|
||||||
|
Start a discussion at https://github.com/qdm12/gluetun/discussions/new
|
||||||
|
OR I WILL INSTA-CLOSE YOUR ISSUE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!---
|
||||||
|
⚠️ Answer the following or I'll insta-close your issue
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Is this urgent?**: No
|
||||||
|
|
||||||
|
**Host OS** (approximate answer is fine too): Ubuntu 18
|
||||||
|
|
||||||
|
**CPU arch** or **device name**: amd64
|
||||||
|
|
||||||
|
**What VPN provider are you using**:
|
||||||
|
|
||||||
|
**What is the version of the program** (See the line at the top of your logs)
|
||||||
|
|
||||||
|
```
|
||||||
|
Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)
|
||||||
|
```
|
||||||
|
|
||||||
|
**What's the problem** 🤔
|
||||||
|
|
||||||
|
That feature doesn't work
|
||||||
|
|
||||||
|
**Share your logs... (careful to remove in example tokens)**
|
||||||
|
|
||||||
|
```log
|
||||||
|
|
||||||
|
PASTE YOUR LOGS
|
||||||
|
IN THERE
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**What are you using to run your container?**: Docker Compose
|
||||||
|
|
||||||
|
<!---
|
||||||
|
💡 You can highlight your code with https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlight
|
||||||
|
-->
|
||||||
|
|
||||||
|
Please also share your configuration file:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
your .yml
|
||||||
|
content
|
||||||
|
in here
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# your docker
|
||||||
|
# run command
|
||||||
|
# in here
|
||||||
|
```
|
||||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,35 +6,12 @@ labels: ":bulb: New provider"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Important notes:
|
One of the following is required:
|
||||||
|
|
||||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
|
||||||
|
|
||||||
## For Wireguard
|
|
||||||
|
|
||||||
Wireguard can be natively supported ONLY if:
|
|
||||||
|
|
||||||
- the `PrivateKey` field value is the same across all servers for one user account
|
|
||||||
- the `Address` field value is:
|
|
||||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
|
||||||
- the same across all servers for one user account
|
|
||||||
- the `PublicKey` field value is:
|
|
||||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
|
||||||
- the same across all servers for one user account
|
|
||||||
- the `Endpoint` field value:
|
|
||||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
|
||||||
- can be determined using a pattern, for example using country codes in hostnames
|
|
||||||
|
|
||||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
|
||||||
|
|
||||||
If **all** of these conditions are met, please provide an answer for each of them.
|
|
||||||
|
|
||||||
## For OpenVPN
|
|
||||||
|
|
||||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
|
||||||
|
|
||||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
|
||||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
|
||||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||||
|
|
||||||
|
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||||
|
you can only use a custom Openvpn configuration file.
|
||||||
|
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.
|
||||||
|
|||||||
188
.github/labels.yml
vendored
188
.github/labels.yml
vendored
@@ -1,152 +1,90 @@
|
|||||||
- name: "Status: 🗯️ Waiting for feedback"
|
- name: "Bug :bug:"
|
||||||
color: "f7d692"
|
color: "b60205"
|
||||||
- name: "Status: 🔴 Blocked"
|
description: ""
|
||||||
color: "f7d692"
|
- name: "Feature request :bulb:"
|
||||||
description: "Blocked by another issue or pull request"
|
color: "0e8a16"
|
||||||
- name: "Status: 📌 Before next release"
|
description: ""
|
||||||
color: "f7d692"
|
- name: "Help wanted :pray:"
|
||||||
description: "Has to be done before the next release"
|
color: "4caf50"
|
||||||
- name: "Status: 🔒 After next release"
|
description: ""
|
||||||
color: "f7d692"
|
- name: "Documentation :memo:"
|
||||||
description: "Will be done after the next release"
|
color: "c5def5"
|
||||||
- name: "Status: 🟡 Nearly resolved"
|
description: ""
|
||||||
color: "f7d692"
|
- name: "Needs more info :thinking:"
|
||||||
description: "This might be resolved or is about to be resolved"
|
color: "795548"
|
||||||
|
description: ""
|
||||||
- name: "Closed: ⚰️ Inactive"
|
|
||||||
color: "959a9c"
|
|
||||||
description: "No answer was received for weeks"
|
|
||||||
- name: "Closed: 👥 Duplicate"
|
|
||||||
color: "959a9c"
|
|
||||||
description: "Issue duplicates an existing issue"
|
|
||||||
- name: "Closed: 🗑️ Bad issue"
|
|
||||||
color: "959a9c"
|
|
||||||
- name: "Closed: ☠️ cannot be done"
|
|
||||||
color: "959a9c"
|
|
||||||
|
|
||||||
- name: "Priority: 🚨 Urgent"
|
|
||||||
color: "03adfc"
|
|
||||||
- name: "Priority: 💤 Low priority"
|
|
||||||
color: "03adfc"
|
|
||||||
|
|
||||||
- name: "Complexity: ☣️ Hard to do"
|
|
||||||
color: "ff9efc"
|
|
||||||
- name: "Complexity: 🟩 Easy to do"
|
|
||||||
color: "ff9efc"
|
|
||||||
|
|
||||||
- name: "Popularity: ❤️🔥 extreme"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Popularity: ❤️ high"
|
|
||||||
color: "ffc7ea"
|
|
||||||
|
|
||||||
# VPN providers
|
# VPN providers
|
||||||
- name: "☁️ AirVPN"
|
- name: ":cloud: Cyberghost"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Custom"
|
description: ""
|
||||||
|
- name: ":cloud: HideMyAss"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Cyberghost"
|
description: ""
|
||||||
|
- name: ":cloud: IPVanish"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Giganews"
|
description: ""
|
||||||
|
- name: ":cloud: IVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ HideMyAss"
|
description: ""
|
||||||
|
- name: ":cloud: FastestVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ IPVanish"
|
description: ""
|
||||||
|
- name: ":cloud: Mullvad"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ IVPN"
|
description: ""
|
||||||
|
- name: ":cloud: NordVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ ExpressVPN"
|
description: ""
|
||||||
|
- name: ":cloud: PIA"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ FastestVPN"
|
description: ""
|
||||||
|
- name: ":cloud: Privado"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Mullvad"
|
description: ""
|
||||||
|
- name: ":cloud: PrivateVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ NordVPN"
|
description: ""
|
||||||
|
- name: ":cloud: ProtonVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Perfect Privacy"
|
- name: ":cloud: PureVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PIA"
|
description: ""
|
||||||
|
- name: ":cloud: Surfshark"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Privado"
|
description: ""
|
||||||
|
- name: ":cloud: Torguard"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PrivateVPN"
|
description: ""
|
||||||
|
- name: ":cloud: VPNUnlimited"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ ProtonVPN"
|
description: ""
|
||||||
|
- name: ":cloud: Vyprvpn"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PureVPN"
|
description: ""
|
||||||
color: "cfe8d4"
|
- name: ":cloud: Windscribe"
|
||||||
- name: "☁️ SlickVPN"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Surfshark"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Torguard"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ VPNSecure.me"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ VPNUnlimited"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Vyprvpn"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ WeVPN"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Windscribe"
|
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
description: ""
|
||||||
|
|
||||||
- name: "Category: User error 🤦"
|
# Problem category
|
||||||
from_name: "Category: Config problem 📝"
|
- name: "Openvpn"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Healthcheck 🩺"
|
description: ""
|
||||||
|
- name: "Unbound (DNS over TLS)"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Documentation ✒️"
|
description: ""
|
||||||
description: "A problem with the readme or a code comment."
|
- name: "Firewall"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Maintenance ⛓️"
|
description: ""
|
||||||
description: "Anything related to code or other maintenance"
|
- name: "HTTP proxy"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Logs 📚"
|
description: ""
|
||||||
description: "Something to change in logs"
|
- name: "Shadowsocks"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Good idea 🎯"
|
description: ""
|
||||||
description: "This is a good idea, judged by the maintainers"
|
- name: "Healthcheck server"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Motivated! 🙌"
|
description: ""
|
||||||
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
|
- name: "Control server"
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Foolproof settings 👼"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Label missing ❗"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: updater ♻️"
|
|
||||||
color: "ffc7ea"
|
|
||||||
description: "Concerns the code to update servers data"
|
|
||||||
- name: "Category: New provider 🆕"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: OpenVPN 🔐"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Wireguard 🔐"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: DNS 📠"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Firewall ⛓️"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Routing 🛤️"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: IPv6 🛰️"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: VPN port forwarding 📥"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: HTTP proxy 🔁"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Shadowsocks 🔁"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: control server ⚙️"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: kernel 🧠"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: public IP service 💬"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: servers storage 📦"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Performance 🚀"
|
|
||||||
color: "ffc7ea"
|
|
||||||
- name: "Category: Investigation 🔍"
|
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
|
description: ""
|
||||||
|
|||||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,12 +0,0 @@
|
|||||||
# Description
|
|
||||||
|
|
||||||
<!-- Please describe the reason for the changes being proposed. -->
|
|
||||||
|
|
||||||
# Issue
|
|
||||||
|
|
||||||
<!-- Please link to the issue(s) this change relates to. -->
|
|
||||||
|
|
||||||
# Assertions
|
|
||||||
|
|
||||||
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
|
|
||||||
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
|
||||||
105
.github/workflows/build.yml
vendored
Normal file
105
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build.yml
|
||||||
|
- cmd/**
|
||||||
|
- internal/**
|
||||||
|
- pkg/**
|
||||||
|
- .dockerignore
|
||||||
|
- .golangci.yml
|
||||||
|
- Dockerfile
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.3.4
|
||||||
|
|
||||||
|
- name: Linting
|
||||||
|
run: docker build --target lint .
|
||||||
|
|
||||||
|
- name: Go mod tidy check
|
||||||
|
run: docker build --target tidy .
|
||||||
|
|
||||||
|
- name: Build test image
|
||||||
|
run: docker build --target test -t test-container .
|
||||||
|
|
||||||
|
- name: Run tests in test container
|
||||||
|
run: |
|
||||||
|
touch coverage.txt
|
||||||
|
docker run --rm \
|
||||||
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
|
test-container
|
||||||
|
|
||||||
|
- name: Code security analysis
|
||||||
|
uses: snyk/actions/golang@master
|
||||||
|
env:
|
||||||
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build final image
|
||||||
|
run: docker build -t final-image .
|
||||||
|
|
||||||
|
- name: Image security analysis
|
||||||
|
uses: snyk/actions/docker@master
|
||||||
|
env:
|
||||||
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
|
with:
|
||||||
|
image: final-image
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [verify]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.3.4
|
||||||
|
|
||||||
|
- uses: docker/setup-qemu-action@v1
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: qmcgaw
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set variables
|
||||||
|
id: vars
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
run: |
|
||||||
|
BRANCH=${GITHUB_REF#refs/heads/}
|
||||||
|
TAG=${GITHUB_REF#refs/tags/}
|
||||||
|
echo ::set-output name=commit::$(git rev-parse --short HEAD)
|
||||||
|
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
if [ "$TAG" != "$GITHUB_REF" ]; then
|
||||||
|
echo ::set-output name=version::$TAG
|
||||||
|
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
|
elif [ "$BRANCH" = "master" ]; then
|
||||||
|
echo ::set-output name=version::latest
|
||||||
|
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
|
else
|
||||||
|
echo ::set-output name=version::$BRANCH
|
||||||
|
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push final image
|
||||||
|
uses: docker/build-push-action@v2.4.0
|
||||||
|
with:
|
||||||
|
platforms: ${{ steps.vars.outputs.platforms }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||||
|
COMMIT=${{ steps.vars.outputs.commit }}
|
||||||
|
VERSION=${{ steps.vars.outputs.version }}
|
||||||
|
ALLTARGETPLATFORMS=${{ steps.vars.outputs.platforms }}
|
||||||
|
tags: |
|
||||||
|
qmcgaw/gluetun:${{ steps.vars.outputs.version }}
|
||||||
|
qmcgaw/private-internet-access:${{ steps.vars.outputs.version }}
|
||||||
|
push: true
|
||||||
|
|
||||||
|
- if: github.event_name == 'push' && github.event.ref == 'refs/heads/master'
|
||||||
|
name: Microbadger hook
|
||||||
|
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/gluetun/l-keGI7p4IhX4QuIDMFYKhsZ1L0=
|
||||||
|
continue-on-error: true
|
||||||
35
.github/workflows/ci-skip.yml
vendored
35
.github/workflows/ci-skip.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: No trigger file paths
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths-ignore:
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
verify:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
- name: No trigger path triggered for required verify workflow.
|
|
||||||
run: exit 0
|
|
||||||
180
.github/workflows/ci.yml
vendored
180
.github/workflows/ci.yml
vendored
@@ -1,180 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- .github/workflows/ci.yml
|
|
||||||
- cmd/**
|
|
||||||
- internal/**
|
|
||||||
- pkg/**
|
|
||||||
- .dockerignore
|
|
||||||
- .golangci.yml
|
|
||||||
- Dockerfile
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
verify:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
env:
|
|
||||||
DOCKER_BUILDKIT: "1"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
|
||||||
with:
|
|
||||||
locale: "US"
|
|
||||||
level: error
|
|
||||||
exclude: |
|
|
||||||
./internal/storage/servers.json
|
|
||||||
*.md
|
|
||||||
|
|
||||||
- name: Linting
|
|
||||||
run: docker build --target lint .
|
|
||||||
|
|
||||||
- name: Mocks check
|
|
||||||
run: docker build --target mocks .
|
|
||||||
|
|
||||||
- name: Build test image
|
|
||||||
run: docker build --target test -t test-container .
|
|
||||||
|
|
||||||
- name: Run tests in test container
|
|
||||||
run: |
|
|
||||||
touch coverage.txt
|
|
||||||
docker run --rm --device /dev/net/tun \
|
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
|
||||||
test-container
|
|
||||||
|
|
||||||
- name: Build final image
|
|
||||||
run: docker build -t final-image .
|
|
||||||
|
|
||||||
verify-private:
|
|
||||||
if: |
|
|
||||||
github.repository == 'qdm12/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
github.event_name == 'release' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
|
||||||
)
|
|
||||||
needs: [verify]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: secrets
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- run: docker build -t qmcgaw/gluetun .
|
|
||||||
|
|
||||||
- name: Setup Go for CI utility
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: ci/go.mod
|
|
||||||
|
|
||||||
- name: Build utility
|
|
||||||
run: go build -C ./ci -o runner ./cmd/main.go
|
|
||||||
|
|
||||||
- name: Run Gluetun container with Mullvad configuration
|
|
||||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
|
||||||
|
|
||||||
- name: Run Gluetun container with ProtonVPN configuration
|
|
||||||
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner protonvpn
|
|
||||||
|
|
||||||
codeql:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
- uses: github/codeql-action/init@v4
|
|
||||||
with:
|
|
||||||
languages: go
|
|
||||||
- uses: github/codeql-action/autobuild@v4
|
|
||||||
- uses: github/codeql-action/analyze@v4
|
|
||||||
|
|
||||||
publish:
|
|
||||||
if: |
|
|
||||||
github.repository == 'qdm12/gluetun' &&
|
|
||||||
(
|
|
||||||
github.event_name == 'push' ||
|
|
||||||
github.event_name == 'release' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
|
||||||
)
|
|
||||||
needs: [verify, verify-private, codeql]
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
# extract metadata (tags, labels) for Docker
|
|
||||||
# https://github.com/docker/metadata-action
|
|
||||||
- name: Extract Docker metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
||||||
images: |
|
|
||||||
ghcr.io/qdm12/gluetun
|
|
||||||
qmcgaw/gluetun
|
|
||||||
qmcgaw/private-internet-access
|
|
||||||
tags: |
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
|
|
||||||
type=semver,pattern=v{{major}}.{{minor}}
|
|
||||||
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
|
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: qmcgaw
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: qdm12
|
|
||||||
password: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Short commit
|
|
||||||
id: shortcommit
|
|
||||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
|
||||||
|
|
||||||
- name: Build and push final image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
build-args: |
|
|
||||||
CREATED=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
|
||||||
COMMIT=${{ steps.shortcommit.outputs.value }}
|
|
||||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
push: true
|
|
||||||
21
.github/workflows/closed-issue.yml
vendored
21
.github/workflows/closed-issue.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Closed issue
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: peter-evans/create-or-update-comment@v5
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
|
|
||||||
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
|
|
||||||
|
|
||||||
This is an automated comment setup because @qdm12 is the sole maintainer of this project
|
|
||||||
which became too popular to monitor issues closed.
|
|
||||||
14
.github/workflows/configs/mlc-config.json
vendored
14
.github/workflows/configs/mlc-config.json
vendored
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"ignorePatterns": [
|
|
||||||
{
|
|
||||||
"pattern": "^https://console.substack.com/p/console-72$"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timeout": "20s",
|
|
||||||
"retryOn429": false,
|
|
||||||
"fallbackRetryDelay": "30s",
|
|
||||||
"aliveStatusCodes": [
|
|
||||||
200,
|
|
||||||
429
|
|
||||||
]
|
|
||||||
}
|
|
||||||
21
.github/workflows/dockerhub-description.yml
vendored
Normal file
21
.github/workflows/dockerhub-description.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Docker Hub description
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- README.md
|
||||||
|
- .github/workflows/dockerhub-description.yml
|
||||||
|
jobs:
|
||||||
|
dockerHubDescription:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2.3.4
|
||||||
|
- name: Docker Hub Description
|
||||||
|
uses: peter-evans/dockerhub-description@v2
|
||||||
|
with:
|
||||||
|
username: qmcgaw
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
repository: qmcgaw/gluetun
|
||||||
|
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
||||||
|
readme-filepath: README.md
|
||||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -7,11 +7,9 @@ on:
|
|||||||
- .github/workflows/labels.yml
|
- .github/workflows/labels.yml
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v2.3.4
|
||||||
- uses: crazy-max/ghaction-github-labeler@v5
|
- uses: crazy-max/ghaction-github-labeler@v3
|
||||||
with:
|
with:
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
|
|||||||
21
.github/workflows/markdown-skip.yml
vendored
21
.github/workflows/markdown-skip.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Markdown
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths-ignore:
|
|
||||||
- "**.md"
|
|
||||||
- .github/workflows/markdown.yml
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- "**.md"
|
|
||||||
- .github/workflows/markdown.yml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
markdown:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
steps:
|
|
||||||
- name: No trigger path triggered for required markdown workflow.
|
|
||||||
run: exit 0
|
|
||||||
47
.github/workflows/markdown.yml
vendored
47
.github/workflows/markdown.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Markdown
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- "**.md"
|
|
||||||
- .github/workflows/markdown.yml
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "**.md"
|
|
||||||
- .github/workflows/markdown.yml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
markdown:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- uses: DavidAnson/markdownlint-cli2-action@v21
|
|
||||||
with:
|
|
||||||
globs: "**.md"
|
|
||||||
config: .markdownlint-cli2.jsonc
|
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
|
||||||
with:
|
|
||||||
locale: "US"
|
|
||||||
level: error
|
|
||||||
pattern: |
|
|
||||||
*.md
|
|
||||||
|
|
||||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
|
||||||
with:
|
|
||||||
use-quiet-mode: yes
|
|
||||||
config-file: .github/workflows/configs/mlc-config.json
|
|
||||||
|
|
||||||
- uses: peter-evans/dockerhub-description@v4
|
|
||||||
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
|
||||||
with:
|
|
||||||
username: qmcgaw
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
repository: qmcgaw/gluetun
|
|
||||||
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
|
||||||
readme-filepath: README.md
|
|
||||||
15
.github/workflows/misspell.yml
vendored
Normal file
15
.github/workflows/misspell.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: Misspells
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
jobs:
|
||||||
|
misspell:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.3.4
|
||||||
|
- uses: reviewdog/action-misspell@v1
|
||||||
|
with:
|
||||||
|
locale: "US"
|
||||||
|
level: error
|
||||||
22
.github/workflows/opened-issue.yml
vendored
22
.github/workflows/opened-issue.yml
vendored
@@ -1,22 +0,0 @@
|
|||||||
name: Opened issue
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: peter-evans/create-or-update-comment@v5
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
@qdm12 is more or less the only maintainer of this project and works on it in his free time.
|
|
||||||
Please:
|
|
||||||
- **do not** ask for updates, be patient
|
|
||||||
- :+1: the issue to show your support instead of commenting
|
|
||||||
@qdm12 usually checks issues at least once a week, if this is a new urgent bug,
|
|
||||||
[revert to an older tagged container image](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md)
|
|
||||||
144
.golangci.yml
144
.golangci.yml
@@ -1,94 +1,43 @@
|
|||||||
version: "2"
|
linters-settings:
|
||||||
|
maligned:
|
||||||
formatters:
|
suggest-new: true
|
||||||
enable:
|
|
||||||
- gci
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|
||||||
linters:
|
|
||||||
settings:
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
goconst:
|
|
||||||
ignore-string-values:
|
|
||||||
# commonly used settings strings
|
|
||||||
- "^disabled$"
|
|
||||||
# Firewall and routing strings
|
|
||||||
- "^(ACCEPT|DROP)$"
|
|
||||||
- "^--delete$"
|
|
||||||
- "^all$"
|
|
||||||
- "^(tcp|udp)$"
|
|
||||||
# Server route strings
|
|
||||||
- "^/status$"
|
|
||||||
|
|
||||||
exclusions:
|
issues:
|
||||||
generated: lax
|
exclude-rules:
|
||||||
presets:
|
- path: _test\.go
|
||||||
- comments
|
linters:
|
||||||
- common-false-positives
|
|
||||||
- legacy
|
|
||||||
- std-error-handling
|
|
||||||
rules:
|
|
||||||
- linters:
|
|
||||||
- containedctx
|
|
||||||
- dupl
|
- dupl
|
||||||
- err113
|
- maligned
|
||||||
- maintidx
|
- goerr113
|
||||||
path: _test\.go
|
- path: internal/server/
|
||||||
- linters:
|
linters:
|
||||||
- dupl
|
- dupl
|
||||||
path: internal\/server\/.+\.go
|
- path: internal/configuration/
|
||||||
- linters:
|
linters:
|
||||||
- ireturn
|
- dupl
|
||||||
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
- text: "exported: exported var Err*"
|
||||||
- linters:
|
linters:
|
||||||
- ireturn
|
|
||||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
|
||||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
|
||||||
- linters:
|
|
||||||
- revive
|
- revive
|
||||||
path: internal\/provider\/(common|utils)\/.+\.go
|
- text: "mnd: Magic number: 0644*"
|
||||||
text: "var-naming: avoid (bad|meaningless) package names"
|
linters:
|
||||||
- linters:
|
- gomnd
|
||||||
- lll
|
- text: "mnd: Magic number: 0400*"
|
||||||
source: "^// https://.+$"
|
linters:
|
||||||
- linters:
|
- gomnd
|
||||||
- err113
|
linters:
|
||||||
- mnd
|
disable-all: true
|
||||||
path: ci\/.+\.go
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
enable:
|
enable:
|
||||||
# - cyclop
|
|
||||||
# - errorlint
|
|
||||||
- asasalint
|
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bidichk
|
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- containedctx
|
- deadcode
|
||||||
- copyloopvar
|
|
||||||
- decorder
|
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- dupword
|
- errcheck
|
||||||
- durationcheck
|
|
||||||
- err113
|
|
||||||
- errchkjson
|
|
||||||
- errname
|
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- fatcontext
|
- exportloopref
|
||||||
- forcetypeassert
|
- gci
|
||||||
- gocheckcompilerdirectives
|
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gocognit
|
- gocognit
|
||||||
@@ -96,44 +45,41 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godot
|
- godot
|
||||||
|
- goerr113
|
||||||
- goheader
|
- goheader
|
||||||
- gomoddirectives
|
- goimports
|
||||||
|
- gomnd
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosmopolitan
|
- gosimple
|
||||||
- grouper
|
- govet
|
||||||
- importas
|
- importas
|
||||||
- interfacebloat
|
- ineffassign
|
||||||
- intrange
|
|
||||||
- ireturn
|
|
||||||
- lll
|
- lll
|
||||||
- maintidx
|
|
||||||
- makezero
|
|
||||||
- mirror
|
|
||||||
- misspell
|
- misspell
|
||||||
- mnd
|
|
||||||
- musttag
|
|
||||||
- nakedret
|
- nakedret
|
||||||
- nestif
|
- nestif
|
||||||
- nilerr
|
- nilerr
|
||||||
- nilnil
|
|
||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- nosprintfhostport
|
|
||||||
- paralleltest
|
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- promlinter
|
|
||||||
- reassign
|
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- tagalign
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- usestdlibvars
|
- unused
|
||||||
- wastedassign
|
- varcheck
|
||||||
- whitespace
|
- whitespace
|
||||||
- zerologlint
|
|
||||||
|
run:
|
||||||
|
skip-dirs:
|
||||||
|
- .devcontainer
|
||||||
|
- .github
|
||||||
|
- doc
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"default": true,
|
|
||||||
"MD013": false,
|
|
||||||
},
|
|
||||||
"ignores": [
|
|
||||||
".github/pull_request_template.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
// This list should be kept to the strict minimum
|
|
||||||
// to develop this project.
|
|
||||||
"recommendations": [
|
|
||||||
"golang.go",
|
|
||||||
"davidanson.vscode-markdownlint",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
29
.vscode/settings.json
vendored
29
.vscode/settings.json
vendored
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
// The settings should be kept to the strict minimum
|
|
||||||
// to develop this project.
|
|
||||||
"files.eol": "\n",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"go.buildTags": "linux",
|
|
||||||
"go.toolsEnvVars": {
|
|
||||||
"CGO_ENABLED": "0"
|
|
||||||
},
|
|
||||||
"go.testEnvVars": {
|
|
||||||
"CGO_ENABLED": "1"
|
|
||||||
},
|
|
||||||
"go.testFlags": [
|
|
||||||
"-v",
|
|
||||||
"-race"
|
|
||||||
],
|
|
||||||
"go.testTimeout": "10s",
|
|
||||||
"go.coverOnSingleTest": true,
|
|
||||||
"go.coverOnSingleTestFile": true,
|
|
||||||
"go.coverOnTestPackage": true,
|
|
||||||
"go.useLanguageServer": true,
|
|
||||||
"[go]": {
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"go.lintTool": "golangci-lint",
|
|
||||||
"go.lintOnSave": "package"
|
|
||||||
}
|
|
||||||
51
.vscode/tasks.json
vendored
51
.vscode/tasks.json
vendored
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Update a VPN provider servers data",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "go",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"./cmd/gluetun/main.go",
|
|
||||||
"update",
|
|
||||||
"${input:updateMode}",
|
|
||||||
"-providers",
|
|
||||||
"${input:provider}"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Add a Gluetun Github Git remote",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "git",
|
|
||||||
"args": [
|
|
||||||
"remote",
|
|
||||||
"add",
|
|
||||||
"${input:githubRemoteUsername}",
|
|
||||||
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "provider",
|
|
||||||
"type": "promptString",
|
|
||||||
"description": "Please enter a provider (or comma separated list of providers)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "updateMode",
|
|
||||||
"type": "pickString",
|
|
||||||
"description": "Update mode to use",
|
|
||||||
"options": [
|
|
||||||
"-maintainer",
|
|
||||||
"-enduser"
|
|
||||||
],
|
|
||||||
"default": "-maintainer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "githubRemoteUsername",
|
|
||||||
"type": "promptString",
|
|
||||||
"description": "Please enter a Github username",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
257
Dockerfile
257
Dockerfile
@@ -1,71 +1,63 @@
|
|||||||
ARG ALPINE_VERSION=3.22
|
ARG ALPINE_VERSION=3.13
|
||||||
ARG GO_ALPINE_VERSION=3.22
|
ARG GO_VERSION=1.16
|
||||||
ARG GO_VERSION=1.25
|
|
||||||
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
|
||||||
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
|
||||||
ARG MOCKGEN_VERSION=v1.6.0
|
|
||||||
ARG BUILDPLATFORM=linux/amd64
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
FROM --platform=$BUILDPLATFORM qmcgaw/xcputranslate:v0.6.0 AS xcputranslate
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||||
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
|
RUN apk --update add git g++
|
||||||
RUN apk --update add git g++ findutils
|
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
ARG GOLANGCI_LINT_VERSION=v1.41.1
|
||||||
COPY --from=mockgen /bin /go/bin/mockgen
|
RUN go get github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}
|
||||||
WORKDIR /tmp/gobuild
|
WORKDIR /tmp/gobuild
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY cmd/ ./cmd/
|
COPY cmd/ ./cmd/
|
||||||
COPY internal/ ./internal/
|
COPY internal/ ./internal/
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS test
|
FROM --platform=$BUILDPLATFORM base AS test
|
||||||
# Note on the go race detector:
|
# Note on the go race detector:
|
||||||
# - we set CGO_ENABLED=1 to have it enabled
|
# - we set CGO_ENABLED=1 to have it enabled
|
||||||
# - we installed g++ to support the race detector
|
# - we installed g++ to support the race detector
|
||||||
ENV CGO_ENABLED=1
|
ENV CGO_ENABLED=1
|
||||||
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
FROM --platform=$BUILDPLATFORM base AS lint
|
||||||
COPY .golangci.yml ./
|
COPY .golangci.yml ./
|
||||||
RUN golangci-lint run
|
RUN golangci-lint run --timeout=10m
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
FROM --platform=$BUILDPLATFORM base AS tidy
|
||||||
RUN git init && \
|
RUN git init && \
|
||||||
git config user.email ci@localhost && \
|
git config user.email ci@localhost && \
|
||||||
git config user.name ci && \
|
git config user.name ci && \
|
||||||
git config core.fileMode false && \
|
git add -A && git commit -m ci && \
|
||||||
git add -A && \
|
sed -i '/\/\/ indirect/d' go.mod && \
|
||||||
git commit -m "snapshot" && \
|
go mod tidy && \
|
||||||
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
|
git diff --exit-code -- go.mod
|
||||||
go generate -run "mockgen" ./... && \
|
|
||||||
git diff --exit-code && \
|
|
||||||
rm -rf .git/
|
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS build
|
FROM --platform=$BUILDPLATFORM base AS build
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
ARG ALLTARGETPLATFORMS=${TARGETPLATFORM}
|
||||||
ARG VERSION=unknown
|
ARG VERSION=unknown
|
||||||
ARG CREATED="an unknown date"
|
ARG BUILD_DATE="an unknown date"
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT=unknown
|
||||||
|
RUN xcputranslate sleep -targetplatform ${TARGETPLATFORM} -buildtime=10s -order=${ALLTARGETPLATFORMS}
|
||||||
RUN GOARCH="$(xcputranslate translate -field arch -targetplatform ${TARGETPLATFORM})" \
|
RUN GOARCH="$(xcputranslate translate -field arch -targetplatform ${TARGETPLATFORM})" \
|
||||||
GOARM="$(xcputranslate translate -field arm -targetplatform ${TARGETPLATFORM})" \
|
GOARM="$(xcputranslate translate -field arm -targetplatform ${TARGETPLATFORM})" \
|
||||||
go build -trimpath -ldflags="-s -w \
|
go build -trimpath -ldflags="-s -w \
|
||||||
-X 'main.version=$VERSION' \
|
-X 'main.version=$VERSION' \
|
||||||
-X 'main.created=$CREATED' \
|
-X 'main.buildDate=$BUILD_DATE' \
|
||||||
-X 'main.commit=$COMMIT' \
|
-X 'main.commit=$COMMIT' \
|
||||||
" -o entrypoint cmd/gluetun/main.go
|
" -o entrypoint cmd/gluetun/main.go
|
||||||
|
|
||||||
FROM alpine:${ALPINE_VERSION}
|
FROM alpine:${ALPINE_VERSION}
|
||||||
ARG VERSION=unknown
|
ARG VERSION=unknown
|
||||||
ARG CREATED="an unknown date"
|
ARG BUILD_DATE="an unknown date"
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT=unknown
|
||||||
LABEL \
|
LABEL \
|
||||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||||
org.opencontainers.image.created=$CREATED \
|
org.opencontainers.image.created=$BUILD_DATE \
|
||||||
org.opencontainers.image.version=$VERSION \
|
org.opencontainers.image.version=$VERSION \
|
||||||
org.opencontainers.image.revision=$COMMIT \
|
org.opencontainers.image.revision=$COMMIT \
|
||||||
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.url="https://github.com/qdm12/gluetun" \
|
||||||
@@ -73,121 +65,75 @@ LABEL \
|
|||||||
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
org.opencontainers.image.source="https://github.com/qdm12/gluetun" \
|
||||||
org.opencontainers.image.title="VPN swiss-knife like client for multiple VPN providers" \
|
org.opencontainers.image.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"
|
org.opencontainers.image.description="VPN swiss-knife like client to tunnel to multiple VPN servers using OpenVPN, IPtables, DNS over TLS, Shadowsocks, an HTTP proxy and Alpine Linux"
|
||||||
ENV VPN_SERVICE_PROVIDER=pia \
|
ENV VPNSP=pia \
|
||||||
VPN_TYPE=openvpn \
|
VERSION_INFORMATION=on \
|
||||||
# Common VPN options
|
PROTOCOL=udp \
|
||||||
VPN_INTERFACE=tun0 \
|
OPENVPN_VERSION=2.5 \
|
||||||
# OpenVPN
|
OPENVPN_VERBOSITY=1 \
|
||||||
OPENVPN_ENDPOINT_IP= \
|
OPENVPN_ROOT=yes \
|
||||||
OPENVPN_ENDPOINT_PORT= \
|
OPENVPN_TARGET_IP= \
|
||||||
OPENVPN_PROTOCOL=udp \
|
OPENVPN_IPV6=off \
|
||||||
|
OPENVPN_CUSTOM_CONFIG= \
|
||||||
|
TZ= \
|
||||||
|
PUID= \
|
||||||
|
PGID= \
|
||||||
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
|
# VPN provider settings
|
||||||
OPENVPN_USER= \
|
OPENVPN_USER= \
|
||||||
OPENVPN_PASSWORD= \
|
OPENVPN_PASSWORD= \
|
||||||
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
|
USER_SECRETFILE=/run/secrets/openvpn_user \
|
||||||
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
|
PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
|
||||||
OPENVPN_VERSION=2.6 \
|
REGION= \
|
||||||
OPENVPN_VERBOSITY=1 \
|
COUNTRY= \
|
||||||
OPENVPN_FLAGS= \
|
CITY= \
|
||||||
OPENVPN_CIPHERS= \
|
PORT= \
|
||||||
OPENVPN_AUTH= \
|
SERVER_HOSTNAME= \
|
||||||
OPENVPN_PROCESS_USER=root \
|
# Mullvad only:
|
||||||
OPENVPN_MSSFIX= \
|
|
||||||
OPENVPN_CUSTOM_CONFIG= \
|
|
||||||
# Wireguard
|
|
||||||
WIREGUARD_ENDPOINT_IP= \
|
|
||||||
WIREGUARD_ENDPOINT_PORT= \
|
|
||||||
WIREGUARD_CONF_SECRETFILE=/run/secrets/wg0.conf \
|
|
||||||
WIREGUARD_PRIVATE_KEY= \
|
|
||||||
WIREGUARD_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
|
|
||||||
WIREGUARD_PRESHARED_KEY= \
|
|
||||||
WIREGUARD_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
|
|
||||||
WIREGUARD_PUBLIC_KEY= \
|
|
||||||
WIREGUARD_ALLOWED_IPS= \
|
|
||||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
|
||||||
WIREGUARD_ADDRESSES= \
|
|
||||||
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
|
||||||
WIREGUARD_MTU=1320 \
|
|
||||||
WIREGUARD_IMPLEMENTATION=auto \
|
|
||||||
# VPN server filtering
|
|
||||||
SERVER_REGIONS= \
|
|
||||||
SERVER_COUNTRIES= \
|
|
||||||
SERVER_CITIES= \
|
|
||||||
SERVER_HOSTNAMES= \
|
|
||||||
SERVER_CATEGORIES= \
|
|
||||||
# # Mullvad only:
|
|
||||||
ISP= \
|
ISP= \
|
||||||
OWNED_ONLY=no \
|
OWNED=no \
|
||||||
# # Private Internet Access only:
|
# Private Internet Access only:
|
||||||
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
PIA_ENCRYPTION=strong \
|
||||||
VPN_PORT_FORWARDING=off \
|
PORT_FORWARDING=off \
|
||||||
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
|
PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
VPN_PORT_FORWARDING_PROVIDER= \
|
# Cyberghost only:
|
||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
CYBERGHOST_GROUP="Premium UDP Europe" \
|
||||||
VPN_PORT_FORWARDING_USERNAME= \
|
|
||||||
VPN_PORT_FORWARDING_PASSWORD= \
|
|
||||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
|
||||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
|
||||||
# # Cyberghost only:
|
|
||||||
OPENVPN_CERT= \
|
|
||||||
OPENVPN_KEY= \
|
|
||||||
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt \
|
||||||
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey \
|
||||||
# # VPNSecure only:
|
# Nordvpn only:
|
||||||
OPENVPN_ENCRYPTED_KEY= \
|
|
||||||
OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key \
|
|
||||||
OPENVPN_KEY_PASSPHRASE= \
|
|
||||||
OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase \
|
|
||||||
# # Nordvpn only:
|
|
||||||
SERVER_NUMBER= \
|
SERVER_NUMBER= \
|
||||||
# # PIA only:
|
# NordVPN and ProtonVPN only:
|
||||||
SERVER_NAMES= \
|
SERVER_NAME= \
|
||||||
# # VPNUnlimited and ProtonVPN only:
|
# ProtonVPN only:
|
||||||
STREAM_ONLY= \
|
|
||||||
FREE_ONLY= \
|
FREE_ONLY= \
|
||||||
# # ProtonVPN only:
|
# Openvpn
|
||||||
SECURE_CORE_ONLY= \
|
OPENVPN_CIPHER= \
|
||||||
TOR_ONLY= \
|
OPENVPN_AUTH= \
|
||||||
# # Surfshark only:
|
# DNS over TLS
|
||||||
MULTIHOP_ONLY= \
|
DOT=on \
|
||||||
# # VPN Secure only:
|
DOT_PROVIDERS=cloudflare \
|
||||||
PREMIUM_ONLY= \
|
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||||
# # PIA and ProtonVPN only:
|
DOT_VERBOSITY=1 \
|
||||||
PORT_FORWARD_ONLY= \
|
DOT_VERBOSITY_DETAILS=0 \
|
||||||
|
DOT_VALIDATION_LOGLEVEL=0 \
|
||||||
|
DOT_CACHING=on \
|
||||||
|
DOT_IPV6=off \
|
||||||
|
BLOCK_MALICIOUS=on \
|
||||||
|
BLOCK_SURVEILLANCE=off \
|
||||||
|
BLOCK_ADS=off \
|
||||||
|
UNBLOCK= \
|
||||||
|
DNS_UPDATE_PERIOD=24h \
|
||||||
|
DNS_PLAINTEXT_ADDRESS=1.1.1.1 \
|
||||||
|
DNS_KEEP_NAMESERVER=off \
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
FIREWALL=on \
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
FIREWALL_INPUT_PORTS= \
|
FIREWALL_INPUT_PORTS= \
|
||||||
FIREWALL_OUTBOUND_SUBNETS= \
|
FIREWALL_OUTBOUND_SUBNETS= \
|
||||||
FIREWALL_DEBUG=off \
|
FIREWALL_DEBUG=off \
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=info \
|
|
||||||
# Health
|
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
|
||||||
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
|
|
||||||
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
|
|
||||||
HEALTH_SMALL_CHECK_TYPE=icmp \
|
|
||||||
HEALTH_RESTART_VPN=on \
|
|
||||||
# DNS
|
|
||||||
DNS_SERVER=on \
|
|
||||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
|
||||||
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
|
||||||
DNS_BLOCK_IPS= \
|
|
||||||
DNS_BLOCK_IP_PREFIXES= \
|
|
||||||
DNS_CACHING=on \
|
|
||||||
DNS_UPSTREAM_IPV6=off \
|
|
||||||
BLOCK_MALICIOUS=on \
|
|
||||||
BLOCK_SURVEILLANCE=off \
|
|
||||||
BLOCK_ADS=off \
|
|
||||||
DNS_UNBLOCK_HOSTNAMES= \
|
|
||||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
|
||||||
DNS_UPDATE_PERIOD=24h \
|
|
||||||
DNS_ADDRESS=127.0.0.1 \
|
|
||||||
DNS_KEEP_NAMESERVER=off \
|
|
||||||
# HTTP proxy
|
# HTTP proxy
|
||||||
HTTPPROXY= \
|
HTTPPROXY= \
|
||||||
HTTPPROXY_LOG=off \
|
HTTPPROXY_LOG=off \
|
||||||
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
HTTPPROXY_PORT=8888 \
|
||||||
HTTPPROXY_STEALTH=off \
|
|
||||||
HTTPPROXY_USER= \
|
HTTPPROXY_USER= \
|
||||||
HTTPPROXY_PASSWORD= \
|
HTTPPROXY_PASSWORD= \
|
||||||
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||||
@@ -195,49 +141,20 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Shadowsocks
|
# Shadowsocks
|
||||||
SHADOWSOCKS=off \
|
SHADOWSOCKS=off \
|
||||||
SHADOWSOCKS_LOG=off \
|
SHADOWSOCKS_LOG=off \
|
||||||
SHADOWSOCKS_LISTENING_ADDRESS=":8388" \
|
SHADOWSOCKS_PORT=8388 \
|
||||||
SHADOWSOCKS_PASSWORD= \
|
SHADOWSOCKS_PASSWORD= \
|
||||||
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||||
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
SHADOWSOCKS_METHOD=chacha20-ietf-poly1305 \
|
||||||
# Control server
|
UPDATER_PERIOD=0
|
||||||
HTTP_CONTROL_SERVER_LOG=on \
|
ENTRYPOINT ["/entrypoint"]
|
||||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
|
||||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
|
||||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
|
||||||
# Server data updater
|
|
||||||
UPDATER_PERIOD=0 \
|
|
||||||
UPDATER_MIN_RATIO=0.8 \
|
|
||||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
|
||||||
UPDATER_PROTONVPN_EMAIL= \
|
|
||||||
UPDATER_PROTONVPN_PASSWORD= \
|
|
||||||
# Public IP
|
|
||||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
|
||||||
PUBLICIP_ENABLED=on \
|
|
||||||
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
|
||||||
PUBLICIP_API_TOKEN= \
|
|
||||||
# Storage
|
|
||||||
STORAGE_FILEPATH=/gluetun/servers.json \
|
|
||||||
# Pprof
|
|
||||||
PPROF_ENABLED=no \
|
|
||||||
PPROF_BLOCK_PROFILE_RATE=0 \
|
|
||||||
PPROF_MUTEX_PROFILE_RATE=0 \
|
|
||||||
PPROF_HTTP_SERVER_ADDRESS=":6060" \
|
|
||||||
# Extras
|
|
||||||
VERSION_INFORMATION=on \
|
|
||||||
TZ= \
|
|
||||||
PUID=1000 \
|
|
||||||
PGID=1000
|
|
||||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
|
||||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /entrypoint healthcheck
|
||||||
ARG TARGETPLATFORM
|
RUN apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn==2.4.11-r0 && \
|
||||||
RUN apk add --no-cache --update -l wget && \
|
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
|
||||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
|
|
||||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
|
|
||||||
apk del openvpn && \
|
apk del openvpn && \
|
||||||
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
apk add --no-cache --update openvpn ca-certificates iptables ip6tables unbound tzdata && \
|
||||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
|
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||||
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
|
||||||
deluser openvpn && \
|
deluser openvpn && \
|
||||||
|
deluser unbound && \
|
||||||
mkdir /gluetun
|
mkdir /gluetun
|
||||||
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
COPY --from=build /tmp/gobuild/entrypoint /entrypoint
|
||||||
|
|||||||
175
README.md
175
README.md
@@ -1,130 +1,125 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, FastestVPN,
|
||||||
|
HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
|
||||||
|
ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN and Windscribe VPN servers
|
||||||
|
using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
|
||||||
|
|
||||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
**ANNOUNCEMENT**:
|
||||||
|
|
||||||

|
<img height="250" src="https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg?sanitize=true">
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/actions/workflows/ci.yml)
|
[](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/gluetun)
|
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
||||||
|
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun)
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags?page=1&ordering=last_updated)
|

|
||||||

|
|
||||||

|

|
||||||
|
[](https://github.com/qdm12/gluetun/commits)
|
||||||
|
|
||||||
[](https://hub.docker.com/r/qmcgaw/gluetun/tags)
|
[](https://github.com/qdm12/gluetun)
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/commits/master)
|
|
||||||
[](https://github.com/qdm12/gluetun/graphs/contributors)
|
|
||||||
[](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)
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Quick links
|
## Quick links
|
||||||
|
|
||||||
- [Setup](#setup)
|
- Problem or suggestion?
|
||||||
- [Features](#features)
|
|
||||||
- Problem?
|
|
||||||
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
|
|
||||||
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||||
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
|
||||||
- Suggestion?
|
|
||||||
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
- [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?
|
- Happy?
|
||||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||||
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
||||||
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
|
|
||||||
- Video:
|
|
||||||
|
|
||||||
[](https://youtu.be/0F6I03LQcI4)
|
|
||||||
|
|
||||||
- [Substack Console interview](https://console.substack.com/p/console-72)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.22 for a small Docker image of 41.1MB
|
- Based on Alpine 3.13 for a small Docker image of 54MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports Openvpn only for now
|
||||||
- Supports Wireguard both kernelspace and userspace
|
|
||||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
|
||||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
|
||||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
|
||||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
|
||||||
- DNS over TLS baked in with service provider(s) of your choice
|
- DNS over TLS baked in with service provider(s) of your choice
|
||||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
||||||
- Choose the vpn network protocol, `udp` or `tcp`
|
- Choose the vpn network protocol, `udp` or `tcp`
|
||||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||||
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
|
||||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
|
||||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||||
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
- VPN server side port forwarding for Private Internet Access and Vyprvpn
|
||||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||||
|
- Subprograms all drop root privileges once launched
|
||||||
|
- Subprograms output streams are all merged together
|
||||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
1. Ensure your `tun` kernel module is setup:
|
||||||
|
|
||||||
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
```sh
|
||||||
|
sudo modprobe tun
|
||||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new/choose)
|
# or, if you don't have modprobe, with
|
||||||
|
sudo insmod /lib/modules/tun.ko
|
||||||
Here's a docker-compose.yml for the laziest:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
---
|
|
||||||
services:
|
|
||||||
gluetun:
|
|
||||||
image: qmcgaw/gluetun
|
|
||||||
# container_name: gluetun
|
|
||||||
# line above must be uncommented to allow external containers to connect.
|
|
||||||
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
devices:
|
|
||||||
- /dev/net/tun:/dev/net/tun
|
|
||||||
ports:
|
|
||||||
- 8888:8888/tcp # HTTP proxy
|
|
||||||
- 8388:8388/tcp # Shadowsocks
|
|
||||||
- 8388:8388/udp # Shadowsocks
|
|
||||||
volumes:
|
|
||||||
- /yourpath:/gluetun
|
|
||||||
environment:
|
|
||||||
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
|
||||||
- VPN_SERVICE_PROVIDER=ivpn
|
|
||||||
- VPN_TYPE=openvpn
|
|
||||||
# OpenVPN:
|
|
||||||
- OPENVPN_USER=
|
|
||||||
- OPENVPN_PASSWORD=
|
|
||||||
# Wireguard:
|
|
||||||
# - WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
|
|
||||||
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
|
||||||
# Timezone for accurate log times
|
|
||||||
- TZ=
|
|
||||||
# Server list updater
|
|
||||||
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
|
||||||
- UPDATER_PERIOD=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
1. Extra steps:
|
||||||
|
- [For Synology users](https://github.com/qdm12/gluetun/wiki/Synology-setup)
|
||||||
|
- [For 32 bit Operating systems (**Rasberry Pis**)](https://github.com/qdm12/gluetun/wiki/32-bit-setup)
|
||||||
|
1. Launch the container with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name gluetun --cap-add=NET_ADMIN \
|
||||||
|
-e VPNSP="private internet access" -e REGION="CA Montreal" \
|
||||||
|
-e OPENVPN_USER=js89ds7 -e OPENVPN_PASSWORD=8fd9s239G \
|
||||||
|
-v /yourpath:/gluetun \
|
||||||
|
qmcgaw/gluetun
|
||||||
|
```
|
||||||
|
|
||||||
|
or use [docker-compose.yml](https://github.com/qdm12/gluetun/blob/master/docker-compose.yml) with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
You should probably check the many [environment variables](https://github.com/qdm12/gluetun/wiki/Environment-variables) available to adapt the container to your needs.
|
||||||
|
|
||||||
|
## Further setup
|
||||||
|
|
||||||
|
The following points are all optional but should give you insights on all the possibilities with this container.
|
||||||
|
|
||||||
|
- [Test your setup](https://github.com/qdm12/gluetun/wiki/Test-your-setup)
|
||||||
|
- [How to connect other containers and devices to Gluetun](https://github.com/qdm12/gluetun/wiki/Connect-to-gluetun)
|
||||||
|
- [VPN server side port forwarding](https://github.com/qdm12/gluetun/wiki/Port-forwarding)
|
||||||
|
- [HTTP control server](https://github.com/qdm12/gluetun/wiki/HTTP-Control-server) to automate things, restart Openvpn etc.
|
||||||
|
- Update the image with `docker pull qmcgaw/gluetun:latest`. See this [Wiki document](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) for Docker tags available.
|
||||||
|
- Use [Docker secrets](https://github.com/qdm12/gluetun/wiki/Docker-secrets) to read your credentials instead of environment variables
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
[](https://github.com/qdm12/gluetun/master/LICENSE)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
[](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)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/ci/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Println("Usage: " + os.Args[0] + " <command>")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "mullvad":
|
|
||||||
err = internal.MullvadTest(ctx)
|
|
||||||
case "protonvpn":
|
|
||||||
err = internal.ProtonVPNTest(ctx)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
|
||||||
}
|
|
||||||
stop()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("❌", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("✅ Test completed successfully.")
|
|
||||||
}
|
|
||||||
36
ci/go.mod
36
ci/go.mod
@@ -1,36 +0,0 @@
|
|||||||
module github.com/qdm12/gluetun/ci
|
|
||||||
|
|
||||||
go 1.25.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/docker/docker v28.5.1+incompatible
|
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
|
||||||
github.com/moby/term v0.5.2 // indirect
|
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
golang.org/x/time v0.14.0 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
|
||||||
97
ci/go.sum
97
ci/go.sum
@@ -1,97 +0,0 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MullvadTest(ctx context.Context) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
"Wireguard address",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading secrets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := []string{
|
|
||||||
"VPN_SERVICE_PROVIDER=mullvad",
|
|
||||||
"VPN_TYPE=wireguard",
|
|
||||||
"LOG_LEVEL=debug",
|
|
||||||
"SERVER_COUNTRIES=USA",
|
|
||||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
|
||||||
"WIREGUARD_ADDRESSES=" + secrets[1],
|
|
||||||
}
|
|
||||||
return simpleTest(ctx, env)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProtonVPNTest(ctx context.Context) error {
|
|
||||||
expectedSecrets := []string{
|
|
||||||
"Wireguard private key",
|
|
||||||
}
|
|
||||||
secrets, err := readSecrets(ctx, expectedSecrets)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading secrets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
env := []string{
|
|
||||||
"VPN_SERVICE_PROVIDER=protonvpn",
|
|
||||||
"VPN_TYPE=wireguard",
|
|
||||||
"LOG_LEVEL=debug",
|
|
||||||
"SERVER_COUNTRIES=United States",
|
|
||||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
|
||||||
}
|
|
||||||
return simpleTest(ctx, env)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readSecrets(ctx context.Context, expectedSecrets []string) (lines []string, err error) {
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
lines = make([]string, 0, len(expectedSecrets))
|
|
||||||
|
|
||||||
for i := range expectedSecrets {
|
|
||||||
fmt.Println("🤫 reading", expectedSecrets[i], "from Stdin...")
|
|
||||||
if !scanner.Scan() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
|
||||||
fmt.Println("🤫 "+expectedSecrets[i], "secret read successfully")
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(lines) < len(expectedSecrets) {
|
|
||||||
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
|
||||||
len(expectedSecrets), len(lines))
|
|
||||||
}
|
|
||||||
for i, line := range lines {
|
|
||||||
if line == "" {
|
|
||||||
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines, nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ptrTo[T any](v T) *T { return &v }
|
|
||||||
|
|
||||||
func simpleTest(ctx context.Context, env []string) error {
|
|
||||||
const timeout = 30 * time.Second
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating Docker client: %w", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
config := &container.Config{
|
|
||||||
Image: "qmcgaw/gluetun",
|
|
||||||
StopTimeout: ptrTo(3),
|
|
||||||
Env: env,
|
|
||||||
}
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
AutoRemove: true,
|
|
||||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
|
||||||
}
|
|
||||||
networkConfig := (*network.NetworkingConfig)(nil)
|
|
||||||
platform := (*v1.Platform)(nil)
|
|
||||||
const containerName = "" // auto-generated name
|
|
||||||
|
|
||||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating container: %w", err)
|
|
||||||
}
|
|
||||||
for _, warning := range response.Warnings {
|
|
||||||
fmt.Println("Warning during container creation:", warning)
|
|
||||||
}
|
|
||||||
containerID := response.ID
|
|
||||||
defer stopContainer(client, containerID)
|
|
||||||
|
|
||||||
beforeStartTime := time.Now()
|
|
||||||
|
|
||||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting container: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return waitForLogLine(ctx, client, containerID, beforeStartTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopContainer(client *client.Client, containerID string) {
|
|
||||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
|
||||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
|
||||||
defer stopCancel()
|
|
||||||
|
|
||||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed to stop container:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
|
||||||
|
|
||||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
|
||||||
beforeStartTime time.Time,
|
|
||||||
) error {
|
|
||||||
logOptions := container.LogsOptions{
|
|
||||||
ShowStdout: true,
|
|
||||||
Follow: true,
|
|
||||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting container logs: %w", err)
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
var linesSeen []string
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
for ctx.Err() == nil {
|
|
||||||
if scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) > 8 { // remove Docker log prefix
|
|
||||||
line = line[8:]
|
|
||||||
}
|
|
||||||
linesSeen = append(linesSeen, line)
|
|
||||||
if successRegexp.MatchString(line) {
|
|
||||||
fmt.Println("✅ Success line logged")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := scanner.Err()
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
return fmt.Errorf("reading log stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scanner is either done or cannot read because of EOF
|
|
||||||
fmt.Println("The log scanner stopped")
|
|
||||||
logSeenLines(linesSeen)
|
|
||||||
|
|
||||||
// Check if the container is still running
|
|
||||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inspecting container: %w", err)
|
|
||||||
}
|
|
||||||
if !inspect.State.Running {
|
|
||||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func logSeenLines(lines []string) {
|
|
||||||
fmt.Println("Logs seen so far:")
|
|
||||||
for _, line := range lines {
|
|
||||||
fmt.Println(" " + line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,213 +4,209 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
nativeos "os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
_ "time/tzdata"
|
|
||||||
|
|
||||||
_ "github.com/breml/rootcerts"
|
"github.com/qdm12/dns/pkg/unbound"
|
||||||
"github.com/qdm12/gluetun/internal/alpine"
|
"github.com/qdm12/gluetun/internal/alpine"
|
||||||
"github.com/qdm12/gluetun/internal/cli"
|
"github.com/qdm12/gluetun/internal/cli"
|
||||||
"github.com/qdm12/gluetun/internal/command"
|
"github.com/qdm12/gluetun/internal/configuration"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/dns"
|
"github.com/qdm12/gluetun/internal/dns"
|
||||||
"github.com/qdm12/gluetun/internal/firewall"
|
"github.com/qdm12/gluetun/internal/firewall"
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
"github.com/qdm12/gluetun/internal/httpproxy"
|
"github.com/qdm12/gluetun/internal/httpproxy"
|
||||||
|
gluetunLogging "github.com/qdm12/gluetun/internal/logging"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/qdm12/gluetun/internal/netlink"
|
|
||||||
"github.com/qdm12/gluetun/internal/openvpn"
|
"github.com/qdm12/gluetun/internal/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
|
||||||
"github.com/qdm12/gluetun/internal/portforward"
|
|
||||||
"github.com/qdm12/gluetun/internal/pprof"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/publicip"
|
"github.com/qdm12/gluetun/internal/publicip"
|
||||||
"github.com/qdm12/gluetun/internal/routing"
|
"github.com/qdm12/gluetun/internal/routing"
|
||||||
"github.com/qdm12/gluetun/internal/server"
|
"github.com/qdm12/gluetun/internal/server"
|
||||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
"github.com/qdm12/gluetun/internal/tun"
|
"github.com/qdm12/gluetun/internal/unix"
|
||||||
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
"github.com/qdm12/gluetun/internal/updater"
|
||||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
versionpkg "github.com/qdm12/gluetun/internal/version"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/golibs/logging"
|
||||||
"github.com/qdm12/gluetun/internal/vpn"
|
"github.com/qdm12/golibs/os"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/golibs/os/user"
|
||||||
"github.com/qdm12/gosettings/reader/sources/env"
|
"github.com/qdm12/golibs/params"
|
||||||
"github.com/qdm12/goshutdown"
|
"github.com/qdm12/goshutdown"
|
||||||
"github.com/qdm12/goshutdown/goroutine"
|
"github.com/qdm12/updated/pkg/dnscrypto"
|
||||||
"github.com/qdm12/goshutdown/group"
|
|
||||||
"github.com/qdm12/goshutdown/order"
|
|
||||||
"github.com/qdm12/gosplash"
|
|
||||||
"github.com/qdm12/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
version = "unknown"
|
version = "unknown"
|
||||||
commit = "unknown"
|
commit = "unknown"
|
||||||
created = "an unknown date"
|
buildDate = "an unknown date"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSetupRouting = errors.New("cannot setup routing")
|
||||||
|
errCreateUser = errors.New("cannot create user")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
buildInfo := models.BuildInformation{
|
buildInfo := models.BuildInformation{
|
||||||
Version: version,
|
Version: version,
|
||||||
Commit: commit,
|
Commit: commit,
|
||||||
Created: created,
|
BuildDate: buildDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
background := context.Background()
|
ctx := context.Background()
|
||||||
signalCh := make(chan os.Signal, 1)
|
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, nativeos.Interrupt)
|
||||||
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
ctx, cancel := context.WithCancel(background)
|
|
||||||
|
|
||||||
logger := log.New(log.SetLevel(log.LevelInfo))
|
logger := logging.NewParent(logging.Settings{})
|
||||||
|
|
||||||
args := os.Args
|
args := nativeos.Args
|
||||||
tun := tun.New()
|
os := os.New()
|
||||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
osUser := user.New()
|
||||||
netLinker := netlink.New(netLinkDebugLogger)
|
unix := unix.New()
|
||||||
cli := cli.New()
|
cli := cli.New()
|
||||||
cmder := command.New()
|
|
||||||
|
|
||||||
reader := reader.New(reader.Settings{
|
|
||||||
Sources: []reader.Source{
|
|
||||||
secrets.New(logger),
|
|
||||||
files.New(logger),
|
|
||||||
env.New(env.Settings{}),
|
|
||||||
},
|
|
||||||
HandleDeprecatedKey: func(source, deprecatedKey, currentKey string) {
|
|
||||||
logger.Warn("You are using the old " + source + " " + deprecatedKey +
|
|
||||||
", please consider changing it to " + currentKey)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
errorCh := make(chan error)
|
errorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
errorCh <- _main(ctx, buildInfo, args, logger, os, osUser, unix, cli)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for OS signal or run error
|
|
||||||
var err error
|
|
||||||
select {
|
select {
|
||||||
case receivedSignal := <-signalCh:
|
case <-ctx.Done():
|
||||||
signal.Stop(signalCh)
|
stop()
|
||||||
fmt.Println("")
|
logger.Warn("Caught OS signal, shutting down")
|
||||||
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
case err := <-errorCh:
|
||||||
cancel()
|
stop()
|
||||||
case err = <-errorCh:
|
|
||||||
close(errorCh)
|
close(errorCh)
|
||||||
if err == nil { // expected exit such as healthcheck
|
if err == nil { // expected exit such as healthcheck
|
||||||
os.Exit(0)
|
nativeos.Exit(0)
|
||||||
}
|
}
|
||||||
logger.Error(err.Error())
|
logger.Error(err)
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown timed sequence, and force exit on second OS signal
|
|
||||||
const shutdownGracePeriod = 5 * time.Second
|
const shutdownGracePeriod = 5 * time.Second
|
||||||
timer := time.NewTimer(shutdownGracePeriod)
|
timer := time.NewTimer(shutdownGracePeriod)
|
||||||
select {
|
select {
|
||||||
case shutdownErr := <-errorCh:
|
case <-errorCh:
|
||||||
timer.Stop()
|
if !timer.Stop() {
|
||||||
if shutdownErr != nil {
|
<-timer.C
|
||||||
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Shutdown successful")
|
logger.Info("Shutdown successful")
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
logger.Warn("Shutdown timed out")
|
logger.Warn("Shutdown timed out")
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var errCommandUnknown = errors.New("command is unknown")
|
nativeos.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:gocognit,gocyclo,maintidx
|
var (
|
||||||
|
errCommandUnknown = errors.New("command is unknown")
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gocognit,gocyclo
|
||||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
args []string, logger logging.ParentLogger, os os.OS,
|
||||||
tun Tun, netLinker netLinker, cmder RunStarter,
|
osUser user.OSUser, unix unix.Unix, cli cli.CLI) error {
|
||||||
cli clier,
|
|
||||||
) error {
|
|
||||||
if len(args) > 1 { // cli operation
|
if len(args) > 1 { // cli operation
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
return cli.HealthCheck(ctx, reader, logger)
|
return cli.HealthCheck(ctx)
|
||||||
case "clientkey":
|
case "clientkey":
|
||||||
return cli.ClientKey(args[2:])
|
return cli.ClientKey(args[2:], os.OpenFile)
|
||||||
case "openvpnconfig":
|
case "openvpnconfig":
|
||||||
return cli.OpenvpnConfig(logger, reader, netLinker)
|
return cli.OpenvpnConfig(os, logger)
|
||||||
case "update":
|
case "update":
|
||||||
return cli.Update(ctx, args[2:], logger)
|
return cli.Update(ctx, args[2:], os, logger)
|
||||||
case "format-servers":
|
|
||||||
return cli.FormatServers(args[2:])
|
|
||||||
case "genkey":
|
|
||||||
return cli.GenKey(args[2:])
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer fmt.Println(gluetunLogo)
|
const clientTimeout = 15 * time.Second
|
||||||
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
|
// Create configurators
|
||||||
|
alpineConf := alpine.NewConfigurator(os.OpenFile, osUser)
|
||||||
|
ovpnConf := openvpn.NewConfigurator(
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
|
||||||
|
os, unix)
|
||||||
|
dnsCrypto := dnscrypto.New(httpClient, "", "")
|
||||||
|
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
|
||||||
|
dnsConf := unbound.NewConfigurator(nil, os.OpenFile, dnsCrypto,
|
||||||
|
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
|
||||||
|
routingConf := routing.NewRouting(
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "routing: "}))
|
||||||
|
firewallConf := firewall.NewConfigurator(
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "firewall: "}),
|
||||||
|
routingConf, os.OpenFile)
|
||||||
|
|
||||||
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
fmt.Println(gluetunLogging.Splash(buildInfo))
|
||||||
|
|
||||||
|
if err := printVersions(ctx, logger, []printVersionElement{
|
||||||
|
{name: "Alpine", getVersion: alpineConf.Version},
|
||||||
|
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
|
||||||
|
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||||
|
{name: "Unbound", getVersion: dnsConf.Version},
|
||||||
|
{name: "IPtables", getVersion: firewallConf.Version},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allSettings configuration.Settings
|
||||||
|
err := allSettings.Read(params.NewEnv(), os,
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "configuration: "}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
splashSettings := gosplash.Settings{
|
logger.Info(allSettings.String())
|
||||||
User: "qdm12",
|
|
||||||
Repository: "gluetun",
|
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
||||||
Emails: []string{"quentin.mcgaw@gmail.com"},
|
return err
|
||||||
Version: buildInfo.Version,
|
|
||||||
Commit: buildInfo.Commit,
|
|
||||||
Created: buildInfo.Created,
|
|
||||||
Announcement: "All control server routes will become private by default after the v3.41.0 release",
|
|
||||||
AnnounceExp: announcementExp,
|
|
||||||
// Sponsor information
|
|
||||||
PaypalUser: "qmcgaw",
|
|
||||||
GithubSponsor: "qdm12",
|
|
||||||
}
|
}
|
||||||
for _, line := range gosplash.MakeLines(splashSettings) {
|
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
||||||
fmt.Println(line)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var allSettings settings.Settings
|
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||||
err = allSettings.Read(reader, logger)
|
storage := storage.New(
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "storage: "}),
|
||||||
|
os, constants.ServersData)
|
||||||
|
allServers, err := storage.SyncServers(constants.GetAllServers())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allSettings.SetDefaults()
|
|
||||||
|
|
||||||
// Note: no need to validate minimal settings for the firewall:
|
// Should never change
|
||||||
// - global log level is parsed below
|
puid, pgid := allSettings.System.PUID, allSettings.System.PGID
|
||||||
// - firewall Debug and Enabled are booleans parsed from source
|
|
||||||
logLevel, err := log.ParseLevel(allSettings.Log.Level)
|
const defaultUsername = "nonrootuser"
|
||||||
|
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("log level: %w", err)
|
return fmt.Errorf("%w: %s", errCreateUser, err)
|
||||||
}
|
}
|
||||||
logger.Patch(log.SetLevel(logLevel))
|
if nonRootUsername != defaultUsername {
|
||||||
netLinker.PatchLoggerLevel(logLevel)
|
logger.Info("using existing username %s corresponding to user id %d", nonRootUsername, puid)
|
||||||
|
|
||||||
routingLogger := logger.New(log.SetComponent("routing"))
|
|
||||||
if *allSettings.Firewall.Debug { // To remove in v4
|
|
||||||
routingLogger.Patch(log.SetLevel(log.LevelDebug))
|
|
||||||
}
|
}
|
||||||
routingConf := routing.New(netLinker, routingLogger)
|
// set it for Unbound
|
||||||
|
// TODO remove this when migrating to qdm12/dns v2
|
||||||
|
allSettings.DNS.Unbound.Username = nonRootUsername
|
||||||
|
|
||||||
defaultRoutes, err := routingConf.DefaultRoutes()
|
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if allSettings.Firewall.Debug {
|
||||||
|
firewallConf.SetDebug()
|
||||||
|
routingConf.SetDebug()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultInterface, defaultGateway, err := routingConf.DefaultRoute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -220,102 +216,23 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
firewallLogger := logger.New(log.SetComponent("firewall"))
|
defaultIP, err := routingConf.DefaultIP()
|
||||||
if *allSettings.Firewall.Debug { // To remove in v4
|
|
||||||
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
|
|
||||||
}
|
|
||||||
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
|
|
||||||
defaultRoutes, localNetworks)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if *allSettings.Firewall.Enabled {
|
firewallConf.SetNetworkInformation(defaultInterface, defaultGateway, localNetworks, defaultIP)
|
||||||
err = firewallConf.SetEnabled(ctx, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
|
||||||
storageLogger := logger.New(log.SetComponent("storage"))
|
|
||||||
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ipv6Supported, err := netLinker.IsIPv6Supported()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
allSettings.Pprof.HTTPServer.Logger = logger.New(log.SetComponent("pprof"))
|
|
||||||
pprofServer, err := pprof.New(allSettings.Pprof)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating Pprof server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
|
||||||
|
|
||||||
const clientTimeout = 15 * time.Second
|
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
|
||||||
// Create configurators
|
|
||||||
alpineConf := alpine.New()
|
|
||||||
ovpnConf := openvpn.New(
|
|
||||||
logger.New(log.SetComponent("openvpn configurator")),
|
|
||||||
cmder, puid, pgid)
|
|
||||||
|
|
||||||
err = printVersions(ctx, logger, []printVersionElement{
|
|
||||||
{name: "Alpine", getVersion: alpineConf.Version},
|
|
||||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
|
||||||
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
|
||||||
{name: "IPtables", getVersion: firewallConf.Version},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(allSettings.String())
|
|
||||||
|
|
||||||
for _, warning := range allSettings.Warnings() {
|
|
||||||
logger.Warn(warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = fs.FileMode(0o644)
|
|
||||||
err = os.MkdirAll("/tmp/gluetun", permission)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.MkdirAll("/gluetun", permission)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultUsername = "nonrootuser"
|
|
||||||
nonRootUsername, err := alpineConf.CreateUser(defaultUsername, puid)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating user: %w", err)
|
|
||||||
}
|
|
||||||
if nonRootUsername != defaultUsername {
|
|
||||||
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
|
||||||
}
|
|
||||||
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
|
||||||
|
|
||||||
if err := routingConf.Setup(); err != nil {
|
if err := routingConf.Setup(); err != nil {
|
||||||
if strings.Contains(err.Error(), "operation not permitted") {
|
if strings.Contains(err.Error(), "operation not permitted") {
|
||||||
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||||
}
|
}
|
||||||
return fmt.Errorf("setting up routing: %w", err)
|
return fmt.Errorf("%w: %s", errSetupRouting, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
routingLogger.Info("routing cleanup...")
|
routingConf.SetVerbose(false)
|
||||||
if err := routingConf.TearDown(); err != nil {
|
if err := routingConf.TearDown(); err != nil {
|
||||||
routingLogger.Error("cannot teardown routing: " + err.Error())
|
logger.Error("cannot teardown routing: " + err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -326,35 +243,41 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = routingConf.AddLocalRules(localNetworks)
|
if err := ovpnConf.CheckTUN(); err != nil {
|
||||||
if err != nil {
|
logger.Warn(err)
|
||||||
return fmt.Errorf("adding local rules: %w", err)
|
err = ovpnConf.CreateTUN()
|
||||||
}
|
|
||||||
|
|
||||||
const tunDevice = "/dev/net/tun"
|
|
||||||
err = tun.Check(tunDevice)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
|
|
||||||
}
|
|
||||||
logger.Info(err.Error() + "; creating it...")
|
|
||||||
err = tun.Create(tunDevice)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating tun device: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, port := range allSettings.Firewall.InputPorts {
|
|
||||||
for _, defaultRoute := range defaultRoutes {
|
|
||||||
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tunnelReadyCh := make(chan struct{})
|
||||||
|
defer close(tunnelReadyCh)
|
||||||
|
|
||||||
|
if allSettings.Firewall.Enabled {
|
||||||
|
err := firewallConf.SetEnabled(ctx, true) // disabled by default
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vpnPort := range allSettings.Firewall.VPNInputPorts {
|
||||||
|
err = firewallConf.SetAllowedPort(ctx, vpnPort, string(constants.TUN))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range allSettings.Firewall.InputPorts {
|
||||||
|
err = firewallConf.SetAllowedPort(ctx, port, defaultInterface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} // TODO move inside firewall?
|
} // TODO move inside firewall?
|
||||||
|
|
||||||
|
healthy := make(chan bool)
|
||||||
|
|
||||||
// Shutdown settings
|
// Shutdown settings
|
||||||
const totalShutdownTimeout = 3 * time.Second
|
|
||||||
const defaultShutdownTimeout = 400 * time.Millisecond
|
const defaultShutdownTimeout = 400 * time.Millisecond
|
||||||
defaultShutdownOnSuccess := func(goRoutineName string) {
|
defaultShutdownOnSuccess := func(goRoutineName string) {
|
||||||
logger.Info(goRoutineName + ": terminated ✔️")
|
logger.Info(goRoutineName + ": terminated ✔️")
|
||||||
@@ -362,156 +285,115 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
defaultShutdownOnFailure := func(goRoutineName string, err error) {
|
defaultShutdownOnFailure := func(goRoutineName string, err error) {
|
||||||
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
|
logger.Warn(goRoutineName + ": " + err.Error() + " ⚠️")
|
||||||
}
|
}
|
||||||
defaultGroupOptions := []group.Option{
|
defaultGoRoutineSettings := goshutdown.GoRoutineSettings{Timeout: defaultShutdownTimeout}
|
||||||
group.OptionTimeout(defaultShutdownTimeout),
|
defaultGroupSettings := goshutdown.GroupSettings{
|
||||||
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
Timeout: defaultShutdownTimeout,
|
||||||
|
OnFailure: defaultShutdownOnFailure,
|
||||||
|
OnSuccess: defaultShutdownOnSuccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupSettings)
|
||||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupSettings)
|
||||||
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupOptions...)
|
otherGroupHandler := goshutdown.NewGroupHandler("other", defaultGroupSettings)
|
||||||
|
|
||||||
if *allSettings.Pprof.Enabled {
|
openvpnLooper := openvpn.NewLooper(allSettings.OpenVPN, nonRootUsername, puid, pgid, allServers,
|
||||||
// TODO run in run loop so this can be patched at runtime
|
ovpnConf, firewallConf, routingConf, logger, httpClient, os.OpenFile, tunnelReadyCh, healthy)
|
||||||
pprofReady := make(chan struct{})
|
openvpnHandler, openvpnCtx, openvpnDone := goshutdown.NewGoRoutineHandler(
|
||||||
pprofHandler, pprofCtx, pprofDone := goshutdown.NewGoRoutineHandler("pprof server")
|
"openvpn", goshutdown.GoRoutineSettings{Timeout: time.Second})
|
||||||
go pprofServer.Run(pprofCtx, pprofReady, pprofDone)
|
// wait for restartOpenvpn
|
||||||
otherGroupHandler.Add(pprofHandler)
|
go openvpnLooper.Run(openvpnCtx, openvpnDone)
|
||||||
<-pprofReady
|
|
||||||
}
|
|
||||||
|
|
||||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
updaterLooper := updater.NewLooper(allSettings.Updater,
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
allServers, storage, openvpnLooper.SetServers, httpClient,
|
||||||
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
logger.NewChild(logging.Settings{Prefix: "updater: "}))
|
||||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsLogger := logger.New(log.SetComponent("dns"))
|
|
||||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
|
||||||
dnsLogger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating DNS loop: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
|
||||||
go dnsLooper.Run(dnsCtx, dnsDone)
|
|
||||||
otherGroupHandler.Add(dnsHandler)
|
|
||||||
|
|
||||||
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
|
||||||
controlGroupHandler.Add(dnsTickerHandler)
|
|
||||||
|
|
||||||
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
|
||||||
logger.New(log.SetComponent("ip getter")))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating public ip loop: %w", err)
|
|
||||||
}
|
|
||||||
publicIPRunError, err := publicIPLooper.Start(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting public ip loop: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
|
||||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
|
|
||||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
|
||||||
healthChecker := healthcheck.NewChecker(healthLogger)
|
|
||||||
|
|
||||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
|
||||||
|
|
||||||
unzipper := unzip.New(httpClient)
|
|
||||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
|
||||||
openvpnFileExtractor := extract.New()
|
|
||||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
|
||||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
|
||||||
openvpnFileExtractor, allSettings.Updater)
|
|
||||||
|
|
||||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
|
||||||
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
|
|
||||||
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
|
||||||
buildInfo, *allSettings.Version.Enabled)
|
|
||||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"vpn", goroutine.OptionTimeout(time.Second))
|
|
||||||
go vpnLooper.Run(vpnCtx, vpnDone)
|
|
||||||
|
|
||||||
updaterLooper := updater.NewLoop(allSettings.Updater,
|
|
||||||
providers, storage, httpClient, updaterLogger)
|
|
||||||
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
|
updaterHandler, updaterCtx, updaterDone := goshutdown.NewGoRoutineHandler(
|
||||||
"updater", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"updater", defaultGoRoutineSettings)
|
||||||
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
// wait for updaterLooper.Restart() or its ticket launched with RunRestartTicker
|
||||||
go updaterLooper.Run(updaterCtx, updaterDone)
|
go updaterLooper.Run(updaterCtx, updaterDone)
|
||||||
tickersGroupHandler.Add(updaterHandler)
|
tickersGroupHandler.Add(updaterHandler)
|
||||||
|
|
||||||
updaterTickerHandler, updaterTickerCtx, updaterTickerDone := goshutdown.NewGoRoutineHandler(
|
unboundLogger := logger.NewChild(logging.Settings{Prefix: "dns over tls: "})
|
||||||
"updater ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
unboundLooper := dns.NewLooper(dnsConf, allSettings.DNS, httpClient,
|
||||||
go updaterLooper.RunRestartTicker(updaterTickerCtx, updaterTickerDone)
|
unboundLogger, os.OpenFile)
|
||||||
controlGroupHandler.Add(updaterTickerHandler)
|
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"unbound", defaultGoRoutineSettings)
|
||||||
|
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
||||||
|
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||||
|
otherGroupHandler.Add(dnsHandler)
|
||||||
|
|
||||||
httpProxyLooper := httpproxy.NewLoop(
|
publicIPLooper := publicip.NewLooper(httpClient,
|
||||||
logger.New(log.SetComponent("http proxy")),
|
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
||||||
|
allSettings.PublicIP, puid, pgid, os)
|
||||||
|
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"public IP", defaultGoRoutineSettings)
|
||||||
|
go publicIPLooper.Run(pubIPCtx, pubIPDone)
|
||||||
|
otherGroupHandler.Add(pubIPHandler)
|
||||||
|
|
||||||
|
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"public IP", defaultGoRoutineSettings)
|
||||||
|
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
|
||||||
|
tickersGroupHandler.Add(pubIPTickerHandler)
|
||||||
|
|
||||||
|
httpProxyLooper := httpproxy.NewLooper(
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "http proxy: "}),
|
||||||
allSettings.HTTPProxy)
|
allSettings.HTTPProxy)
|
||||||
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
|
httpProxyHandler, httpProxyCtx, httpProxyDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http proxy", defaultGoRoutineSettings)
|
||||||
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
|
go httpProxyLooper.Run(httpProxyCtx, httpProxyDone)
|
||||||
otherGroupHandler.Add(httpProxyHandler)
|
otherGroupHandler.Add(httpProxyHandler)
|
||||||
|
|
||||||
shadowsocksLooper := shadowsocks.NewLoop(allSettings.Shadowsocks,
|
shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks,
|
||||||
logger.New(log.SetComponent("shadowsocks")))
|
logger.NewChild(logging.Settings{Prefix: "shadowsocks: "}))
|
||||||
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
|
shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler(
|
||||||
"shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"shadowsocks proxy", defaultGoRoutineSettings)
|
||||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||||
otherGroupHandler.Add(shadowsocksHandler)
|
otherGroupHandler.Add(shadowsocksHandler)
|
||||||
|
|
||||||
|
eventsRoutingHandler, eventsRoutingCtx, eventsRoutingDone := goshutdown.NewGoRoutineHandler(
|
||||||
|
"events routing", defaultGoRoutineSettings)
|
||||||
|
go routeReadyEvents(eventsRoutingCtx, eventsRoutingDone, buildInfo, tunnelReadyCh,
|
||||||
|
unboundLooper, updaterLooper, publicIPLooper, routingConf, logger, httpClient,
|
||||||
|
allSettings.VersionInformation, allSettings.OpenVPN.Provider.PortForwarding.Enabled, openvpnLooper.PortForward,
|
||||||
|
)
|
||||||
|
controlGroupHandler.Add(eventsRoutingHandler)
|
||||||
|
|
||||||
|
controlServerAddress := ":" + strconv.Itoa(int(allSettings.ControlServer.Port))
|
||||||
|
controlServerLogging := allSettings.ControlServer.Log
|
||||||
|
httpServer := server.New(controlServerAddress, controlServerLogging,
|
||||||
|
logger.NewChild(logging.Settings{Prefix: "http server: "}),
|
||||||
|
buildInfo, openvpnLooper, unboundLooper, updaterLooper, publicIPLooper)
|
||||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http server", defaultGoRoutineSettings)
|
||||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
go httpServer.Run(httpServerCtx, httpServerDone)
|
||||||
logger.New(log.SetComponent("http server")),
|
|
||||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
|
||||||
storage, ipv6Supported)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("setting up control server: %w", err)
|
|
||||||
}
|
|
||||||
httpServerReady := make(chan struct{})
|
|
||||||
go httpServer.Run(httpServerCtx, httpServerReady, httpServerDone)
|
|
||||||
<-httpServerReady
|
|
||||||
controlGroupHandler.Add(httpServerHandler)
|
controlGroupHandler.Add(httpServerHandler)
|
||||||
|
|
||||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
healthcheckServer := healthcheck.NewServer(constants.HealthcheckAddress,
|
||||||
order.OptionTimeout(totalShutdownTimeout),
|
logger.NewChild(logging.Settings{Prefix: "healthcheck: "}))
|
||||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
order.OptionOnFailure(defaultShutdownOnFailure))
|
"HTTP health server", defaultGoRoutineSettings)
|
||||||
|
go healthcheckServer.Run(healthServerCtx, healthy, healthServerDone)
|
||||||
|
|
||||||
|
const orderShutdownTimeout = 3 * time.Second
|
||||||
|
orderSettings := goshutdown.OrderSettings{
|
||||||
|
Timeout: orderShutdownTimeout,
|
||||||
|
OnFailure: defaultShutdownOnFailure,
|
||||||
|
OnSuccess: defaultShutdownOnSuccess,
|
||||||
|
}
|
||||||
|
orderHandler := goshutdown.NewOrder("gluetun", orderSettings)
|
||||||
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||||
vpnHandler, otherGroupHandler)
|
openvpnHandler, otherGroupHandler)
|
||||||
|
|
||||||
// Start VPN for the first time in a blocking call
|
// Start openvpn for the first time in a blocking call
|
||||||
// until the VPN is launched
|
// until openvpn is launched
|
||||||
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
_, _ = openvpnLooper.SetStatus(constants.Running) // TODO option to disable with variable
|
||||||
|
|
||||||
select {
|
<-ctx.Done()
|
||||||
case <-ctx.Done():
|
|
||||||
stoppers := []interface {
|
if allSettings.OpenVPN.Provider.PortForwarding.Enabled {
|
||||||
String() string
|
logger.Info("Clearing forwarded port status file %s", allSettings.OpenVPN.Provider.PortForwarding.Filepath)
|
||||||
Stop() error
|
if err := os.Remove(allSettings.OpenVPN.Provider.PortForwarding.Filepath); err != nil {
|
||||||
}{
|
logger.Error(err)
|
||||||
portForwardLooper, publicIPLooper,
|
|
||||||
}
|
}
|
||||||
for _, stopper := range stoppers {
|
|
||||||
err := stopper.Stop()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case err := <-portForwardRunError:
|
|
||||||
logger.Errorf("port forwarding loop crashed: %s", err)
|
|
||||||
case err := <-publicIPRunError:
|
|
||||||
logger.Errorf("public IP loop crashed: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderHandler.Shutdown(context.Background())
|
return orderHandler.Shutdown(context.Background())
|
||||||
@@ -522,13 +404,8 @@ type printVersionElement struct {
|
|||||||
getVersion func(ctx context.Context) (version string, err error)
|
getVersion func(ctx context.Context) (version string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type infoer interface {
|
func printVersions(ctx context.Context, logger logging.Logger,
|
||||||
Info(s string)
|
elements []printVersionElement) (err error) {
|
||||||
}
|
|
||||||
|
|
||||||
func printVersions(ctx context.Context, logger infoer,
|
|
||||||
elements []printVersionElement,
|
|
||||||
) (err error) {
|
|
||||||
const timeout = 5 * time.Second
|
const timeout = 5 * time.Second
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -536,7 +413,7 @@ func printVersions(ctx context.Context, logger infoer,
|
|||||||
for _, element := range elements {
|
for _, element := range elements {
|
||||||
version, err := element.getVersion(ctx)
|
version, err := element.getVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting %s version: %w", element.name, err)
|
return err
|
||||||
}
|
}
|
||||||
logger.Info(element.name + " version: " + version)
|
logger.Info(element.name + " version: " + version)
|
||||||
}
|
}
|
||||||
@@ -544,92 +421,76 @@ func printVersions(ctx context.Context, logger infoer,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type netLinker interface {
|
func routeReadyEvents(ctx context.Context, done chan<- struct{}, buildInfo models.BuildInformation,
|
||||||
Addresser
|
tunnelReadyCh <-chan struct{},
|
||||||
Router
|
unboundLooper dns.Looper, updaterLooper updater.Looper, publicIPLooper publicip.Looper,
|
||||||
Ruler
|
routing routing.Routing, logger logging.Logger, httpClient *http.Client,
|
||||||
Linker
|
versionInformation, portForwardingEnabled bool, startPortForward func(vpnGateway net.IP)) {
|
||||||
IsWireguardSupported() (ok bool, err error)
|
defer close(done)
|
||||||
IsIPv6Supported() (ok bool, err error)
|
|
||||||
PatchLoggerLevel(level log.Level)
|
// for linters only
|
||||||
|
var restartTickerContext context.Context
|
||||||
|
var restartTickerCancel context.CancelFunc = func() {}
|
||||||
|
|
||||||
|
unboundTickerDone := make(chan struct{})
|
||||||
|
close(unboundTickerDone)
|
||||||
|
updaterTickerDone := make(chan struct{})
|
||||||
|
close(updaterTickerDone)
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
restartTickerCancel() // for linters only
|
||||||
|
<-unboundTickerDone
|
||||||
|
<-updaterTickerDone
|
||||||
|
return
|
||||||
|
case <-tunnelReadyCh: // blocks until openvpn is connected
|
||||||
|
vpnDestination, err := routing.VPNDestinationIP()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(err)
|
||||||
|
} else {
|
||||||
|
logger.Info("VPN routing IP address: %s", vpnDestination)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Addresser interface {
|
if unboundLooper.GetSettings().Enabled {
|
||||||
AddrList(link netlink.Link, family int) (
|
_, _ = unboundLooper.SetStatus(constants.Running)
|
||||||
addresses []netlink.Addr, err error)
|
|
||||||
AddrReplace(link netlink.Link, addr netlink.Addr) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Router interface {
|
restartTickerCancel() // stop previous restart tickers
|
||||||
RouteList(family int) (routes []netlink.Route, err error)
|
<-unboundTickerDone
|
||||||
RouteAdd(route netlink.Route) error
|
<-updaterTickerDone
|
||||||
RouteDel(route netlink.Route) error
|
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
|
||||||
RouteReplace(route netlink.Route) error
|
|
||||||
|
// Runs the Public IP getter job once
|
||||||
|
_, _ = publicIPLooper.SetStatus(constants.Running)
|
||||||
|
if !versionInformation {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
type Ruler interface {
|
if first {
|
||||||
RuleList(family int) (rules []netlink.Rule, err error)
|
first = false
|
||||||
RuleAdd(rule netlink.Rule) error
|
message, err := versionpkg.GetMessage(ctx, buildInfo, httpClient)
|
||||||
RuleDel(rule netlink.Rule) error
|
if err != nil {
|
||||||
|
logger.Error("cannot get version information: " + err.Error())
|
||||||
|
} else {
|
||||||
|
logger.Info(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Linker interface {
|
unboundTickerDone = make(chan struct{})
|
||||||
LinkList() (links []netlink.Link, err error)
|
updaterTickerDone = make(chan struct{})
|
||||||
LinkByName(name string) (link netlink.Link, err error)
|
go unboundLooper.RunRestartTicker(restartTickerContext, unboundTickerDone)
|
||||||
LinkByIndex(index int) (link netlink.Link, err error)
|
go updaterLooper.RunRestartTicker(restartTickerContext, updaterTickerDone)
|
||||||
LinkAdd(link netlink.Link) (linkIndex int, err error)
|
if portForwardingEnabled {
|
||||||
LinkDel(link netlink.Link) (err error)
|
// vpnGateway required only for PIA
|
||||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
vpnGateway, err := routing.VPNLocalGatewayIP()
|
||||||
LinkSetDown(link netlink.Link) (err error)
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
logger.Info("VPN gateway IP address: %s", vpnGateway)
|
||||||
|
startPortForward(vpnGateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clier interface {
|
|
||||||
ClientKey(args []string) error
|
|
||||||
FormatServers(args []string) error
|
|
||||||
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
|
|
||||||
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
|
|
||||||
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
|
|
||||||
GenKey(args []string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tun interface {
|
|
||||||
Check(tunDevice string) error
|
|
||||||
Create(tunDevice string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunStarter interface {
|
|
||||||
Run(cmd *exec.Cmd) (output string, err error)
|
|
||||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, err error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const gluetunLogo = ` @@@
|
|
||||||
@@@@
|
|
||||||
@@@@@@
|
|
||||||
@@@@.@@ @@@@@@@@@@
|
|
||||||
@@@@.@@@ @@@@@@@@==@@@@
|
|
||||||
@@@.@..@@ @@@@@@@=@..==@@@@
|
|
||||||
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
|
||||||
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
|
||||||
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
|
||||||
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
|
||||||
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
|
||||||
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
|
||||||
@@@@.@--@@@@@@@@@@================@@@@@@@
|
|
||||||
@@@..--@@*@@@@@@================@@@@+*@@
|
|
||||||
@@@.---@@.@@@@=================@@@@--@@
|
|
||||||
@@@-.---@@@@@@================@@@@*--@@@
|
|
||||||
@@@.:-#@@@@@@===============*@@@@.---@@
|
|
||||||
@@@.-------.@@@============@@@@@@.--@@@
|
|
||||||
@@@..--------:@@@=========@@@@@@@@.--@@@
|
|
||||||
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
|
||||||
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
|
||||||
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
|
||||||
@@@@@@=============@@@@@ @@@...------------.*@@@
|
|
||||||
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
|
||||||
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
|
||||||
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
|
||||||
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
|
||||||
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
|
||||||
@@@@@@@@ @@@@ @@@@
|
|
||||||
`
|
|
||||||
|
|||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
version: "3.7"
|
||||||
|
services:
|
||||||
|
gluetun:
|
||||||
|
image: qmcgaw/gluetun
|
||||||
|
container_name: gluetun
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
network_mode: bridge
|
||||||
|
ports:
|
||||||
|
- 8888:8888/tcp # HTTP proxy
|
||||||
|
- 8388:8388/tcp # Shadowsocks
|
||||||
|
- 8388:8388/udp # Shadowsocks
|
||||||
|
- 8000:8000/tcp # Built-in HTTP control server
|
||||||
|
# command:
|
||||||
|
volumes:
|
||||||
|
- /yourpath:/gluetun
|
||||||
|
environment:
|
||||||
|
# More variables are available, see the readme table
|
||||||
|
- OPENVPN_USER=
|
||||||
|
- OPENVPN_PASSWORD=
|
||||||
|
- VPNSP=private internet access
|
||||||
|
# Timezone for accurate logs times
|
||||||
|
- TZ=
|
||||||
|
restart: always
|
||||||
74
go.mod
74
go.mod
@@ -1,69 +1,17 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.25.0
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-srp v0.0.7
|
github.com/fatih/color v1.12.0
|
||||||
github.com/breml/rootcerts v0.3.3
|
|
||||||
github.com/fatih/color v1.18.0
|
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/klauspost/compress v1.18.1
|
github.com/qdm12/dns v1.8.0
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/qdm12/goshutdown v0.1.0
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88
|
github.com/qdm12/ss-server v0.2.0
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/qdm12/gosplash v0.2.0
|
github.com/vishvananda/netlink v1.1.0
|
||||||
github.com/qdm12/gotree v0.3.0
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
|
||||||
github.com/qdm12/log v0.1.0
|
inet.af/netaddr v0.0.0-20210511181906-37180328850c
|
||||||
github.com/qdm12/ss-server v0.6.0
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
github.com/ulikunitz/xz v0.5.15
|
|
||||||
github.com/vishvananda/netlink v1.3.1
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
|
||||||
golang.org/x/net v0.47.0
|
|
||||||
golang.org/x/sys v0.38.0
|
|
||||||
golang.org/x/text v0.31.0
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
|
||||||
gopkg.in/ini.v1 v1.67.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
|
||||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
|
||||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
|
||||||
github.com/prometheus/common v0.60.1 // indirect
|
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
|
||||||
golang.org/x/time v0.3.0 // indirect
|
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
289
go.sum
289
go.sum
@@ -1,192 +1,163 @@
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
|
||||||
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
|
||||||
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
|
||||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
|
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
|
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
|
||||||
|
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
|
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
|
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||||
|
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||||
|
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||||
|
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||||
|
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
|
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
|
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
|
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||||
|
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
||||||
|
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
|
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||||
|
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
|
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||||
|
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/qdm12/dns v1.8.0 h1:GZ40kptmfDHOMNxBKWSA4zrbNyGm41BA57zv2MaDtCI=
|
||||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/qdm12/dns v1.8.0/go.mod h1:P2mm63NDYZdx2NAd5CVLM0FBnNdi1ZgVjsRSnX+96vg=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb h1:5WkOssTWl6Tv2H7VFb2jwB08A7BxxNCebkkpvz1PzrY=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
||||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
github.com/qdm12/goshutdown v0.1.0 h1:lmwnygdXtnr2pa6VqfR/bm8077/BnBef1+7CP96B7Sw=
|
||||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
github.com/qdm12/goshutdown v0.1.0/go.mod h1:/LP3MWLqI+wGH/ijfaUG+RHzBbKXIiVKnrg5vXOCf6Q=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/qdm12/ss-server v0.2.0 h1:+togLzeeLAJ68MD1JqOWvYi9rl9t/fx1Qh7wKzZhY1g=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/qdm12/ss-server v0.2.0/go.mod h1:+1bWO1EfWNvsGM5Cuep6vneChK2OHniqtAsED9Fh1y0=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88 h1:GJ5FALvJ3UmHjVaNYebrfV5zF5You4dq8HfRWZy2loM=
|
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
|
||||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
|
||||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
|
||||||
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
|
||||||
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
|
||||||
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
|
||||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
|
||||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
|
||||||
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
|
|
||||||
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
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/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||||
|
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||||
|
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||||
|
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
|
||||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
|
||||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
inet.af/netaddr v0.0.0-20210511181906-37180328850c h1:rzDy/tC8LjEdN94+i0Bu22tTo/qE9cvhKyfD0HMU0NU=
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
|
// Package alpine defines a configurator to interact with the Alpine operating system.
|
||||||
package alpine
|
package alpine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/user"
|
"context"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/os"
|
||||||
|
"github.com/qdm12/golibs/os/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Alpine struct {
|
type Configurator interface {
|
||||||
alpineReleasePath string
|
CreateUser(username string, uid int) (createdUsername string, err error)
|
||||||
passwdPath string
|
Version(ctx context.Context) (version string, err error)
|
||||||
lookupID func(uid string) (*user.User, error)
|
|
||||||
lookup func(username string) (*user.User, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Alpine {
|
type configurator struct {
|
||||||
return &Alpine{
|
openFile os.OpenFileFunc
|
||||||
alpineReleasePath: "/etc/alpine-release",
|
osUser user.OSUser
|
||||||
passwdPath: "/etc/passwd",
|
}
|
||||||
lookupID: user.LookupId,
|
|
||||||
lookup: user.Lookup,
|
func NewConfigurator(openFile os.OpenFileFunc, osUser user.OSUser) Configurator {
|
||||||
|
return &configurator{
|
||||||
|
openFile: openFile,
|
||||||
|
osUser: osUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ package alpine
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUserAlreadyExists = errors.New("user already exists")
|
var (
|
||||||
|
ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
|
)
|
||||||
|
|
||||||
// CreateUser creates a user in Alpine with the given UID.
|
// CreateUser creates a user in Alpine with the given UID.
|
||||||
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
func (c *configurator) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||||
UIDStr := strconv.Itoa(uid)
|
UIDStr := strconv.Itoa(uid)
|
||||||
u, err := a.lookupID(UIDStr)
|
u, err := c.osUser.LookupID(UIDStr)
|
||||||
_, unknownUID := err.(user.UnknownUserIdError)
|
_, unknownUID := err.(user.UnknownUserIdError)
|
||||||
if err != nil && !unknownUID {
|
if err != nil && !unknownUID {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -27,7 +28,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
|||||||
return u.Username, nil
|
return u.Username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err = a.lookup(username)
|
u, err = c.osUser.Lookup(username)
|
||||||
_, unknownUsername := err.(user.UnknownUserError)
|
_, unknownUsername := err.(user.UnknownUserError)
|
||||||
if err != nil && !unknownUsername {
|
if err != nil && !unknownUsername {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -38,8 +39,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
|||||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
const permission = fs.FileMode(0o644)
|
file, err := c.openFile("/etc/passwd", os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Alpine) Version(context.Context) (version string, err error) {
|
func (c *configurator) Version(ctx context.Context) (version string, err error) {
|
||||||
file, err := os.OpenFile(a.alpineReleasePath, os.O_RDONLY, 0)
|
file, err := c.openFile("/etc/alpine-release", os.O_RDONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package cli
|
|||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
func (c *CLI) CI(context.Context) error {
|
func (c *cli) CI(context context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
|
// Package cli defines an interface CLI to run command line operations.
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
type CLI struct {
|
import (
|
||||||
repoServersPath string
|
"context"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/logging"
|
||||||
|
"github.com/qdm12/golibs/os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI interface {
|
||||||
|
ClientKey(args []string, openFile os.OpenFileFunc) error
|
||||||
|
HealthCheck(ctx context.Context) error
|
||||||
|
OpenvpnConfig(os os.OS, logger logging.Logger) error
|
||||||
|
Update(ctx context.Context, args []string, os os.OS, logger logging.Logger) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *CLI {
|
type cli struct{}
|
||||||
return &CLI{
|
|
||||||
repoServersPath: "./internal/storage/servers.json",
|
func New() CLI {
|
||||||
}
|
return &cli{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *CLI) ClientKey(args []string) error {
|
func (c *cli) ClientKey(args []string, openFile os.OpenFileFunc) error {
|
||||||
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("clientkey", flag.ExitOnError)
|
||||||
const openVPNClientKeyPath = "/gluetun/client.key" // TODO deduplicate?
|
filepath := flagSet.String("path", constants.ClientKey, "file path to the client.key file")
|
||||||
filepath := flagSet.String("path", openVPNClientKeyPath, "file path to the client.key file")
|
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
file, err := os.OpenFile(*filepath, os.O_RDONLY, 0)
|
file, err := openFile(*filepath, os.O_RDONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,9 @@ func (c *CLI) ClientKey(args []string) error {
|
|||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s := string(data)
|
s := string(data)
|
||||||
s = strings.ReplaceAll(s, "\n", "")
|
s = strings.ReplaceAll(s, "\n", "")
|
||||||
s = strings.ReplaceAll(s, "\r", "")
|
s = strings.ReplaceAll(s, "\r", "")
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
|
||||||
"golang.org/x/text/cases"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
|
||||||
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
|
||||||
)
|
|
||||||
|
|
||||||
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
|
||||||
provider string, titleCaser cases.Caser,
|
|
||||||
) {
|
|
||||||
boolPtr, ok := providerToFormat[provider]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
|
||||||
}
|
|
||||||
flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CLI) FormatServers(args []string) error {
|
|
||||||
var format, output string
|
|
||||||
allProviders := providers.All()
|
|
||||||
allProviderFlags := make([]string, len(allProviders))
|
|
||||||
for i, provider := range allProviders {
|
|
||||||
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
providersToFormat := make(map[string]*bool, len(allProviders))
|
|
||||||
for _, provider := range allProviderFlags {
|
|
||||||
providersToFormat[provider] = new(bool)
|
|
||||||
}
|
|
||||||
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
|
||||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
|
|
||||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
|
||||||
titleCaser := cases.Title(language.English)
|
|
||||||
for _, provider := range allProviderFlags {
|
|
||||||
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
|
||||||
}
|
|
||||||
if err := flagSet.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note the format is validated by storage.Format
|
|
||||||
|
|
||||||
// Verify only one provider is set to be formatted.
|
|
||||||
var providers []string
|
|
||||||
for provider, formatPtr := range providersToFormat {
|
|
||||||
if *formatPtr {
|
|
||||||
providers = append(providers, provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch len(providers) {
|
|
||||||
case 0:
|
|
||||||
return fmt.Errorf("%w", ErrProviderUnspecified)
|
|
||||||
case 1:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: %d specified: %s",
|
|
||||||
ErrMultipleProvidersToFormat, len(providers),
|
|
||||||
strings.Join(providers, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerToFormat string
|
|
||||||
for _, providerToFormat = range allProviders {
|
|
||||||
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := newNoopLogger()
|
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating servers storage: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted, err := storage.Format(providerToFormat, format)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("formatting servers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output = filepath.Clean(output)
|
|
||||||
const permission = fs.FileMode(0o644)
|
|
||||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening output file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprint(file, formatted)
|
|
||||||
if err != nil {
|
|
||||||
_ = file.Close()
|
|
||||||
return fmt.Errorf("writing to output file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = file.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("closing output file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *CLI) GenKey(args []string) (err error) {
|
|
||||||
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
|
|
||||||
err = flagSet.Parse(args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyLength = 128 / 8
|
|
||||||
keyBytes := make([]byte, keyLength)
|
|
||||||
|
|
||||||
_, _ = rand.Read(keyBytes)
|
|
||||||
|
|
||||||
key := base58Encode(keyBytes)
|
|
||||||
fmt.Println(key)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func base58Encode(data []byte) string {
|
|
||||||
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
||||||
const radix = 58
|
|
||||||
|
|
||||||
zcount := 0
|
|
||||||
for zcount < len(data) && data[zcount] == 0 {
|
|
||||||
zcount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// integer simplification of ceil(log(256)/log(58))
|
|
||||||
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
|
|
||||||
size := zcount + ceilLog256Div58
|
|
||||||
|
|
||||||
output := make([]byte, size)
|
|
||||||
|
|
||||||
high := size - 1
|
|
||||||
for _, b := range data {
|
|
||||||
i := size - 1
|
|
||||||
for carry := uint32(b); i > high || carry != 0; i-- {
|
|
||||||
carry += 256 * uint32(output[i]) //nolint:mnd
|
|
||||||
output[i] = byte(carry % radix)
|
|
||||||
carry /= radix
|
|
||||||
}
|
|
||||||
high = i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the additional "zero-gap" in the output buffer
|
|
||||||
additionalZeroGapEnd := zcount
|
|
||||||
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
|
|
||||||
additionalZeroGapEnd++
|
|
||||||
}
|
|
||||||
|
|
||||||
val := output[additionalZeroGapEnd-zcount:]
|
|
||||||
size = len(val)
|
|
||||||
for i := range val {
|
|
||||||
output[i] = alphabet[val[i]]
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(output[:size])
|
|
||||||
}
|
|
||||||
@@ -2,41 +2,19 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||||
"github.com/qdm12/gosettings/reader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) {
|
func (c *cli) HealthCheck(ctx context.Context) error {
|
||||||
// Extract the health server port from the configuration.
|
|
||||||
var config settings.Health
|
|
||||||
err = config.Read(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.SetDefaults()
|
|
||||||
|
|
||||||
err = config.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, port, err := net.SplitHostPort(config.ServerAddress)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = 10 * time.Second
|
const timeout = 10 * time.Second
|
||||||
httpClient := &http.Client{Timeout: timeout}
|
httpClient := &http.Client{Timeout: timeout}
|
||||||
client := healthcheck.NewClient(httpClient)
|
healthchecker := healthcheck.NewChecker(httpClient)
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
const url = "http://" + constants.HealthcheckAddress
|
||||||
url := "http://127.0.0.1:" + port
|
return healthchecker.Check(ctx, url)
|
||||||
return client.Check(ctx, url)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import "github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
|
|
||||||
type Source interface {
|
|
||||||
Read() (settings settings.Settings, err error)
|
|
||||||
ReadHealth() (health settings.Health, err error)
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
type noopLogger struct{}
|
|
||||||
|
|
||||||
func newNoopLogger() *noopLogger {
|
|
||||||
return new(noopLogger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *noopLogger) Info(string) {}
|
|
||||||
func (l *noopLogger) Warn(string) {}
|
|
||||||
@@ -1,92 +1,36 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
"github.com/qdm12/golibs/logging"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/golibs/os"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenvpnConfigLogger interface {
|
func (c *cli) OpenvpnConfig(os os.OS, logger logging.Logger) error {
|
||||||
Info(s string)
|
var allSettings configuration.Settings
|
||||||
Warn(s string)
|
err := allSettings.Read(params.NewEnv(), os, logger)
|
||||||
}
|
|
||||||
|
|
||||||
type Unzipper interface {
|
|
||||||
FetchAndExtract(ctx context.Context, url string) (
|
|
||||||
contents map[string][]byte, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParallelResolver interface {
|
|
||||||
Resolve(ctx context.Context, settings resolver.ParallelSettings) (
|
|
||||||
hostToIPs map[string][]netip.Addr, warnings []string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPFetcher interface {
|
|
||||||
String() string
|
|
||||||
CanFetchAnyIP() bool
|
|
||||||
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPv6Checker interface {
|
|
||||||
IsIPv6Supported() (supported bool, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|
||||||
ipv6Checker IPv6Checker,
|
|
||||||
) error {
|
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
allServers, err := storage.New(logger, os, constants.ServersData).
|
||||||
var allSettings settings.Settings
|
SyncServers(constants.GetAllServers())
|
||||||
err = allSettings.Read(reader, logger)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allSettings.SetDefaults()
|
providerConf := provider.New(allSettings.OpenVPN.Provider.Name, allServers, time.Now)
|
||||||
|
connection, err := providerConf.GetOpenVPNConnection(allSettings.OpenVPN.Provider.ServerSelection)
|
||||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
|
||||||
return fmt.Errorf("validating settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unused by this CLI command
|
|
||||||
unzipper := (Unzipper)(nil)
|
|
||||||
client := (*http.Client)(nil)
|
|
||||||
warner := (Warner)(nil)
|
|
||||||
parallelResolver := (ParallelResolver)(nil)
|
|
||||||
ipFetcher := (IPFetcher)(nil)
|
|
||||||
openvpnFileExtractor := extract.New()
|
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
|
||||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
|
||||||
connection, err := providerConf.GetConnection(
|
|
||||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
lines := providerConf.BuildConf(connection, "nonroortuser", allSettings.OpenVPN)
|
||||||
lines := providerConf.OpenVPNConfig(connection,
|
|
||||||
allSettings.VPN.OpenVPN, ipv6Supported)
|
|
||||||
|
|
||||||
fmt.Println(strings.Join(lines, "\n"))
|
fmt.Println(strings.Join(lines, "\n"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,127 +6,69 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
|
||||||
"github.com/qdm12/gluetun/internal/publicip/api"
|
|
||||||
"github.com/qdm12/gluetun/internal/storage"
|
"github.com/qdm12/gluetun/internal/storage"
|
||||||
"github.com/qdm12/gluetun/internal/updater"
|
"github.com/qdm12/gluetun/internal/updater"
|
||||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
"github.com/qdm12/golibs/logging"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/golibs/os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
ErrNoFileOrStdoutFlag = errors.New("at least one of -file or -stdout must be specified")
|
||||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
ErrSyncServers = errors.New("cannot sync hardcoded and persisted servers")
|
||||||
ErrUsernameMissing = errors.New("username is required for this provider")
|
ErrUpdateServerInformation = errors.New("cannot update server information")
|
||||||
ErrPasswordMissing = errors.New("password is required for this provider")
|
ErrWriteToFile = errors.New("cannot write updated information to file")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdaterLogger interface {
|
func (c *cli) Update(ctx context.Context, args []string, os os.OS, logger logging.Logger) error {
|
||||||
Info(s string)
|
options := configuration.Updater{CLI: true}
|
||||||
Warn(s string)
|
var flushToFile bool
|
||||||
Error(s string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
|
||||||
options := settings.Updater{}
|
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
|
||||||
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
|
||||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
flagSet.BoolVar(&flushToFile, "file", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&options.Stdout, "stdout", false, "Write results to console to modify the program (for maintainers)")
|
||||||
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
|
||||||
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||||
const defaultMinRatio = 0.8
|
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
|
||||||
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers")
|
||||||
"Minimum ratio of servers to find for the update to succeed")
|
flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers")
|
||||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
flagSet.BoolVar(&options.Ipvanish, "ipvanish", false, "Update IpVanish servers")
|
||||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers")
|
||||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
|
||||||
flagSet.StringVar(&protonUsername, "proton-username", "",
|
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
|
||||||
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
|
||||||
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
|
||||||
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
flagSet.BoolVar(&options.Privatevpn, "privatevpn", false, "Update Private VPN servers")
|
||||||
|
flagSet.BoolVar(&options.Protonvpn, "protonvpn", false, "Update Protonvpn servers")
|
||||||
|
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
|
||||||
|
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
|
||||||
|
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers")
|
||||||
|
flagSet.BoolVar(&options.VPNUnlimited, "vpnunlimited", false, "Update VPN Unlimited servers")
|
||||||
|
flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers")
|
||||||
|
flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !flushToFile && !options.Stdout {
|
||||||
if !endUserMode && !maintainerMode {
|
return ErrNoFileOrStdoutFlag
|
||||||
return fmt.Errorf("%w", ErrModeUnspecified)
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateAll {
|
|
||||||
options.Providers = providers.All()
|
|
||||||
} else {
|
|
||||||
if csvProviders == "" {
|
|
||||||
return fmt.Errorf("%w", ErrNoProviderSpecified)
|
|
||||||
}
|
|
||||||
options.Providers = strings.Split(csvProviders, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
|
||||||
if protonEmail == "" && protonUsername != "" {
|
|
||||||
protonEmail = protonUsername + "@protonmail.com"
|
|
||||||
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
|
||||||
"This assumes the email is " + protonEmail + " and may not work.")
|
|
||||||
}
|
|
||||||
options.ProtonEmail = &protonEmail
|
|
||||||
options.ProtonPassword = &protonPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
options.SetDefaults(options.Providers[0])
|
|
||||||
|
|
||||||
err := options.Validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("options validation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
serversDataPath := constants.ServersData
|
|
||||||
if maintainerMode {
|
|
||||||
serversDataPath = ""
|
|
||||||
}
|
|
||||||
storage, err := storage.New(logger, serversDataPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating servers storage: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientTimeout = 10 * time.Second
|
const clientTimeout = 10 * time.Second
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
unzipper := unzip.New(httpClient)
|
storage := storage.New(logger, os, constants.ServersData)
|
||||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
currentServers, err := storage.SyncServers(constants.GetAllServers())
|
||||||
nameTokenPairs := []api.NameToken{
|
|
||||||
{Name: string(api.IPInfo), Token: ipToken},
|
|
||||||
{Name: string(api.IP2Location)},
|
|
||||||
{Name: string(api.IfConfigCo)},
|
|
||||||
}
|
|
||||||
fetchers, err := api.New(nameTokenPairs, httpClient)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating public IP fetchers: %w", err)
|
return fmt.Errorf("%w: %s", ErrSyncServers, err)
|
||||||
}
|
}
|
||||||
ipFetcher := api.NewResilient(fetchers, logger)
|
updater := updater.New(options, httpClient, currentServers, logger)
|
||||||
|
allServers, err := updater.UpdateServers(ctx)
|
||||||
openvpnFileExtractor := extract.New()
|
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
|
||||||
|
|
||||||
updater := updater.New(httpClient, storage, providers, logger)
|
|
||||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("updating server information: %w", err)
|
return fmt.Errorf("%w: %s", ErrUpdateServerInformation, err)
|
||||||
}
|
}
|
||||||
|
if flushToFile {
|
||||||
if maintainerMode {
|
if err := storage.FlushToFile(allServers); err != nil {
|
||||||
err := storage.FlushToFile(c.repoServersPath)
|
return fmt.Errorf("%w: %s", ErrWriteToFile, err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("writing servers data to embedded JSON file: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
type Warner interface {
|
|
||||||
Warn(s string)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
// Cmder handles running subprograms synchronously and asynchronously.
|
|
||||||
type Cmder struct{}
|
|
||||||
|
|
||||||
func New() *Cmder {
|
|
||||||
return &Cmder{}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type execCmd interface {
|
|
||||||
CombinedOutput() ([]byte, error)
|
|
||||||
StdoutPipe() (io.ReadCloser, error)
|
|
||||||
StderrPipe() (io.ReadCloser, error)
|
|
||||||
Start() error
|
|
||||||
Wait() error
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: interfaces_local.go
|
|
||||||
|
|
||||||
// Package command is a generated GoMock package.
|
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
io "io"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockexecCmd is a mock of execCmd interface.
|
|
||||||
type MockexecCmd struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockexecCmdMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
|
|
||||||
type MockexecCmdMockRecorder struct {
|
|
||||||
mock *MockexecCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockexecCmd creates a new mock instance.
|
|
||||||
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
|
|
||||||
mock := &MockexecCmd{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockexecCmdMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// CombinedOutput mocks base method.
|
|
||||||
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CombinedOutput")
|
|
||||||
ret0, _ := ret[0].([]byte)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// CombinedOutput indicates an expected call of CombinedOutput.
|
|
||||||
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start mocks base method.
|
|
||||||
func (m *MockexecCmd) Start() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Start")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start indicates an expected call of Start.
|
|
||||||
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
|
|
||||||
}
|
|
||||||
|
|
||||||
// StderrPipe mocks base method.
|
|
||||||
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "StderrPipe")
|
|
||||||
ret0, _ := ret[0].(io.ReadCloser)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// StderrPipe indicates an expected call of StderrPipe.
|
|
||||||
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdoutPipe mocks base method.
|
|
||||||
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "StdoutPipe")
|
|
||||||
ret0, _ := ret[0].(io.ReadCloser)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdoutPipe indicates an expected call of StdoutPipe.
|
|
||||||
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait mocks base method.
|
|
||||||
func (m *MockexecCmd) Wait() error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Wait")
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait indicates an expected call of Wait.
|
|
||||||
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run runs a command in a blocking manner, returning its output and
|
|
||||||
// an error if it failed.
|
|
||||||
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
|
|
||||||
return run(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(cmd execCmd) (output string, err error) {
|
|
||||||
stdout, err := cmd.CombinedOutput()
|
|
||||||
output = string(stdout)
|
|
||||||
output = strings.TrimSuffix(output, "\n")
|
|
||||||
lines := stringToLines(output)
|
|
||||||
for i := range lines {
|
|
||||||
lines[i] = strings.TrimPrefix(lines[i], "'")
|
|
||||||
lines[i] = strings.TrimSuffix(lines[i], "'")
|
|
||||||
}
|
|
||||||
output = strings.Join(lines, "\n")
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToLines(s string) (lines []string) {
|
|
||||||
s = strings.TrimSuffix(s, "\n")
|
|
||||||
return strings.Split(s, "\n")
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_run(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
errDummy := errors.New("dummy")
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
stdout []byte
|
|
||||||
cmdErr error
|
|
||||||
output string
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
"no output": {},
|
|
||||||
"cmd error": {
|
|
||||||
stdout: []byte("'hello \nworld'\n"),
|
|
||||||
cmdErr: errDummy,
|
|
||||||
output: "hello \nworld",
|
|
||||||
err: errDummy,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
|
|
||||||
mockCmd := NewMockexecCmd(ctrl)
|
|
||||||
|
|
||||||
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
|
|
||||||
|
|
||||||
output, err := run(mockCmd)
|
|
||||||
|
|
||||||
if testCase.err != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.output, output)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCommandEmpty = errors.New("command is empty")
|
|
||||||
ErrSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
|
||||||
ErrDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
|
||||||
ErrEscapeUnterminated = errors.New("unterminated backslash-escape")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Split splits a command string into a slice of arguments.
|
|
||||||
// This is especially important for commands such as:
|
|
||||||
// /bin/sh -c "echo hello"
|
|
||||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
|
||||||
// It supports backslash-escapes, single-quotes and double-quotes.
|
|
||||||
// It does not support:
|
|
||||||
// - the $" quoting style.
|
|
||||||
// - expansion (brace, shell or pathname).
|
|
||||||
func Split(command string) (words []string, err error) {
|
|
||||||
if command == "" {
|
|
||||||
return nil, fmt.Errorf("%w", ErrCommandEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferSize = 1024
|
|
||||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
|
||||||
|
|
||||||
startIndex := 0
|
|
||||||
|
|
||||||
for startIndex < len(command) {
|
|
||||||
// skip any split characters at the start
|
|
||||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
|
||||||
switch {
|
|
||||||
case strings.ContainsRune(" \n\t", character):
|
|
||||||
startIndex += runeSize
|
|
||||||
case character == '\\':
|
|
||||||
// Look ahead to eventually skip an escaped newline
|
|
||||||
if command[startIndex+runeSize:] == "" {
|
|
||||||
return nil, fmt.Errorf("%w: %q", ErrEscapeUnterminated, command)
|
|
||||||
}
|
|
||||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
|
||||||
if character == '\n' {
|
|
||||||
startIndex += runeSize + runeSize // backslash and newline
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var word string
|
|
||||||
buffer.Reset()
|
|
||||||
word, startIndex, err = splitWord(command, startIndex, buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
|
||||||
}
|
|
||||||
words = append(words, word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return words, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WARNING: buffer must be cleared before calling this function.
|
|
||||||
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
cursor := startIndex
|
|
||||||
for cursor < len(input) {
|
|
||||||
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += runeLength
|
|
||||||
if character == '"' ||
|
|
||||||
character == '\'' ||
|
|
||||||
character == '\\' ||
|
|
||||||
character == ' ' ||
|
|
||||||
character == '\n' ||
|
|
||||||
character == '\t' {
|
|
||||||
buffer.WriteString(input[startIndex : cursor-runeLength])
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.ContainsRune(" \n\t", character): // spacing character
|
|
||||||
return buffer.String(), cursor, nil
|
|
||||||
case character == '"':
|
|
||||||
return handleDoubleQuoted(input, cursor, buffer)
|
|
||||||
case character == '\'':
|
|
||||||
return handleSingleQuoted(input, cursor, buffer)
|
|
||||||
case character == '\\':
|
|
||||||
return handleEscaped(input, cursor, buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.WriteString(input[startIndex:])
|
|
||||||
return buffer.String(), len(input), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
cursor := startIndex
|
|
||||||
for cursor < len(input) {
|
|
||||||
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += nextRuneLength
|
|
||||||
switch nextCharacter {
|
|
||||||
case '"': // end of the double quoted string
|
|
||||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
|
||||||
return splitWord(input, cursor, buffer)
|
|
||||||
case '\\': // escaped character
|
|
||||||
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
|
||||||
cursor += escapedRuneLength
|
|
||||||
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
|
||||||
if escapedCharacter != '\n' {
|
|
||||||
// skip backslash entirely for the newline character
|
|
||||||
buffer.WriteRune(escapedCharacter)
|
|
||||||
}
|
|
||||||
startIndex = cursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrDoubleQuoteUnterminated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
|
||||||
if closingQuoteIndex == -1 {
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrSingleQuoteUnterminated)
|
|
||||||
}
|
|
||||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
|
||||||
const singleQuoteRuneLength = 1
|
|
||||||
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
|
||||||
return splitWord(input, startIndex, buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
|
||||||
word string, newStartIndex int, err error,
|
|
||||||
) {
|
|
||||||
if input[startIndex:] == "" {
|
|
||||||
return "", 0, fmt.Errorf("%w", ErrEscapeUnterminated)
|
|
||||||
}
|
|
||||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
|
||||||
if character != '\n' { // backslash-escaped newline is ignored
|
|
||||||
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
|
||||||
}
|
|
||||||
startIndex += runeLength
|
|
||||||
return splitWord(input, startIndex, buffer)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Split(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
command string
|
|
||||||
words []string
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
command: "",
|
|
||||||
errWrapped: ErrCommandEmpty,
|
|
||||||
errMessage: "command is empty",
|
|
||||||
},
|
|
||||||
"concrete_sh_command": {
|
|
||||||
command: `/bin/sh -c "echo 123"`,
|
|
||||||
words: []string{"/bin/sh", "-c", "echo 123"},
|
|
||||||
},
|
|
||||||
"single_word": {
|
|
||||||
command: "word1",
|
|
||||||
words: []string{"word1"},
|
|
||||||
},
|
|
||||||
"two_words_single_space": {
|
|
||||||
command: "word1 word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"two_words_multiple_space": {
|
|
||||||
command: "word1 word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"two_words_no_expansion": {
|
|
||||||
command: "word1* word2?",
|
|
||||||
words: []string{"word1*", "word2?"},
|
|
||||||
},
|
|
||||||
"escaped_single quote": {
|
|
||||||
command: "ain\\'t good",
|
|
||||||
words: []string{"ain't", "good"},
|
|
||||||
},
|
|
||||||
"escaped_single_quote_all_single_quoted": {
|
|
||||||
command: "'ain'\\''t good'",
|
|
||||||
words: []string{"ain't good"},
|
|
||||||
},
|
|
||||||
"empty_single_quoted": {
|
|
||||||
command: "word1 '' word2",
|
|
||||||
words: []string{"word1", "", "word2"},
|
|
||||||
},
|
|
||||||
"escaped_newline": {
|
|
||||||
command: "word1\\\nword2",
|
|
||||||
words: []string{"word1word2"},
|
|
||||||
},
|
|
||||||
"quoted_newline": {
|
|
||||||
command: "text \"with\na\" quoted newline",
|
|
||||||
words: []string{"text", "with\na", "quoted", "newline"},
|
|
||||||
},
|
|
||||||
"quoted_escaped_newline": {
|
|
||||||
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
|
||||||
words: []string{"word1\\d\\\" word2word3 word4"},
|
|
||||||
},
|
|
||||||
"escaped_separated_newline": {
|
|
||||||
command: "word1 \\\n word2",
|
|
||||||
words: []string{"word1", "word2"},
|
|
||||||
},
|
|
||||||
"double_quotes_no_spacing": {
|
|
||||||
command: "word1\"word2\"word3",
|
|
||||||
words: []string{"word1word2word3"},
|
|
||||||
},
|
|
||||||
"unterminated_single_quote": {
|
|
||||||
command: "'abc'\\''def",
|
|
||||||
errWrapped: ErrSingleQuoteUnterminated,
|
|
||||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
|
||||||
},
|
|
||||||
"unterminated_double_quote": {
|
|
||||||
command: "\"abc'def",
|
|
||||||
errWrapped: ErrDoubleQuoteUnterminated,
|
|
||||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
|
||||||
},
|
|
||||||
"unterminated_escape": {
|
|
||||||
command: "abc\\",
|
|
||||||
errWrapped: ErrEscapeUnterminated,
|
|
||||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
|
||||||
},
|
|
||||||
"unterminated_escape_only": {
|
|
||||||
command: " \\",
|
|
||||||
errWrapped: ErrEscapeUnterminated,
|
|
||||||
errMessage: `unterminated backslash-escape: " \\"`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
words, err := Split(testCase.command)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.words, words)
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start launches a command and streams stdout and stderr to channels.
|
|
||||||
// All the channels returned are ready only and won't be closed
|
|
||||||
// if the command fails later.
|
|
||||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
|
||||||
stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, startErr error,
|
|
||||||
) {
|
|
||||||
return start(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|
||||||
waitError <-chan error, startErr error,
|
|
||||||
) {
|
|
||||||
stop := make(chan struct{})
|
|
||||||
stdoutReady := make(chan struct{})
|
|
||||||
stdoutLinesCh := make(chan string)
|
|
||||||
stdoutDone := make(chan struct{})
|
|
||||||
stderrReady := make(chan struct{})
|
|
||||||
stderrLinesCh := make(chan string)
|
|
||||||
stderrDone := make(chan struct{})
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
_ = stdout.Close()
|
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
|
||||||
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
_ = stdout.Close()
|
|
||||||
_ = stderr.Close()
|
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
|
||||||
<-stderrDone
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
waitErrorCh := make(chan error)
|
|
||||||
go func() {
|
|
||||||
err := cmd.Wait()
|
|
||||||
_ = stdout.Close()
|
|
||||||
_ = stderr.Close()
|
|
||||||
close(stop)
|
|
||||||
<-stdoutDone
|
|
||||||
<-stderrDone
|
|
||||||
waitErrorCh <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamToChannel(ready chan<- struct{},
|
|
||||||
stop <-chan struct{}, done chan<- struct{},
|
|
||||||
stream io.Reader, lines chan<- string,
|
|
||||||
) {
|
|
||||||
defer close(done)
|
|
||||||
close(ready)
|
|
||||||
scanner := bufio.NewScanner(stream)
|
|
||||||
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
|
|
||||||
const maxCapacity = 20 * 1024 * 1024 // 20MB
|
|
||||||
scanner.Buffer(lineBuffer, maxCapacity)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
// scanner is closed if the context is canceled
|
|
||||||
// or if the command failed starting because the
|
|
||||||
// stream is closed (io.EOF error).
|
|
||||||
lines <- scanner.Text()
|
|
||||||
}
|
|
||||||
err := scanner.Err()
|
|
||||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore the error if it is stopped.
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
lines <- "stream error: " + err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func linesToReadCloser(lines []string) io.ReadCloser {
|
|
||||||
s := strings.Join(lines, "\n")
|
|
||||||
return io.NopCloser(bytes.NewBufferString(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_start(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
errDummy := errors.New("dummy")
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
stdout []string
|
|
||||||
stdoutPipeErr error
|
|
||||||
stderr []string
|
|
||||||
stderrPipeErr error
|
|
||||||
startErr error
|
|
||||||
waitErr error
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
"no output": {},
|
|
||||||
"success": {
|
|
||||||
stdout: []string{"hello", "world"},
|
|
||||||
stderr: []string{"some", "error"},
|
|
||||||
},
|
|
||||||
"stdout pipe error": {
|
|
||||||
stdoutPipeErr: errDummy,
|
|
||||||
err: errDummy,
|
|
||||||
},
|
|
||||||
"stderr pipe error": {
|
|
||||||
stderrPipeErr: errDummy,
|
|
||||||
err: errDummy,
|
|
||||||
},
|
|
||||||
"start error": {
|
|
||||||
startErr: errDummy,
|
|
||||||
err: errDummy,
|
|
||||||
},
|
|
||||||
"wait error": {
|
|
||||||
waitErr: errDummy,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
|
|
||||||
stdout := linesToReadCloser(testCase.stdout)
|
|
||||||
stderr := linesToReadCloser(testCase.stderr)
|
|
||||||
|
|
||||||
mockCmd := NewMockexecCmd(ctrl)
|
|
||||||
|
|
||||||
mockCmd.EXPECT().StdoutPipe().
|
|
||||||
Return(stdout, testCase.stdoutPipeErr)
|
|
||||||
if testCase.stdoutPipeErr == nil {
|
|
||||||
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
|
|
||||||
if testCase.stderrPipeErr == nil {
|
|
||||||
mockCmd.EXPECT().Start().Return(testCase.startErr)
|
|
||||||
if testCase.startErr == nil {
|
|
||||||
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdoutLines, stderrLines, waitError, err := start(mockCmd)
|
|
||||||
|
|
||||||
if testCase.err != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
|
||||||
assert.Nil(t, stdoutLines)
|
|
||||||
assert.Nil(t, stderrLines)
|
|
||||||
assert.Nil(t, waitError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var stdoutIndex, stderrIndex int
|
|
||||||
|
|
||||||
done := false
|
|
||||||
for !done {
|
|
||||||
select {
|
|
||||||
case line := <-stdoutLines:
|
|
||||||
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
|
||||||
stdoutIndex++
|
|
||||||
case line := <-stderrLines:
|
|
||||||
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
|
||||||
stderrIndex++
|
|
||||||
case err := <-waitError:
|
|
||||||
if testCase.waitErr != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
|
||||||
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
internal/configuration/configuration.go
Normal file
3
internal/configuration/configuration.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package configuration reads initial settings from environment variables
|
||||||
|
// and secret files.
|
||||||
|
package configuration
|
||||||
6
internal/configuration/constants.go
Normal file
6
internal/configuration/constants.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
const (
|
||||||
|
lastIndent = "|--"
|
||||||
|
indent = " "
|
||||||
|
)
|
||||||
70
internal/configuration/cyberghost.go
Normal file
70
internal/configuration/cyberghost.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) cyberghostLines() (lines []string) {
|
||||||
|
lines = append(lines, lastIndent+"Server group: "+settings.ServerSelection.Group)
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.ExtraConfigOptions.ClientKey != "" {
|
||||||
|
lines = append(lines, lastIndent+"Client key is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.ExtraConfigOptions.ClientCertificate != "" {
|
||||||
|
lines = append(lines, lastIndent+"Client certificate is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readCyberghost(r reader) (err error) {
|
||||||
|
settings.Name = constants.Cyberghost
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ExtraConfigOptions.ClientKey, err = readClientKey(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ExtraConfigOptions.ClientCertificate, err = readClientCertificate(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Group, err = r.env.Inside("CYBERGHOST_GROUP",
|
||||||
|
constants.CyberghostGroupChoices(), params.Default("Premium UDP Europe"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.CyberghostRegionChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.CyberghostHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
117
internal/configuration/dns.go
Normal file
117
internal/configuration/dns.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/pkg/blacklist"
|
||||||
|
"github.com/qdm12/dns/pkg/unbound"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS contains settings to configure Unbound for DNS over TLS operation.
|
||||||
|
type DNS struct { //nolint:maligned
|
||||||
|
Enabled bool
|
||||||
|
PlaintextAddress net.IP
|
||||||
|
KeepNameserver bool
|
||||||
|
UpdatePeriod time.Duration
|
||||||
|
Unbound unbound.Settings
|
||||||
|
BlacklistBuild blacklist.BuilderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *DNS) String() string {
|
||||||
|
return strings.Join(settings.lines(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *DNS) lines() (lines []string) {
|
||||||
|
lines = append(lines, lastIndent+"DNS:")
|
||||||
|
|
||||||
|
if settings.PlaintextAddress != nil {
|
||||||
|
lines = append(lines, indent+lastIndent+"Plaintext address: "+settings.PlaintextAddress.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.KeepNameserver {
|
||||||
|
lines = append(lines, indent+lastIndent+"Keep nameserver (disabled blocking): yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.Enabled {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, indent+lastIndent+"DNS over TLS:")
|
||||||
|
|
||||||
|
lines = append(lines, indent+indent+lastIndent+"Unbound:")
|
||||||
|
for _, line := range settings.Unbound.Lines() {
|
||||||
|
lines = append(lines, indent+indent+indent+line)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, indent+indent+lastIndent+"Blacklist:")
|
||||||
|
for _, line := range settings.BlacklistBuild.Lines(indent, lastIndent) {
|
||||||
|
lines = append(lines, indent+indent+indent+line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.UpdatePeriod > 0 {
|
||||||
|
lines = append(lines, indent+indent+lastIndent+"Update: every "+settings.UpdatePeriod.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnboundSettings = errors.New("failed getting Unbound settings")
|
||||||
|
ErrBlacklistSettings = errors.New("failed getting DNS blacklist settings")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *DNS) read(r reader) (err error) {
|
||||||
|
settings.Enabled, err = r.env.OnOff("DOT", params.Default("on"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain DNS settings
|
||||||
|
if err := settings.readDNSPlaintext(r.env); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settings.KeepNameserver, err = r.env.OnOff("DNS_KEEP_NAMESERVER", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS over TLS external settings
|
||||||
|
if err := settings.readBlacklistBuilding(r); err != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrBlacklistSettings, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.UpdatePeriod, err = r.env.Duration("DNS_UPDATE_PERIOD", params.Default("24h"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbound settings
|
||||||
|
if err := settings.readUnbound(r); err != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnboundSettings, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDNSAddressNotAnIP = errors.New("DNS plaintext address is not an IP address")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *DNS) readDNSPlaintext(env params.Env) error {
|
||||||
|
s, err := env.Get("DNS_PLAINTEXT_ADDRESS", params.Default("1.1.1.1"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.PlaintextAddress = net.ParseIP(s)
|
||||||
|
if settings.PlaintextAddress == nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrDNSAddressNotAnIP, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
internal/configuration/dns_test.go
Normal file
76
internal/configuration/dns_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/pkg/blacklist"
|
||||||
|
"github.com/qdm12/dns/pkg/provider"
|
||||||
|
"github.com/qdm12/dns/pkg/unbound"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_DNS_Lines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testCases := map[string]struct {
|
||||||
|
settings DNS
|
||||||
|
lines []string
|
||||||
|
}{
|
||||||
|
"disabled DOT": {
|
||||||
|
settings: DNS{
|
||||||
|
PlaintextAddress: net.IP{1, 1, 1, 1},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--DNS:",
|
||||||
|
" |--Plaintext address: 1.1.1.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enabled DOT": {
|
||||||
|
settings: DNS{
|
||||||
|
Enabled: true,
|
||||||
|
KeepNameserver: true,
|
||||||
|
Unbound: unbound.Settings{
|
||||||
|
Providers: []provider.Provider{
|
||||||
|
provider.Cloudflare(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BlacklistBuild: blacklist.BuilderSettings{
|
||||||
|
BlockMalicious: true,
|
||||||
|
BlockAds: true,
|
||||||
|
BlockSurveillance: true,
|
||||||
|
},
|
||||||
|
UpdatePeriod: time.Hour,
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--DNS:",
|
||||||
|
" |--Keep nameserver (disabled blocking): yes",
|
||||||
|
" |--DNS over TLS:",
|
||||||
|
" |--Unbound:",
|
||||||
|
" |--DNS over TLS providers:",
|
||||||
|
" |--Cloudflare",
|
||||||
|
" |--Listening port: 0",
|
||||||
|
" |--Access control:",
|
||||||
|
" |--Allowed:",
|
||||||
|
" |--Caching: disabled",
|
||||||
|
" |--IPv4 resolution: disabled",
|
||||||
|
" |--IPv6 resolution: disabled",
|
||||||
|
" |--Verbosity level: 0/5",
|
||||||
|
" |--Verbosity details level: 0/4",
|
||||||
|
" |--Validation log level: 0/2",
|
||||||
|
" |--Username: ",
|
||||||
|
" |--Blacklist:",
|
||||||
|
" |--Blocked categories: malicious, surveillance, ads",
|
||||||
|
" |--Update: every 1h0m0s",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
lines := testCase.settings.lines()
|
||||||
|
assert.Equal(t, testCase.lines, lines)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
87
internal/configuration/dnsblacklist.go
Normal file
87
internal/configuration/dnsblacklist.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *DNS) readBlacklistBuilding(r reader) (err error) {
|
||||||
|
settings.BlacklistBuild.BlockMalicious, err = r.env.OnOff("BLOCK_MALICIOUS", params.Default("on"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.BlacklistBuild.BlockSurveillance, err = r.env.OnOff("BLOCK_SURVEILLANCE", params.Default("on"),
|
||||||
|
params.RetroKeys([]string{"BLOCK_NSA"}, r.onRetroActive))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.BlacklistBuild.BlockAds, err = r.env.OnOff("BLOCK_ADS", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.readPrivateAddresses(r.env); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.readBlacklistUnblockedHostnames(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidPrivateAddress = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *DNS) readPrivateAddresses(env params.Env) (err error) {
|
||||||
|
privateAddresses, err := env.CSV("DOT_PRIVATE_ADDRESS")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(privateAddresses) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := make([]netaddr.IP, 0, len(privateAddresses))
|
||||||
|
ipPrefixes := make([]netaddr.IPPrefix, 0, len(privateAddresses))
|
||||||
|
|
||||||
|
for _, address := range privateAddresses {
|
||||||
|
ip, err := netaddr.ParseIP(address)
|
||||||
|
if err == nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipPrefix, err := netaddr.ParseIPPrefix(address)
|
||||||
|
if err == nil {
|
||||||
|
ipPrefixes = append(ipPrefixes, ipPrefix)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidPrivateAddress, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.BlacklistBuild.AddBlockedIPs = append(settings.BlacklistBuild.AddBlockedIPs, ips...)
|
||||||
|
settings.BlacklistBuild.AddBlockedIPPrefixes = append(settings.BlacklistBuild.AddBlockedIPPrefixes, ipPrefixes...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *DNS) readBlacklistUnblockedHostnames(r reader) (err error) {
|
||||||
|
hostnames, err := r.env.CSV("UNBLOCK")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(hostnames) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, hostname := range hostnames {
|
||||||
|
if !r.regex.MatchHostname(hostname) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidHostname, hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.BlacklistBuild.AllowedHosts = append(settings.BlacklistBuild.AllowedHosts, hostnames...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
43
internal/configuration/fastestvpn.go
Normal file
43
internal/configuration/fastestvpn.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) fastestvpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readFastestvpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Fastestvpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.FastestvpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.FastestvpnCountriesChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
internal/configuration/firewall.go
Normal file
89
internal/configuration/firewall.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Firewall contains settings to customize the firewall operation.
|
||||||
|
type Firewall struct {
|
||||||
|
VPNInputPorts []uint16
|
||||||
|
InputPorts []uint16
|
||||||
|
OutboundSubnets []net.IPNet
|
||||||
|
Enabled bool
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) String() string {
|
||||||
|
return strings.Join(settings.lines(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) lines() (lines []string) {
|
||||||
|
if !settings.Enabled {
|
||||||
|
lines = append(lines, lastIndent+"Firewall: disabled ⚠️")
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, lastIndent+"Firewall:")
|
||||||
|
|
||||||
|
if settings.Debug {
|
||||||
|
lines = append(lines, indent+lastIndent+"Debug: on")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.VPNInputPorts) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"VPN input ports: "+
|
||||||
|
strings.Join(uint16sToStrings(settings.VPNInputPorts), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.InputPorts) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"Input ports: "+
|
||||||
|
strings.Join(uint16sToStrings(settings.InputPorts), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.OutboundSubnets) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"Outbound subnets: "+
|
||||||
|
strings.Join(ipNetsToStrings(settings.OutboundSubnets), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) read(r reader) (err error) {
|
||||||
|
settings.Enabled, err = r.env.OnOff("FIREWALL", params.Default("on"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Debug, err = r.env.OnOff("FIREWALL_DEBUG", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.readVPNInputPorts(r.env); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.readInputPorts(r.env); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.readOutboundSubnets(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) readVPNInputPorts(env params.Env) (err error) {
|
||||||
|
settings.VPNInputPorts, err = readCSVPorts(env, "FIREWALL_VPN_INPUT_PORTS")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) readInputPorts(env params.Env) (err error) {
|
||||||
|
settings.InputPorts, err = readCSVPorts(env, "FIREWALL_INPUT_PORTS")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Firewall) readOutboundSubnets(r reader) (err error) {
|
||||||
|
retroOption := params.RetroKeys([]string{"EXTRA_SUBNETS"}, r.onRetroActive)
|
||||||
|
settings.OutboundSubnets, err = readCSVIPNets(r.env, "FIREWALL_OUTBOUND_SUBNETS", retroOption)
|
||||||
|
return err
|
||||||
|
}
|
||||||
61
internal/configuration/hidemyass.go
Normal file
61
internal/configuration/hidemyass.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) hideMyAssLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readHideMyAss(r reader) (err error) {
|
||||||
|
settings.Name = constants.HideMyAss
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.HideMyAssCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.HideMyAssCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.HideMyAssCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.HideMyAssHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
105
internal/configuration/httpproxy.go
Normal file
105
internal/configuration/httpproxy.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPProxy contains settings to configure the HTTP proxy.
|
||||||
|
type HTTPProxy struct {
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Port uint16
|
||||||
|
Enabled bool
|
||||||
|
Stealth bool
|
||||||
|
Log bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *HTTPProxy) String() string {
|
||||||
|
return strings.Join(settings.lines(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *HTTPProxy) lines() (lines []string) {
|
||||||
|
if !settings.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, lastIndent+"HTTP proxy:")
|
||||||
|
|
||||||
|
lines = append(lines, indent+lastIndent+"Port: "+strconv.Itoa(int(settings.Port)))
|
||||||
|
|
||||||
|
if settings.User != "" {
|
||||||
|
lines = append(lines, indent+lastIndent+"Authentication: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.Log {
|
||||||
|
lines = append(lines, indent+lastIndent+"Log: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.Stealth {
|
||||||
|
lines = append(lines, indent+lastIndent+"Stealth: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *HTTPProxy) read(r reader) (err error) {
|
||||||
|
settings.Enabled, err = r.env.OnOff("HTTPPROXY", params.Default("off"),
|
||||||
|
params.RetroKeys([]string{"TINYPROXY", "PROXY"}, r.onRetroActive))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.User, err = r.getFromEnvOrSecretFile("HTTPPROXY_USER", false, // compulsory
|
||||||
|
[]string{"TINYPROXY_USER", "PROXY_USER"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Password, err = r.getFromEnvOrSecretFile("HTTPPROXY_PASSWORD", false,
|
||||||
|
[]string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Stealth, err = r.env.OnOff("HTTPPROXY_STEALTH", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.readLog(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var warning string
|
||||||
|
settings.Port, warning, err = r.env.ListeningPort("HTTPPROXY_PORT", params.Default("8888"),
|
||||||
|
params.RetroKeys([]string{"TINYPROXY_PORT", "PROXY_PORT"}, r.onRetroActive))
|
||||||
|
if len(warning) > 0 {
|
||||||
|
r.logger.Warn(warning)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *HTTPProxy) readLog(r reader) error {
|
||||||
|
s, err := r.env.Get("HTTPPROXY_LOG",
|
||||||
|
params.RetroKeys([]string{"PROXY_LOG_LEVEL", "TINYPROXY_LOG"}, r.onRetroActive))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "on":
|
||||||
|
settings.Log = true
|
||||||
|
// Retro compatibility
|
||||||
|
case "info", "connect", "notice":
|
||||||
|
settings.Log = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
52
internal/configuration/ipvanish.go
Normal file
52
internal/configuration/ipvanish.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) ipvanishLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readIpvanish(r reader) (err error) {
|
||||||
|
settings.Name = constants.Ipvanish
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IpvanishCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IpvanishCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
192
internal/configuration/ipvanish_test.go
Normal file
192
internal/configuration/ipvanish_test.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params/mock_params"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Provider_ipvanishLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
settings Provider
|
||||||
|
lines []string
|
||||||
|
}{
|
||||||
|
"empty settings": {},
|
||||||
|
"full settings": {
|
||||||
|
settings: Provider{
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"A", "B"},
|
||||||
|
Cities: []string{"C", "D"},
|
||||||
|
Hostnames: []string{"E", "F"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Countries: A, B",
|
||||||
|
"|--Cities: C, D",
|
||||||
|
"|--Hostnames: E, F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lines := testCase.settings.ipvanishLines()
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.lines, lines)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Provider_readIpvanish(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var errDummy = errors.New("dummy test error")
|
||||||
|
|
||||||
|
type singleStringCall struct {
|
||||||
|
call bool
|
||||||
|
value string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sliceStringCall struct {
|
||||||
|
call bool
|
||||||
|
values []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
protocol singleStringCall
|
||||||
|
targetIP singleStringCall
|
||||||
|
countries sliceStringCall
|
||||||
|
cities sliceStringCall
|
||||||
|
hostnames sliceStringCall
|
||||||
|
settings Provider
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"protocol error": {
|
||||||
|
protocol: singleStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"target IP error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"countries error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"cities error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"hostnames error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true},
|
||||||
|
hostnames: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"default settings": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true},
|
||||||
|
hostnames: sliceStringCall{call: true},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"set settings": {
|
||||||
|
protocol: singleStringCall{call: true, value: constants.TCP},
|
||||||
|
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
|
||||||
|
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
|
||||||
|
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
|
||||||
|
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
TCP: true,
|
||||||
|
TargetIP: net.IPv4(1, 2, 3, 4),
|
||||||
|
Countries: []string{"A", "B"},
|
||||||
|
Cities: []string{"C", "D"},
|
||||||
|
Hostnames: []string{"E", "F"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
env := mock_params.NewMockEnv(ctrl)
|
||||||
|
if testCase.protocol.call {
|
||||||
|
env.EXPECT().Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
|
||||||
|
Return(testCase.protocol.value, testCase.protocol.err)
|
||||||
|
}
|
||||||
|
if testCase.targetIP.call {
|
||||||
|
env.EXPECT().Get("OPENVPN_TARGET_IP").
|
||||||
|
Return(testCase.targetIP.value, testCase.targetIP.err)
|
||||||
|
}
|
||||||
|
if testCase.countries.call {
|
||||||
|
env.EXPECT().CSVInside("COUNTRY", constants.IpvanishCountryChoices()).
|
||||||
|
Return(testCase.countries.values, testCase.countries.err)
|
||||||
|
}
|
||||||
|
if testCase.cities.call {
|
||||||
|
env.EXPECT().CSVInside("CITY", constants.IpvanishCityChoices()).
|
||||||
|
Return(testCase.cities.values, testCase.cities.err)
|
||||||
|
}
|
||||||
|
if testCase.hostnames.call {
|
||||||
|
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices()).
|
||||||
|
Return(testCase.hostnames.values, testCase.hostnames.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := reader{env: env}
|
||||||
|
|
||||||
|
var settings Provider
|
||||||
|
err := settings.readIpvanish(r)
|
||||||
|
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.settings, settings)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
52
internal/configuration/ivpn.go
Normal file
52
internal/configuration/ivpn.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) ivpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readIvpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Ivpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IvpnCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IvpnCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
192
internal/configuration/ivpn_test.go
Normal file
192
internal/configuration/ivpn_test.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params/mock_params"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Provider_ivpnLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
settings Provider
|
||||||
|
lines []string
|
||||||
|
}{
|
||||||
|
"empty settings": {},
|
||||||
|
"full settings": {
|
||||||
|
settings: Provider{
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"A", "B"},
|
||||||
|
Cities: []string{"C", "D"},
|
||||||
|
Hostnames: []string{"E", "F"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Countries: A, B",
|
||||||
|
"|--Cities: C, D",
|
||||||
|
"|--Hostnames: E, F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lines := testCase.settings.ivpnLines()
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.lines, lines)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Provider_readIvpn(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var errDummy = errors.New("dummy test error")
|
||||||
|
|
||||||
|
type singleStringCall struct {
|
||||||
|
call bool
|
||||||
|
value string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sliceStringCall struct {
|
||||||
|
call bool
|
||||||
|
values []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
protocol singleStringCall
|
||||||
|
targetIP singleStringCall
|
||||||
|
countries sliceStringCall
|
||||||
|
cities sliceStringCall
|
||||||
|
hostnames sliceStringCall
|
||||||
|
settings Provider
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"protocol error": {
|
||||||
|
protocol: singleStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"target IP error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"countries error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"cities error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"hostnames error": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true},
|
||||||
|
hostnames: sliceStringCall{call: true, err: errDummy},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"default settings": {
|
||||||
|
protocol: singleStringCall{call: true},
|
||||||
|
targetIP: singleStringCall{call: true},
|
||||||
|
countries: sliceStringCall{call: true},
|
||||||
|
cities: sliceStringCall{call: true},
|
||||||
|
hostnames: sliceStringCall{call: true},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"set settings": {
|
||||||
|
protocol: singleStringCall{call: true, value: constants.TCP},
|
||||||
|
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
|
||||||
|
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
|
||||||
|
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
|
||||||
|
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
TCP: true,
|
||||||
|
TargetIP: net.IPv4(1, 2, 3, 4),
|
||||||
|
Countries: []string{"A", "B"},
|
||||||
|
Cities: []string{"C", "D"},
|
||||||
|
Hostnames: []string{"E", "F"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
env := mock_params.NewMockEnv(ctrl)
|
||||||
|
if testCase.protocol.call {
|
||||||
|
env.EXPECT().Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
|
||||||
|
Return(testCase.protocol.value, testCase.protocol.err)
|
||||||
|
}
|
||||||
|
if testCase.targetIP.call {
|
||||||
|
env.EXPECT().Get("OPENVPN_TARGET_IP").
|
||||||
|
Return(testCase.targetIP.value, testCase.targetIP.err)
|
||||||
|
}
|
||||||
|
if testCase.countries.call {
|
||||||
|
env.EXPECT().CSVInside("COUNTRY", constants.IvpnCountryChoices()).
|
||||||
|
Return(testCase.countries.values, testCase.countries.err)
|
||||||
|
}
|
||||||
|
if testCase.cities.call {
|
||||||
|
env.EXPECT().CSVInside("CITY", constants.IvpnCityChoices()).
|
||||||
|
Return(testCase.cities.values, testCase.cities.err)
|
||||||
|
}
|
||||||
|
if testCase.hostnames.call {
|
||||||
|
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices()).
|
||||||
|
Return(testCase.hostnames.values, testCase.hostnames.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := reader{env: env}
|
||||||
|
|
||||||
|
var settings Provider
|
||||||
|
err := settings.readIvpn(r)
|
||||||
|
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.settings, settings)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
55
internal/configuration/keys.go
Normal file
55
internal/configuration/keys.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readClientKey(r reader) (clientKey string, err error) {
|
||||||
|
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", constants.ClientKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return extractClientKey(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errDecodePEMBlockClientKey = errors.New("cannot decode PEM block from client key")
|
||||||
|
|
||||||
|
func extractClientKey(b []byte) (key string, err error) {
|
||||||
|
pemBlock, _ := pem.Decode(b)
|
||||||
|
if pemBlock == nil {
|
||||||
|
return "", errDecodePEMBlockClientKey
|
||||||
|
}
|
||||||
|
parsedBytes := pem.EncodeToMemory(pemBlock)
|
||||||
|
s := string(parsedBytes)
|
||||||
|
s = strings.ReplaceAll(s, "\n", "")
|
||||||
|
s = strings.TrimPrefix(s, "-----BEGIN PRIVATE KEY-----")
|
||||||
|
s = strings.TrimSuffix(s, "-----END PRIVATE KEY-----")
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClientCertificate(r reader) (clientCertificate string, err error) {
|
||||||
|
b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", constants.ClientCertificate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return extractClientCertificate(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errDecodePEMBlockClientCert = errors.New("cannot decode PEM block from client certificate")
|
||||||
|
|
||||||
|
func extractClientCertificate(b []byte) (certificate string, err error) {
|
||||||
|
pemBlock, _ := pem.Decode(b)
|
||||||
|
if pemBlock == nil {
|
||||||
|
return "", errDecodePEMBlockClientCert
|
||||||
|
}
|
||||||
|
parsedBytes := pem.EncodeToMemory(pemBlock)
|
||||||
|
s := string(parsedBytes)
|
||||||
|
s = strings.ReplaceAll(s, "\n", "")
|
||||||
|
s = strings.TrimPrefix(s, "-----BEGIN CERTIFICATE-----")
|
||||||
|
s = strings.TrimSuffix(s, "-----END CERTIFICATE-----")
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
174
internal/configuration/keys_test.go
Normal file
174
internal/configuration/keys_test.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_extractClientKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const validPEM = `
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxX
|
||||||
|
ywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL
|
||||||
|
2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlG
|
||||||
|
AB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+Hx
|
||||||
|
Ymy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVo
|
||||||
|
rV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/
|
||||||
|
lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChU
|
||||||
|
i2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ez
|
||||||
|
L19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4L
|
||||||
|
O66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdr
|
||||||
|
pimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3
|
||||||
|
bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyM
|
||||||
|
yqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqS
|
||||||
|
xrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+
|
||||||
|
iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y
|
||||||
|
0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBW
|
||||||
|
IsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPu
|
||||||
|
K5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9Fkrp
|
||||||
|
B6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hE
|
||||||
|
rVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1N
|
||||||
|
KvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hn
|
||||||
|
kIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C1
|
||||||
|
12Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgK
|
||||||
|
VwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+s
|
||||||
|
qsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jb
|
||||||
|
kh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhz
|
||||||
|
nQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sG
|
||||||
|
mw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4
|
||||||
|
TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMe
|
||||||
|
xFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0
|
||||||
|
SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oe
|
||||||
|
o8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscP
|
||||||
|
yFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99
|
||||||
|
OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYy
|
||||||
|
zNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQi
|
||||||
|
S5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5
|
||||||
|
///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4Y
|
||||||
|
JLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3
|
||||||
|
z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSX
|
||||||
|
yQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9Ulw
|
||||||
|
khtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzR
|
||||||
|
nULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR
|
||||||
|
7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn
|
||||||
|
2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopu
|
||||||
|
AnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh
|
||||||
|
8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4Kxdi
|
||||||
|
JKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggyn
|
||||||
|
q00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT
|
||||||
|
+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZ
|
||||||
|
zZjrL52saevO25cigVl+hxcnY8DTpbk=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
const validKeyString = "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCrQDrezCptkWxXywm3KdXtvti+rPF3vfzOmXRKiKXDMpMxzoiaD5Wspirxxjr4C+B14xTwZjJZfxJL2HpPdOeBmA5tmAoGUESspnzxR/N1T4Uggx0vlAzFo0UZ0sutV6CJK19Kk38REwlGAB8gl6JYeSUuu6qREjrLVwFRH72acvC/p4jBki/MjAfEaeHc0yDJT9jpjpchw+HxYmy+1BnfNTAfGDdTVx9qWb+ByQ7xfvzuD9AOeqiWApDzZIuDDsaWn2orv+syoJVorV52/F+75zks6+fzQ+0sotBlRyvsZKGi80F89RIHwG+5LNSuRDWnVvrwv1oc6V2/lMidwT7yb0kXt0IRW6JzbaHyB2LkPazBlr6IPNupk83x9t2Buw0HI2SQKHMKOChUi2/906yLUOo3QpAi3Wk1c/Xu9DvGR/pOA15WCakiAfG3Fq6hUxNncmpOMeOLF/ezL19jZ3KA4E2Te4+GA0NYlXgkDbsIILWapHwqHXcDukynHisr7RawjrvXoLyasm4LO66aNXK9wtipSMDA7tdlQP6Xe9bHflDHxwreiuEGxnrsvLU7LHBWdD7UT2/u8zdrpimqi4L7W5p5aOBMn8jSVCO9+4CAxiLlc2qx5vb4/EPMsdQfacYP7vY9iVh/qPi3bcUVGUlg8wAJDrTksxU1K3FVR7BEPwIDAQABAoICAAhyrbTJ+5nWH7MhCASqIqyMyqJ1Y6AVlkAW397BaPP9Lbe6SZDYDfkrZVjx/3y3EUafgivtzrQNibiGIFqFGNqSxrtvUadIFGsz91vrwb3aw2V8MldjhVHGoSUJ+hQ+C2RY6GWEazNLbhyu6tovwMl+iHAKv/pSHOZlD2KSH0dcPjYmLJ/n90Wu7r8ovgSnwalMsBWtfBUlVaMTyOuNCQ2y0QHnrusElD8p2EGtynftXMrdqtTcBi8IR2BKaHt5oiBSEum/mPmxZE16p/tUreBWIsLtjE663htimMc2QJtzx2mDeIqSiGYrfxdyd7d1E/SCXPS9a9ObS42k6FSn8NPuK5kN6fPV5EDM2CqKEt9QZPlyrjZIuffOZtJj0xPuTwhRle4SOtyjn2c/vsv9FkrpB6B1v7T4+SSOIedOYkL+FP/IexMNG/ZTB5Y2hrZ03JW9RGpFAa4//qGG2qUCR3hErVS6v58qO/3+TCFSn/TI8AfcTcJbes3yTbVyLH6NAjATfYqJDJJFf+PG0qKc8q1NKvXmT+x4JiBBM32cOg11GPflxIZSKi9He50hnPGnC042N06ba/pkUPG49XwE37hnkIGmcFGcDIMDTEZnPBogPFqGpepYdwGWxbadRiUoX2wgurHRRmA0YM32MjVky9C112Q/Jy9PIk/qdjYdWfAhAoIBAQDcvxfUx7MKMFgYYm4E51X+7B9QoxdhVaxcoVgKVwfvedsLi0Bk1B1JVSXqnNfyDZbpxFz2v5Xd/dSit2rjnfBm+DoJYN9ZNnrbIH+sqsO1DuHZvMZlRDJbpt7PpVH/pcf7rEWRY+avkMMsiGwI/ruDs17eu7jULeG7N4jbkh1mdvF7K56O6Xe8jGJu5qaOPRWOkABK1cEOjQ5hB1iAwO/ua5hehP87SvbYzIhznQTE3AqTWgWbIyC4R85U7tS9hsXnSQ/ICM9pWcyN0Y667LwR2tX0QKl5M/YoM0sGmw+VQED8O2R45jTzSAcox77dRg55Pp3Xexsp2iVvaTIeAaevAoIBAQDGmZS1gFO4TEgQXHdmibLizDUHLuw662GC+3Hilx+nZBZtWOc6t8yquUyggSKQmBDiKAf0ipMexFao+5I3StJJ2P4Vel95Vcu8KgqCF736Q1iNgDHuW8ho8e0y+YE371x5co3POGC0SfbcnRTXQx2+wWXzZDh+KtnaDUyDN12/qCIUyAuSVLwEM28ZFM3qadG1aUdCB5oeo8jfgsg6YSukm4uE/tuI3/wAI7FkaCqvt/zkLauRff5FcNa7os4EKtNnGfebxscPyFYpMsW9VI0rfmYz02gho33lnofs4o8x/gxh6t5zfVbsZ7vUiSDJBahWboG9aE99OY2TKb6ibsBxAoIBAQChDBVR2oPnqg+Lcrw7fZ8Cxbeu992F2KBQUDHQEWCruSYyzNwk84+OQb3Q5a6yXHG+iNEd//ZRp+8q60/jUgXiybRlxTQNfS6ykYo0Kb1wabQiS5Qeq1tl/F9P9JfXQFafaTaz9MOHUMDjy3+uLFIXqpRLQX995R9rm/+P2ZDzgVF5///E2dXOTElACax3117TzIE6F6qqeASGi3ppLNmfAwZ95t/inTVsRARE/MhO6w4YJLQ0U7N6XoDM/BVfVGUr8OS/lpXjkW0oBjvwaehnylUPxuEdmOg8ufdBkX0T8XW3z4jkn2cAGouGl/vKqWLD2AgF/j16Ejn/hyrWM3TnAoIBAA6lSssrwIDJ11KljwSXyQJirtJtymv56cIACwD7xhDRF7pOoRa6cTRx383CWCszm6Mh8pw9D+Zn8kAZ9UlwkhtyDiLFWH8ZLaIds5Kub4siJkihGI2MZTYgCS8GKVpXo4ktQnnynWcOQU85okzRnULw/jS5wlTDkjc7XdYbYiV9H65KplfPOeJRbLL7zsensBhhwCiFaP8zct/QxDVR7yb/dYWESepJIktcVnuiFuvIdLTbDVj4YqT6UkuaEPlLszVaO+FYAlwOmRQGs4Bn2NVJR/4wa/B3HxSs4Tc96fN02bLq4CbCKoPajoZ46lsIuMZO9fBi3eHNObyNiopuAnECggEBAJiJ0tK/PGh+Q9uv57Z4QcmbawoxMQW1qK/rLYwacYsSpzo8VhbZf+Jh8biMg9AIQsLWnqmB3gmndePArGXkSxnilRozNLaeclTZy7rh00BctTEfgee4KxdiJKkJlVK0CE8I6txVRqkoOMyxsk1kRZ4l2yW2nxzyWlJKwvD75x2PQ6xWvpLAggynq00I3MzNIgR123jytN1NyC7l+mnGoC23ToXM7B3/PQjGYTq3jawKomrX1cmwzKBT+pzjtJSWvMeUEZQS1PpOhxpPBRHagdKXt+ug2DqDtU6rfpDGtTBh5QNkg5SA7lxZzZjrL52saevO25cigVl+hxcnY8DTpbk=" //nolint:lll
|
||||||
|
testCases := map[string]struct {
|
||||||
|
b []byte
|
||||||
|
key string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no input": {
|
||||||
|
err: errDecodePEMBlockClientKey,
|
||||||
|
},
|
||||||
|
"bad input": {
|
||||||
|
b: []byte{1, 2, 3},
|
||||||
|
err: errDecodePEMBlockClientKey,
|
||||||
|
},
|
||||||
|
"valid key": {
|
||||||
|
b: []byte(validPEM),
|
||||||
|
key: validKeyString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
key, err := extractClientKey(testCase.b)
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.key, key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_extractClientCertificate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const validPEM = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJS
|
||||||
|
TzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4x
|
||||||
|
GzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5m
|
||||||
|
b0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlow
|
||||||
|
fTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3li
|
||||||
|
ZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8G
|
||||||
|
CSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJT
|
||||||
|
NZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E
|
||||||
|
7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPh
|
||||||
|
EzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXm
|
||||||
|
PFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0D
|
||||||
|
Jt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAz
|
||||||
|
KI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ
|
||||||
|
2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8
|
||||||
|
SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVH
|
||||||
|
euH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOi
|
||||||
|
O2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMC
|
||||||
|
AwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIB
|
||||||
|
DQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCG
|
||||||
|
SAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0G
|
||||||
|
A1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYT
|
||||||
|
AlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5B
|
||||||
|
LjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJp
|
||||||
|
bmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEA
|
||||||
|
ystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmP
|
||||||
|
Pb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDao
|
||||||
|
hQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6
|
||||||
|
lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+v
|
||||||
|
r54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORh
|
||||||
|
obElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNF
|
||||||
|
bcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0Uyb
|
||||||
|
aobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQ
|
||||||
|
rRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMud
|
||||||
|
xXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1Tp
|
||||||
|
iOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
const validCertificateString = "MIIGrDCCBJSgAwIBAgIEAdTnfTANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJSTzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQKEw9DeWJlckdob3N0IFMuQS4xGzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMB4XDTIwMDcwNDE1MjkzNloXDTMwMDcwMjE1MjkzNlowfTELMAkGA1UEBhMCUk8xEjAQBgNVBAcMCUJ1Y2hhcmVzdDEYMBYGA1UECgwPQ3liZXJHaG9zdCBTLkEuMR0wGwYDVQQDDBRjLmoua2xhdmVyQGdtYWlsLmNvbTEhMB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAobp2NlGUHMNBe08YEOnVG3QJjF3ZaXbRhE/II9rmtgJTNZtDohGChvFlNRsExKzVrKxHCeuJkVffwzQ6fYk4/M1RdYLJUh0UVw3e4WdApw8E7TJZxDYm4SHQNXUvt1Rt5TjslcXxIpDZgrMSc/kHROYEL9tdgdzPZErUJehXyJPhEzIrzmAJh501x7WwKPz9ctSVlItyavqEWFF2vyUa6X9DYmD9mQTz5c+VXNO5DkXmPFBIaEVDnvFtcjGJ56yEvFnWVukL+OUX7ezowrIOFOcp9udjgpeiHq+XvsQ6ER0DJt25MiEId3NjkxtZ8BitDftTcLN/kt81hWKT7adMVc3kpIZ80cxrwRCttMd7sHAzKI9u7pMxv10eUOsIEY87ewBe3l6KvEnjA+9uIjim6gLLebDIaEH50Ee9PzNJ8fqQ2u54Ab4bt00/H1sUnJ6Ss/+WsQDOK1BsPRKKcnHZntOlHrs2Tu5+txKNU2cOapI8SjVULUNKrRXASbpfWnLUfri/HO742bJb/TjkOJcOxta3hTPFAhaRWBusVlB41XVHeuH5DAhugYXeSNK6/6Ul8YvKUNH/7QbxuGIGXfth19Xl4QLI1umyEjZopSlt3tOiO2V1soVNSQCCfxXVoCTMESMLjhkjWdmBDhdy2GTW7S4YoJfqVKiS18rYkN7I4ZMCAwEAAaOCATQwggEwMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDQGCWCGSAGG+EIBDQQnFiVDeWJlckdob3N0IEdlbmVyYXRlZCBVc2VyIENlcnRpZmljYXRlMBEGCWCGSAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQULwUtU5s6pL2NN9gPeEnKX0dhwiswga0GA1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYTAlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5BLjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJpbmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzANBgkqhkiG9w0BAQsFAAOCAgEAystGIMYhQWaEdTqlnLCytrr8657t+PuidZMNNIaPB3wN2Fi2xKf14DTg03mqxjmPPb+f+PVNIOV5PdWD4jcQwOP1GEboGV0DFzlRGeAtDcvKwdee4oASJbZq1CETqDaohQTxKEWC+UBk2F36nOaEI6Sab+Mb4cR9//PAwvzOqrXuGF5NuIOX7eFtCMQSgQq6lRRqTQjekm0Dxigx4JA92Jo2qZRwCJ0T3IXBJGL831HCFJbDWv8PV3lsfFb/i2+vr54uywFQVWWp18dYi97gipfuQ4zRg2Ldx5aXSmnhhKpg5ioZvtk043QofF12YORhobElqavRbvvhZvlCouvcuoq9QKi7IPe5SJZkZ1X7ezMesCwBzwFpt6vRUAcslsNFbcYS1iSENlY/PTcDqBhbKuc9yAhq+/aUgaY/8VF5RWVzSRZufbf3BPwOkE4K0UybaobO/YX0JOkCacAD+4tdR6YSXNIMMRAOCBQvxbxFXaHzhwhzBAjdsC56FrJKwXvQrRLU3tF4P0zFMeNTay8uTtUXugDK7EnklLESuYdpUJ8bUMlAUhJBi6UFI9/icMudxXvLRvhnBW9EtKib5JnVFUovcEUt+3EJbyst05nkL4YPjQS4TC9DHdo5SyRAy1TpiOCYTbretAFZRhh6ycUN5hBeN8GMQxiMreMtDV4PEIQ=" //nolint:lll
|
||||||
|
testCases := map[string]struct {
|
||||||
|
b []byte
|
||||||
|
certificate string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no input": {
|
||||||
|
err: errDecodePEMBlockClientCert,
|
||||||
|
},
|
||||||
|
"bad input": {
|
||||||
|
b: []byte{1, 2, 3},
|
||||||
|
err: errDecodePEMBlockClientCert,
|
||||||
|
},
|
||||||
|
"valid key": {
|
||||||
|
b: []byte(validPEM),
|
||||||
|
certificate: validCertificateString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
certificate, err := extractClientCertificate(testCase.b)
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, testCase.certificate, certificate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
22
internal/configuration/lines.go
Normal file
22
internal/configuration/lines.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uint16sToStrings(uint16s []uint16) (strings []string) {
|
||||||
|
strings = make([]string, len(uint16s))
|
||||||
|
for i := range uint16s {
|
||||||
|
strings[i] = strconv.Itoa(int(uint16s[i]))
|
||||||
|
}
|
||||||
|
return strings
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipNetsToStrings(ipNets []net.IPNet) (strings []string) {
|
||||||
|
strings = make([]string, len(ipNets))
|
||||||
|
for i := range ipNets {
|
||||||
|
strings[i] = ipNets[i].String()
|
||||||
|
}
|
||||||
|
return strings
|
||||||
|
}
|
||||||
88
internal/configuration/mullvad.go
Normal file
88
internal/configuration/mullvad.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) mullvadLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.ISPs) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"ISPs: "+commaJoin(settings.ServerSelection.ISPs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.ServerSelection.CustomPort > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Custom port: "+strconv.Itoa(int(settings.ServerSelection.CustomPort)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.ExtraConfigOptions.OpenVPNIPv6 {
|
||||||
|
lines = append(lines, lastIndent+"IPv6: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readMullvad(r reader) (err error) {
|
||||||
|
settings.Name = constants.Mullvad
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.MullvadCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.MullvadCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.MullvadHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.MullvadISPChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.CustomPort, err = readCustomPort(r.env, settings.ServerSelection.TCP,
|
||||||
|
[]uint16{80, 443, 1401}, []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Owned, err = r.env.YesNo("OWNED", params.Default("no"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ExtraConfigOptions.OpenVPNIPv6, err = r.env.OnOff("OPENVPN_IPV6", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
91
internal/configuration/nordvpn.go
Normal file
91
internal/configuration/nordvpn.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) nordvpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Names) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if numbersUint16 := settings.ServerSelection.Numbers; len(numbersUint16) > 0 {
|
||||||
|
numbersString := make([]string, len(numbersUint16))
|
||||||
|
for i, numberUint16 := range numbersUint16 {
|
||||||
|
numbersString[i] = strconv.Itoa(int(numberUint16))
|
||||||
|
}
|
||||||
|
lines = append(lines, lastIndent+"Numbers: "+commaJoin(numbersString))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readNordvpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Nordvpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.NordvpnRegionChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.NordvpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.NordvpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Numbers, err = readNordVPNServerNumbers(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNordVPNServerNumbers(env params.Env) (numbers []uint16, err error) {
|
||||||
|
const possiblePortsCount = 65537
|
||||||
|
possibilities := make([]string, possiblePortsCount)
|
||||||
|
for i := range possibilities {
|
||||||
|
possibilities[i] = fmt.Sprintf("%d", i)
|
||||||
|
}
|
||||||
|
possibilities[65536] = ""
|
||||||
|
values, err := env.CSVInside("SERVER_NUMBER", possibilities)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
numbers = make([]uint16, len(values))
|
||||||
|
for i := range values {
|
||||||
|
n, err := strconv.Atoi(values[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
numbers[i] = uint16(n)
|
||||||
|
}
|
||||||
|
return numbers, nil
|
||||||
|
}
|
||||||
190
internal/configuration/openvpn.go
Normal file
190
internal/configuration/openvpn.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenVPN contains settings to configure the OpenVPN client.
|
||||||
|
type OpenVPN struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Verbosity int `json:"verbosity"`
|
||||||
|
MSSFix uint16 `json:"mssfix"`
|
||||||
|
Root bool `json:"run_as_root"`
|
||||||
|
Cipher string `json:"cipher"`
|
||||||
|
Auth string `json:"auth"`
|
||||||
|
Provider Provider `json:"provider"`
|
||||||
|
Config string `json:"custom_config"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *OpenVPN) String() string {
|
||||||
|
return strings.Join(settings.lines(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *OpenVPN) lines() (lines []string) {
|
||||||
|
lines = append(lines, lastIndent+"OpenVPN:")
|
||||||
|
|
||||||
|
lines = append(lines, indent+lastIndent+"Version: "+settings.Version)
|
||||||
|
|
||||||
|
lines = append(lines, indent+lastIndent+"Verbosity level: "+strconv.Itoa(settings.Verbosity))
|
||||||
|
|
||||||
|
if settings.Root {
|
||||||
|
lines = append(lines, indent+lastIndent+"Run as root: enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.Cipher) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"Custom cipher: "+settings.Cipher)
|
||||||
|
}
|
||||||
|
if len(settings.Auth) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.Config) > 0 {
|
||||||
|
lines = append(lines, indent+lastIndent+"Custom configuration: "+settings.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.Provider.Name == "" {
|
||||||
|
lines = append(lines, indent+lastIndent+"Provider: custom configuration")
|
||||||
|
} else {
|
||||||
|
lines = append(lines, indent+lastIndent+"Provider:")
|
||||||
|
for _, line := range settings.Provider.lines() {
|
||||||
|
lines = append(lines, indent+indent+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidVPNProvider = errors.New("invalid VPN provider")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *OpenVPN) read(r reader) (err error) {
|
||||||
|
vpnsp, err := r.env.Inside("VPNSP", []string{
|
||||||
|
"cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn",
|
||||||
|
"privado", "pia", "private internet access", "privatevpn", "protonvpn",
|
||||||
|
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"},
|
||||||
|
params.Default("private internet access"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if vpnsp == "pia" { // retro compatibility
|
||||||
|
vpnsp = "private internet access"
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Provider.Name = vpnsp
|
||||||
|
|
||||||
|
settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
customConfig := settings.Config != ""
|
||||||
|
|
||||||
|
if customConfig {
|
||||||
|
settings.Provider.Name = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialsRequired := !customConfig && settings.Provider.Name != constants.VPNUnlimited
|
||||||
|
|
||||||
|
settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove spaces in user ID to simplify user's life, thanks @JeordyR
|
||||||
|
settings.User = strings.ReplaceAll(settings.User, " ", "")
|
||||||
|
|
||||||
|
if settings.Provider.Name == constants.Mullvad {
|
||||||
|
settings.Password = "m"
|
||||||
|
} else {
|
||||||
|
settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", credentialsRequired, []string{"PASSWORD"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Version, err = r.env.Inside("OPENVPN_VERSION",
|
||||||
|
[]string{constants.Openvpn24, constants.Openvpn25}, params.Default(constants.Openvpn25))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Verbosity, err = r.env.IntRange("OPENVPN_VERBOSITY", 0, 6, params.Default("1")) //nolint:gomnd
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Root, err = r.env.YesNo("OPENVPN_ROOT", params.Default("yes"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Cipher, err = r.env.Get("OPENVPN_CIPHER")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Auth, err = r.env.Get("OPENVPN_AUTH")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxMSSFix = 10000
|
||||||
|
mssFix, err := r.env.IntRange("OPENVPN_MSSFIX", 0, maxMSSFix, params.Default("0"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settings.MSSFix = uint16(mssFix)
|
||||||
|
return settings.readProvider(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *OpenVPN) readProvider(r reader) error {
|
||||||
|
var readProvider func(r reader) error
|
||||||
|
switch settings.Provider.Name {
|
||||||
|
case "": // custom config
|
||||||
|
readProvider = func(r reader) error { return nil }
|
||||||
|
case constants.Cyberghost:
|
||||||
|
readProvider = settings.Provider.readCyberghost
|
||||||
|
case constants.Fastestvpn:
|
||||||
|
readProvider = settings.Provider.readFastestvpn
|
||||||
|
case constants.HideMyAss:
|
||||||
|
readProvider = settings.Provider.readHideMyAss
|
||||||
|
case constants.Ipvanish:
|
||||||
|
readProvider = settings.Provider.readIpvanish
|
||||||
|
case constants.Ivpn:
|
||||||
|
readProvider = settings.Provider.readIvpn
|
||||||
|
case constants.Mullvad:
|
||||||
|
readProvider = settings.Provider.readMullvad
|
||||||
|
case constants.Nordvpn:
|
||||||
|
readProvider = settings.Provider.readNordvpn
|
||||||
|
case constants.Privado:
|
||||||
|
readProvider = settings.Provider.readPrivado
|
||||||
|
case constants.PrivateInternetAccess:
|
||||||
|
readProvider = settings.Provider.readPrivateInternetAccess
|
||||||
|
case constants.Privatevpn:
|
||||||
|
readProvider = settings.Provider.readPrivatevpn
|
||||||
|
case constants.Protonvpn:
|
||||||
|
readProvider = settings.Provider.readProtonvpn
|
||||||
|
case constants.Purevpn:
|
||||||
|
readProvider = settings.Provider.readPurevpn
|
||||||
|
case constants.Surfshark:
|
||||||
|
readProvider = settings.Provider.readSurfshark
|
||||||
|
case constants.Torguard:
|
||||||
|
readProvider = settings.Provider.readTorguard
|
||||||
|
case constants.VPNUnlimited:
|
||||||
|
readProvider = settings.Provider.readVPNUnlimited
|
||||||
|
case constants.Vyprvpn:
|
||||||
|
readProvider = settings.Provider.readVyprvpn
|
||||||
|
case constants.Windscribe:
|
||||||
|
readProvider = settings.Provider.readWindscribe
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidVPNProvider, settings.Provider.Name)
|
||||||
|
}
|
||||||
|
return readProvider(r)
|
||||||
|
}
|
||||||
63
internal/configuration/openvpn_test.go
Normal file
63
internal/configuration/openvpn_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_OpenVPN_JSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
in := OpenVPN{
|
||||||
|
Root: true,
|
||||||
|
Provider: Provider{
|
||||||
|
Name: "name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(in, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `{
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"verbosity": 0,
|
||||||
|
"mssfix": 0,
|
||||||
|
"run_as_root": true,
|
||||||
|
"cipher": "",
|
||||||
|
"auth": "",
|
||||||
|
"provider": {
|
||||||
|
"name": "name",
|
||||||
|
"server_selection": {
|
||||||
|
"tcp": false,
|
||||||
|
"regions": null,
|
||||||
|
"group": "",
|
||||||
|
"countries": null,
|
||||||
|
"cities": null,
|
||||||
|
"hostnames": null,
|
||||||
|
"names": null,
|
||||||
|
"isps": null,
|
||||||
|
"owned": false,
|
||||||
|
"custom_port": 0,
|
||||||
|
"numbers": null,
|
||||||
|
"encryption_preset": "",
|
||||||
|
"free_only": false,
|
||||||
|
"stream_only": false
|
||||||
|
},
|
||||||
|
"extra_config": {
|
||||||
|
"encryption_preset": "",
|
||||||
|
"openvpn_ipv6": false
|
||||||
|
},
|
||||||
|
"port_forwarding": {
|
||||||
|
"enabled": false,
|
||||||
|
"filepath": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"custom_config": "",
|
||||||
|
"version": ""
|
||||||
|
}`, string(data))
|
||||||
|
var out OpenVPN
|
||||||
|
err = json.Unmarshal(data, &out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, in, out)
|
||||||
|
}
|
||||||
61
internal/configuration/privado.go
Normal file
61
internal/configuration/privado.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) privadoLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readPrivado(r reader) (err error) {
|
||||||
|
settings.Name = constants.Privado
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivadoCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PrivadoRegionChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivadoCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivadoHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
97
internal/configuration/privateinternetaccess.go
Normal file
97
internal/configuration/privateinternetaccess.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) privateinternetaccessLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Names) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Names))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, lastIndent+"Encryption preset: "+settings.ServerSelection.EncryptionPreset)
|
||||||
|
|
||||||
|
if settings.ServerSelection.CustomPort > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Custom port: "+strconv.Itoa(int(settings.ServerSelection.CustomPort)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.PortForwarding.Enabled {
|
||||||
|
lines = append(lines, lastIndent+"Port forwarding:")
|
||||||
|
for _, line := range settings.PortForwarding.lines() {
|
||||||
|
lines = append(lines, indent+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readPrivateInternetAccess(r reader) (err error) {
|
||||||
|
settings.Name = constants.PrivateInternetAccess
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionPreset, err := r.env.Inside("PIA_ENCRYPTION",
|
||||||
|
[]string{constants.PIAEncryptionPresetNone, constants.PIAEncryptionPresetNormal, constants.PIAEncryptionPresetStrong},
|
||||||
|
params.RetroKeys([]string{"ENCRYPTION"}, r.onRetroActive),
|
||||||
|
params.Default(constants.PIACertificateStrong),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settings.ServerSelection.EncryptionPreset = encryptionPreset
|
||||||
|
settings.ExtraConfigOptions.EncryptionPreset = encryptionPreset
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PIAGeoChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PIAHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_NAME", constants.PIANameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.CustomPort, err = readPortOrZero(r.env, "PORT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.PortForwarding.Enabled, err = r.env.OnOff("PORT_FORWARDING", params.Default("off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.PortForwarding.Enabled {
|
||||||
|
settings.PortForwarding.Filepath, err = r.env.Path("PORT_FORWARDING_STATUS_FILE",
|
||||||
|
params.Default("/tmp/gluetun/forwarded_port"), params.CaseSensitiveValue())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
52
internal/configuration/privatevpn.go
Normal file
52
internal/configuration/privatevpn.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) privatevpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readPrivatevpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Privatevpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivatevpnCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivatevpnCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivatevpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
85
internal/configuration/protonvpn.go
Normal file
85
internal/configuration/protonvpn.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) protonvpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Names) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Names))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.ServerSelection.FreeOnly {
|
||||||
|
lines = append(lines, lastIndent+"Free only: yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readProtonvpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Protonvpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.CustomPort, err = readPortOrZero(r.env, "PORT")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ProtonvpnCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.ProtonvpnRegionChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ProtonvpnCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.ProtonvpnNameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.ProtonvpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.FreeOnly, err = r.env.YesNo("FREE_ONLY", params.Default("no"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
125
internal/configuration/provider.go
Normal file
125
internal/configuration/provider.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider contains settings specific to a VPN provider.
|
||||||
|
type Provider struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ServerSelection ServerSelection `json:"server_selection"`
|
||||||
|
ExtraConfigOptions ExtraConfigOptions `json:"extra_config"`
|
||||||
|
PortForwarding PortForwarding `json:"port_forwarding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) lines() (lines []string) {
|
||||||
|
lines = append(lines, lastIndent+strings.Title(settings.Name)+" settings:")
|
||||||
|
|
||||||
|
selection := settings.ServerSelection
|
||||||
|
|
||||||
|
lines = append(lines, indent+lastIndent+"Network protocol: "+protoToString(selection.TCP))
|
||||||
|
|
||||||
|
if selection.TargetIP != nil {
|
||||||
|
lines = append(lines, indent+lastIndent+"Target IP address: "+selection.TargetIP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerLines []string
|
||||||
|
switch strings.ToLower(settings.Name) {
|
||||||
|
case "cyberghost":
|
||||||
|
providerLines = settings.cyberghostLines()
|
||||||
|
case "fastestvpn":
|
||||||
|
providerLines = settings.fastestvpnLines()
|
||||||
|
case "hidemyass":
|
||||||
|
providerLines = settings.hideMyAssLines()
|
||||||
|
case "ipvanish":
|
||||||
|
providerLines = settings.ipvanishLines()
|
||||||
|
case "ivpn":
|
||||||
|
providerLines = settings.ivpnLines()
|
||||||
|
case "mullvad":
|
||||||
|
providerLines = settings.mullvadLines()
|
||||||
|
case "nordvpn":
|
||||||
|
providerLines = settings.nordvpnLines()
|
||||||
|
case "privado":
|
||||||
|
providerLines = settings.privadoLines()
|
||||||
|
case "privatevpn":
|
||||||
|
providerLines = settings.privatevpnLines()
|
||||||
|
case "private internet access":
|
||||||
|
providerLines = settings.privateinternetaccessLines()
|
||||||
|
case "protonvpn":
|
||||||
|
providerLines = settings.protonvpnLines()
|
||||||
|
case "purevpn":
|
||||||
|
providerLines = settings.purevpnLines()
|
||||||
|
case "surfshark":
|
||||||
|
providerLines = settings.surfsharkLines()
|
||||||
|
case "torguard":
|
||||||
|
providerLines = settings.torguardLines()
|
||||||
|
case strings.ToLower(constants.VPNUnlimited):
|
||||||
|
providerLines = settings.vpnUnlimitedLines()
|
||||||
|
case "vyprvpn":
|
||||||
|
providerLines = settings.vyprvpnLines()
|
||||||
|
case "windscribe":
|
||||||
|
providerLines = settings.windscribeLines()
|
||||||
|
default:
|
||||||
|
panic(`Missing lines method for provider "` +
|
||||||
|
settings.Name + `"! Please create a Github issue.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range providerLines {
|
||||||
|
lines = append(lines, indent+line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func commaJoin(slice []string) string {
|
||||||
|
return strings.Join(slice, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readProtocol(env params.Env) (tcp bool, err error) {
|
||||||
|
protocol, err := env.Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, params.Default(constants.UDP))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return protocol == constants.TCP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func protoToString(tcp bool) string {
|
||||||
|
if tcp {
|
||||||
|
return constants.TCP
|
||||||
|
}
|
||||||
|
return constants.UDP
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTargetIP(env params.Env) (targetIP net.IP, err error) {
|
||||||
|
return readIP(env, "OPENVPN_TARGET_IP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCustomPort(env params.Env, tcp bool,
|
||||||
|
allowedTCP, allowedUDP []uint16) (port uint16, err error) {
|
||||||
|
port, err = readPortOrZero(env, "PORT")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if port == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcp {
|
||||||
|
for i := range allowedTCP {
|
||||||
|
if allowedTCP[i] == port {
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%w: port %d for TCP protocol", ErrInvalidPort, port)
|
||||||
|
}
|
||||||
|
for i := range allowedUDP {
|
||||||
|
if allowedUDP[i] == port {
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%w: port %d for UDP protocol", ErrInvalidPort, port)
|
||||||
|
}
|
||||||
382
internal/configuration/provider_test.go
Normal file
382
internal/configuration/provider_test.go
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/golibs/params/mock_params"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errDummy = errors.New("dummy")
|
||||||
|
|
||||||
|
func Test_Provider_lines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
settings Provider
|
||||||
|
lines []string
|
||||||
|
}{
|
||||||
|
"cyberghost": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Cyberghost,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Group: "group",
|
||||||
|
Regions: []string{"a", "El country"},
|
||||||
|
},
|
||||||
|
ExtraConfigOptions: ExtraConfigOptions{
|
||||||
|
ClientKey: "a",
|
||||||
|
ClientCertificate: "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Cyberghost settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Server group: group",
|
||||||
|
" |--Regions: a, El country",
|
||||||
|
" |--Client key is set",
|
||||||
|
" |--Client certificate is set",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fastestvpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Fastestvpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Hostnames: []string{"a", "b"},
|
||||||
|
Countries: []string{"c", "d"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Fastestvpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Hostnames: a, b",
|
||||||
|
" |--Countries: c, d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hidemyass": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.HideMyAss,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e", "f"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Hidemyass settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e, f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipvanish": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ipvanish,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e", "f"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Ipvanish settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e, f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ivpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Ivpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e", "f"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Ivpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e, f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mullvad": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Mullvad,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
ISPs: []string{"e", "f"},
|
||||||
|
CustomPort: 1,
|
||||||
|
},
|
||||||
|
ExtraConfigOptions: ExtraConfigOptions{
|
||||||
|
OpenVPNIPv6: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Mullvad settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--ISPs: e, f",
|
||||||
|
" |--Custom port: 1",
|
||||||
|
" |--IPv6: enabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nordvpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Nordvpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
Numbers: []uint16{1, 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Nordvpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
" |--Numbers: 1, 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"privado": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Privado,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Hostnames: []string{"a", "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Privado settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Hostnames: a, b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"privatevpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Privatevpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Hostnames: []string{"a", "b"},
|
||||||
|
Countries: []string{"c", "d"},
|
||||||
|
Cities: []string{"e", "f"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Privatevpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: c, d",
|
||||||
|
" |--Cities: e, f",
|
||||||
|
" |--Hostnames: a, b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"protonvpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Protonvpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Regions: []string{"c", "d"},
|
||||||
|
Cities: []string{"e", "f"},
|
||||||
|
Names: []string{"g", "h"},
|
||||||
|
Hostnames: []string{"i", "j"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Protonvpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Regions: c, d",
|
||||||
|
" |--Cities: e, f",
|
||||||
|
" |--Names: g, h",
|
||||||
|
" |--Hostnames: i, j",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"private internet access": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.PrivateInternetAccess,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
EncryptionPreset: constants.PIAEncryptionPresetStrong,
|
||||||
|
CustomPort: 1,
|
||||||
|
},
|
||||||
|
PortForwarding: PortForwarding{
|
||||||
|
Enabled: true,
|
||||||
|
Filepath: string("/here"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Private Internet Access settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
" |--Encryption preset: strong",
|
||||||
|
" |--Custom port: 1",
|
||||||
|
" |--Port forwarding:",
|
||||||
|
" |--File path: /here",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"purevpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Purevpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
Countries: []string{"c", "d"},
|
||||||
|
Cities: []string{"e", "f"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Purevpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
" |--Countries: c, d",
|
||||||
|
" |--Cities: e, f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"surfshark": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Surfshark,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Surfshark settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"torguard": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Torguard,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Torguard settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constants.VPNUnlimited: {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.VPNUnlimited,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Countries: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e", "f"},
|
||||||
|
FreeOnly: true,
|
||||||
|
StreamOnly: true,
|
||||||
|
},
|
||||||
|
ExtraConfigOptions: ExtraConfigOptions{
|
||||||
|
ClientKey: "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Vpn Unlimited settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Countries: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e, f",
|
||||||
|
" |--Free servers only",
|
||||||
|
" |--Stream servers only",
|
||||||
|
" |--Client key is set",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"vyprvpn": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Vyprvpn,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Vyprvpn settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"windscribe": {
|
||||||
|
settings: Provider{
|
||||||
|
Name: constants.Windscribe,
|
||||||
|
ServerSelection: ServerSelection{
|
||||||
|
Regions: []string{"a", "b"},
|
||||||
|
Cities: []string{"c", "d"},
|
||||||
|
Hostnames: []string{"e", "f"},
|
||||||
|
CustomPort: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lines: []string{
|
||||||
|
"|--Windscribe settings:",
|
||||||
|
" |--Network protocol: udp",
|
||||||
|
" |--Regions: a, b",
|
||||||
|
" |--Cities: c, d",
|
||||||
|
" |--Hostnames: e, f",
|
||||||
|
" |--Custom port: 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lines := testCase.settings.lines()
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.lines, lines)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_readProtocol(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
mockStr string
|
||||||
|
mockErr error
|
||||||
|
tcp bool
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"error": {
|
||||||
|
mockErr: errDummy,
|
||||||
|
err: errDummy,
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
mockStr: "tcp",
|
||||||
|
tcp: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
env := mock_params.NewMockEnv(ctrl)
|
||||||
|
env.EXPECT().
|
||||||
|
Inside("PROTOCOL", []string{"tcp", "udp"}, gomock.Any()).
|
||||||
|
Return(testCase.mockStr, testCase.mockErr)
|
||||||
|
|
||||||
|
tcp, err := readProtocol(env)
|
||||||
|
|
||||||
|
if testCase.err != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.tcp, tcp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
46
internal/configuration/publicip.go
Normal file
46
internal/configuration/publicip.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicIP struct {
|
||||||
|
Period time.Duration `json:"period"`
|
||||||
|
IPFilepath string `json:"ip_filepath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *PublicIP) String() string {
|
||||||
|
return strings.Join(settings.lines(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *PublicIP) lines() (lines []string) {
|
||||||
|
if settings.Period == 0 {
|
||||||
|
lines = append(lines, lastIndent+"Public IP getter: disabled")
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, lastIndent+"Public IP getter:")
|
||||||
|
lines = append(lines, indent+lastIndent+"Fetch period: "+settings.Period.String())
|
||||||
|
lines = append(lines, indent+lastIndent+"IP file: "+settings.IPFilepath)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *PublicIP) read(r reader) (err error) {
|
||||||
|
settings.Period, err = r.env.Duration("PUBLICIP_PERIOD", params.Default("12h"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.IPFilepath, err = r.env.Path("PUBLICIP_FILE", params.CaseSensitiveValue(),
|
||||||
|
params.Default("/tmp/gluetun/ip"),
|
||||||
|
params.RetroKeys([]string{"IP_STATUS_FILE"}, r.onRetroActive))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
61
internal/configuration/purevpn.go
Normal file
61
internal/configuration/purevpn.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (settings *Provider) purevpnLines() (lines []string) {
|
||||||
|
if len(settings.ServerSelection.Regions) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Countries) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Cities) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings.ServerSelection.Hostnames) > 0 {
|
||||||
|
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings *Provider) readPurevpn(r reader) (err error) {
|
||||||
|
settings.Name = constants.Purevpn
|
||||||
|
|
||||||
|
settings.ServerSelection.TCP, err = readProtocol(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PurevpnRegionChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PurevpnCountryChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PurevpnCityChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PurevpnHostnameChoices())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
129
internal/configuration/reader.go
Normal file
129
internal/configuration/reader.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/logging"
|
||||||
|
"github.com/qdm12/golibs/os"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
"github.com/qdm12/golibs/verification"
|
||||||
|
)
|
||||||
|
|
||||||
|
type reader struct {
|
||||||
|
env params.Env
|
||||||
|
logger logging.Logger
|
||||||
|
regex verification.Regex
|
||||||
|
os os.OS
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReader(env params.Env, os os.OS, logger logging.Logger) reader {
|
||||||
|
return reader{
|
||||||
|
env: env,
|
||||||
|
logger: logger,
|
||||||
|
regex: verification.NewRegex(),
|
||||||
|
os: os,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) onRetroActive(oldKey, newKey string) {
|
||||||
|
r.logger.Warn(
|
||||||
|
"You are using the old environment variable %s, please consider changing it to %s",
|
||||||
|
oldKey, newKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidPort = errors.New("invalid port")
|
||||||
|
)
|
||||||
|
|
||||||
|
func readCSVPorts(env params.Env, key string) (ports []uint16, err error) {
|
||||||
|
s, err := env.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(s) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
portsStr := strings.Split(s, ",")
|
||||||
|
ports = make([]uint16, len(portsStr))
|
||||||
|
for i, portStr := range portsStr {
|
||||||
|
portInt, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %q from environment variable %s: %s",
|
||||||
|
ErrInvalidPort, portStr, key, err)
|
||||||
|
} else if portInt <= 0 || portInt > 65535 {
|
||||||
|
return nil, fmt.Errorf("%w: %d from environment variable %s: must be between 1 and 65535",
|
||||||
|
ErrInvalidPort, portInt, key)
|
||||||
|
}
|
||||||
|
ports[i] = uint16(portInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidIPNet = errors.New("invalid IP network")
|
||||||
|
)
|
||||||
|
|
||||||
|
func readCSVIPNets(env params.Env, key string, options ...params.OptionSetter) (
|
||||||
|
ipNets []net.IPNet, err error) {
|
||||||
|
s, err := env.Get(key, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ipNetsStr := strings.Split(s, ",")
|
||||||
|
ipNets = make([]net.IPNet, len(ipNetsStr))
|
||||||
|
for i, ipNetStr := range ipNetsStr {
|
||||||
|
_, ipNet, err := net.ParseCIDR(ipNetStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %q from environment variable %s: %s",
|
||||||
|
ErrInvalidIPNet, ipNetStr, key, err)
|
||||||
|
} else if ipNet == nil {
|
||||||
|
return nil, fmt.Errorf("%w: %q from environment variable %s: subnet is nil",
|
||||||
|
ErrInvalidIPNet, ipNetStr, key)
|
||||||
|
}
|
||||||
|
ipNets[i] = *ipNet
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipNets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidIP = errors.New("invalid IP address")
|
||||||
|
)
|
||||||
|
|
||||||
|
func readIP(env params.Env, key string) (ip net.IP, err error) {
|
||||||
|
s, err := env.Get(key)
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = net.ParseIP(s)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPortOrZero(env params.Env, key string) (port uint16, err error) {
|
||||||
|
s, err := env.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) == 0 || s == "0" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Port(key)
|
||||||
|
}
|
||||||
109
internal/configuration/secrets.go
Normal file
109
internal/configuration/secrets.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/qdm12/golibs/os"
|
||||||
|
"github.com/qdm12/golibs/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrGetSecretFilepath = errors.New("cannot get secret file path from env")
|
||||||
|
ErrReadSecretFile = errors.New("cannot read secret file")
|
||||||
|
ErrSecretFileIsEmpty = errors.New("secret file is empty")
|
||||||
|
ErrReadNonSecretFile = errors.New("cannot read non secret file")
|
||||||
|
ErrFilesDoNotExist = errors.New("files do not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKeys []string) (value string, err error) {
|
||||||
|
envOptions := []params.OptionSetter{
|
||||||
|
params.Compulsory(), // to fallback on file reading
|
||||||
|
params.CaseSensitiveValue(),
|
||||||
|
params.Unset(),
|
||||||
|
params.RetroKeys(retroKeys, r.onRetroActive),
|
||||||
|
}
|
||||||
|
value, envErr := r.env.Get(envKey, envOptions...)
|
||||||
|
if envErr == nil {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSecretFile := "/run/secrets/" + strings.ToLower(envKey)
|
||||||
|
filepath, err := r.env.Get(envKey+"_SECRETFILE",
|
||||||
|
params.CaseSensitiveValue(),
|
||||||
|
params.Default(defaultSecretFile),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrGetSecretFilepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, fileErr := r.os.OpenFile(filepath, os.O_RDONLY, 0)
|
||||||
|
if os.IsNotExist(fileErr) {
|
||||||
|
if compulsory {
|
||||||
|
return "", envErr
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
} else if fileErr != nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, fileErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string(b)
|
||||||
|
value = strings.TrimSuffix(value, "\n")
|
||||||
|
if compulsory && len(value) == 0 {
|
||||||
|
return "", ErrSecretFileIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to read from the secret file then the non secret file.
|
||||||
|
func (r *reader) getFromFileOrSecretFile(secretName, filepath string) (
|
||||||
|
b []byte, err error) {
|
||||||
|
defaultSecretFile := "/run/secrets/" + strings.ToLower(secretName)
|
||||||
|
secretFilepath, err := r.env.Get(strings.ToUpper(secretName)+"_SECRETFILE",
|
||||||
|
params.CaseSensitiveValue(),
|
||||||
|
params.Default(defaultSecretFile),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return b, fmt.Errorf("%w: %s", ErrGetSecretFilepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = readFromFile(r.os.OpenFile, secretFilepath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return b, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
|
||||||
|
} else if err == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret file does not exist, try the non secret file
|
||||||
|
b, err = readFromFile(r.os.OpenFile, filepath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrReadSecretFile, err)
|
||||||
|
} else if err == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %s and %s", ErrFilesDoNotExist, secretFilepath, filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFromFile(openFile os.OpenFileFunc, filepath string) (b []byte, err error) {
|
||||||
|
file, err := openFile(filepath, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err = io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
63
internal/configuration/selection.go
Normal file
63
internal/configuration/selection.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerSelection struct { //nolint:maligned
|
||||||
|
// Common
|
||||||
|
TCP bool `json:"tcp"` // UDP if TCP is false
|
||||||
|
TargetIP net.IP `json:"target_ip,omitempty"`
|
||||||
|
// TODO comments
|
||||||
|
// Cyberghost, PIA, Protonvpn, Surfshark, Windscribe, Vyprvpn, NordVPN
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
|
||||||
|
// Cyberghost
|
||||||
|
Group string `json:"group"`
|
||||||
|
|
||||||
|
// Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited
|
||||||
|
Countries []string `json:"countries"`
|
||||||
|
// HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe
|
||||||
|
Cities []string `json:"cities"`
|
||||||
|
// Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited
|
||||||
|
Hostnames []string `json:"hostnames"`
|
||||||
|
Names []string `json:"names"` // Protonvpn
|
||||||
|
|
||||||
|
// Mullvad
|
||||||
|
ISPs []string `json:"isps"`
|
||||||
|
Owned bool `json:"owned"`
|
||||||
|
|
||||||
|
// Mullvad, Windscribe, PIA
|
||||||
|
CustomPort uint16 `json:"custom_port"`
|
||||||
|
|
||||||
|
// NordVPN
|
||||||
|
Numbers []uint16 `json:"numbers"`
|
||||||
|
|
||||||
|
// PIA
|
||||||
|
EncryptionPreset string `json:"encryption_preset"`
|
||||||
|
|
||||||
|
// ProtonVPN
|
||||||
|
FreeOnly bool `json:"free_only"`
|
||||||
|
|
||||||
|
// VPNUnlimited
|
||||||
|
StreamOnly bool `json:"stream_only"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtraConfigOptions struct {
|
||||||
|
ClientCertificate string `json:"-"` // Cyberghost
|
||||||
|
ClientKey string `json:"-"` // Cyberghost, VPNUnlimited
|
||||||
|
EncryptionPreset string `json:"encryption_preset"` // PIA
|
||||||
|
OpenVPNIPv6 bool `json:"openvpn_ipv6"` // Mullvad
|
||||||
|
}
|
||||||
|
|
||||||
|
// PortForwarding contains settings for port forwarding.
|
||||||
|
type PortForwarding struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Filepath string `json:"filepath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PortForwarding) lines() (lines []string) {
|
||||||
|
return []string{
|
||||||
|
lastIndent + "File path: " + p.Filepath,
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user