Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2afa988174 | ||
|
|
a35c994bc8 | ||
|
|
0fad44fb68 | ||
|
|
4f9dcff3f4 | ||
|
|
1abc90970d | ||
|
|
a445ba072c | ||
|
|
9e5624d32b | ||
|
|
815fcdb711 | ||
|
|
0bb9f62755 | ||
|
|
93567a7804 | ||
|
|
0afbb71634 | ||
|
|
9f39d47150 | ||
|
|
f9490656eb | ||
|
|
482421dda3 | ||
|
|
03f1fea123 | ||
|
|
31284542a2 | ||
|
|
5ff5fc4a5e | ||
|
|
5b93464fef | ||
|
|
debf3474e7 | ||
|
|
2853ca9033 | ||
|
|
74d059dd77 | ||
|
|
9963e18a8a | ||
|
|
41cd8fb30d | ||
|
|
9ed6cd978d | ||
|
|
c4b9d459ed | ||
|
|
6e99ca573e | ||
|
|
2cf4d6b469 | ||
|
|
a17776673b | ||
|
|
fcdba0a3cc | ||
|
|
4712d0cf79 | ||
|
|
113c113615 | ||
|
|
6023eb1878 | ||
|
|
a1ece20617 | ||
|
|
0bc67b73a8 | ||
|
|
c7ab5bd34c | ||
|
|
843bf08aa1 | ||
|
|
5b25cc95a9 | ||
|
|
0fddbc54a2 | ||
|
|
11fcfb7d19 | ||
|
|
3cd7d7edcb | ||
|
|
30609b6fe9 | ||
|
|
8a0921748b | ||
|
|
3fac02a82a | ||
|
|
f11f142bee | ||
|
|
596faef8f2 | ||
|
|
3d1b6bc861 | ||
|
|
46ad576233 | ||
|
|
46beaac34b | ||
|
|
3025476e8b | ||
|
|
cd6f9493a4 | ||
|
|
9984ad22d7 | ||
|
|
3565ba67c4 | ||
|
|
ffb0bec4da | ||
|
|
4d2b8787e0 | ||
|
|
d4831ad4a6 | ||
|
|
9e1b53a732 | ||
|
|
d0113849d6 | ||
|
|
7b25fdfee8 | ||
|
|
5ed6e82922 | ||
|
|
7dbd14df27 | ||
|
|
96d8b53338 | ||
|
|
2bd19640d9 | ||
|
|
1047508bd7 | ||
|
|
eb49306b80 | ||
|
|
43da9ddbb3 | ||
|
|
7fbc5c3c07 | ||
|
|
e03f545e07 | ||
|
|
942f1f2c0f | ||
|
|
baf566d7a5 | ||
|
|
6712adfe6b | ||
|
|
2e2e5f9df5 | ||
|
|
35e9b2365d | ||
|
|
b0b769d2c1 | ||
|
|
d3c7d3c7bc | ||
|
|
65f49ea012 | ||
|
|
5687555921 | ||
|
|
0fb75036a0 | ||
|
|
2b513dd43d | ||
|
|
687d9b4736 | ||
|
|
c70c2ef932 | ||
|
|
af3ada109b | ||
|
|
9d40564734 | ||
|
|
3734815ada | ||
|
|
b9cc5c1fdc | ||
|
|
c646ca5766 | ||
|
|
1394be5143 | ||
|
|
93442526f8 | ||
|
|
d85402050b | ||
|
|
b1c62cb525 | ||
|
|
fae64a297a | ||
|
|
6e2682a9ce | ||
|
|
555049f09c | ||
|
|
712f7c3d35 | ||
|
|
7a51c211cd | ||
|
|
c48189c1c4 | ||
|
|
9803fa1cfd | ||
|
|
cf756f561a | ||
|
|
a4021fedc3 | ||
|
|
31a36a9250 | ||
|
|
36fe349b70 | ||
|
|
3ef1cfd97c | ||
|
|
669feb45f1 | ||
|
|
85890520ab | ||
|
|
340016521e | ||
|
|
ef523df42c | ||
|
|
5306e3bab1 | ||
|
|
72a49afd2b | ||
|
|
9b8edbb81e | ||
|
|
a1554feb3f | ||
|
|
490410bf09 | ||
|
|
8c113f5268 | ||
|
|
075cbd5a0f | ||
|
|
d82df2b431 | ||
|
|
a09f8214d9 | ||
|
|
396e9c003e | ||
|
|
b0c4a28be6 | ||
|
|
85325e4a31 | ||
|
|
9933dd3ec5 | ||
|
|
13532c8b4b | ||
|
|
3926797295 | ||
|
|
febd3f784f | ||
|
|
61b053f0e1 | ||
|
|
8dae352ccc | ||
|
|
e890c50da6 | ||
|
|
ddd9f4d021 | ||
|
|
7e58b4baee | ||
|
|
a21fbb9a4f | ||
|
|
3b7d27c919 | ||
|
|
68ddbfc0fe | ||
|
|
a2047cb800 | ||
|
|
fdd499146c | ||
|
|
37900341cf | ||
|
|
36bb368cad | ||
|
|
f9bdb219d0 | ||
|
|
0374c14e42 | ||
|
|
a035a151bd | ||
|
|
e69966381d | ||
|
|
94dfb2b1f2 | ||
|
|
92011205be | ||
|
|
c9707646bd | ||
|
|
c50705736b | ||
|
|
ec284c17f4 | ||
|
|
ad6c52dc4c | ||
|
|
5f182febae | ||
|
|
86d82c1098 | ||
|
|
842b9004da | ||
|
|
6ac7ca4f0f | ||
|
|
ddfcbe1bee | ||
|
|
88fd9388e4 | ||
|
|
69aafa53c9 | ||
|
|
3473fe9c15 | ||
|
|
c655500045 | ||
|
|
96a8015af6 | ||
|
|
ddd3876f92 | ||
|
|
f1f34722ee | ||
|
|
937c667ca8 | ||
|
|
3c45f57aaa | ||
|
|
30640eefe2 | ||
|
|
8567522594 | ||
|
|
bd8214e648 | ||
|
|
a61302f135 | ||
|
|
3dfb43e117 | ||
|
|
2388e0550b | ||
|
|
a7d70dd9a3 | ||
|
|
76a4bb5dc3 | ||
|
|
3daf15a612 | ||
|
|
81ffbaf057 | ||
|
|
abe9dcbe33 | ||
|
|
3c8e80a1a4 | ||
|
|
694988b32f | ||
|
|
ea31886299 | ||
|
|
5b2923ca65 | ||
|
|
432eaa6c04 | ||
|
|
5fd0af9395 | ||
|
|
03deb9aed0 | ||
|
|
cbdd1a933c | ||
|
|
99e9bc87cf | ||
|
|
9ef14ee070 | ||
|
|
7842ff4cdc | ||
|
|
3d6d03b327 | ||
|
|
7ebbaf4351 | ||
|
|
c665b13cec | ||
|
|
970b21a6eb | ||
|
|
62747f1eb8 | ||
|
|
a2e76e1683 | ||
|
|
07651683f9 | ||
|
|
429aea8e0f | ||
|
|
01fa9934bc | ||
|
|
ff7cadb43b | ||
|
|
540acc915d | ||
|
|
703a546c1d | ||
|
|
4851bd70da | ||
|
|
a2b3d7e30c | ||
|
|
4d60b71583 | ||
|
|
3f130931d2 | ||
|
|
946f055fed | ||
|
|
eaece0cb8e | ||
|
|
4203f4fabf | ||
|
|
c39edb6378 | ||
|
|
b3cc2781ff | ||
|
|
12c411e203 | ||
|
|
3bf937d705 | ||
|
|
bc55c25e73 | ||
|
|
897a9d7f57 | ||
|
|
4a128677dd | ||
|
|
9233f3f5ba | ||
|
|
11c2354408 | ||
|
|
1f2882434a | ||
|
|
01aaf2c86a | ||
|
|
d260ac7a49 | ||
|
|
0bea0d4ecd | ||
|
|
59994bd6e7 | ||
|
|
62799d2449 |
@@ -1,5 +1,4 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
devcontainer.json
|
devcontainer.json
|
||||||
docker-compose.yml
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
README.md
|
README.md
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FROM qmcgaw/godevcontainer
|
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||||
RUN apk add wireguard-tools htop openssl
|
RUN apk add wireguard-tools htop openssl
|
||||||
|
|||||||
@@ -2,68 +2,47 @@
|
|||||||
|
|
||||||
Development container that can be used with VSCode.
|
Development container that can be used with VSCode.
|
||||||
|
|
||||||
It works on Linux, Windows and OSX.
|
It works on Linux, Windows (WSL2) and OSX.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [VS code](https://code.visualstudio.com/download) installed
|
- [VS code](https://code.visualstudio.com/download) installed
|
||||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Create the following files on your host if you don't have them:
|
1. Create the following files and directory on your host if you don't have them:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
touch ~/.gitconfig ~/.zsh_history
|
touch ~/.gitconfig ~/.zsh_history
|
||||||
|
mkdir -p ~/.ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
|
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||||
|
|
||||||
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is 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 `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
### Customize the image
|
For any customization to take effect, you should "rebuild and reopen":
|
||||||
|
|
||||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||||
|
2. Select `Dev-Containers: Rebuild Container`
|
||||||
|
|
||||||
```Dockerfile
|
Changes you can make are notably:
|
||||||
FROM qmcgaw/godevcontainer
|
|
||||||
RUN apk add curl
|
|
||||||
```
|
|
||||||
|
|
||||||
To rebuild the image, either:
|
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
||||||
|
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
||||||
|
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
||||||
|
|
||||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
```json
|
||||||
- With a terminal, go to this directory and `docker-compose build`
|
// Welcome script
|
||||||
|
{
|
||||||
### Customize VS code settings
|
"source": "/yourpath/.welcome.sh",
|
||||||
|
"target": "/root/.welcome.sh",
|
||||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
"type": "bind"
|
||||||
|
},
|
||||||
### Entrypoint script
|
|
||||||
|
|
||||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/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). You can also now do it directly with VSCode without restarting the container.
|
|
||||||
|
|
||||||
### 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"],`.
|
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||||
1. In the VS code command palette, rebuild the container.
|
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
{
|
{
|
||||||
"name": "gluetun-dev",
|
"name": "gluetun-dev",
|
||||||
"dockerComposeFile": [
|
// User defined settings
|
||||||
"docker-compose.yml"
|
"containerEnv": {
|
||||||
|
"TZ": ""
|
||||||
|
},
|
||||||
|
// Fixed settings
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "./Dockerfile"
|
||||||
|
},
|
||||||
|
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||||
|
"capAdd": [
|
||||||
|
"NET_ADMIN", // Gluetun specific
|
||||||
|
"SYS_PTRACE" // for dlv Go debugging
|
||||||
],
|
],
|
||||||
"service": "vscode",
|
"securityOpt": [
|
||||||
"runServices": [
|
"seccomp=unconfined" // for dlv Go debugging
|
||||||
"vscode"
|
],
|
||||||
|
"mounts": [
|
||||||
|
// Zsh commands history persistence
|
||||||
|
{
|
||||||
|
"source": "${localEnv:HOME}/.zsh_history",
|
||||||
|
"target": "/root/.zsh_history",
|
||||||
|
"type": "bind"
|
||||||
|
},
|
||||||
|
// Git configuration file
|
||||||
|
{
|
||||||
|
"source": "${localEnv:HOME}/.gitconfig",
|
||||||
|
"target": "/root/.gitconfig",
|
||||||
|
"type": "bind"
|
||||||
|
},
|
||||||
|
// SSH directory for Linux, OSX and WSL
|
||||||
|
// On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||||
|
// created in the container. On Windows, files are copied
|
||||||
|
// from /mnt/ssh to ~/.ssh to fix permissions.
|
||||||
|
{
|
||||||
|
"source": "${localEnv:HOME}/.ssh",
|
||||||
|
"target": "/mnt/ssh",
|
||||||
|
"type": "bind"
|
||||||
|
},
|
||||||
|
// Docker socket to access the host Docker server
|
||||||
|
{
|
||||||
|
"source": "/var/run/docker.sock",
|
||||||
|
"target": "/var/run/docker.sock",
|
||||||
|
"type": "bind"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"shutdownAction": "stopCompose",
|
|
||||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
|
||||||
"workspaceFolder": "/workspace",
|
|
||||||
// "overrideCommand": "",
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@@ -47,7 +81,11 @@
|
|||||||
},
|
},
|
||||||
"gopls": {
|
"gopls": {
|
||||||
"usePlaceholders": false,
|
"usePlaceholders": false,
|
||||||
"staticcheck": true
|
"staticcheck": true,
|
||||||
|
"ui.diagnostic.analyses": {
|
||||||
|
"ST1000": false
|
||||||
|
},
|
||||||
|
"formatting.gofumpt": true,
|
||||||
},
|
},
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
"go.lintOnSave": "package",
|
"go.lintOnSave": "package",
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
|
|
||||||
services:
|
|
||||||
vscode:
|
|
||||||
build: .
|
|
||||||
volumes:
|
|
||||||
- ../:/workspace
|
|
||||||
# Docker socket to access Docker server
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
# 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.
|
|
||||||
- ~/.ssh:/mnt/ssh
|
|
||||||
# Shell history persistence
|
|
||||||
- ~/.zsh_history:/root/.zsh_history
|
|
||||||
# Git config
|
|
||||||
- ~/.gitconfig:/root/.gitconfig
|
|
||||||
environment:
|
|
||||||
- TZ=
|
|
||||||
cap_add:
|
|
||||||
# For debugging with dlv
|
|
||||||
- SYS_PTRACE
|
|
||||||
- NET_ADMIN
|
|
||||||
security_opt:
|
|
||||||
# For debugging with dlv
|
|
||||||
- seccomp:unconfined
|
|
||||||
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
|
|
||||||
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -50,6 +50,7 @@ body:
|
|||||||
- Cyberghost
|
- Cyberghost
|
||||||
- ExpressVPN
|
- ExpressVPN
|
||||||
- FastestVPN
|
- FastestVPN
|
||||||
|
- Giganews
|
||||||
- HideMyAss
|
- HideMyAss
|
||||||
- IPVanish
|
- IPVanish
|
||||||
- IVPN
|
- IVPN
|
||||||
|
|||||||
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
One of the following is required:
|
Important notes:
|
||||||
|
|
||||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
- 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 structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||||
|
- 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's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
|
||||||
|
|||||||
14
.github/labels.yml
vendored
14
.github/labels.yml
vendored
@@ -9,6 +9,9 @@
|
|||||||
- name: "Status: 🔒 After next release"
|
- name: "Status: 🔒 After next release"
|
||||||
color: "f7d692"
|
color: "f7d692"
|
||||||
description: "Will be done after the next release"
|
description: "Will be done after the next release"
|
||||||
|
- name: "Status: 🟡 Nearly resolved"
|
||||||
|
color: "f7d692"
|
||||||
|
description: "This might be resolved or is about to be resolved"
|
||||||
|
|
||||||
- name: "Closed: ⚰️ Inactive"
|
- name: "Closed: ⚰️ Inactive"
|
||||||
color: "959a9c"
|
color: "959a9c"
|
||||||
@@ -43,6 +46,8 @@
|
|||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ Cyberghost"
|
- name: "☁️ Cyberghost"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
- name: "☁️ Giganews"
|
||||||
|
color: "cfe8d4"
|
||||||
- name: "☁️ HideMyAss"
|
- name: "☁️ HideMyAss"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ IPVanish"
|
- name: "☁️ IPVanish"
|
||||||
@@ -86,7 +91,8 @@
|
|||||||
- name: "☁️ Windscribe"
|
- name: "☁️ Windscribe"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
|
|
||||||
- name: "Category: Config problem 📝"
|
- name: "Category: User error 🤦"
|
||||||
|
from_name: "Category: Config problem 📝"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: Healthcheck 🩺"
|
- name: "Category: Healthcheck 🩺"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
@@ -138,3 +144,9 @@
|
|||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
- name: "Category: public IP service 💬"
|
- name: "Category: public IP service 💬"
|
||||||
color: "ffc7ea"
|
color: "ffc7ea"
|
||||||
|
- name: "Category: servers storage 📦"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Performance 🚀"
|
||||||
|
color: "ffc7ea"
|
||||||
|
- name: "Category: Investigation 🔍"
|
||||||
|
color: "ffc7ea"
|
||||||
|
|||||||
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 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/)
|
||||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: "1"
|
DOCKER_BUILDKIT: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
- uses: reviewdog/action-misspell@v1
|
||||||
with:
|
with:
|
||||||
@@ -59,13 +59,43 @@ jobs:
|
|||||||
- name: Run tests in test container
|
- name: Run tests in test container
|
||||||
run: |
|
run: |
|
||||||
touch coverage.txt
|
touch coverage.txt
|
||||||
docker run --rm \
|
docker run --rm --device /dev/net/tun \
|
||||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||||
test-container
|
test-container
|
||||||
|
|
||||||
- name: Build final image
|
- name: Build final image
|
||||||
run: docker build -t 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:
|
codeql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -73,15 +103,15 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "^1.22"
|
go-version-file: go.mod
|
||||||
- uses: github/codeql-action/init@v3
|
- uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
- uses: github/codeql-action/autobuild@v3
|
- uses: github/codeql-action/autobuild@v4
|
||||||
- uses: github/codeql-action/analyze@v3
|
- uses: github/codeql-action/analyze@v4
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
if: |
|
if: |
|
||||||
@@ -91,14 +121,14 @@ jobs:
|
|||||||
github.event_name == 'release' ||
|
github.event_name == 'release' ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
)
|
)
|
||||||
needs: [verify, codeql]
|
needs: [verify, verify-private, codeql]
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# extract metadata (tags, labels) for Docker
|
# extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
@@ -138,7 +168,7 @@ jobs:
|
|||||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Build and push final image
|
- name: Build and push final image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
2
.github/workflows/closed-issue.yml
vendored
2
.github/workflows/closed-issue.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/create-or-update-comment@v4
|
- uses: peter-evans/create-or-update-comment@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
|||||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,6 +8,7 @@
|
|||||||
"retryOn429": false,
|
"retryOn429": false,
|
||||||
"fallbackRetryDelay": "30s",
|
"fallbackRetryDelay": "30s",
|
||||||
"aliveStatusCodes": [
|
"aliveStatusCodes": [
|
||||||
200
|
200,
|
||||||
|
429
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
.github/workflows/labels.yml
vendored
2
.github/workflows/labels.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: crazy-max/ghaction-github-labeler@v5
|
- uses: crazy-max/ghaction-github-labeler@v5
|
||||||
with:
|
with:
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
|
|||||||
6
.github/workflows/markdown.yml
vendored
6
.github/workflows/markdown.yml
vendored
@@ -18,12 +18,12 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
- uses: DavidAnson/markdownlint-cli2-action@v21
|
||||||
with:
|
with:
|
||||||
globs: "**.md"
|
globs: "**.md"
|
||||||
config: .markdownlint.json
|
config: .markdownlint-cli2.jsonc
|
||||||
|
|
||||||
- uses: reviewdog/action-misspell@v1
|
- uses: reviewdog/action-misspell@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/opened-issue.yml
vendored
2
.github/workflows/opened-issue.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: peter-evans/create-or-update-comment@v4
|
- uses: peter-evans/create-or-update-comment@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
|||||||
127
.golangci.yml
127
.golangci.yml
@@ -1,54 +1,73 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
|
|
||||||
issues:
|
formatters:
|
||||||
exclude-rules:
|
enable:
|
||||||
- path: _test\.go
|
- gci
|
||||||
linters:
|
- gofumpt
|
||||||
- dupl
|
- goimports
|
||||||
- goerr113
|
exclusions:
|
||||||
- containedctx
|
generated: lax
|
||||||
- goconst
|
paths:
|
||||||
- maintidx
|
- third_party$
|
||||||
- path: "internal\\/server\\/.+\\.go"
|
- builtin$
|
||||||
linters:
|
- examples$
|
||||||
- dupl
|
|
||||||
- path: "internal\\/configuration\\/settings\\/.+\\.go"
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
|
||||||
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
|
|
||||||
linters:
|
|
||||||
- gomnd
|
|
||||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
|
||||||
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
|
|
||||||
linters:
|
|
||||||
- gomnd
|
|
||||||
- linters:
|
|
||||||
- lll
|
|
||||||
source: "^//go:generate .+$"
|
|
||||||
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
|
||||||
linters:
|
|
||||||
- ireturn
|
|
||||||
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
|
|
||||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
|
||||||
linters:
|
|
||||||
- ireturn
|
|
||||||
- path: "internal\\/firewall\\/.*\\.go"
|
|
||||||
text: "string `-i ` has [1-9][0-9]* occurrences, make it a constant"
|
|
||||||
linters:
|
|
||||||
- goconst
|
|
||||||
- path: "internal\\/provider\\/ipvanish\\/updater\\/servers.go"
|
|
||||||
text: "string ` in ` has 3 occurrences, make it a constant"
|
|
||||||
linters:
|
|
||||||
- goconst
|
|
||||||
- path: "internal\\/vpn\\/portforward.go"
|
|
||||||
text: 'directive `//nolint:ireturn` is unused for linter "ireturn"'
|
|
||||||
linters:
|
|
||||||
- nolintlint
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
settings:
|
||||||
|
misspell:
|
||||||
|
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:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- containedctx
|
||||||
|
- dupl
|
||||||
|
- err113
|
||||||
|
- maintidx
|
||||||
|
path: _test\.go
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
path: internal\/server\/.+\.go
|
||||||
|
- linters:
|
||||||
|
- ireturn
|
||||||
|
text: returns interface \(github\.com\/vishvananda\/netlink\.Link\)
|
||||||
|
- linters:
|
||||||
|
- ireturn
|
||||||
|
path: internal\/openvpn\/pkcs8\/descbc\.go
|
||||||
|
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
||||||
|
- linters:
|
||||||
|
- revive
|
||||||
|
path: internal\/provider\/(common|utils)\/.+\.go
|
||||||
|
text: "var-naming: avoid (bad|meaningless) package names"
|
||||||
|
- linters:
|
||||||
|
- lll
|
||||||
|
source: "^// https://.+$"
|
||||||
|
- linters:
|
||||||
|
- err113
|
||||||
|
- mnd
|
||||||
|
path: ci\/.+\.go
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
enable:
|
enable:
|
||||||
# - cyclop
|
# - cyclop
|
||||||
# - errorlint
|
# - errorlint
|
||||||
@@ -57,18 +76,18 @@ linters:
|
|||||||
- bidichk
|
- bidichk
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- containedctx
|
- containedctx
|
||||||
|
- copyloopvar
|
||||||
- decorder
|
- decorder
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- dupword
|
- dupword
|
||||||
- durationcheck
|
- durationcheck
|
||||||
|
- err113
|
||||||
- errchkjson
|
- errchkjson
|
||||||
- errname
|
- errname
|
||||||
- execinquery
|
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- fatcontext
|
||||||
- forcetypeassert
|
- forcetypeassert
|
||||||
- gci
|
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
@@ -77,10 +96,7 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godot
|
- godot
|
||||||
- goerr113
|
|
||||||
- goheader
|
- goheader
|
||||||
- goimports
|
|
||||||
- gomnd
|
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
@@ -88,12 +104,14 @@ linters:
|
|||||||
- grouper
|
- grouper
|
||||||
- importas
|
- importas
|
||||||
- interfacebloat
|
- interfacebloat
|
||||||
|
- intrange
|
||||||
- ireturn
|
- ireturn
|
||||||
- lll
|
- lll
|
||||||
- maintidx
|
- maintidx
|
||||||
- makezero
|
- makezero
|
||||||
- mirror
|
- mirror
|
||||||
- misspell
|
- misspell
|
||||||
|
- mnd
|
||||||
- musttag
|
- musttag
|
||||||
- nakedret
|
- nakedret
|
||||||
- nestif
|
- nestif
|
||||||
@@ -111,7 +129,6 @@ linters:
|
|||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- tagalign
|
- tagalign
|
||||||
- tenv
|
|
||||||
- thelper
|
- thelper
|
||||||
- tparallel
|
- tparallel
|
||||||
- unconvert
|
- unconvert
|
||||||
|
|||||||
9
.markdownlint-cli2.jsonc
Normal file
9
.markdownlint-cli2.jsonc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
},
|
||||||
|
"ignores": [
|
||||||
|
".github/pull_request_template.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"MD013": false
|
|
||||||
}
|
|
||||||
35
.vscode/launch.json
vendored
35
.vscode/launch.json
vendored
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Update a VPN provider servers data",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"program": "cmd/gluetun/main.go",
|
|
||||||
"args": [
|
|
||||||
"update",
|
|
||||||
"${input:updateMode}",
|
|
||||||
"-providers",
|
|
||||||
"${input:provider}"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "provider",
|
|
||||||
"type": "promptString",
|
|
||||||
"description": "Please enter a provider (or comma separated list of providers)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "updateMode",
|
|
||||||
"type": "pickString",
|
|
||||||
"description": "Update mode to use",
|
|
||||||
"options": [
|
|
||||||
"-maintainer",
|
|
||||||
"-enduser"
|
|
||||||
],
|
|
||||||
"default": "-maintainer"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
51
.vscode/tasks.json
vendored
Normal file
51
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
76
Dockerfile
76
Dockerfile
@@ -1,14 +1,14 @@
|
|||||||
ARG ALPINE_VERSION=3.20
|
ARG ALPINE_VERSION=3.22
|
||||||
ARG GO_ALPINE_VERSION=3.20
|
ARG GO_ALPINE_VERSION=3.22
|
||||||
ARG GO_VERSION=1.22
|
ARG GO_VERSION=1.25
|
||||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
||||||
ARG GOLANGCI_LINT_VERSION=v1.56.2
|
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
||||||
ARG MOCKGEN_VERSION=v1.6.0
|
ARG MOCKGEN_VERSION=v1.6.0
|
||||||
ARG BUILDPLATFORM=linux/amd64
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
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${GO_ALPINE_VERSION} AS base
|
||||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||||
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
|||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||||
COPY .golangci.yml ./
|
COPY .golangci.yml ./
|
||||||
RUN golangci-lint run --timeout=10m
|
RUN golangci-lint run
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||||
RUN git init && \
|
RUN git init && \
|
||||||
@@ -91,6 +91,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
OPENVPN_CIPHERS= \
|
OPENVPN_CIPHERS= \
|
||||||
OPENVPN_AUTH= \
|
OPENVPN_AUTH= \
|
||||||
OPENVPN_PROCESS_USER=root \
|
OPENVPN_PROCESS_USER=root \
|
||||||
|
OPENVPN_MSSFIX= \
|
||||||
OPENVPN_CUSTOM_CONFIG= \
|
OPENVPN_CUSTOM_CONFIG= \
|
||||||
# Wireguard
|
# Wireguard
|
||||||
WIREGUARD_ENDPOINT_IP= \
|
WIREGUARD_ENDPOINT_IP= \
|
||||||
@@ -105,7 +106,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||||
WIREGUARD_ADDRESSES= \
|
WIREGUARD_ADDRESSES= \
|
||||||
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||||
WIREGUARD_MTU=1400 \
|
WIREGUARD_MTU=1320 \
|
||||||
WIREGUARD_IMPLEMENTATION=auto \
|
WIREGUARD_IMPLEMENTATION=auto \
|
||||||
# VPN server filtering
|
# VPN server filtering
|
||||||
SERVER_REGIONS= \
|
SERVER_REGIONS= \
|
||||||
@@ -124,6 +125,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
VPN_PORT_FORWARDING_USERNAME= \
|
VPN_PORT_FORWARDING_USERNAME= \
|
||||||
VPN_PORT_FORWARDING_PASSWORD= \
|
VPN_PORT_FORWARDING_PASSWORD= \
|
||||||
|
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||||
|
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||||
# # Cyberghost only:
|
# # Cyberghost only:
|
||||||
OPENVPN_CERT= \
|
OPENVPN_CERT= \
|
||||||
OPENVPN_KEY= \
|
OPENVPN_KEY= \
|
||||||
@@ -138,15 +141,17 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
SERVER_NUMBER= \
|
SERVER_NUMBER= \
|
||||||
# # PIA only:
|
# # PIA only:
|
||||||
SERVER_NAMES= \
|
SERVER_NAMES= \
|
||||||
# # ProtonVPN only:
|
# # VPNUnlimited and ProtonVPN only:
|
||||||
|
STREAM_ONLY= \
|
||||||
FREE_ONLY= \
|
FREE_ONLY= \
|
||||||
|
# # ProtonVPN only:
|
||||||
SECURE_CORE_ONLY= \
|
SECURE_CORE_ONLY= \
|
||||||
TOR_ONLY= \
|
TOR_ONLY= \
|
||||||
# # Surfshark only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
# # VPN Secure only:
|
# # VPN Secure only:
|
||||||
PREMIUM_ONLY= \
|
PREMIUM_ONLY= \
|
||||||
# # PIA only:
|
# # PIA and ProtonVPN only:
|
||||||
PORT_FORWARD_ONLY= \
|
PORT_FORWARD_ONLY= \
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||||
@@ -158,23 +163,23 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
LOG_LEVEL=info \
|
LOG_LEVEL=info \
|
||||||
# Health
|
# Health
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
|
||||||
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
|
||||||
HEALTH_VPN_DURATION_INITIAL=6s \
|
HEALTH_SMALL_CHECK_TYPE=icmp \
|
||||||
HEALTH_VPN_DURATION_ADDITION=5s \
|
HEALTH_RESTART_VPN=on \
|
||||||
# DNS over TLS
|
# DNS
|
||||||
DOT=on \
|
DNS_SERVER=on \
|
||||||
DOT_PROVIDERS=cloudflare \
|
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||||
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 \
|
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||||
DOT_VERBOSITY=1 \
|
DNS_BLOCK_IPS= \
|
||||||
DOT_VERBOSITY_DETAILS=0 \
|
DNS_BLOCK_IP_PREFIXES= \
|
||||||
DOT_VALIDATION_LOGLEVEL=0 \
|
DNS_CACHING=on \
|
||||||
DOT_CACHING=on \
|
DNS_UPSTREAM_IPV6=off \
|
||||||
DOT_IPV6=off \
|
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
BLOCK_SURVEILLANCE=off \
|
BLOCK_SURVEILLANCE=off \
|
||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
UNBLOCK= \
|
DNS_UNBLOCK_HOSTNAMES= \
|
||||||
|
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||||
DNS_UPDATE_PERIOD=24h \
|
DNS_UPDATE_PERIOD=24h \
|
||||||
DNS_ADDRESS=127.0.0.1 \
|
DNS_ADDRESS=127.0.0.1 \
|
||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
@@ -197,15 +202,21 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Control server
|
# Control server
|
||||||
HTTP_CONTROL_SERVER_LOG=on \
|
HTTP_CONTROL_SERVER_LOG=on \
|
||||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||||
|
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||||
|
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||||
# Server data updater
|
# Server data updater
|
||||||
UPDATER_PERIOD=0 \
|
UPDATER_PERIOD=0 \
|
||||||
UPDATER_MIN_RATIO=0.8 \
|
UPDATER_MIN_RATIO=0.8 \
|
||||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||||
|
UPDATER_PROTONVPN_EMAIL= \
|
||||||
|
UPDATER_PROTONVPN_PASSWORD= \
|
||||||
# Public IP
|
# Public IP
|
||||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||||
PUBLICIP_PERIOD=12h \
|
PUBLICIP_ENABLED=on \
|
||||||
PUBLICIP_API=ipinfo \
|
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
||||||
PUBLICIP_API_TOKEN= \
|
PUBLICIP_API_TOKEN= \
|
||||||
|
# Storage
|
||||||
|
STORAGE_FILEPATH=/gluetun/servers.json \
|
||||||
# Pprof
|
# Pprof
|
||||||
PPROF_ENABLED=no \
|
PPROF_ENABLED=no \
|
||||||
PPROF_BLOCK_PROFILE_RATE=0 \
|
PPROF_BLOCK_PROFILE_RATE=0 \
|
||||||
@@ -214,8 +225,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Extras
|
# Extras
|
||||||
VERSION_INFORMATION=on \
|
VERSION_INFORMATION=on \
|
||||||
TZ= \
|
TZ= \
|
||||||
PUID= \
|
PUID=1000 \
|
||||||
PGID=
|
PGID=1000
|
||||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
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=3 CMD /gluetun-entrypoint healthcheck
|
||||||
@@ -224,10 +235,9 @@ RUN apk add --no-cache --update -l wget && \
|
|||||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
|
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 && \
|
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 unbound tzdata && \
|
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
||||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
|
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 /gluetun-entrypoint
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,6 +1,8 @@
|
|||||||
# Gluetun VPN client
|
# Gluetun VPN client
|
||||||
|
|
||||||
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
⚠️ 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-army-knife-like VPN client to multiple VPN service providers
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -26,7 +28,6 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
[](https://github.com/qdm12/gluetun/issues)
|
[](https://github.com/qdm12/gluetun/issues)
|
||||||
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
[](https://github.com/qdm12/gluetun/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
|
|
||||||
[](https://github.com/qdm12/gluetun)
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -56,8 +57,8 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
- Based on Alpine 3.22 for a small Docker image of 41.1MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **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: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- Supports Wireguard both kernelspace and userspace
|
||||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||||
@@ -73,9 +74,8 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
|||||||
- [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/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
- 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) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
- 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)
|
||||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||||
- Unbound subprogram drops root privileges once launched
|
|
||||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
@@ -89,7 +89,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
|||||||
Here's a docker-compose.yml for the laziest:
|
Here's a docker-compose.yml for the laziest:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
version: "3"
|
---
|
||||||
services:
|
services:
|
||||||
gluetun:
|
gluetun:
|
||||||
image: qmcgaw/gluetun
|
image: qmcgaw/gluetun
|
||||||
|
|||||||
35
ci/cmd/main.go
Normal file
35
ci/cmd/main.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
Normal file
36
ci/go.mod
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
Normal file
97
ci/go.sum
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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=
|
||||||
27
ci/internal/mullvad.go
Normal file
27
ci/internal/mullvad.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
25
ci/internal/protonvpn.go
Normal file
25
ci/internal/protonvpn.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
42
ci/internal/secrets.go
Normal file
42
ci/internal/secrets.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
134
ci/internal/simple.go
Normal file
134
ci/internal/simple.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -13,9 +15,9 @@ import (
|
|||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
_ "github.com/breml/rootcerts"
|
_ "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/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||||
@@ -32,7 +34,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/pprof"
|
"github.com/qdm12/gluetun/internal/pprof"
|
||||||
"github.com/qdm12/gluetun/internal/provider"
|
"github.com/qdm12/gluetun/internal/provider"
|
||||||
"github.com/qdm12/gluetun/internal/publicip"
|
"github.com/qdm12/gluetun/internal/publicip"
|
||||||
pubipapi "github.com/qdm12/gluetun/internal/publicip/api"
|
|
||||||
"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"
|
||||||
@@ -42,7 +43,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||||
"github.com/qdm12/gluetun/internal/vpn"
|
"github.com/qdm12/gluetun/internal/vpn"
|
||||||
"github.com/qdm12/golibs/command"
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gosettings/reader/sources/env"
|
"github.com/qdm12/gosettings/reader/sources/env"
|
||||||
"github.com/qdm12/goshutdown"
|
"github.com/qdm12/goshutdown"
|
||||||
@@ -51,7 +51,6 @@ import (
|
|||||||
"github.com/qdm12/goshutdown/order"
|
"github.com/qdm12/goshutdown/order"
|
||||||
"github.com/qdm12/gosplash"
|
"github.com/qdm12/gosplash"
|
||||||
"github.com/qdm12/log"
|
"github.com/qdm12/log"
|
||||||
"github.com/qdm12/updated/pkg/dnscrypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
@@ -80,7 +79,7 @@ func main() {
|
|||||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||||
netLinker := netlink.New(netLinkDebugLogger)
|
netLinker := netlink.New(netLinkDebugLogger)
|
||||||
cli := cli.New()
|
cli := cli.New()
|
||||||
cmder := command.NewCmder()
|
cmder := command.New()
|
||||||
|
|
||||||
reader := reader.New(reader.Settings{
|
reader := reader.New(reader.Settings{
|
||||||
Sources: []reader.Source{
|
Sources: []reader.Source{
|
||||||
@@ -99,11 +98,13 @@ func main() {
|
|||||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Wait for OS signal or run error
|
||||||
var err error
|
var err error
|
||||||
select {
|
select {
|
||||||
case signal := <-signalCh:
|
case receivedSignal := <-signalCh:
|
||||||
|
signal.Stop(signalCh)
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
|
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||||
cancel()
|
cancel()
|
||||||
case err = <-errorCh:
|
case err = <-errorCh:
|
||||||
close(errorCh)
|
close(errorCh)
|
||||||
@@ -114,15 +115,14 @@ func main() {
|
|||||||
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 shutdownErr := <-errorCh:
|
||||||
if !timer.Stop() {
|
timer.Stop()
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
if shutdownErr != nil {
|
if shutdownErr != nil {
|
||||||
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr)
|
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,21 +134,17 @@ func main() {
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
logger.Warn("Shutdown timed out")
|
logger.Warn("Shutdown timed out")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
case signal := <-signalCh:
|
|
||||||
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var errCommandUnknown = errors.New("command is unknown")
|
||||||
errCommandUnknown = errors.New("command is unknown")
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gocognit,gocyclo,maintidx
|
//nolint:gocognit,gocyclo,maintidx
|
||||||
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 log.LoggerInterface, reader *reader.Reader,
|
||||||
tun Tun, netLinker netLinker, cmder command.RunStarter,
|
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||||
cli clier) 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":
|
||||||
@@ -161,12 +157,16 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return cli.Update(ctx, args[2:], logger)
|
return cli.Update(ctx, args[2:], logger)
|
||||||
case "format-servers":
|
case "format-servers":
|
||||||
return cli.FormatServers(args[2:])
|
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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z")
|
defer fmt.Println(gluetunLogo)
|
||||||
|
|
||||||
|
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
Version: buildInfo.Version,
|
Version: buildInfo.Version,
|
||||||
Commit: buildInfo.Commit,
|
Commit: buildInfo.Commit,
|
||||||
Created: buildInfo.Created,
|
Created: buildInfo.Created,
|
||||||
Announcement: "Wiki moved to https://github.com/qdm12/gluetun-wiki",
|
Announcement: "All control server routes will become private by default after the v3.41.0 release",
|
||||||
AnnounceExp: announcementExp,
|
AnnounceExp: announcementExp,
|
||||||
// Sponsor information
|
// Sponsor information
|
||||||
PaypalUser: "qmcgaw",
|
PaypalUser: "qmcgaw",
|
||||||
@@ -188,7 +188,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allSettings settings.Settings
|
var allSettings settings.Settings
|
||||||
err = allSettings.Read(reader)
|
err = allSettings.Read(reader, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||||
storageLogger := logger.New(log.SetComponent("storage"))
|
storageLogger := logger.New(log.SetComponent("storage"))
|
||||||
storage, err := storage.New(storageLogger, constants.ServersData)
|
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = allSettings.Validate(storage, ipv6Supported)
|
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -269,16 +269,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
ovpnConf := openvpn.New(
|
ovpnConf := openvpn.New(
|
||||||
logger.New(log.SetComponent("openvpn configurator")),
|
logger.New(log.SetComponent("openvpn configurator")),
|
||||||
cmder, puid, pgid)
|
cmder, puid, pgid)
|
||||||
dnsCrypto := dnscrypto.New(httpClient, "", "")
|
|
||||||
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
|
|
||||||
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
|
|
||||||
"/etc/unbound", "/usr/sbin/unbound", cacertsPath)
|
|
||||||
|
|
||||||
err = printVersions(ctx, logger, []printVersionElement{
|
err = printVersions(ctx, logger, []printVersionElement{
|
||||||
{name: "Alpine", getVersion: alpineConf.Version},
|
{name: "Alpine", getVersion: alpineConf.Version},
|
||||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||||
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||||
{name: "Unbound", getVersion: dnsConf.Version},
|
|
||||||
{name: "IPtables", getVersion: firewallConf.Version},
|
{name: "IPtables", getVersion: firewallConf.Version},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,10 +286,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
logger.Warn(warning)
|
logger.Warn(warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
const permission = fs.FileMode(0o644)
|
||||||
|
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
err = os.MkdirAll("/gluetun", permission)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,15 +304,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
if nonRootUsername != defaultUsername {
|
if nonRootUsername != defaultUsername {
|
||||||
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
logger.Info("using existing username " + nonRootUsername + " corresponding to user id " + fmt.Sprint(puid))
|
||||||
}
|
}
|
||||||
// set it for Unbound
|
|
||||||
// TODO remove this when migrating to qdm12/dns v2
|
|
||||||
allSettings.DNS.DoT.Unbound.Username = nonRootUsername
|
|
||||||
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
allSettings.VPN.OpenVPN.ProcessUser = nonRootUsername
|
||||||
|
|
||||||
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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?")
|
||||||
@@ -373,7 +364,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
}
|
}
|
||||||
defaultGroupOptions := []group.Option{
|
defaultGroupOptions := []group.Option{
|
||||||
group.OptionTimeout(defaultShutdownTimeout),
|
group.OptionTimeout(defaultShutdownTimeout),
|
||||||
group.OptionOnSuccess(defaultShutdownOnSuccess)}
|
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
|
}
|
||||||
|
|
||||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||||
@@ -390,51 +382,60 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||||
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
||||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unboundLogger := logger.New(log.SetComponent("dns"))
|
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||||
unboundLogger)
|
dnsLogger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating DNS loop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||||
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
||||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
go dnsLooper.Run(dnsCtx, dnsDone)
|
||||||
otherGroupHandler.Add(dnsHandler)
|
otherGroupHandler.Add(dnsHandler)
|
||||||
|
|
||||||
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||||
controlGroupHandler.Add(dnsTickerHandler)
|
controlGroupHandler.Add(dnsTickerHandler)
|
||||||
|
|
||||||
publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API)
|
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
||||||
ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken)
|
logger.New(log.SetComponent("ip getter")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating public IP API client: %w", err)
|
return fmt.Errorf("creating public ip loop: %w", err)
|
||||||
}
|
}
|
||||||
publicIPLooper := publicip.NewLoop(ipFetcher,
|
|
||||||
logger.New(log.SetComponent("ip getter")),
|
|
||||||
allSettings.PublicIP, puid, pgid)
|
|
||||||
publicIPRunError, err := publicIPLooper.Start(ctx)
|
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting public ip loop: %w", err)
|
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"))
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
|
|
||||||
unzipper := unzip.New(httpClient)
|
unzipper := unzip.New(httpClient)
|
||||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||||
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||||
|
openvpnFileExtractor, allSettings.Updater)
|
||||||
|
|
||||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||||
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
|
||||||
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
|
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||||
buildInfo, *allSettings.Version.Enabled)
|
buildInfo, *allSettings.Version.Enabled)
|
||||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||||
"vpn", goroutine.OptionTimeout(time.Second))
|
"vpn", goroutine.OptionTimeout(time.Second))
|
||||||
@@ -468,13 +469,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||||
otherGroupHandler.Add(shadowsocksHandler)
|
otherGroupHandler.Add(shadowsocksHandler)
|
||||||
|
|
||||||
controlServerAddress := *allSettings.ControlServer.Address
|
|
||||||
controlServerLogging := *allSettings.ControlServer.Log
|
|
||||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||||
logger.New(log.SetComponent("http server")),
|
logger.New(log.SetComponent("http server")),
|
||||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper,
|
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||||
storage, ipv6Supported)
|
storage, ipv6Supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("setting up control server: %w", err)
|
return fmt.Errorf("setting up control server: %w", err)
|
||||||
@@ -484,12 +483,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
<-httpServerReady
|
<-httpServerReady
|
||||||
controlGroupHandler.Add(httpServerHandler)
|
controlGroupHandler.Add(httpServerHandler)
|
||||||
|
|
||||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
|
||||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
|
||||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
|
||||||
|
|
||||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||||
order.OptionTimeout(totalShutdownTimeout),
|
order.OptionTimeout(totalShutdownTimeout),
|
||||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
@@ -534,7 +527,8 @@ type infoer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printVersions(ctx context.Context, logger infoer,
|
func printVersions(ctx context.Context, logger infoer,
|
||||||
elements []printVersionElement) (err error) {
|
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()
|
||||||
@@ -595,9 +589,47 @@ type clier interface {
|
|||||||
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
|
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
|
||||||
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
|
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
|
||||||
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
|
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
|
||||||
|
GenKey(args []string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tun interface {
|
type Tun interface {
|
||||||
Check(tunDevice string) error
|
Check(tunDevice string) error
|
||||||
Create(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 = ` @@@
|
||||||
|
@@@@
|
||||||
|
@@@@@@
|
||||||
|
@@@@.@@ @@@@@@@@@@
|
||||||
|
@@@@.@@@ @@@@@@@@==@@@@
|
||||||
|
@@@.@..@@ @@@@@@@=@..==@@@@
|
||||||
|
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
||||||
|
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
||||||
|
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
||||||
|
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
||||||
|
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
||||||
|
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
||||||
|
@@@@.@--@@@@@@@@@@================@@@@@@@
|
||||||
|
@@@..--@@*@@@@@@================@@@@+*@@
|
||||||
|
@@@.---@@.@@@@=================@@@@--@@
|
||||||
|
@@@-.---@@@@@@================@@@@*--@@@
|
||||||
|
@@@.:-#@@@@@@===============*@@@@.---@@
|
||||||
|
@@@.-------.@@@============@@@@@@.--@@@
|
||||||
|
@@@..--------:@@@=========@@@@@@@@.--@@@
|
||||||
|
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
||||||
|
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
||||||
|
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
||||||
|
@@@@@@=============@@@@@ @@@...------------.*@@@
|
||||||
|
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
||||||
|
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
||||||
|
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
||||||
|
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
||||||
|
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
||||||
|
@@@@@@@@ @@@@ @@@@
|
||||||
|
`
|
||||||
|
|||||||
67
go.mod
67
go.mod
@@ -1,56 +1,69 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.22
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.2.17
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/fatih/color v1.17.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.17.8
|
github.com/klauspost/compress v1.18.1
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/qdm12/dns v1.11.0
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88
|
||||||
github.com/qdm12/gosettings v0.4.2
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.0
|
github.com/qdm12/gosplash v0.2.0
|
||||||
github.com/qdm12/gotree v0.2.0
|
github.com/qdm12/gotree v0.3.0
|
||||||
github.com/qdm12/log v0.1.0
|
github.com/qdm12/log v0.1.0
|
||||||
github.com/qdm12/ss-server v0.6.0
|
github.com/qdm12/ss-server v0.6.0
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/ulikunitz/xz v0.5.11
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.org/x/sys v0.20.0
|
golang.org/x/net v0.47.0
|
||||||
golang.org/x/text v0.15.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 v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/miekg/dns v1.1.40 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // 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/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/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 // indirect
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
271
go.sum
271
go.sum
@@ -1,69 +1,45 @@
|
|||||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
|
||||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
|
||||||
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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
|
||||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
github.com/google/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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
@@ -73,136 +49,123 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 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/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||||
github.com/qdm12/gosettings v0.4.2 h1:Gb39NScPr7OQV+oy0o1OD7A121udITDJuUGa7ljDF58=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/qdm12/gosettings v0.4.2/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88 h1:GJ5FALvJ3UmHjVaNYebrfV5zF5You4dq8HfRWZy2loM=
|
||||||
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20251123213823-54e987293e88/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||||
|
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 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
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 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||||
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
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 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
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 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
|
||||||
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
|
||||||
github.com/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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
|
||||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
|
||||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
|
||||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
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=
|
||||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
|
|
||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
|
|
||||||
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/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
|
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/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-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/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
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/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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/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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
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.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-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-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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=
|
||||||
@@ -212,26 +175,18 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
|
|||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
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 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
||||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69/go.mod h1:Tk5Ip2TuxaWGpccL7//rAsLRH6RQ/jfqTGxuN/+i/FQ=
|
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs=
|
|
||||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package alpine
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
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 (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||||
@@ -39,7 +38,8 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
|||||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ 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", "")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,13 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFormatNotRecognized = errors.New("format is not recognized")
|
|
||||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||||
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
||||||
)
|
)
|
||||||
|
|
||||||
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
||||||
provider string, titleCaser cases.Caser) {
|
provider string, titleCaser cases.Caser,
|
||||||
|
) {
|
||||||
boolPtr, ok := providerToFormat[provider]
|
boolPtr, ok := providerToFormat[provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
||||||
@@ -43,7 +44,7 @@ func (c *CLI) FormatServers(args []string) error {
|
|||||||
providersToFormat[provider] = new(bool)
|
providersToFormat[provider] = new(bool)
|
||||||
}
|
}
|
||||||
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
||||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
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")
|
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||||
titleCaser := cases.Title(language.English)
|
titleCaser := cases.Title(language.English)
|
||||||
for _, provider := range allProviderFlags {
|
for _, provider := range allProviderFlags {
|
||||||
@@ -53,9 +54,7 @@ func (c *CLI) FormatServers(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if format != "markdown" {
|
// Note the format is validated by storage.Format
|
||||||
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify only one provider is set to be formatted.
|
// Verify only one provider is set to be formatted.
|
||||||
var providers []string
|
var providers []string
|
||||||
@@ -87,10 +86,14 @@ func (c *CLI) FormatServers(args []string) error {
|
|||||||
return fmt.Errorf("creating servers storage: %w", err)
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := storage.FormatToMarkdown(providerToFormat)
|
formatted, err := storage.Format(providerToFormat, format)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("formatting servers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
output = filepath.Clean(output)
|
output = filepath.Clean(output)
|
||||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
|
const permission = fs.FileMode(0o644)
|
||||||
|
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("opening output file: %w", err)
|
return fmt.Errorf("opening output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
66
internal/cli/genkey.go
Normal file
66
internal/cli/genkey.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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])
|
||||||
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import "github.com/qdm12/golibs/logging"
|
|
||||||
|
|
||||||
type noopLogger struct{}
|
type noopLogger struct{}
|
||||||
|
|
||||||
func newNoopLogger() *noopLogger {
|
func newNoopLogger() *noopLogger {
|
||||||
return new(noopLogger)
|
return new(noopLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *noopLogger) Debug(string) {}
|
func (l *noopLogger) Info(string) {}
|
||||||
func (l *noopLogger) Info(string) {}
|
func (l *noopLogger) Warn(string) {}
|
||||||
func (l *noopLogger) Warn(string) {}
|
|
||||||
func (l *noopLogger) Error(string) {}
|
|
||||||
func (l *noopLogger) PatchLevel(logging.Level) {}
|
|
||||||
func (l *noopLogger) PatchPrefix(string) {}
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type ParallelResolver interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IPFetcher interface {
|
type IPFetcher interface {
|
||||||
|
String() string
|
||||||
|
CanFetchAnyIP() bool
|
||||||
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,24 +44,26 @@ type IPv6Checker interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||||
ipv6Checker IPv6Checker) error {
|
ipv6Checker IPv6Checker,
|
||||||
|
) error {
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
storage, err := storage.New(logger, constants.ServersData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var allSettings settings.Settings
|
var allSettings settings.Settings
|
||||||
err = allSettings.Read(reader)
|
err = allSettings.Read(reader, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
allSettings.SetDefaults()
|
||||||
|
|
||||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = allSettings.Validate(storage, ipv6Supported); err != nil {
|
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||||
return fmt.Errorf("validating settings: %w", err)
|
return fmt.Errorf("validating settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
|||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||||
connection, err := providerConf.GetConnection(
|
connection, err := providerConf.GetConnection(
|
||||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||||
|
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||||
|
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdaterLogger interface {
|
type UpdaterLogger interface {
|
||||||
@@ -35,7 +38,7 @@ type UpdaterLogger interface {
|
|||||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||||
options := settings.Updater{}
|
options := settings.Updater{}
|
||||||
var endUserMode, maintainerMode, updateAll bool
|
var endUserMode, maintainerMode, updateAll bool
|
||||||
var csvProviders, ipToken string
|
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(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||||
@@ -47,6 +50,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||||
|
flagSet.StringVar(&protonUsername, "proton-username", "",
|
||||||
|
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||||
|
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||||
|
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -64,6 +71,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
options.Providers = strings.Split(csvProviders, ",")
|
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])
|
options.SetDefaults(options.Providers[0])
|
||||||
|
|
||||||
err := options.Validate()
|
err := options.Validate()
|
||||||
@@ -71,7 +88,11 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
return fmt.Errorf("options validation failed: %w", err)
|
return fmt.Errorf("options validation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
storage, err := storage.New(logger, constants.ServersData)
|
serversDataPath := constants.ServersData
|
||||||
|
if maintainerMode {
|
||||||
|
serversDataPath = ""
|
||||||
|
}
|
||||||
|
storage, err := storage.New(logger, serversDataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating servers storage: %w", err)
|
return fmt.Errorf("creating servers storage: %w", err)
|
||||||
}
|
}
|
||||||
@@ -80,14 +101,21 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
unzipper := unzip.New(httpClient)
|
unzipper := unzip.New(httpClient)
|
||||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||||
ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken)
|
nameTokenPairs := []api.NameToken{
|
||||||
if err != nil {
|
{Name: string(api.IPInfo), Token: ipToken},
|
||||||
return fmt.Errorf("creating public IP API client: %w", err)
|
{Name: string(api.IP2Location)},
|
||||||
|
{Name: string(api.IfConfigCo)},
|
||||||
}
|
}
|
||||||
|
fetchers, err := api.New(nameTokenPairs, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||||
|
}
|
||||||
|
ipFetcher := api.NewResilient(fetchers, logger)
|
||||||
|
|
||||||
openvpnFileExtractor := extract.New()
|
openvpnFileExtractor := extract.New()
|
||||||
|
|
||||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||||
|
|
||||||
updater := updater.New(httpClient, storage, providers, logger)
|
updater := updater.New(httpClient, storage, providers, logger)
|
||||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||||
|
|||||||
8
internal/command/cmder.go
Normal file
8
internal/command/cmder.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
// Cmder handles running subprograms synchronously and asynchronously.
|
||||||
|
type Cmder struct{}
|
||||||
|
|
||||||
|
func New() *Cmder {
|
||||||
|
return &Cmder{}
|
||||||
|
}
|
||||||
11
internal/command/interfaces_local.go
Normal file
11
internal/command/interfaces_local.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type execCmd interface {
|
||||||
|
CombinedOutput() ([]byte, error)
|
||||||
|
StdoutPipe() (io.ReadCloser, error)
|
||||||
|
StderrPipe() (io.ReadCloser, error)
|
||||||
|
Start() error
|
||||||
|
Wait() error
|
||||||
|
}
|
||||||
3
internal/command/mocks_generate_test.go
Normal file
3
internal/command/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
||||||
108
internal/command/mocks_local_test.go
Normal file
108
internal/command/mocks_local_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// 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))
|
||||||
|
}
|
||||||
30
internal/command/run.go
Normal file
30
internal/command/run.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
54
internal/command/run_test.go
Normal file
54
internal/command/run_test.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
150
internal/command/split.go
Normal file
150
internal/command/split.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
110
internal/command/split_test.go
Normal file
110
internal/command/split_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
100
internal/command/start.go
Normal file
100
internal/command/start.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/command/start_test.go
Normal file
118
internal/command/start_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
internal/configuration/settings/deprecated.go
Normal file
27
internal/configuration/settings/deprecated.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||||
|
keyToMessage := map[string]string{
|
||||||
|
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||||
|
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||||
|
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||||
|
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
||||||
|
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||||
|
}
|
||||||
|
sortedKeys := maps.Keys(keyToMessage)
|
||||||
|
slices.Sort(sortedKeys)
|
||||||
|
warnings = make([]string, 0, len(keyToMessage))
|
||||||
|
for _, key := range sortedKeys {
|
||||||
|
if r.Get(key) != nil {
|
||||||
|
warnings = append(warnings, keyToMessage[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -11,10 +15,31 @@ import (
|
|||||||
|
|
||||||
// DNS contains settings to configure DNS.
|
// DNS contains settings to configure DNS.
|
||||||
type DNS struct {
|
type DNS struct {
|
||||||
|
// ServerEnabled is true if the server should be running
|
||||||
|
// and used. It defaults to true, and cannot be nil
|
||||||
|
// in the internal state.
|
||||||
|
ServerEnabled *bool
|
||||||
|
// UpstreamType can be dot or plain, and defaults to dot.
|
||||||
|
UpstreamType string `json:"upstream_type"`
|
||||||
|
// UpdatePeriod is the period to update DNS block lists.
|
||||||
|
// It can be set to 0 to disable the update.
|
||||||
|
// It defaults to 24h and cannot be nil in
|
||||||
|
// the internal state.
|
||||||
|
UpdatePeriod *time.Duration
|
||||||
|
// Providers is a list of DNS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// Blacklist contains settings to configure the filter
|
||||||
|
// block lists.
|
||||||
|
Blacklist DNSBlacklist
|
||||||
// ServerAddress is the DNS server to use inside
|
// ServerAddress is the DNS server to use inside
|
||||||
// the Go program and for the system.
|
// the Go program and for the system.
|
||||||
// It defaults to '127.0.0.1' to be used with the
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
// DoT server. It cannot be the zero value in the internal
|
// local server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress netip.Addr
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the existing DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
@@ -23,20 +48,40 @@ type DNS struct {
|
|||||||
// outside the VPN tunnel since it would go through
|
// outside the VPN tunnel since it would go through
|
||||||
// the local DNS server of your Docker/Kubernetes
|
// the local DNS server of your Docker/Kubernetes
|
||||||
// configuration, which is likely not going through the tunnel.
|
// configuration, which is likely not going through the tunnel.
|
||||||
// This will also disable the DNS over TLS server and the
|
// This will also disable the DNS forwarder server and the
|
||||||
// `ServerAddress` field will be ignored.
|
// `ServerAddress` field will be ignored.
|
||||||
// It defaults to false and cannot be nil in the
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
KeepNameserver *bool
|
||||||
// DOT contains settings to configure the DoT
|
|
||||||
// server.
|
|
||||||
DoT DoT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||||
|
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||||
|
)
|
||||||
|
|
||||||
func (d DNS) validate() (err error) {
|
func (d DNS) validate() (err error) {
|
||||||
err = d.DoT.validate()
|
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
||||||
|
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const minUpdatePeriod = 30 * time.Second
|
||||||
|
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||||
|
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||||
|
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validating DoT settings: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -44,9 +89,15 @@ func (d DNS) validate() (err error) {
|
|||||||
|
|
||||||
func (d *DNS) Copy() (copied DNS) {
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
return DNS{
|
return DNS{
|
||||||
|
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
||||||
|
UpstreamType: d.UpstreamType,
|
||||||
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
ServerAddress: d.ServerAddress,
|
ServerAddress: d.ServerAddress,
|
||||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
DoT: d.DoT.copy(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +105,48 @@ func (d *DNS) Copy() (copied DNS) {
|
|||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (d *DNS) overrideWith(other DNS) {
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
|
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
||||||
|
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||||
|
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||||
|
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||||
|
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||||
|
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||||
|
d.Blacklist.overrideWith(other.Blacklist)
|
||||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
d.DoT.overrideWith(other.DoT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) setDefaults() {
|
func (d *DNS) setDefaults() {
|
||||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
|
||||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
|
||||||
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
|
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||||
|
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||||
|
provider.Cloudflare().Name,
|
||||||
|
})
|
||||||
|
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||||
|
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||||
|
d.Blacklist.setDefaults()
|
||||||
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
|
||||||
|
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
|
||||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
d.DoT.setDefaults()
|
}
|
||||||
|
|
||||||
|
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
|
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
||||||
|
return d.ServerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
provider, err := providers.Get(d.Providers[0])
|
||||||
|
if err != nil {
|
||||||
|
// Settings should be validated before calling this function,
|
||||||
|
// so an error happening here is a programming error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.Plain.IPv4[0].Addr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DNS) String() string {
|
func (d DNS) String() string {
|
||||||
@@ -77,11 +160,63 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||||
node.AppendNode(d.DoT.toLinesNode())
|
|
||||||
|
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
|
||||||
|
if !*d.ServerEnabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||||
|
|
||||||
|
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||||
|
for _, provider := range d.Providers {
|
||||||
|
upstreamResolvers.Append(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||||
|
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||||
|
|
||||||
|
update := "disabled"
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
|
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||||
|
|
||||||
|
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,10 +227,5 @@ func (d *DNS) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.DoT.read(r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/blacklist"
|
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -21,6 +22,9 @@ type DNSBlacklist struct {
|
|||||||
AddBlockedHosts []string
|
AddBlockedHosts []string
|
||||||
AddBlockedIPs []netip.Addr
|
AddBlockedIPs []netip.Addr
|
||||||
AddBlockedIPPrefixes []netip.Prefix
|
AddBlockedIPPrefixes []netip.Prefix
|
||||||
|
// RebindingProtectionExemptHostnames is a list of hostnames
|
||||||
|
// exempt from DNS rebinding protection.
|
||||||
|
RebindingProtectionExemptHostnames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DNSBlacklist) setDefaults() {
|
func (b *DNSBlacklist) setDefaults() {
|
||||||
@@ -32,8 +36,9 @@ func (b *DNSBlacklist) setDefaults() {
|
|||||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||||
|
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b DNSBlacklist) validate() (err error) {
|
func (b DNSBlacklist) validate() (err error) {
|
||||||
@@ -49,18 +54,25 @@ func (b DNSBlacklist) validate() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||||
|
if !hostRegex.MatchString(host) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||||
return DNSBlacklist{
|
return DNSBlacklist{
|
||||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||||
|
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,18 +84,23 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
|||||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||||
|
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
||||||
|
other.RebindingProtectionExemptHostnames)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
|
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||||
return blacklist.BuilderSettings{
|
settings blockbuilder.Settings,
|
||||||
BlockMalicious: *b.BlockMalicious,
|
) {
|
||||||
BlockAds: *b.BlockAds,
|
return blockbuilder.Settings{
|
||||||
BlockSurveillance: *b.BlockSurveillance,
|
Client: client,
|
||||||
|
BlockMalicious: b.BlockMalicious,
|
||||||
|
BlockAds: b.BlockAds,
|
||||||
|
BlockSurveillance: b.BlockSurveillance,
|
||||||
AllowedHosts: b.AllowedHosts,
|
AllowedHosts: b.AllowedHosts,
|
||||||
AddBlockedHosts: b.AddBlockedHosts,
|
AddBlockedHosts: b.AddBlockedHosts,
|
||||||
AddBlockedIPs: netipAddressesToNetaddrIPs(b.AddBlockedIPs),
|
AddBlockedIPs: b.AddBlockedIPs,
|
||||||
AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes),
|
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b DNSBlacklist) String() string {
|
func (b DNSBlacklist) String() string {
|
||||||
@@ -98,30 +115,37 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
||||||
|
|
||||||
if len(b.AllowedHosts) > 0 {
|
if len(b.AllowedHosts) > 0 {
|
||||||
allowedHostsNode := node.Appendf("Allowed hosts:")
|
allowedHostsNode := node.Append("Allowed hosts:")
|
||||||
for _, host := range b.AllowedHosts {
|
for _, host := range b.AllowedHosts {
|
||||||
allowedHostsNode.Appendf(host)
|
allowedHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedHosts) > 0 {
|
if len(b.AddBlockedHosts) > 0 {
|
||||||
blockedHostsNode := node.Appendf("Blocked hosts:")
|
blockedHostsNode := node.Append("Blocked hosts:")
|
||||||
for _, host := range b.AddBlockedHosts {
|
for _, host := range b.AddBlockedHosts {
|
||||||
blockedHostsNode.Appendf(host)
|
blockedHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedIPs) > 0 {
|
if len(b.AddBlockedIPs) > 0 {
|
||||||
blockedIPsNode := node.Appendf("Blocked IP addresses:")
|
blockedIPsNode := node.Append("Blocked IP addresses:")
|
||||||
for _, ip := range b.AddBlockedIPs {
|
for _, ip := range b.AddBlockedIPs {
|
||||||
blockedIPsNode.Appendf(ip.String())
|
blockedIPsNode.Append(ip.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.AddBlockedIPPrefixes) > 0 {
|
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||||
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
|
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
|
||||||
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||||
blockedIPPrefixesNode.Appendf(ipNetwork.String())
|
blockedIPPrefixesNode.Append(ipNetwork.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
||||||
|
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
||||||
|
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||||
|
exemptHostsNode.Append(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,24 +169,47 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||||
|
|
||||||
|
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||||
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
ipPrefixes []netip.Prefix, err error,
|
||||||
)
|
) {
|
||||||
|
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
// TODO v4 remove this block below
|
||||||
ipPrefixes []netip.Prefix, err error) {
|
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ips = append(ips, privateIPs...)
|
||||||
|
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||||
|
|
||||||
|
return ips, ipPrefixes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
|
||||||
|
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||||
|
ipPrefixes []netip.Prefix, err error,
|
||||||
|
) {
|
||||||
|
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||||
if len(privateAddresses) == 0 {
|
if len(privateAddresses) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gosettings"
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DoT contains settings to configure the DoT server.
|
|
||||||
type DoT struct {
|
|
||||||
// Enabled is true if the DoT server should be running
|
|
||||||
// and used. It defaults to true, and cannot be nil
|
|
||||||
// in the internal state.
|
|
||||||
Enabled *bool
|
|
||||||
// UpdatePeriod is the period to update DNS block
|
|
||||||
// lists and cryptographic files for DNSSEC validation.
|
|
||||||
// It can be set to 0 to disable the update.
|
|
||||||
// It defaults to 24h and cannot be nil in
|
|
||||||
// the internal state.
|
|
||||||
UpdatePeriod *time.Duration
|
|
||||||
// Unbound contains settings to configure Unbound.
|
|
||||||
Unbound Unbound
|
|
||||||
// Blacklist contains settings to configure the filter
|
|
||||||
// block lists.
|
|
||||||
Blacklist DNSBlacklist
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d DoT) validate() (err error) {
|
|
||||||
const minUpdatePeriod = 30 * time.Second
|
|
||||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
|
||||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Unbound.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) copy() (copied DoT) {
|
|
||||||
return DoT{
|
|
||||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
|
||||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
|
||||||
Unbound: d.Unbound.copy(),
|
|
||||||
Blacklist: d.Blacklist.copy(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
|
||||||
// settings object with any field set in the other
|
|
||||||
// settings.
|
|
||||||
func (d *DoT) overrideWith(other DoT) {
|
|
||||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
|
||||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
|
||||||
d.Unbound.overrideWith(other.Unbound)
|
|
||||||
d.Blacklist.overrideWith(other.Blacklist)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) setDefaults() {
|
|
||||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
|
||||||
const defaultUpdatePeriod = 24 * time.Hour
|
|
||||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
|
||||||
d.Unbound.setDefaults()
|
|
||||||
d.Blacklist.setDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) String() string {
|
|
||||||
return d.toLinesNode().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
|
||||||
node = gotree.New("DNS over TLS settings:")
|
|
||||||
|
|
||||||
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
|
||||||
if !*d.Enabled {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
update := "disabled" //nolint:goconst
|
|
||||||
if *d.UpdatePeriod > 0 {
|
|
||||||
update = "every " + d.UpdatePeriod.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Update period: %s", update)
|
|
||||||
|
|
||||||
node.AppendNode(d.Unbound.toLinesNode())
|
|
||||||
node.AppendNode(d.Blacklist.toLinesNode())
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) read(reader *reader.Reader) (err error) {
|
|
||||||
d.Enabled, err = reader.BoolPtr("DOT")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Unbound.read(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.read(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -30,13 +30,14 @@ var (
|
|||||||
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||||
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
|
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
|
||||||
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
|
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
|
||||||
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
|
|
||||||
ErrRegionNotValid = errors.New("the region specified is not valid")
|
ErrRegionNotValid = errors.New("the region specified is not valid")
|
||||||
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||||
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||||
|
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||||
|
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
|
||||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
|
|||||||
if len(f.OutboundSubnets) > 0 {
|
if len(f.OutboundSubnets) > 0 {
|
||||||
outboundSubnets := node.Appendf("Outbound subnets:")
|
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||||
for _, subnet := range f.OutboundSubnets {
|
for _, subnet := range f.OutboundSubnets {
|
||||||
subnet := subnet
|
|
||||||
outboundSubnets.Appendf("%s", &subnet)
|
outboundSubnets.Appendf("%s", &subnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ func Test_Firewall_validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
@@ -17,34 +18,51 @@ type Health struct {
|
|||||||
// for the health check server.
|
// for the health check server.
|
||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
// ReadHeaderTimeout is the HTTP server header read timeout
|
// TargetAddresses are the addresses (host or host:port)
|
||||||
// duration of the HTTP server. It defaults to 100 milliseconds.
|
// to TCP TLS dial to periodically for the health check.
|
||||||
ReadHeaderTimeout time.Duration
|
// Addresses after the first one are used as fallbacks for retries.
|
||||||
// ReadTimeout is the HTTP read timeout duration of the
|
// It cannot be empty in the internal state.
|
||||||
// HTTP server. It defaults to 500 milliseconds.
|
TargetAddresses []string
|
||||||
ReadTimeout time.Duration
|
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
|
||||||
// TargetAddress is the address (host or host:port)
|
// in the health checker. The slice can be set to a single
|
||||||
// to TCP dial to periodically for the health check.
|
// unspecified address (0.0.0.0) such that the VPN server IP is used,
|
||||||
// It cannot be the empty string in the internal state.
|
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
|
||||||
TargetAddress string
|
// and cannot be left empty in the internal state.
|
||||||
// SuccessWait is the duration to wait to re-run the
|
ICMPTargetIPs []netip.Addr
|
||||||
// healthcheck after a successful healthcheck.
|
// SmallCheckType is the type of small health check to perform.
|
||||||
// It defaults to 5 seconds and cannot be zero in
|
// It can be "icmp" or "dns", and defaults to "icmp".
|
||||||
// the internal state.
|
// Note it changes automatically to dns if icmp is not supported.
|
||||||
SuccessWait time.Duration
|
SmallCheckType string
|
||||||
// VPN has health settings specific to the VPN loop.
|
// RestartVPN indicates whether to restart the VPN connection
|
||||||
VPN HealthyWait
|
// when the healthcheck fails.
|
||||||
|
RestartVPN *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
|
||||||
|
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
|
||||||
|
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
|
||||||
|
)
|
||||||
|
|
||||||
func (h Health) Validate() (err error) {
|
func (h Health) Validate() (err error) {
|
||||||
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server listening address is not valid: %w", err)
|
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.VPN.validate()
|
for _, ip := range h.ICMPTargetIPs {
|
||||||
|
switch {
|
||||||
|
case !ip.IsValid():
|
||||||
|
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
|
||||||
|
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
|
||||||
|
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
|
||||||
|
ErrICMPTargetIPsNotCompatible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("health VPN settings: %w", err)
|
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -52,12 +70,11 @@ func (h Health) Validate() (err error) {
|
|||||||
|
|
||||||
func (h *Health) copy() (copied Health) {
|
func (h *Health) copy() (copied Health) {
|
||||||
return Health{
|
return Health{
|
||||||
ServerAddress: h.ServerAddress,
|
ServerAddress: h.ServerAddress,
|
||||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
TargetAddresses: h.TargetAddresses,
|
||||||
ReadTimeout: h.ReadTimeout,
|
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs),
|
||||||
TargetAddress: h.TargetAddress,
|
SmallCheckType: h.SmallCheckType,
|
||||||
SuccessWait: h.SuccessWait,
|
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||||
VPN: h.VPN.copy(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,23 +83,21 @@ func (h *Health) copy() (copied Health) {
|
|||||||
// settings.
|
// settings.
|
||||||
func (h *Health) OverrideWith(other Health) {
|
func (h *Health) OverrideWith(other Health) {
|
||||||
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||||
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses)
|
||||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs)
|
||||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
|
||||||
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||||
h.VPN.overrideWith(other.VPN)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) SetDefaults() {
|
func (h *Health) SetDefaults() {
|
||||||
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||||
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"})
|
||||||
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{
|
||||||
const defaultReadTimeout = 500 * time.Millisecond
|
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
||||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
||||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
})
|
||||||
const defaultSuccessWait = 5 * time.Second
|
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
|
||||||
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
|
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||||
h.VPN.setDefaults()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Health) String() string {
|
func (h Health) String() string {
|
||||||
@@ -92,28 +107,40 @@ func (h Health) String() string {
|
|||||||
func (h Health) toLinesNode() (node *gotree.Node) {
|
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Health settings:")
|
node = gotree.New("Health settings:")
|
||||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||||
node.Appendf("Target address: %s", h.TargetAddress)
|
targetAddrs := node.Appendf("Target addresses:")
|
||||||
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
for _, targetAddr := range h.TargetAddresses {
|
||||||
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
targetAddrs.Append(targetAddr)
|
||||||
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
}
|
||||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
switch h.SmallCheckType {
|
||||||
|
case "icmp":
|
||||||
|
icmpNode := node.Appendf("Small health check type: ICMP echo request")
|
||||||
|
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
|
||||||
|
icmpNode.Appendf("ICMP target IP: VPN server IP address")
|
||||||
|
} else {
|
||||||
|
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
|
||||||
|
for _, ip := range h.ICMPTargetIPs {
|
||||||
|
icmpIPs.Append(ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "dns":
|
||||||
|
node.Appendf("Small health check type: Plain DNS lookup over UDP")
|
||||||
|
}
|
||||||
|
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Health) Read(r *reader.Reader) (err error) {
|
func (h *Health) Read(r *reader.Reader) (err error) {
|
||||||
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||||
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
|
||||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
|
||||||
|
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
|
||||||
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
|
||||||
err = h.VPN.read(r)
|
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("VPN health settings: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gosettings"
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthyWait struct {
|
|
||||||
// Initial is the initial duration to wait for the program
|
|
||||||
// to be healthy before taking action.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
Initial *time.Duration
|
|
||||||
// Addition is the duration to add to the Initial duration
|
|
||||||
// after Initial has expired to wait longer for the program
|
|
||||||
// to be healthy.
|
|
||||||
// It cannot be nil in the internal state.
|
|
||||||
Addition *time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) validate() (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthyWait) copy() (copied HealthyWait) {
|
|
||||||
return HealthyWait{
|
|
||||||
Initial: gosettings.CopyPointer(h.Initial),
|
|
||||||
Addition: gosettings.CopyPointer(h.Addition),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
|
||||||
// settings object with any field set in the other
|
|
||||||
// settings.
|
|
||||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
|
||||||
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
|
|
||||||
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthyWait) setDefaults() {
|
|
||||||
const initialDurationDefault = 6 * time.Second
|
|
||||||
const additionDurationDefault = 5 * time.Second
|
|
||||||
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
|
|
||||||
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) String() string {
|
|
||||||
return h.toLinesNode("Health").String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
|
||||||
node = gotree.New(kind + " wait durations:")
|
|
||||||
node.Appendf("Initial duration: %s", *h.Initial)
|
|
||||||
node.Appendf("Additional duration: %s", *h.Addition)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthyWait) read(r *reader.Reader) (err error) {
|
|
||||||
h.Initial, err = r.DurationPtr(
|
|
||||||
"HEALTH_VPN_DURATION_INITIAL",
|
|
||||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Addition, err = r.DurationPtr(
|
|
||||||
"HEALTH_VPN_DURATION_ADDITION",
|
|
||||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,30 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
func boolPtr(b bool) *bool { return &b }
|
import gomock "github.com/golang/mock/gomock"
|
||||||
func uint8Ptr(n uint8) *uint8 { return &n }
|
|
||||||
|
type sourceKeyValue struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
|
||||||
|
source := NewMockSource(ctrl)
|
||||||
|
var previousCall *gomock.Call
|
||||||
|
for _, keyValue := range keyValues {
|
||||||
|
transformedKey := keyValue.key
|
||||||
|
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
|
||||||
|
if previousCall != nil {
|
||||||
|
keyTransformCall.After(previousCall)
|
||||||
|
}
|
||||||
|
isSet := keyValue.value != ""
|
||||||
|
previousCall = source.EXPECT().Get(transformedKey).
|
||||||
|
Return(keyValue.value, isSet).After(keyTransformCall)
|
||||||
|
if isSet {
|
||||||
|
previousCall = source.EXPECT().KeyTransform(keyValue.key).
|
||||||
|
Return(transformedKey).After(previousCall)
|
||||||
|
previousCall = source.EXPECT().String().
|
||||||
|
Return("mock source").After(previousCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|||||||
5
internal/configuration/settings/interfaces.go
Normal file
5
internal/configuration/settings/interfaces.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
type Warner interface {
|
||||||
|
Warn(message string)
|
||||||
|
}
|
||||||
4
internal/configuration/settings/mocks_generate_test.go
Normal file
4
internal/configuration/settings/mocks_generate_test.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
|
||||||
|
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source
|
||||||
77
internal/configuration/settings/mocks_reader_test.go
Normal file
77
internal/configuration/settings/mocks_reader_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
|
||||||
|
|
||||||
|
// Package settings is a generated GoMock package.
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockSource is a mock of Source interface.
|
||||||
|
type MockSource struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockSourceMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockSourceMockRecorder is the mock recorder for MockSource.
|
||||||
|
type MockSourceMockRecorder struct {
|
||||||
|
mock *MockSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockSource creates a new mock instance.
|
||||||
|
func NewMockSource(ctrl *gomock.Controller) *MockSource {
|
||||||
|
mock := &MockSource{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockSourceMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mocks base method.
|
||||||
|
func (m *MockSource) Get(arg0 string) (string, bool) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Get", arg0)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
ret1, _ := ret[1].(bool)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get indicates an expected call of Get.
|
||||||
|
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyTransform mocks base method.
|
||||||
|
func (m *MockSource) KeyTransform(arg0 string) string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "KeyTransform", arg0)
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyTransform indicates an expected call of KeyTransform.
|
||||||
|
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String mocks base method.
|
||||||
|
func (m *MockSource) String() string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "String")
|
||||||
|
ret0, _ := ret[0].(string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String indicates an expected call of String.
|
||||||
|
func (mr *MockSourceMockRecorder) String() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
|
||||||
|
}
|
||||||
46
internal/configuration/settings/mocks_test.go
Normal file
46
internal/configuration/settings/mocks_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
|
||||||
|
|
||||||
|
// Package settings is a generated GoMock package.
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockWarner is a mock of Warner interface.
|
||||||
|
type MockWarner struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockWarnerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||||
|
type MockWarnerMockRecorder struct {
|
||||||
|
mock *MockWarner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockWarner creates a new mock instance.
|
||||||
|
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||||
|
mock := &MockWarner{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn mocks base method.
|
||||||
|
func (m *MockWarner) Warn(arg0 string) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "Warn", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn indicates an expected call of Warn.
|
||||||
|
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"inet.af/netaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func netipAddressToNetaddrIP(address netip.Addr) (ip netaddr.IP) {
|
|
||||||
if address.Is4() {
|
|
||||||
return netaddr.IPFrom4(address.As4())
|
|
||||||
}
|
|
||||||
return netaddr.IPFrom16(address.As16())
|
|
||||||
}
|
|
||||||
|
|
||||||
func netipAddressesToNetaddrIPs(addresses []netip.Addr) (ips []netaddr.IP) {
|
|
||||||
ips = make([]netaddr.IP, len(addresses))
|
|
||||||
for i := range addresses {
|
|
||||||
ips[i] = netipAddressToNetaddrIP(addresses[i])
|
|
||||||
}
|
|
||||||
return ips
|
|
||||||
}
|
|
||||||
|
|
||||||
func netipPrefixToNetaddrIPPrefix(prefix netip.Prefix) (ipPrefix netaddr.IPPrefix) {
|
|
||||||
netaddrIP := netipAddressToNetaddrIP(prefix.Addr())
|
|
||||||
bits := prefix.Bits()
|
|
||||||
return netaddr.IPPrefixFrom(netaddrIP, uint8(bits))
|
|
||||||
}
|
|
||||||
|
|
||||||
func netipPrefixesToNetaddrIPPrefixes(prefixes []netip.Prefix) (ipPrefixes []netaddr.IPPrefix) {
|
|
||||||
ipPrefixes = make([]netaddr.IPPrefix, len(prefixes))
|
|
||||||
for i := range ipPrefixes {
|
|
||||||
ipPrefixes[i] = netipPrefixToNetaddrIPPrefix(prefixes[i])
|
|
||||||
}
|
|
||||||
return ipPrefixes
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@ package settings
|
|||||||
// and SERVER_REGIONS is now the continent field for servers.
|
// and SERVER_REGIONS is now the continent field for servers.
|
||||||
// TODO v4 remove.
|
// TODO v4 remove.
|
||||||
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
|
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
|
||||||
updatedSelection ServerSelection) {
|
updatedSelection ServerSelection,
|
||||||
|
) {
|
||||||
validRegionsMap := stringSliceToMap(validRegions)
|
validRegionsMap := stringSliceToMap(validRegions)
|
||||||
validCountriesMap := stringSliceToMap(validCountries)
|
validCountriesMap := stringSliceToMap(validCountries)
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateOpenVPNConfigFilepath(isCustom bool,
|
func validateOpenVPNConfigFilepath(isCustom bool,
|
||||||
confFile string) (err error) {
|
confFile string,
|
||||||
|
) (err error) {
|
||||||
if !isCustom {
|
if !isCustom {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -179,7 +180,8 @@ func validateOpenVPNConfigFilepath(isCustom bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateOpenVPNClientCertificate(vpnProvider,
|
func validateOpenVPNClientCertificate(vpnProvider,
|
||||||
clientCert string) (err error) {
|
clientCert string,
|
||||||
|
) (err error) {
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case
|
case
|
||||||
providers.Airvpn,
|
providers.Airvpn,
|
||||||
@@ -226,7 +228,8 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateOpenVPNEncryptedKey(vpnProvider,
|
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||||
encryptedPrivateKey string) (err error) {
|
encryptedPrivateKey string,
|
||||||
|
) (err error) {
|
||||||
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||||
return fmt.Errorf("%w", ErrMissingValue)
|
return fmt.Errorf("%w", ErrMissingValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ func Test_ivpnAccountID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(testCase.s, func(t *testing.T) {
|
t.Run(testCase.s, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
@@ -24,6 +25,12 @@ type OpenVPNSelection struct {
|
|||||||
// and can be udp or tcp. It cannot be the empty string
|
// and can be udp or tcp. It cannot be the empty string
|
||||||
// in the internal state.
|
// in the internal state.
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
|
// EndpointIP is the server endpoint IP address.
|
||||||
|
// If set, it overrides any IP address from the picked
|
||||||
|
// built-in server connection. To indicate it should
|
||||||
|
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||||
|
// It can never be the zero value in the internal state.
|
||||||
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// CustomPort is the OpenVPN server endpoint port.
|
// CustomPort is the OpenVPN server endpoint port.
|
||||||
// It can be set to 0 to indicate no custom port should
|
// It can be set to 0 to indicate no custom port should
|
||||||
// be used. It cannot be nil in the internal state.
|
// be used. It cannot be nil in the internal state.
|
||||||
@@ -50,6 +57,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
|
|
||||||
// Validate TCP
|
// Validate TCP
|
||||||
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
||||||
|
providers.Giganews,
|
||||||
providers.Ipvanish,
|
providers.Ipvanish,
|
||||||
providers.Perfectprivacy,
|
providers.Perfectprivacy,
|
||||||
providers.Privado,
|
providers.Privado,
|
||||||
@@ -67,7 +75,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
providers.Privatevpn, providers.Torguard:
|
providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Ipvanish, providers.Nordvpn,
|
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||||
providers.Privado, providers.Purevpn,
|
providers.Privado, providers.Purevpn,
|
||||||
providers.Surfshark, providers.VPNSecure,
|
providers.Surfshark, providers.VPNSecure,
|
||||||
providers.VPNUnlimited, providers.Vyprvpn:
|
providers.VPNUnlimited, providers.Vyprvpn:
|
||||||
@@ -141,6 +149,7 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
|||||||
return OpenVPNSelection{
|
return OpenVPNSelection{
|
||||||
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||||
Protocol: o.Protocol,
|
Protocol: o.Protocol,
|
||||||
|
EndpointIP: o.EndpointIP,
|
||||||
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||||
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||||
}
|
}
|
||||||
@@ -150,12 +159,14 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
|||||||
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||||
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||||
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||||
|
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
|
||||||
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||||
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||||
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||||
|
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
|
||||||
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||||
|
|
||||||
var defaultEncPreset string
|
var defaultEncPreset string
|
||||||
@@ -173,6 +184,10 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node = gotree.New("OpenVPN server selection settings:")
|
node = gotree.New("OpenVPN server selection settings:")
|
||||||
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||||
|
|
||||||
|
if !o.EndpointIP.IsUnspecified() {
|
||||||
|
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
|
||||||
|
}
|
||||||
|
|
||||||
if *o.CustomPort != 0 {
|
if *o.CustomPort != 0 {
|
||||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||||
}
|
}
|
||||||
@@ -192,6 +207,9 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
|||||||
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
||||||
|
|
||||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||||
|
|
||||||
|
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||||
|
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ type PortForwarding struct {
|
|||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
Filepath *string `json:"status_file_path"`
|
Filepath *string `json:"status_file_path"`
|
||||||
|
// UpCommand is the command to use when the port forwarding is up.
|
||||||
|
// It can be the empty string to indicate not to run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
UpCommand *string `json:"up_command"`
|
||||||
|
// DownCommand is the command to use after the port forwarding goes down.
|
||||||
|
// It can be the empty string to indicate to NOT run a command.
|
||||||
|
// It cannot be nil in the internal state.
|
||||||
|
DownCommand *string `json:"down_command"`
|
||||||
// ListeningPort is the port traffic would be redirected to from the
|
// ListeningPort is the port traffic would be redirected to from the
|
||||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||||
// is its default as well.
|
// is its default as well.
|
||||||
@@ -52,6 +60,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
|||||||
validProviders := []string{
|
validProviders := []string{
|
||||||
providers.Perfectprivacy,
|
providers.Perfectprivacy,
|
||||||
providers.PrivateInternetAccess,
|
providers.PrivateInternetAccess,
|
||||||
|
providers.Privatevpn,
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
}
|
}
|
||||||
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
||||||
@@ -83,6 +92,8 @@ func (p *PortForwarding) Copy() (copied PortForwarding) {
|
|||||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
Provider: gosettings.CopyPointer(p.Provider),
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
|
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||||
|
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||||
Username: p.Username,
|
Username: p.Username,
|
||||||
Password: p.Password,
|
Password: p.Password,
|
||||||
@@ -93,6 +104,8 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
|||||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
|
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||||
|
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||||
@@ -102,6 +115,8 @@ func (p *PortForwarding) setDefaults() {
|
|||||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
|
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||||
|
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +149,13 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
node.Appendf("Forwarded port file path: %s", filepath)
|
node.Appendf("Forwarded port file path: %s", filepath)
|
||||||
|
|
||||||
|
if *p.UpCommand != "" {
|
||||||
|
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
||||||
|
}
|
||||||
|
if *p.DownCommand != "" {
|
||||||
|
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
||||||
|
}
|
||||||
|
|
||||||
if p.Username != "" {
|
if p.Username != "" {
|
||||||
credentialsNode := node.Appendf("Credentials:")
|
credentialsNode := node.Appendf("Credentials:")
|
||||||
credentialsNode.Appendf("Username: %s", p.Username)
|
credentialsNode.Appendf("Username: %s", p.Username)
|
||||||
@@ -162,6 +184,12 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
|||||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
|
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||||
|
reader.ForceLowercase(false))
|
||||||
|
|
||||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
settings := PortForwarding{
|
settings := PortForwarding{
|
||||||
Enabled: boolPtr(false),
|
Enabled: ptrTo(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
s := settings.String()
|
s := settings.String()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Provider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||||
// Validate Name
|
// Validate Name
|
||||||
var validNames []string
|
var validNames []string
|
||||||
if vpnType == vpn.OpenVPN {
|
if vpnType == vpn.OpenVPN {
|
||||||
@@ -48,7 +48,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
|||||||
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.ServerSelection.validate(p.Name, storage)
|
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server selection: %w", err)
|
return fmt.Errorf("server selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/publicip/api"
|
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
@@ -13,24 +12,28 @@ import (
|
|||||||
|
|
||||||
// PublicIP contains settings for port forwarding.
|
// PublicIP contains settings for port forwarding.
|
||||||
type PublicIP struct {
|
type PublicIP struct {
|
||||||
// Period is the period to get the public IP address.
|
// Enabled is set to true to fetch the public ip address
|
||||||
// It can be set to 0 to disable periodic checking.
|
// information on VPN connection. It defaults to true.
|
||||||
// It cannot be nil for the internal state.
|
Enabled *bool
|
||||||
// TODO change to value and add enabled field
|
|
||||||
Period *time.Duration
|
|
||||||
// IPFilepath is the public IP address status file path
|
// IPFilepath is the public IP address status file path
|
||||||
// to use. It can be the empty string to indicate not
|
// to use. It can be the empty string to indicate not
|
||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
// internal state
|
// internal state
|
||||||
IPFilepath *string
|
IPFilepath *string
|
||||||
// API is the API name to use to fetch public IP information.
|
// APIs is the list of public ip APIs to use to fetch public IP information.
|
||||||
// It can be ipinfo or ip2location. It defaults to ipinfo.
|
// If there is more than one API, the first one is used
|
||||||
API string
|
// by default and the others are used as fallbacks in case of
|
||||||
// APIToken is the token to use for the IP data service
|
// the service rate limiting us. It defaults to use all services,
|
||||||
// such as ipinfo.io. It can be the empty string to
|
// with the first one being ipinfo.io for historical reasons.
|
||||||
// indicate not to use a token. It cannot be nil for the
|
APIs []PublicIPAPI
|
||||||
// internal state.
|
}
|
||||||
APIToken *string
|
|
||||||
|
type PublicIPAPI struct {
|
||||||
|
// Name is the name of the public ip API service.
|
||||||
|
// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
|
||||||
|
Name string
|
||||||
|
// Token is the token to use for the public ip API service.
|
||||||
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWith deep copies the receiving settings, overrides the copy with
|
// UpdateWith deep copies the receiving settings, overrides the copy with
|
||||||
@@ -48,12 +51,6 @@ func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) validate() (err error) {
|
func (p PublicIP) validate() (err error) {
|
||||||
const minPeriod = 5 * time.Second
|
|
||||||
if *p.Period < minPeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be at least %s",
|
|
||||||
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *p.IPFilepath != "" { // optional
|
if *p.IPFilepath != "" { // optional
|
||||||
_, err := filepath.Abs(*p.IPFilepath)
|
_, err := filepath.Abs(*p.IPFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,9 +58,11 @@ func (p PublicIP) validate() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = api.ParseProvider(p.API)
|
for _, publicIPAPI := range p.APIs {
|
||||||
if err != nil {
|
_, err = api.ParseProvider(publicIPAPI.Name)
|
||||||
return fmt.Errorf("API name: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("API name: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -71,26 +70,27 @@ func (p PublicIP) validate() (err error) {
|
|||||||
|
|
||||||
func (p *PublicIP) copy() (copied PublicIP) {
|
func (p *PublicIP) copy() (copied PublicIP) {
|
||||||
return PublicIP{
|
return PublicIP{
|
||||||
Period: gosettings.CopyPointer(p.Period),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
||||||
API: p.API,
|
APIs: gosettings.CopySlice(p.APIs),
|
||||||
APIToken: gosettings.CopyPointer(p.APIToken),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||||
p.Period = gosettings.OverrideWithPointer(p.Period, other.Period)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
||||||
p.API = gosettings.OverrideWithComparable(p.API, other.API)
|
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
|
||||||
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicIP) setDefaults() {
|
func (p *PublicIP) setDefaults() {
|
||||||
const defaultPeriod = 12 * time.Hour
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
|
||||||
p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod)
|
|
||||||
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
|
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
|
||||||
p.API = gosettings.DefaultComparable(p.API, "ipinfo")
|
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
|
||||||
p.APIToken = gosettings.DefaultPointer(p.APIToken, "")
|
{Name: string(api.IPInfo)},
|
||||||
|
{Name: string(api.Cloudflare)},
|
||||||
|
{Name: string(api.IfConfigCo)},
|
||||||
|
{Name: string(api.IP2Location)},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) String() string {
|
func (p PublicIP) String() string {
|
||||||
@@ -98,41 +98,78 @@ func (p PublicIP) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||||
|
if !*p.Enabled {
|
||||||
|
return gotree.New("Public IP settings: disabled")
|
||||||
|
}
|
||||||
|
|
||||||
node = gotree.New("Public IP settings:")
|
node = gotree.New("Public IP settings:")
|
||||||
|
|
||||||
if *p.Period == 0 {
|
|
||||||
node.Appendf("Enabled: no")
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePeriod := "disabled"
|
|
||||||
if *p.Period > 0 {
|
|
||||||
updatePeriod = "every " + p.Period.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Fetching: %s", updatePeriod)
|
|
||||||
|
|
||||||
if *p.IPFilepath != "" {
|
if *p.IPFilepath != "" {
|
||||||
node.Appendf("IP file path: %s", *p.IPFilepath)
|
node.Appendf("IP file path: %s", *p.IPFilepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Appendf("Public IP data API: %s", p.API)
|
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
|
||||||
|
if p.APIs[0].Token != "" {
|
||||||
if *p.APIToken != "" {
|
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
|
||||||
node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken))
|
}
|
||||||
|
node.Append(baseAPIString)
|
||||||
|
if len(p.APIs) > 1 {
|
||||||
|
backupAPIsNode := node.Append("Public IP data backup APIs:")
|
||||||
|
for i := 1; i < len(p.APIs); i++ {
|
||||||
|
message := p.APIs[i].Name
|
||||||
|
if p.APIs[i].Token != "" {
|
||||||
|
message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
|
||||||
|
}
|
||||||
|
backupAPIsNode.Append(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicIP) read(r *reader.Reader) (err error) {
|
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
|
||||||
p.Period, err = r.DurationPtr("PUBLICIP_PERIOD")
|
p.Enabled, err = readPublicIPEnabled(r, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.IPFilepath = r.Get("PUBLICIP_FILE",
|
p.IPFilepath = r.Get("PUBLICIP_FILE",
|
||||||
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
|
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
|
||||||
p.API = r.String("PUBLICIP_API")
|
|
||||||
p.APIToken = r.Get("PUBLICIP_API_TOKEN")
|
apiNames := r.CSV("PUBLICIP_API")
|
||||||
|
if len(apiNames) > 0 {
|
||||||
|
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
|
||||||
|
p.APIs = make([]PublicIPAPI, len(apiNames))
|
||||||
|
for i := range apiNames {
|
||||||
|
p.APIs[i].Name = apiNames[i]
|
||||||
|
var token string
|
||||||
|
if i < len(apiTokens) { // only set token if it exists
|
||||||
|
token = apiTokens[i]
|
||||||
|
}
|
||||||
|
p.APIs[i].Token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
|
||||||
|
enabled *bool, err error,
|
||||||
|
) {
|
||||||
|
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if periodPtr == nil {
|
||||||
|
return r.BoolPtr("PUBLICIP_ENABLED")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *periodPtr == 0 {
|
||||||
|
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
|
||||||
|
return ptrTo(false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
|
||||||
|
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
|
||||||
|
"Please migrate to use PUBLICIP_ENABLED only in the future.")
|
||||||
|
return ptrTo(true), nil
|
||||||
|
}
|
||||||
|
|||||||
161
internal/configuration/settings/publicip_test.go
Normal file
161
internal/configuration/settings/publicip_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_PublicIP_read(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
makeReader func(ctrl *gomock.Controller) *reader.Reader
|
||||||
|
makeWarner func(ctrl *gomock.Controller) Warner
|
||||||
|
settings PublicIP
|
||||||
|
errWrapped error
|
||||||
|
errMessage string
|
||||||
|
}{
|
||||||
|
"nothing_read": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single_api_no_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single_api_with_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_no_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo"},
|
||||||
|
{Name: "ip2location"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_with_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz,abc"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
{Name: "ip2location", Token: "abc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple_apis_with_and_without_token": {
|
||||||
|
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||||
|
source := newMockSource(ctrl, []sourceKeyValue{
|
||||||
|
{key: "PUBLICIP_PERIOD"},
|
||||||
|
{key: "PUBLICIP_ENABLED"},
|
||||||
|
{key: "IP_STATUS_FILE"},
|
||||||
|
{key: "PUBLICIP_FILE"},
|
||||||
|
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||||
|
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||||
|
})
|
||||||
|
return reader.New(reader.Settings{
|
||||||
|
Sources: []reader.Source{source},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
settings: PublicIP{
|
||||||
|
APIs: []PublicIPAPI{
|
||||||
|
{Name: "ipinfo", Token: "xyz"},
|
||||||
|
{Name: "ip2location"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
reader := testCase.makeReader(ctrl)
|
||||||
|
var warner Warner
|
||||||
|
if testCase.makeWarner != nil {
|
||||||
|
warner = testCase.makeWarner(ctrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings PublicIP
|
||||||
|
err := settings.read(reader, warner)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.settings, settings)
|
||||||
|
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||||
|
if testCase.errWrapped != nil {
|
||||||
|
assert.EqualError(t, err, testCase.errMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -19,6 +22,14 @@ type ControlServer struct {
|
|||||||
// Log can be true or false to enable logging on requests.
|
// Log can be true or false to enable logging on requests.
|
||||||
// It cannot be nil in the internal state.
|
// It cannot be nil in the internal state.
|
||||||
Log *bool
|
Log *bool
|
||||||
|
// AuthFilePath is the path to the file containing the authentication
|
||||||
|
// configuration for the middleware.
|
||||||
|
// It cannot be empty in the internal state and defaults to
|
||||||
|
// /gluetun/auth/config.toml.
|
||||||
|
AuthFilePath string
|
||||||
|
// AuthDefaultRole is a JSON encoded object defining the default role
|
||||||
|
// that applies to all routes without a previously user-defined role assigned to.
|
||||||
|
AuthDefaultRole string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) validate() (err error) {
|
func (c ControlServer) validate() (err error) {
|
||||||
@@ -39,13 +50,30 @@ func (c ControlServer) validate() (err error) {
|
|||||||
ErrControlServerPrivilegedPort, port, uid)
|
ErrControlServerPrivilegedPort, port, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
||||||
|
jsonDecoder.DisallowUnknownFields()
|
||||||
|
var role auth.Role
|
||||||
|
err = jsonDecoder.Decode(&role)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if role.Auth != "" {
|
||||||
|
err = role.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("default authentication role is not valid: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlServer) copy() (copied ControlServer) {
|
func (c *ControlServer) copy() (copied ControlServer) {
|
||||||
return ControlServer{
|
return ControlServer{
|
||||||
Address: gosettings.CopyPointer(c.Address),
|
Address: gosettings.CopyPointer(c.Address),
|
||||||
Log: gosettings.CopyPointer(c.Log),
|
Log: gosettings.CopyPointer(c.Log),
|
||||||
|
AuthFilePath: c.AuthFilePath,
|
||||||
|
AuthDefaultRole: c.AuthDefaultRole,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +83,22 @@ func (c *ControlServer) copy() (copied ControlServer) {
|
|||||||
func (c *ControlServer) overrideWith(other ControlServer) {
|
func (c *ControlServer) overrideWith(other ControlServer) {
|
||||||
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||||
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||||
|
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||||
|
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlServer) setDefaults() {
|
func (c *ControlServer) setDefaults() {
|
||||||
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||||
c.Log = gosettings.DefaultPointer(c.Log, true)
|
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||||
|
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||||
|
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
||||||
|
if c.AuthDefaultRole != "{}" {
|
||||||
|
var role auth.Role
|
||||||
|
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||||
|
role.Name = "default"
|
||||||
|
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
||||||
|
c.AuthDefaultRole = string(roleBytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ControlServer) String() string {
|
func (c ControlServer) String() string {
|
||||||
@@ -70,6 +109,12 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
|||||||
node = gotree.New("Control server settings:")
|
node = gotree.New("Control server settings:")
|
||||||
node.Appendf("Listening address: %s", *c.Address)
|
node.Appendf("Listening address: %s", *c.Address)
|
||||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||||
|
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||||
|
if c.AuthDefaultRole != "{}" {
|
||||||
|
var role auth.Role
|
||||||
|
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||||
|
node.AppendNode(role.ToLinesNode())
|
||||||
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +123,11 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||||
|
|
||||||
|
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||||
|
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package settings
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
@@ -17,17 +16,11 @@ import (
|
|||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerSelection struct { //nolint:maligned
|
type ServerSelection struct {
|
||||||
// VPN is the VPN type which can be 'openvpn'
|
// VPN is the VPN type which can be 'openvpn'
|
||||||
// or 'wireguard'. It cannot be the empty string
|
// or 'wireguard'. It cannot be the empty string
|
||||||
// in the internal state.
|
// in the internal state.
|
||||||
VPN string `json:"vpn"`
|
VPN string `json:"vpn"`
|
||||||
// TargetIP is the server endpoint IP address to use.
|
|
||||||
// It will override any IP address from the picked
|
|
||||||
// built-in server. It cannot be the empty value in the internal
|
|
||||||
// state, and can be set to the unspecified address to indicate
|
|
||||||
// there is not target IP address to use.
|
|
||||||
TargetIP netip.Addr `json:"target_ip"`
|
|
||||||
// Countries is the list of countries to filter VPN servers with.
|
// Countries is the list of countries to filter VPN servers with.
|
||||||
Countries []string `json:"countries"`
|
Countries []string `json:"countries"`
|
||||||
// Categories is the list of categories to filter VPN servers with.
|
// Categories is the list of categories to filter VPN servers with.
|
||||||
@@ -91,14 +84,15 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||||
storage Storage) (err error) {
|
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||||
|
) (err error) {
|
||||||
switch ss.VPN {
|
switch ss.VPN {
|
||||||
case vpn.OpenVPN, vpn.Wireguard:
|
case vpn.OpenVPN, vpn.Wireguard:
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
|
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err // already wrapped error
|
return err // already wrapped error
|
||||||
}
|
}
|
||||||
@@ -111,7 +105,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
|||||||
*ss = surfsharkRetroRegion(*ss)
|
*ss = surfsharkRetroRegion(*ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider)
|
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||||
}
|
}
|
||||||
@@ -142,19 +136,20 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getLocationFilterChoices(vpnServiceProvider string,
|
func getLocationFilterChoices(vpnServiceProvider string,
|
||||||
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
|
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
|
||||||
err error) {
|
filterChoices models.FilterChoices, err error,
|
||||||
filterChoices = storage.GetFilterChoices(vpnServiceProvider)
|
) {
|
||||||
|
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
|
||||||
|
|
||||||
if vpnServiceProvider == providers.Surfshark {
|
if vpnServiceProvider == providers.Surfshark {
|
||||||
// // Retro compatibility
|
// // Retro compatibility
|
||||||
// TODO v4 remove
|
// TODO v4 remove
|
||||||
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
|
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
|
||||||
err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions)
|
err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Only return error comparing with newer regions, we don't want to confuse the user
|
// Only return error comparing with newer regions, we don't want to confuse the user
|
||||||
// with the retro regions in the error message.
|
// with the retro regions in the error message.
|
||||||
err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions)
|
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
|
||||||
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,44 +160,53 @@ func getLocationFilterChoices(vpnServiceProvider string,
|
|||||||
// validateServerFilters validates filters against the choices given as arguments.
|
// validateServerFilters validates filters against the choices given as arguments.
|
||||||
// Set an argument to nil to pass the check for a particular filter.
|
// Set an argument to nil to pass the check for a particular filter.
|
||||||
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
||||||
vpnServiceProvider string) (err error) {
|
vpnServiceProvider string, warner Warner,
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries)
|
) (err error) {
|
||||||
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if vpnServiceProvider == providers.Custom && len(settings.Names) == 1 {
|
if vpnServiceProvider == providers.Custom {
|
||||||
// Allow a single name to be specified for the custom provider in case
|
switch len(settings.Names) {
|
||||||
// the user wants to use VPN server side port forwarding with PIA
|
case 0:
|
||||||
// which requires a server name for TLS verification.
|
case 1:
|
||||||
filterChoices.Names = settings.Names
|
// Allow a single name to be specified for the custom provider in case
|
||||||
|
// the user wants to use VPN server side port forwarding with PIA
|
||||||
|
// which requires a server name for TLS verification.
|
||||||
|
filterChoices.Names = settings.Names
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %d names specified instead of "+
|
||||||
|
"0 or 1 for the custom provider",
|
||||||
|
ErrNameNotValid, len(settings.Names))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
|
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
|
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
|
||||||
}
|
}
|
||||||
@@ -210,6 +214,43 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func atLeastOneIsOneOfCaseInsensitive(values, choices []string,
|
||||||
|
warner Warner,
|
||||||
|
) (err error) {
|
||||||
|
if len(values) > 0 && len(choices) == 0 {
|
||||||
|
return fmt.Errorf("%w", validate.ErrNoChoice)
|
||||||
|
}
|
||||||
|
|
||||||
|
set := make(map[string]struct{}, len(choices))
|
||||||
|
for _, choice := range choices {
|
||||||
|
lowercaseChoice := strings.ToLower(choice)
|
||||||
|
set[lowercaseChoice] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidValues := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
lowercaseValue := strings.ToLower(value)
|
||||||
|
_, ok := set[lowercaseValue]
|
||||||
|
if ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
invalidValues = append(invalidValues, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(invalidValues) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case len(values):
|
||||||
|
return fmt.Errorf("%w: none of %s is one of the choices available %s",
|
||||||
|
validate.ErrValueNotOneOf, strings.Join(values, ", "), strings.Join(choices, ", "))
|
||||||
|
default:
|
||||||
|
warner.Warn(fmt.Sprintf("values %s are not in choices %s",
|
||||||
|
strings.Join(invalidValues, ", "), strings.Join(choices, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvider string) error {
|
func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvider string) error {
|
||||||
switch {
|
switch {
|
||||||
case *settings.FreeOnly &&
|
case *settings.FreeOnly &&
|
||||||
@@ -229,6 +270,8 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
|||||||
switch {
|
switch {
|
||||||
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
|
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
|
||||||
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
|
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
|
||||||
|
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
|
||||||
|
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
|
||||||
case *settings.StreamOnly &&
|
case *settings.StreamOnly &&
|
||||||
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
||||||
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
|
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
|
||||||
@@ -249,7 +292,6 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
|||||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||||
return ServerSelection{
|
return ServerSelection{
|
||||||
VPN: ss.VPN,
|
VPN: ss.VPN,
|
||||||
TargetIP: ss.TargetIP,
|
|
||||||
Countries: gosettings.CopySlice(ss.Countries),
|
Countries: gosettings.CopySlice(ss.Countries),
|
||||||
Categories: gosettings.CopySlice(ss.Categories),
|
Categories: gosettings.CopySlice(ss.Categories),
|
||||||
Regions: gosettings.CopySlice(ss.Regions),
|
Regions: gosettings.CopySlice(ss.Regions),
|
||||||
@@ -273,7 +315,6 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
|||||||
|
|
||||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||||
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||||
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
|
||||||
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||||
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||||
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||||
@@ -296,7 +337,6 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
|||||||
|
|
||||||
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||||
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||||
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
|
||||||
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||||
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||||
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||||
@@ -304,11 +344,8 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
|||||||
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||||
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||||
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||||
defaultPortForwardOnly := false
|
defaultPortForwardOnly := portForwardingEnabled &&
|
||||||
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
||||||
providers.PrivateInternetAccess, providers.Protonvpn) {
|
|
||||||
defaultPortForwardOnly = true
|
|
||||||
}
|
|
||||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
ss.Wireguard.setDefaults()
|
ss.Wireguard.setDefaults()
|
||||||
@@ -321,9 +358,6 @@ func (ss ServerSelection) String() string {
|
|||||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("Server selection settings:")
|
node = gotree.New("Server selection settings:")
|
||||||
node.Appendf("VPN type: %s", ss.VPN)
|
node.Appendf("VPN type: %s", ss.VPN)
|
||||||
if !ss.TargetIP.IsUnspecified() {
|
|
||||||
node.Appendf("Target IP address: %s", ss.TargetIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ss.Countries) > 0 {
|
if len(ss.Countries) > 0 {
|
||||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||||
@@ -410,15 +444,10 @@ func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ServerSelection) read(r *reader.Reader,
|
func (ss *ServerSelection) read(r *reader.Reader,
|
||||||
vpnProvider, vpnType string) (err error) {
|
vpnProvider, vpnType string,
|
||||||
|
) (err error) {
|
||||||
ss.VPN = vpnType
|
ss.VPN = vpnType
|
||||||
|
|
||||||
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
|
||||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
countriesRetroKeys := []string{"COUNTRY"}
|
countriesRetroKeys := []string{"COUNTRY"}
|
||||||
if vpnProvider == providers.Cyberghost {
|
if vpnProvider == providers.Cyberghost {
|
||||||
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Settings struct {
|
|||||||
Log Log
|
Log Log
|
||||||
PublicIP PublicIP
|
PublicIP PublicIP
|
||||||
Shadowsocks Shadowsocks
|
Shadowsocks Shadowsocks
|
||||||
|
Storage Storage
|
||||||
System System
|
System System
|
||||||
Updater Updater
|
Updater Updater
|
||||||
Version Version
|
Version Version
|
||||||
@@ -29,14 +30,16 @@ type Settings struct {
|
|||||||
Pprof pprof.Settings
|
Pprof pprof.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storage interface {
|
type FilterChoicesGetter interface {
|
||||||
GetFilterChoices(provider string) models.FilterChoices
|
GetFilterChoices(provider string) models.FilterChoices
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates all the settings and returns an error
|
// Validate validates all the settings and returns an error
|
||||||
// if one of them is not valid.
|
// if one of them is not valid.
|
||||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
|
func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool,
|
||||||
|
warner Warner,
|
||||||
|
) (err error) {
|
||||||
nameToValidation := map[string]func() error{
|
nameToValidation := map[string]func() error{
|
||||||
"control server": s.ControlServer.validate,
|
"control server": s.ControlServer.validate,
|
||||||
"dns": s.DNS.validate,
|
"dns": s.DNS.validate,
|
||||||
@@ -46,12 +49,13 @@ func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
|
|||||||
"log": s.Log.validate,
|
"log": s.Log.validate,
|
||||||
"public ip check": s.PublicIP.validate,
|
"public ip check": s.PublicIP.validate,
|
||||||
"shadowsocks": s.Shadowsocks.validate,
|
"shadowsocks": s.Shadowsocks.validate,
|
||||||
|
"storage": s.Storage.validate,
|
||||||
"system": s.System.validate,
|
"system": s.System.validate,
|
||||||
"updater": s.Updater.Validate,
|
"updater": s.Updater.Validate,
|
||||||
"version": s.Version.validate,
|
"version": s.Version.validate,
|
||||||
// Pprof validation done in pprof constructor
|
// Pprof validation done in pprof constructor
|
||||||
"VPN": func() error {
|
"VPN": func() error {
|
||||||
return s.VPN.Validate(storage, ipv6Supported)
|
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +79,7 @@ func (s *Settings) copy() (copied Settings) {
|
|||||||
Log: s.Log.copy(),
|
Log: s.Log.copy(),
|
||||||
PublicIP: s.PublicIP.copy(),
|
PublicIP: s.PublicIP.copy(),
|
||||||
Shadowsocks: s.Shadowsocks.copy(),
|
Shadowsocks: s.Shadowsocks.copy(),
|
||||||
|
Storage: s.Storage.copy(),
|
||||||
System: s.System.copy(),
|
System: s.System.copy(),
|
||||||
Updater: s.Updater.copy(),
|
Updater: s.Updater.copy(),
|
||||||
Version: s.Version.copy(),
|
Version: s.Version.copy(),
|
||||||
@@ -84,7 +89,8 @@ func (s *Settings) copy() (copied Settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Settings) OverrideWith(other Settings,
|
func (s *Settings) OverrideWith(other Settings,
|
||||||
storage Storage, ipv6Supported bool) (err error) {
|
filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner,
|
||||||
|
) (err error) {
|
||||||
patchedSettings := s.copy()
|
patchedSettings := s.copy()
|
||||||
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
||||||
patchedSettings.DNS.overrideWith(other.DNS)
|
patchedSettings.DNS.overrideWith(other.DNS)
|
||||||
@@ -94,12 +100,13 @@ func (s *Settings) OverrideWith(other Settings,
|
|||||||
patchedSettings.Log.overrideWith(other.Log)
|
patchedSettings.Log.overrideWith(other.Log)
|
||||||
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
||||||
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
||||||
|
patchedSettings.Storage.overrideWith(other.Storage)
|
||||||
patchedSettings.System.overrideWith(other.System)
|
patchedSettings.System.overrideWith(other.System)
|
||||||
patchedSettings.Updater.overrideWith(other.Updater)
|
patchedSettings.Updater.overrideWith(other.Updater)
|
||||||
patchedSettings.Version.overrideWith(other.Version)
|
patchedSettings.Version.overrideWith(other.Version)
|
||||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||||
err = patchedSettings.Validate(storage, ipv6Supported)
|
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -116,6 +123,7 @@ func (s *Settings) SetDefaults() {
|
|||||||
s.Log.setDefaults()
|
s.Log.setDefaults()
|
||||||
s.PublicIP.setDefaults()
|
s.PublicIP.setDefaults()
|
||||||
s.Shadowsocks.setDefaults()
|
s.Shadowsocks.setDefaults()
|
||||||
|
s.Storage.setDefaults()
|
||||||
s.System.setDefaults()
|
s.System.setDefaults()
|
||||||
s.Version.setDefaults()
|
s.Version.setDefaults()
|
||||||
s.VPN.setDefaults()
|
s.VPN.setDefaults()
|
||||||
@@ -138,6 +146,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
|||||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||||
node.AppendNode(s.ControlServer.toLinesNode())
|
node.AppendNode(s.ControlServer.toLinesNode())
|
||||||
|
node.AppendNode(s.Storage.toLinesNode())
|
||||||
node.AppendNode(s.System.toLinesNode())
|
node.AppendNode(s.System.toLinesNode())
|
||||||
node.AppendNode(s.PublicIP.toLinesNode())
|
node.AppendNode(s.PublicIP.toLinesNode())
|
||||||
node.AppendNode(s.Updater.toLinesNode())
|
node.AppendNode(s.Updater.toLinesNode())
|
||||||
@@ -168,16 +177,21 @@ func (s Settings) Warnings() (warnings []string) {
|
|||||||
// TODO remove in v4
|
// TODO remove in v4
|
||||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||||
" so the DNS over TLS (DoT) server will not be used."+
|
" so the local forwarding DNS server will not be used."+
|
||||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
|
||||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||||
" corresponding to the first DoT provider chosen is used.")
|
" corresponding to the first DNS provider chosen is used.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return warnings
|
return warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Settings) Read(r *reader.Reader) (err error) {
|
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||||
|
warnings := readObsolete(r)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
warner.Warn(warning)
|
||||||
|
}
|
||||||
|
|
||||||
readFunctions := map[string]func(r *reader.Reader) error{
|
readFunctions := map[string]func(r *reader.Reader) error{
|
||||||
"control server": s.ControlServer.read,
|
"control server": s.ControlServer.read,
|
||||||
"DNS": s.DNS.read,
|
"DNS": s.DNS.read,
|
||||||
@@ -185,13 +199,16 @@ func (s *Settings) Read(r *reader.Reader) (err error) {
|
|||||||
"health": s.Health.Read,
|
"health": s.Health.Read,
|
||||||
"http proxy": s.HTTPProxy.read,
|
"http proxy": s.HTTPProxy.read,
|
||||||
"log": s.Log.read,
|
"log": s.Log.read,
|
||||||
"public ip": s.PublicIP.read,
|
"public ip": func(r *reader.Reader) error {
|
||||||
"shadowsocks": s.Shadowsocks.read,
|
return s.PublicIP.read(r, warner)
|
||||||
"system": s.System.read,
|
},
|
||||||
"updater": s.Updater.read,
|
"shadowsocks": s.Shadowsocks.read,
|
||||||
"version": s.Version.read,
|
"storage": s.Storage.read,
|
||||||
"VPN": s.VPN.read,
|
"system": s.System.read,
|
||||||
"profiling": s.Pprof.Read,
|
"updater": s.Updater.read,
|
||||||
|
"version": s.Version.read,
|
||||||
|
"VPN": s.VPN.read,
|
||||||
|
"profiling": s.Pprof.Read,
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, read := range readFunctions {
|
for name, read := range readFunctions {
|
||||||
|
|||||||
@@ -40,59 +40,57 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── DNS settings:
|
├── DNS settings:
|
||||||
| ├── Keep existing nameserver(s): no
|
| ├── Keep existing nameserver(s): no
|
||||||
| ├── DNS server address to use: 127.0.0.1
|
| ├── DNS server address to use: 127.0.0.1
|
||||||
| └── DNS over TLS settings:
|
| ├── DNS forwarder server enabled: yes
|
||||||
| ├── Enabled: yes
|
| ├── Upstream resolver type: dot
|
||||||
| ├── Update period: every 24h0m0s
|
| ├── Upstream resolvers:
|
||||||
| ├── Unbound settings:
|
| | └── Cloudflare
|
||||||
| | ├── Authoritative servers:
|
| ├── Caching: yes
|
||||||
| | | └── Cloudflare
|
| ├── IPv6: no
|
||||||
| | ├── Caching: yes
|
| ├── Update period: every 24h0m0s
|
||||||
| | ├── IPv6: no
|
| └── DNS filtering settings:
|
||||||
| | ├── Verbosity level: 1
|
| ├── Block malicious: yes
|
||||||
| | ├── Verbosity details level: 0
|
| ├── Block ads: no
|
||||||
| | ├── Validation log level: 0
|
| └── Block surveillance: yes
|
||||||
| | ├── System user: root
|
|
||||||
| | └── Allowed networks:
|
|
||||||
| | ├── 0.0.0.0/0
|
|
||||||
| | └── ::/0
|
|
||||||
| └── DNS filtering settings:
|
|
||||||
| ├── Block malicious: yes
|
|
||||||
| ├── Block ads: no
|
|
||||||
| └── Block surveillance: yes
|
|
||||||
├── Firewall settings:
|
├── Firewall settings:
|
||||||
| └── Enabled: yes
|
| └── Enabled: yes
|
||||||
├── Log settings:
|
├── Log settings:
|
||||||
| └── Log level: INFO
|
| └── Log level: INFO
|
||||||
├── Health settings:
|
├── Health settings:
|
||||||
| ├── Server listening address: 127.0.0.1:9999
|
| ├── Server listening address: 127.0.0.1:9999
|
||||||
| ├── Target address: cloudflare.com:443
|
| ├── Target addresses:
|
||||||
| ├── Duration to wait after success: 5s
|
| | ├── cloudflare.com:443
|
||||||
| ├── Read header timeout: 100ms
|
| | └── github.com:443
|
||||||
| ├── Read timeout: 500ms
|
| ├── Small health check type: ICMP echo request
|
||||||
| └── VPN wait durations:
|
| | └── ICMP target IPs:
|
||||||
| ├── Initial duration: 6s
|
| | ├── 1.1.1.1
|
||||||
| └── Additional duration: 5s
|
| | └── 8.8.8.8
|
||||||
|
| └── Restart VPN on healthcheck failure: yes
|
||||||
├── Shadowsocks server settings:
|
├── Shadowsocks server settings:
|
||||||
| └── Enabled: no
|
| └── Enabled: no
|
||||||
├── HTTP proxy settings:
|
├── HTTP proxy settings:
|
||||||
| └── Enabled: no
|
| └── Enabled: no
|
||||||
├── Control server settings:
|
├── Control server settings:
|
||||||
| ├── Listening address: :8000
|
| ├── Listening address: :8000
|
||||||
| └── Logging: yes
|
| ├── Logging: yes
|
||||||
|
| └── Authentication file path: /gluetun/auth/config.toml
|
||||||
|
├── Storage settings:
|
||||||
|
| └── Filepath: /gluetun/servers.json
|
||||||
├── OS Alpine settings:
|
├── OS Alpine settings:
|
||||||
| ├── Process UID: 1000
|
| ├── Process UID: 1000
|
||||||
| └── Process GID: 1000
|
| └── Process GID: 1000
|
||||||
├── Public IP settings:
|
├── Public IP settings:
|
||||||
| ├── Fetching: every 12h0m0s
|
|
||||||
| ├── IP file path: /tmp/gluetun/ip
|
| ├── IP file path: /tmp/gluetun/ip
|
||||||
| └── Public IP data API: ipinfo
|
| ├── Public IP data base API: ipinfo
|
||||||
|
| └── Public IP data backup APIs:
|
||||||
|
| ├── cloudflare
|
||||||
|
| ├── ifconfigco
|
||||||
|
| └── ip2location
|
||||||
└── Version settings:
|
└── Version settings:
|
||||||
└── Enabled: yes`,
|
└── Enabled: yes`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Shadowsocks struct {
|
|||||||
// It defaults to false, and cannot be nil in the internal state.
|
// It defaults to false, and cannot be nil in the internal state.
|
||||||
Enabled *bool
|
Enabled *bool
|
||||||
// Settings are settings for the TCP+UDP server.
|
// Settings are settings for the TCP+UDP server.
|
||||||
tcpudp.Settings
|
Settings tcpudp.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Shadowsocks) validate() (err error) {
|
func (s Shadowsocks) validate() (err error) {
|
||||||
|
|||||||
59
internal/configuration/settings/storage.go
Normal file
59
internal/configuration/settings/storage.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gotree"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage contains settings to configure the storage.
|
||||||
|
type Storage struct {
|
||||||
|
// Filepath is the path to the servers.json file. An empty string disables on-disk storage.
|
||||||
|
Filepath *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Storage) validate() (err error) {
|
||||||
|
if *s.Filepath != "" { // optional
|
||||||
|
_, err := filepath.Abs(*s.Filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filepath is not valid: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) copy() (copied Storage) {
|
||||||
|
return Storage{
|
||||||
|
Filepath: gosettings.CopyPointer(s.Filepath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) overrideWith(other Storage) {
|
||||||
|
s.Filepath = gosettings.OverrideWithPointer(s.Filepath, other.Filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) setDefaults() {
|
||||||
|
const defaultFilepath = "/gluetun/servers.json"
|
||||||
|
s.Filepath = gosettings.DefaultPointer(s.Filepath, defaultFilepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Storage) String() string {
|
||||||
|
return s.toLinesNode().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Storage) toLinesNode() (node *gotree.Node) {
|
||||||
|
if *s.Filepath == "" {
|
||||||
|
return gotree.New("Storage settings: disabled")
|
||||||
|
}
|
||||||
|
node = gotree.New("Storage settings:")
|
||||||
|
node.Appendf("Filepath: %s", *s.Filepath)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) read(r *reader.Reader) (err error) {
|
||||||
|
s.Filepath = r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func surfsharkRetroRegion(selection ServerSelection) (
|
func surfsharkRetroRegion(selection ServerSelection) (
|
||||||
updatedSelection ServerSelection) {
|
updatedSelection ServerSelection,
|
||||||
|
) {
|
||||||
locationData := servers.LocationData()
|
locationData := servers.LocationData()
|
||||||
|
|
||||||
retroToLocation := make(map[string]servers.ServerLocation, len(locationData))
|
retroToLocation := make(map[string]servers.ServerLocation, len(locationData))
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/provider"
|
|
||||||
"github.com/qdm12/dns/pkg/unbound"
|
|
||||||
"github.com/qdm12/gosettings"
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unbound is settings for the Unbound program.
|
|
||||||
type Unbound struct {
|
|
||||||
Providers []string `json:"providers"`
|
|
||||||
Caching *bool `json:"caching"`
|
|
||||||
IPv6 *bool `json:"ipv6"`
|
|
||||||
VerbosityLevel *uint8 `json:"verbosity_level"`
|
|
||||||
VerbosityDetailsLevel *uint8 `json:"verbosity_details_level"`
|
|
||||||
ValidationLogLevel *uint8 `json:"validation_log_level"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Allowed []netip.Prefix `json:"allowed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unbound) setDefaults() {
|
|
||||||
if len(u.Providers) == 0 {
|
|
||||||
u.Providers = []string{
|
|
||||||
provider.Cloudflare().String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Caching = gosettings.DefaultPointer(u.Caching, true)
|
|
||||||
u.IPv6 = gosettings.DefaultPointer(u.IPv6, false)
|
|
||||||
|
|
||||||
const defaultVerbosityLevel = 1
|
|
||||||
u.VerbosityLevel = gosettings.DefaultPointer(u.VerbosityLevel, defaultVerbosityLevel)
|
|
||||||
|
|
||||||
const defaultVerbosityDetailsLevel = 0
|
|
||||||
u.VerbosityDetailsLevel = gosettings.DefaultPointer(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
|
|
||||||
|
|
||||||
const defaultValidationLogLevel = 0
|
|
||||||
u.ValidationLogLevel = gosettings.DefaultPointer(u.ValidationLogLevel, defaultValidationLogLevel)
|
|
||||||
|
|
||||||
if u.Allowed == nil {
|
|
||||||
u.Allowed = []netip.Prefix{
|
|
||||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
|
||||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Username = gosettings.DefaultComparable(u.Username, "root")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
|
|
||||||
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
|
|
||||||
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u Unbound) validate() (err error) {
|
|
||||||
for _, s := range u.Providers {
|
|
||||||
_, err := provider.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxVerbosityLevel = 5
|
|
||||||
if *u.VerbosityLevel > maxVerbosityLevel {
|
|
||||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
|
||||||
ErrUnboundVerbosityLevelNotValid,
|
|
||||||
*u.VerbosityLevel,
|
|
||||||
maxVerbosityLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxVerbosityDetailsLevel = 4
|
|
||||||
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
|
|
||||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
|
||||||
ErrUnboundVerbosityDetailsLevelNotValid,
|
|
||||||
*u.VerbosityDetailsLevel,
|
|
||||||
maxVerbosityDetailsLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxValidationLogLevel = 2
|
|
||||||
if *u.ValidationLogLevel > maxValidationLogLevel {
|
|
||||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
|
||||||
ErrUnboundValidationLogLevelNotValid,
|
|
||||||
*u.ValidationLogLevel, maxValidationLogLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Unbound) copy() (copied Unbound) {
|
|
||||||
return Unbound{
|
|
||||||
Providers: gosettings.CopySlice(u.Providers),
|
|
||||||
Caching: gosettings.CopyPointer(u.Caching),
|
|
||||||
IPv6: gosettings.CopyPointer(u.IPv6),
|
|
||||||
VerbosityLevel: gosettings.CopyPointer(u.VerbosityLevel),
|
|
||||||
VerbosityDetailsLevel: gosettings.CopyPointer(u.VerbosityDetailsLevel),
|
|
||||||
ValidationLogLevel: gosettings.CopyPointer(u.ValidationLogLevel),
|
|
||||||
Username: u.Username,
|
|
||||||
Allowed: gosettings.CopySlice(u.Allowed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unbound) overrideWith(other Unbound) {
|
|
||||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
|
||||||
u.Caching = gosettings.OverrideWithPointer(u.Caching, other.Caching)
|
|
||||||
u.IPv6 = gosettings.OverrideWithPointer(u.IPv6, other.IPv6)
|
|
||||||
u.VerbosityLevel = gosettings.OverrideWithPointer(u.VerbosityLevel, other.VerbosityLevel)
|
|
||||||
u.VerbosityDetailsLevel = gosettings.OverrideWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
|
|
||||||
u.ValidationLogLevel = gosettings.OverrideWithPointer(u.ValidationLogLevel, other.ValidationLogLevel)
|
|
||||||
u.Username = gosettings.OverrideWithComparable(u.Username, other.Username)
|
|
||||||
u.Allowed = gosettings.OverrideWithSlice(u.Allowed, other.Allowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
|
|
||||||
providers := make([]provider.Provider, len(u.Providers))
|
|
||||||
for i := range providers {
|
|
||||||
providers[i], err = provider.Parse(u.Providers[i])
|
|
||||||
if err != nil {
|
|
||||||
return settings, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = 53
|
|
||||||
|
|
||||||
return unbound.Settings{
|
|
||||||
ListeningPort: port,
|
|
||||||
IPv4: true,
|
|
||||||
Providers: providers,
|
|
||||||
Caching: *u.Caching,
|
|
||||||
IPv6: *u.IPv6,
|
|
||||||
VerbosityLevel: *u.VerbosityLevel,
|
|
||||||
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
|
|
||||||
ValidationLogLevel: *u.ValidationLogLevel,
|
|
||||||
AccessControl: unbound.AccessControlSettings{
|
|
||||||
Allowed: netipPrefixesToNetaddrIPPrefixes(u.Allowed),
|
|
||||||
},
|
|
||||||
Username: u.Username,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrConvertingNetip = errors.New("converting net.IP to netip.Addr failed")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 netip.Addr, err error) {
|
|
||||||
s := u.Providers[0]
|
|
||||||
provider, err := provider.Parse(s)
|
|
||||||
if err != nil {
|
|
||||||
return ipv4, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := provider.DNS().IPv4[0]
|
|
||||||
ipv4, ok := netip.AddrFromSlice(ip)
|
|
||||||
if !ok {
|
|
||||||
return ipv4, fmt.Errorf("%w: for ip %s (%#v)",
|
|
||||||
ErrConvertingNetip, ip, ip)
|
|
||||||
}
|
|
||||||
return ipv4.Unmap(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Unbound) String() string {
|
|
||||||
return u.toLinesNode().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Unbound) toLinesNode() (node *gotree.Node) {
|
|
||||||
node = gotree.New("Unbound settings:")
|
|
||||||
|
|
||||||
authServers := node.Appendf("Authoritative servers:")
|
|
||||||
for _, provider := range u.Providers {
|
|
||||||
authServers.Appendf(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(u.Caching))
|
|
||||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(u.IPv6))
|
|
||||||
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
|
|
||||||
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
|
|
||||||
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
|
|
||||||
node.Appendf("System user: %s", u.Username)
|
|
||||||
|
|
||||||
allowedNetworks := node.Appendf("Allowed networks:")
|
|
||||||
for _, network := range u.Allowed {
|
|
||||||
allowedNetworks.Appendf(network.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unbound) read(reader *reader.Reader) (err error) {
|
|
||||||
u.Providers = reader.CSV("DOT_PROVIDERS")
|
|
||||||
|
|
||||||
u.Caching, err = reader.BoolPtr("DOT_CACHING")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.VerbosityLevel, err = reader.Uint8Ptr("DOT_VERBOSITY")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.VerbosityDetailsLevel, err = reader.Uint8Ptr("DOT_VERBOSITY_DETAILS")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.ValidationLogLevel, err = reader.Uint8Ptr("DOT_VALIDATION_LOGLEVEL")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Unbound_JSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
settings := Unbound{
|
|
||||||
Providers: []string{"cloudflare"},
|
|
||||||
Caching: boolPtr(true),
|
|
||||||
IPv6: boolPtr(false),
|
|
||||||
VerbosityLevel: uint8Ptr(1),
|
|
||||||
VerbosityDetailsLevel: nil,
|
|
||||||
ValidationLogLevel: uint8Ptr(0),
|
|
||||||
Username: "user",
|
|
||||||
Allowed: []netip.Prefix{
|
|
||||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
|
||||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(settings)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
const expected = `{"providers":["cloudflare"],"caching":true,"ipv6":false,` +
|
|
||||||
`"verbosity_level":1,"verbosity_details_level":null,"validation_log_level":0,` +
|
|
||||||
`"username":"user","allowed":["0.0.0.0/0","::/0"]}`
|
|
||||||
|
|
||||||
assert.Equal(t, expected, string(b))
|
|
||||||
|
|
||||||
var resultSettings Unbound
|
|
||||||
err = json.Unmarshal(b, &resultSettings)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, settings, resultSettings)
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package settings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ type Updater struct {
|
|||||||
// Providers is the list of VPN service providers
|
// Providers is the list of VPN service providers
|
||||||
// to update server information for.
|
// to update server information for.
|
||||||
Providers []string
|
Providers []string
|
||||||
|
// ProtonEmail is the email to authenticate with the Proton API.
|
||||||
|
ProtonEmail *string
|
||||||
|
// ProtonPassword is the password to authenticate with the Proton API.
|
||||||
|
ProtonPassword *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) Validate() (err error) {
|
func (u Updater) Validate() (err error) {
|
||||||
@@ -51,6 +56,18 @@ func (u Updater) Validate() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if provider == providers.Protonvpn {
|
||||||
|
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
|
||||||
|
if authenticatedAPI {
|
||||||
|
switch {
|
||||||
|
case *u.ProtonEmail == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
|
||||||
|
case *u.ProtonPassword == "":
|
||||||
|
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -58,10 +75,12 @@ func (u Updater) Validate() (err error) {
|
|||||||
|
|
||||||
func (u *Updater) copy() (copied Updater) {
|
func (u *Updater) copy() (copied Updater) {
|
||||||
return Updater{
|
return Updater{
|
||||||
Period: gosettings.CopyPointer(u.Period),
|
Period: gosettings.CopyPointer(u.Period),
|
||||||
DNSAddress: u.DNSAddress,
|
DNSAddress: u.DNSAddress,
|
||||||
MinRatio: u.MinRatio,
|
MinRatio: u.MinRatio,
|
||||||
Providers: gosettings.CopySlice(u.Providers),
|
Providers: gosettings.CopySlice(u.Providers),
|
||||||
|
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||||
|
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +92,8 @@ func (u *Updater) overrideWith(other Updater) {
|
|||||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||||
|
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||||
|
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||||
@@ -87,6 +108,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
|||||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||||
u.Providers = []string{vpnProvider}
|
u.Providers = []string{vpnProvider}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set these to empty strings to avoid nil pointer panics
|
||||||
|
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
|
||||||
|
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Updater) String() string {
|
func (u Updater) String() string {
|
||||||
@@ -103,6 +128,10 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||||
|
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||||
|
node.Appendf("Proton API email: %s", *u.ProtonEmail)
|
||||||
|
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||||
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -125,6 +154,16 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
|||||||
|
|
||||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||||
|
|
||||||
|
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
|
||||||
|
if u.ProtonEmail == nil {
|
||||||
|
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
|
||||||
|
if protonUsername != "" {
|
||||||
|
protonEmail := protonUsername + "@protonmail.com"
|
||||||
|
u.ProtonEmail = &protonEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ type VPN struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||||
func (v *VPN) Validate(storage Storage, ipv6Supported bool) (err error) {
|
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
|
||||||
// Validate Type
|
// Validate Type
|
||||||
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
||||||
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
|
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
|
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = v.Provider.validate(v.Type, storage)
|
err = v.Provider.validate(v.Type, filterChoicesGetter, warner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("provider settings: %w", err)
|
return fmt.Errorf("provider settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ type Wireguard struct {
|
|||||||
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
||||||
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
||||||
// It cannot be zero in the internal state, and defaults to
|
// It cannot be zero in the internal state, and defaults to
|
||||||
// 1400. Note it is not the wireguard-go MTU default of 1420
|
// 1320. Note it is not the wireguard-go MTU default of 1420
|
||||||
// because this impacts bandwidth a lot on some VPN providers,
|
// because this impacts bandwidth a lot on some VPN providers,
|
||||||
// see https://github.com/qdm12/gluetun/issues/1650.
|
// see https://github.com/qdm12/gluetun/issues/1650.
|
||||||
|
// It has been lowered to 1320 following quite a bit of
|
||||||
|
// investigation in the issue:
|
||||||
|
// https://github.com/qdm12/gluetun/issues/2533.
|
||||||
MTU uint16 `json:"mtu"`
|
MTU uint16 `json:"mtu"`
|
||||||
// Implementation is the Wireguard implementation to use.
|
// Implementation is the Wireguard implementation to use.
|
||||||
// It can be "auto", "userspace" or "kernelspace".
|
// It can be "auto", "userspace" or "kernelspace".
|
||||||
@@ -191,7 +194,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
|||||||
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
||||||
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
||||||
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
||||||
const defaultMTU = 1400
|
const defaultMTU = 1320
|
||||||
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
||||||
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
||||||
}
|
}
|
||||||
@@ -215,12 +218,12 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
addressesNode := node.Appendf("Interface addresses:")
|
addressesNode := node.Appendf("Interface addresses:")
|
||||||
for _, address := range w.Addresses {
|
for _, address := range w.Addresses {
|
||||||
addressesNode.Appendf(address.String())
|
addressesNode.Append(address.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedIPsNode := node.Appendf("Allowed IPs:")
|
allowedIPsNode := node.Appendf("Allowed IPs:")
|
||||||
for _, allowedIP := range w.AllowedIPs {
|
for _, allowedIP := range w.AllowedIPs {
|
||||||
allowedIPsNode.Appendf(allowedIP.String())
|
allowedIPsNode.Append(allowedIP.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if *w.PersistentKeepaliveInterval > 0 {
|
if *w.PersistentKeepaliveInterval > 0 {
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
|
|
||||||
type WireguardSelection struct {
|
type WireguardSelection struct {
|
||||||
// EndpointIP is the server endpoint IP address.
|
// EndpointIP is the server endpoint IP address.
|
||||||
// It is only used with VPN providers generating Wireguard
|
// It is notably required with the custom provider.
|
||||||
// configurations specific to each server and user.
|
// Otherwise it overrides any IP address from the picked
|
||||||
// To indicate it should not be used, it should be set
|
// built-in server connection. To indicate it should
|
||||||
// to netip.IPv4Unspecified(). It can never be the zero value
|
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||||
// in the internal state.
|
// It can never be the zero value in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// EndpointPort is a the server port to use for the VPN server.
|
||||||
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||||
@@ -155,7 +155,8 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
||||||
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w - note this MUST be an IP address, "+
|
||||||
|
"see https://github.com/qdm12/gluetun/issues/788", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ type WireguardConfig struct {
|
|||||||
EndpointPort *string
|
EndpointPort *string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
|
||||||
regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
||||||
iniFile, err := ini.InsensitiveLoad(path)
|
iniFile, err := ini.InsensitiveLoad(path)
|
||||||
@@ -68,18 +66,18 @@ func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseWireguardInterfaceSection(interfaceSection *ini.Section) (
|
func parseWireguardInterfaceSection(interfaceSection *ini.Section) (
|
||||||
privateKey, addresses *string) {
|
privateKey, addresses *string,
|
||||||
|
) {
|
||||||
privateKey = getINIKeyFromSection(interfaceSection, "PrivateKey")
|
privateKey = getINIKeyFromSection(interfaceSection, "PrivateKey")
|
||||||
addresses = getINIKeyFromSection(interfaceSection, "Address")
|
addresses = getINIKeyFromSection(interfaceSection, "Address")
|
||||||
return privateKey, addresses
|
return privateKey, addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
|
||||||
ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseWireguardPeerSection(peerSection *ini.Section) (
|
func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||||
preSharedKey, publicKey, endpointIP, endpointPort *string) {
|
preSharedKey, publicKey, endpointIP, endpointPort *string,
|
||||||
|
) {
|
||||||
preSharedKey = getINIKeyFromSection(peerSection, "PresharedKey")
|
preSharedKey = getINIKeyFromSection(peerSection, "PresharedKey")
|
||||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||||
@@ -96,9 +94,7 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
|||||||
return preSharedKey, publicKey, endpointIP, endpointPort
|
return preSharedKey, publicKey, endpointIP, endpointPort
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
|
||||||
regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func getINIKeyFromSection(section *ini.Section, key string) (value *string) {
|
func getINIKeyFromSection(section *ini.Section, key string) (value *string) {
|
||||||
iniKey, err := section.GetKey(key)
|
iniKey, err := section.GetKey(key)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -72,12 +73,12 @@ PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
|||||||
}
|
}
|
||||||
|
|
||||||
for testName, testCase := range testCases {
|
for testName, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
t.Run(testName, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
||||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
|
const permission = fs.FileMode(0o600)
|
||||||
|
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
wireguard, err := ParseWireguardConf(configFile)
|
wireguard, err := ParseWireguardConf(configFile)
|
||||||
@@ -121,7 +122,6 @@ Address = 10.38.22.35/32
|
|||||||
}
|
}
|
||||||
|
|
||||||
for testName, testCase := range testCases {
|
for testName, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
t.Run(testName, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -182,7 +182,6 @@ Endpoint = 1.2.3.4:51820`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for testName, testCase := range testCases {
|
for testName, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
t.Run(testName, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package secrets
|
package secrets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -38,7 +39,8 @@ func Test_Source_Get(t *testing.T) {
|
|||||||
"empty_secret_file": {
|
"empty_secret_file": {
|
||||||
makeSource: func(tempDir string) (source *Source, err error) {
|
makeSource: func(tempDir string) (source *Source, err error) {
|
||||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||||
err = os.WriteFile(secretFilepath, nil, os.ModePerm)
|
const permission = fs.FileMode(0o600)
|
||||||
|
err = os.WriteFile(secretFilepath, nil, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -53,7 +55,8 @@ func Test_Source_Get(t *testing.T) {
|
|||||||
"default_secret_file": {
|
"default_secret_file": {
|
||||||
makeSource: func(tempDir string) (source *Source, err error) {
|
makeSource: func(tempDir string) (source *Source, err error) {
|
||||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
const permission = fs.FileMode(0o600)
|
||||||
|
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,8 @@ func Test_Source_Get(t *testing.T) {
|
|||||||
"env_specified_secret_file": {
|
"env_specified_secret_file": {
|
||||||
makeSource: func(tempDir string) (source *Source, err error) {
|
makeSource: func(tempDir string) (source *Source, err error) {
|
||||||
secretFilepath := filepath.Join(tempDir, "test_file_custom")
|
secretFilepath := filepath.Join(tempDir, "test_file_custom")
|
||||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
const permission = fs.FileMode(0o600)
|
||||||
|
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,6 @@ func Test_Source_Get(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
for name, testCase := range testCases {
|
||||||
testCase := testCase
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package constants
|
|||||||
|
|
||||||
import "github.com/fatih/color"
|
import "github.com/fatih/color"
|
||||||
|
|
||||||
func ColorUnbound() *color.Color {
|
|
||||||
return color.New(color.FgCyan)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ColorOpenvpn() *color.Color {
|
func ColorOpenvpn() *color.Color {
|
||||||
return color.New(color.FgHiMagenta)
|
return color.New(color.FgHiMagenta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const (
|
|||||||
Example = "example"
|
Example = "example"
|
||||||
Expressvpn = "expressvpn"
|
Expressvpn = "expressvpn"
|
||||||
Fastestvpn = "fastestvpn"
|
Fastestvpn = "fastestvpn"
|
||||||
|
Giganews = "giganews"
|
||||||
HideMyAss = "hidemyass"
|
HideMyAss = "hidemyass"
|
||||||
Ipvanish = "ipvanish"
|
Ipvanish = "ipvanish"
|
||||||
Ivpn = "ivpn"
|
Ivpn = "ivpn"
|
||||||
@@ -37,6 +38,7 @@ func All() []string {
|
|||||||
Cyberghost,
|
Cyberghost,
|
||||||
Expressvpn,
|
Expressvpn,
|
||||||
Fastestvpn,
|
Fastestvpn,
|
||||||
|
Giganews,
|
||||||
HideMyAss,
|
HideMyAss,
|
||||||
Ipvanish,
|
Ipvanish,
|
||||||
Ivpn,
|
Ivpn,
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/unbound"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Configurator interface {
|
|
||||||
SetupFiles(ctx context.Context) error
|
|
||||||
MakeUnboundConf(settings unbound.Settings) (err error)
|
|
||||||
Start(ctx context.Context, verbosityDetailsLevel uint8) (
|
|
||||||
stdoutLines, stderrLines chan string, waitError chan error, err error)
|
|
||||||
Version(ctx context.Context) (version string, err error)
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logLevel uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
levelDebug logLevel = iota
|
|
||||||
levelInfo
|
|
||||||
levelWarn
|
|
||||||
levelError
|
|
||||||
)
|
|
||||||
|
|
||||||
func (l *Loop) collectLines(ctx context.Context, done chan<- struct{},
|
|
||||||
stdout, stderr chan string) {
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
var line string
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// Context should only be canceled after stdout and stderr are done
|
|
||||||
// being written to.
|
|
||||||
close(stdout)
|
|
||||||
close(stderr)
|
|
||||||
return
|
|
||||||
case line = <-stderr:
|
|
||||||
case line = <-stdout:
|
|
||||||
}
|
|
||||||
|
|
||||||
line, level := processLogLine(line)
|
|
||||||
switch level {
|
|
||||||
case levelDebug:
|
|
||||||
l.logger.Debug(line)
|
|
||||||
case levelInfo:
|
|
||||||
l.logger.Info(line)
|
|
||||||
case levelWarn:
|
|
||||||
l.logger.Warn(line)
|
|
||||||
case levelError:
|
|
||||||
l.logger.Error(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var unboundPrefix = regexp.MustCompile(`\[[0-9]{10}\] unbound\[[0-9]+:[0|1]\] `)
|
|
||||||
|
|
||||||
func processLogLine(s string) (filtered string, level logLevel) {
|
|
||||||
prefix := unboundPrefix.FindString(s)
|
|
||||||
filtered = s[len(prefix):]
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(filtered, "notice: "):
|
|
||||||
filtered = strings.TrimPrefix(filtered, "notice: ")
|
|
||||||
level = levelInfo
|
|
||||||
case strings.HasPrefix(filtered, "info: "):
|
|
||||||
filtered = strings.TrimPrefix(filtered, "info: ")
|
|
||||||
level = levelInfo
|
|
||||||
case strings.HasPrefix(filtered, "warn: "):
|
|
||||||
filtered = strings.TrimPrefix(filtered, "warn: ")
|
|
||||||
level = levelWarn
|
|
||||||
case strings.HasPrefix(filtered, "error: "):
|
|
||||||
filtered = strings.TrimPrefix(filtered, "error: ")
|
|
||||||
level = levelError
|
|
||||||
default:
|
|
||||||
level = levelInfo
|
|
||||||
}
|
|
||||||
filtered = constants.ColorUnbound().Sprintf(filtered)
|
|
||||||
return filtered, level
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_processLogLine(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := map[string]struct {
|
|
||||||
s string
|
|
||||||
filtered string
|
|
||||||
level logLevel
|
|
||||||
}{
|
|
||||||
"empty string": {"", "", levelInfo},
|
|
||||||
"random string": {"asdasqdb", "asdasqdb", levelInfo},
|
|
||||||
"unbound notice": {
|
|
||||||
"[1594595249] unbound[75:0] notice: init module 0: validator",
|
|
||||||
"init module 0: validator",
|
|
||||||
levelInfo},
|
|
||||||
"unbound info": {
|
|
||||||
"[1594595249] unbound[75:0] info: init module 0: validator",
|
|
||||||
"init module 0: validator",
|
|
||||||
levelInfo},
|
|
||||||
"unbound warn": {
|
|
||||||
"[1594595249] unbound[75:0] warn: init module 0: validator",
|
|
||||||
"init module 0: validator",
|
|
||||||
levelWarn},
|
|
||||||
"unbound error": {
|
|
||||||
"[1594595249] unbound[75:0] error: init module 0: validator",
|
|
||||||
"init module 0: validator",
|
|
||||||
levelError},
|
|
||||||
"unbound unknown": {
|
|
||||||
"[1594595249] unbound[75:0] BLA: init module 0: validator",
|
|
||||||
"BLA: init module 0: validator",
|
|
||||||
levelInfo},
|
|
||||||
}
|
|
||||||
for name, tc := range tests {
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
filtered, level := processLogLine(tc.s)
|
|
||||||
assert.Equal(t, tc.filtered, filtered)
|
|
||||||
assert.Equal(t, tc.level, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,13 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/blacklist"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/dns/state"
|
"github.com/qdm12/gluetun/internal/dns/state"
|
||||||
@@ -14,28 +17,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Loop struct {
|
type Loop struct {
|
||||||
statusManager *loopstate.State
|
statusManager *loopstate.State
|
||||||
state *state.State
|
state *state.State
|
||||||
conf Configurator
|
server *server.Server
|
||||||
resolvConf string
|
filter *mapfilter.Filter
|
||||||
blockBuilder blacklist.Builder
|
localResolvers []netip.Addr
|
||||||
client *http.Client
|
resolvConf string
|
||||||
logger Logger
|
client *http.Client
|
||||||
userTrigger bool
|
logger Logger
|
||||||
start <-chan struct{}
|
userTrigger bool
|
||||||
running chan<- models.LoopStatus
|
start <-chan struct{}
|
||||||
stop <-chan struct{}
|
running chan<- models.LoopStatus
|
||||||
stopped chan<- struct{}
|
stop <-chan struct{}
|
||||||
updateTicker <-chan struct{}
|
stopped chan<- struct{}
|
||||||
backoffTime time.Duration
|
updateTicker <-chan struct{}
|
||||||
timeNow func() time.Time
|
backoffTime time.Duration
|
||||||
timeSince func(time.Time) time.Duration
|
timeNow func() time.Time
|
||||||
|
timeSince func(time.Time) time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBackoffTime = 10 * time.Second
|
const defaultBackoffTime = 10 * time.Second
|
||||||
|
|
||||||
func NewLoop(conf Configurator, settings settings.DNS,
|
func NewLoop(settings settings.DNS,
|
||||||
client *http.Client, logger Logger) *Loop {
|
client *http.Client, logger Logger,
|
||||||
|
) (loop *Loop, err error) {
|
||||||
start := make(chan struct{})
|
start := make(chan struct{})
|
||||||
running := make(chan models.LoopStatus)
|
running := make(chan models.LoopStatus)
|
||||||
stop := make(chan struct{})
|
stop := make(chan struct{})
|
||||||
@@ -45,12 +50,19 @@ func NewLoop(conf Configurator, settings settings.DNS,
|
|||||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||||
state := state.New(statusManager, settings, updateTicker)
|
state := state.New(statusManager, settings, updateTicker)
|
||||||
|
|
||||||
|
filter, err := mapfilter.New(mapfilter.Settings{
|
||||||
|
Logger: buildFilterLogger(logger),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating map filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &Loop{
|
return &Loop{
|
||||||
statusManager: statusManager,
|
statusManager: statusManager,
|
||||||
state: state,
|
state: state,
|
||||||
conf: conf,
|
server: nil,
|
||||||
|
filter: filter,
|
||||||
resolvConf: "/etc/resolv.conf",
|
resolvConf: "/etc/resolv.conf",
|
||||||
blockBuilder: blacklist.NewBuilder(client),
|
|
||||||
client: client,
|
client: client,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
userTrigger: true,
|
userTrigger: true,
|
||||||
@@ -62,7 +74,7 @@ func NewLoop(conf Configurator, settings settings.DNS,
|
|||||||
backoffTime: defaultBackoffTime,
|
backoffTime: defaultBackoffTime,
|
||||||
timeNow: time.Now,
|
timeNow: time.Now,
|
||||||
timeSince: time.Since,
|
timeSince: time.Since,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
||||||
@@ -92,3 +104,15 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
|
|||||||
l.statusManager.SetStatus(status)
|
l.statusManager.SetStatus(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type filterLogger struct {
|
||||||
|
logger Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *filterLogger) Log(msg string) {
|
||||||
|
l.logger.Info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFilterLogger(logger Logger) *filterLogger {
|
||||||
|
return &filterLogger{logger: logger}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,46 +2,35 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
// Try with user provided plaintext ip address
|
targetIP := settings.GetFirstPlaintextIPv4()
|
||||||
// if it's not 127.0.0.1 (default for DoT)
|
|
||||||
targetIP := settings.ServerAddress
|
|
||||||
if targetIP.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
|
||||||
if fallback {
|
|
||||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
|
||||||
} else {
|
|
||||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
|
||||||
}
|
|
||||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
|
||||||
const keepNameserver = false
|
|
||||||
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
|
||||||
if err != nil {
|
|
||||||
l.logger.Error(err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use first plaintext DNS IPv4 address
|
|
||||||
targetIP, err := settings.DoT.Unbound.GetFirstPlaintextIPv4()
|
|
||||||
if err != nil {
|
|
||||||
// Unbound should always have a default provider
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallback {
|
if fallback {
|
||||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||||
} else {
|
} else {
|
||||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||||
}
|
}
|
||||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
|
||||||
const keepNameserver = false
|
const dialTimeout = 3 * time.Second
|
||||||
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
const defaultDNSPort = 53
|
||||||
|
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||||
|
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||||
|
Timeout: dialTimeout,
|
||||||
|
}
|
||||||
|
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||||
|
|
||||||
|
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||||
|
IPs: []netip.Addr{targetIP},
|
||||||
|
ResolvPath: l.resolvConf,
|
||||||
|
}
|
||||||
|
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
l.localResolvers, err = nameserver.GetPrivateDNSServers()
|
||||||
|
if err != nil {
|
||||||
|
l.logger.Error("getting private DNS servers: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if *l.GetSettings().KeepNameserver {
|
if *l.GetSettings().KeepNameserver {
|
||||||
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
||||||
"this will likely leak DNS traffic outside the VPN " +
|
"this will likely leak DNS traffic outside the VPN " +
|
||||||
@@ -26,20 +34,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
// Upper scope variables for Unbound only
|
// Upper scope variables for the DNS forwarder server only
|
||||||
// Their values are to be used if DOT=off
|
// Their values are to be used if DOT=off
|
||||||
waitError := make(chan error)
|
var runError <-chan error
|
||||||
unboundCancel := func() { waitError <- nil }
|
|
||||||
closeStreams := func() {}
|
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
for !*settings.KeepNameserver && *settings.ServerEnabled {
|
||||||
var err error
|
var err error
|
||||||
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
runError, err = l.setupServer(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
l.backoffTime = defaultBackoffTime
|
l.backoffTime = defaultBackoffTime
|
||||||
l.logger.Info("ready")
|
l.logger.Info("ready")
|
||||||
l.signalOrSetStatus(constants.Running)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,55 +54,65 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, errUpdateFiles) {
|
if !errors.Is(err, errUpdateBlockLists) {
|
||||||
const fallback = true
|
const fallback = true
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
}
|
}
|
||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
|
settings = l.GetSettings()
|
||||||
}
|
}
|
||||||
|
l.signalOrSetStatus(constants.Running)
|
||||||
|
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
if !*settings.KeepNameserver && !*settings.ServerEnabled {
|
||||||
const fallback = false
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.userTrigger = false
|
l.userTrigger = false
|
||||||
|
|
||||||
stayHere := true
|
exitLoop := l.runWait(ctx, runError)
|
||||||
for stayHere {
|
if exitLoop {
|
||||||
select {
|
return
|
||||||
case <-ctx.Done():
|
|
||||||
unboundCancel()
|
|
||||||
<-waitError
|
|
||||||
close(waitError)
|
|
||||||
closeStreams()
|
|
||||||
return
|
|
||||||
case <-l.stop:
|
|
||||||
l.userTrigger = true
|
|
||||||
l.logger.Info("stopping")
|
|
||||||
const fallback = false
|
|
||||||
l.useUnencryptedDNS(fallback)
|
|
||||||
unboundCancel()
|
|
||||||
<-waitError
|
|
||||||
// do not close waitError or the waitError
|
|
||||||
// select case will trigger
|
|
||||||
closeStreams()
|
|
||||||
l.stopped <- struct{}{}
|
|
||||||
case <-l.start:
|
|
||||||
l.userTrigger = true
|
|
||||||
l.logger.Info("starting")
|
|
||||||
stayHere = false
|
|
||||||
case err := <-waitError: // unexpected error
|
|
||||||
closeStreams()
|
|
||||||
|
|
||||||
unboundCancel()
|
|
||||||
l.statusManager.SetStatus(constants.Crashed)
|
|
||||||
const fallback = true
|
|
||||||
l.useUnencryptedDNS(fallback)
|
|
||||||
l.logAndWait(ctx, err)
|
|
||||||
stayHere = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop bool) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if !*l.GetSettings().KeepNameserver {
|
||||||
|
l.stopServer()
|
||||||
|
// TODO revert OS and Go nameserver when exiting
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case <-l.stop:
|
||||||
|
l.userTrigger = true
|
||||||
|
l.logger.Info("stopping")
|
||||||
|
if !*l.GetSettings().KeepNameserver {
|
||||||
|
const fallback = false
|
||||||
|
l.useUnencryptedDNS(fallback)
|
||||||
|
l.stopServer()
|
||||||
|
}
|
||||||
|
l.stopped <- struct{}{}
|
||||||
|
case <-l.start:
|
||||||
|
l.userTrigger = true
|
||||||
|
l.logger.Info("starting")
|
||||||
|
return false
|
||||||
|
case err := <-runError: // unexpected error
|
||||||
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
|
const fallback = true
|
||||||
|
l.useUnencryptedDNS(fallback)
|
||||||
|
l.logAndWait(ctx, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Loop) stopServer() {
|
||||||
|
stopErr := l.server.Stop()
|
||||||
|
if stopErr != nil {
|
||||||
|
l.logger.Error("stopping server: " + stopErr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,124 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/doh"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/dot"
|
||||||
|
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||||
|
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/middlewares/localdns"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/plain"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) GetSettings() (settings settings.DNS) { return l.state.GetSettings() }
|
func (l *Loop) GetSettings() (settings settings.DNS) { return l.state.GetSettings() }
|
||||||
|
|
||||||
func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||||
outcome string) {
|
outcome string,
|
||||||
|
) {
|
||||||
return l.state.SetSettings(ctx, settings)
|
return l.state.SetSettings(ctx, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildServerSettings(settings settings.DNS,
|
||||||
|
filter *mapfilter.Filter, localResolvers []netip.Addr,
|
||||||
|
logger Logger) (
|
||||||
|
serverSettings server.Settings, err error,
|
||||||
|
) {
|
||||||
|
serverSettings.Logger = logger
|
||||||
|
|
||||||
|
providersData := provider.NewProviders()
|
||||||
|
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
|
||||||
|
for i := range settings.Providers {
|
||||||
|
var err error
|
||||||
|
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // this should already had been checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipVersion := "ipv4"
|
||||||
|
if *settings.IPv6 {
|
||||||
|
ipVersion = "ipv6"
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialer server.Dialer
|
||||||
|
switch settings.UpstreamType {
|
||||||
|
case "dot":
|
||||||
|
dialerSettings := dot.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = dot.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||||
|
}
|
||||||
|
case "doh":
|
||||||
|
dialerSettings := doh.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = doh.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
|
||||||
|
}
|
||||||
|
case "plain":
|
||||||
|
dialerSettings := plain.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = plain.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown upstream type: " + settings.UpstreamType)
|
||||||
|
}
|
||||||
|
serverSettings.Dialer = dialer
|
||||||
|
|
||||||
|
if *settings.Caching {
|
||||||
|
lruCache, err := lru.New(lru.Settings{})
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||||
|
}
|
||||||
|
cacheMiddleware, err := cachemiddleware.New(cachemiddleware.Settings{
|
||||||
|
Cache: lruCache,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating cache middleware: %w", err)
|
||||||
|
}
|
||||||
|
serverSettings.Middlewares = append(serverSettings.Middlewares, cacheMiddleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterMiddleware, err := filtermiddleware.New(filtermiddleware.Settings{
|
||||||
|
Filter: filter,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating filter middleware: %w", err)
|
||||||
|
}
|
||||||
|
serverSettings.Middlewares = append(serverSettings.Middlewares, filterMiddleware)
|
||||||
|
|
||||||
|
localResolversAddrPorts := make([]netip.AddrPort, len(localResolvers))
|
||||||
|
const defaultDNSPort = 53
|
||||||
|
for i, addr := range localResolvers {
|
||||||
|
localResolversAddrPorts[i] = netip.AddrPortFrom(addr, defaultDNSPort)
|
||||||
|
}
|
||||||
|
localDNSMiddleware, err := localdns.New(localdns.Settings{
|
||||||
|
Resolvers: localResolversAddrPorts, // auto-detected at container start only
|
||||||
|
Logger: logger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating local DNS middleware: %w", err)
|
||||||
|
}
|
||||||
|
// Place after cache middleware, since we want to avoid caching for local
|
||||||
|
// hostnames that may change regularly.
|
||||||
|
// Place after filter middleware to avoid conflicts with the rebinding protection.
|
||||||
|
serverSettings.Middlewares = append(serverSettings.Middlewares, localDNSMiddleware)
|
||||||
|
|
||||||
|
return serverSettings, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,59 +4,57 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/dns/pkg/check"
|
"github.com/qdm12/dns/v2/pkg/check"
|
||||||
"github.com/qdm12/dns/pkg/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errUpdateFiles = errors.New("cannot update files")
|
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||||
|
|
||||||
// Returning cancel == nil signals we want to re-run setupUnbound
|
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
||||||
// Returning err == errUpdateFiles signals we should not fall back
|
|
||||||
// on the plaintext DNS as DOT is still up and running.
|
|
||||||
func (l *Loop) setupUnbound(ctx context.Context) (
|
|
||||||
cancel context.CancelFunc, waitError chan error, closeStreams func(), err error) {
|
|
||||||
err = l.updateFiles(ctx)
|
err = l.updateFiles(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil,
|
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
|
||||||
fmt.Errorf("%w: %s", errUpdateFiles, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
unboundCtx, cancel := context.WithCancel(context.Background())
|
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
||||||
stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx,
|
|
||||||
*settings.DoT.Unbound.VerbosityDetailsLevel)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
return nil, fmt.Errorf("building server settings: %w", err)
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
linesCollectionCtx, linesCollectionCancel := context.WithCancel(context.Background())
|
server, err := server.New(serverSettings)
|
||||||
lineCollectionDone := make(chan struct{})
|
if err != nil {
|
||||||
go l.collectLines(linesCollectionCtx, lineCollectionDone,
|
return nil, fmt.Errorf("creating server: %w", err)
|
||||||
stdoutLines, stderrLines)
|
|
||||||
closeStreams = func() {
|
|
||||||
linesCollectionCancel()
|
|
||||||
<-lineCollectionDone
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// use Unbound
|
runError, err = server.Start(ctx)
|
||||||
nameserver.UseDNSInternally(settings.ServerAddress.AsSlice())
|
if err != nil {
|
||||||
err = nameserver.UseDNSSystemWide(l.resolvConf, settings.ServerAddress.AsSlice(),
|
return nil, fmt.Errorf("starting server: %w", err)
|
||||||
*settings.KeepNameserver)
|
}
|
||||||
|
l.server = server
|
||||||
|
|
||||||
|
// use internal DNS server
|
||||||
|
const defaultDNSPort = 53
|
||||||
|
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||||
|
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
|
||||||
|
})
|
||||||
|
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||||
|
IPs: []netip.Addr{settings.ServerAddress},
|
||||||
|
ResolvPath: l.resolvConf,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil {
|
err = check.WaitForDNS(ctx, check.Settings{})
|
||||||
cancel()
|
if err != nil {
|
||||||
<-waitError
|
l.stopServer()
|
||||||
close(waitError)
|
return nil, err
|
||||||
closeStreams()
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cancel, waitError, closeStreams, nil
|
return runError, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ func (s *State) GetSettings() (settings settings.DNS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||||
outcome string) {
|
outcome string,
|
||||||
|
) {
|
||||||
s.settingsMu.Lock()
|
s.settingsMu.Lock()
|
||||||
|
|
||||||
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
||||||
@@ -26,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Check for only update period change
|
// Check for only update period change
|
||||||
tempSettings := s.settings.Copy()
|
tempSettings := s.settings.Copy()
|
||||||
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
||||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||||
|
|
||||||
s.settings = settings
|
s.settings = settings
|
||||||
@@ -39,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||||
if *settings.DoT.Enabled {
|
if *settings.ServerEnabled {
|
||||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||||
}
|
}
|
||||||
return outcome
|
return outcome
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user