Compare commits

..

7 Commits

Author SHA1 Message Date
Quentin McGaw
67ae5f5065 feat(server): role based authentication system (#2434)
- Parse toml configuration file, see https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md#authentication
- Retro-compatible with existing AND documented routes, until after v3.41 release
- Log a warning if an unprotected-by-default route is accessed unprotected
- Authentication methods: none, apikey, basic
- `genkey` command to generate API keys
- move log middleware to internal/server/middlewares/log

Co-authored-by: Joe Jose <45399349+joejose97@users.noreply.github.com>
2024-09-29 17:53:17 +00:00
Quentin McGaw
cbfdb25190 fix(settings): prevent using FREE_ONLY and PORT_FORWARD_ONLY together with protonvpn (see #2470) 2024-09-29 17:53:17 +00:00
Quentin McGaw
638f233b3c fix(storage): add missing selection fields to build noServerFoundError
- `STREAM_ONLY`, `PORT_FORWARD_ONLY`, `SECURE_CORE_ONLY`, `TOR_ONLY` and target ip options affected
- Refers to issue #2470
2024-09-29 17:53:17 +00:00
Quentin McGaw
c450c54d67 fix(ivpn): split city into city and region
- Fix bad city values containing a comma
- update ivpn servers data
2024-09-29 17:53:17 +00:00
Quentin McGaw
d166314f8b fix(nordvpn): remove commas from region values 2024-09-29 17:53:17 +00:00
Quentin McGaw
7064a44403 fix(pia): support port forwarding using Wireguard (#2420)
- Build API IP address using the first 2 bytes of the gateway IP and adding `128.1` to it
- API IP address is valid for both OpenVPN and Wireguard
- Fix #2320
2024-09-29 17:53:17 +00:00
Quentin McGaw
c33158c13c fix(firewall): delete chain rules by line number (#2411)
- Fix #2334
- Parsing of iptables chains, contributing to progress for #1856
2024-09-29 17:53:04 +00:00
443 changed files with 6250 additions and 10217 deletions

View File

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

View File

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

View File

@@ -2,47 +2,68 @@
Development container that can be used with VSCode. Development container that can be used with VSCode.
It works on Linux, Windows (WSL2) and OSX. It works on Linux, Windows and OSX.
## Requirements ## Requirements
- [VS code](https://code.visualstudio.com/download) installed - [VS code](https://code.visualstudio.com/download) installed
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed - [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
- [Docker](https://www.docker.com/products/docker-desktop) installed and running - [Docker](https://www.docker.com/products/docker-desktop) installed and running
- [Docker Compose](https://docs.docker.com/compose/install/) installed
## Setup ## Setup
1. Create the following files and directory on your host if you don't have them: 1. Create the following files on your host if you don't have them:
```sh ```sh
touch ~/.gitconfig ~/.zsh_history touch ~/.gitconfig ~/.zsh_history
mkdir -p ~/.ssh
``` ```
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker. Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
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 `Dev Containers: Open Folder in Container...` and choose the project directory. 1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
## Customization ## Customization
For any customization to take effect, you should "rebuild and reopen": ### Customize the image
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P) You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
2. Select `Dev Containers: Rebuild Container`
Changes you can make are notably: ```Dockerfile
FROM qmcgaw/godevcontainer
RUN apk add curl
```
- Changes to the Docker image in [Dockerfile](Dockerfile) To rebuild the image, either:
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
```json - With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
// Welcome script - With a terminal, go to this directory and `docker-compose build`
{
"source": "/yourpath/.welcome.sh", ### Customize VS code settings
"target": "/root/.welcome.sh",
"type": "bind" You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
},
### 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
``` ```
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/). 1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
1. In the VS code command palette, rebuild the container.

View File

@@ -1,50 +1,16 @@
{ {
"name": "gluetun-dev", "name": "gluetun-dev",
// User defined settings "dockerComposeFile": [
"containerEnv": { "docker-compose.yml"
"TZ": ""
},
// Fixed settings
"build": {
"dockerfile": "./Dockerfile"
},
"postCreateCommand": "~/.windows.sh && go mod download",
"capAdd": [
"NET_ADMIN", // Gluetun specific
"SYS_PTRACE" // for dlv Go debugging
], ],
"securityOpt": [ "service": "vscode",
"seccomp=unconfined" // for dlv Go debugging "runServices": [
], "vscode"
"mounts": [
// 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": [
@@ -81,8 +47,7 @@
}, },
"gopls": { "gopls": {
"usePlaceholders": false, "usePlaceholders": false,
"staticcheck": true, "staticcheck": true
"formatting.gofumpt": true,
}, },
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",
"go.lintOnSave": "package", "go.lintOnSave": "package",

View File

@@ -0,0 +1,28 @@
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" ]

View File

@@ -50,7 +50,6 @@ body:
- Cyberghost - Cyberghost
- ExpressVPN - ExpressVPN
- FastestVPN - FastestVPN
- Giganews
- HideMyAss - HideMyAss
- IPVanish - IPVanish
- IVPN - IVPN

14
.github/labels.yml vendored
View File

@@ -9,9 +9,6 @@
- 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"
@@ -46,8 +43,6 @@
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"
@@ -91,8 +86,7 @@
- name: "☁️ Windscribe" - name: "☁️ Windscribe"
color: "cfe8d4" color: "cfe8d4"
- name: "Category: User error 🤦" - name: "Category: Config problem 📝"
from_name: "Category: Config problem 📝"
color: "ffc7ea" color: "ffc7ea"
- name: "Category: Healthcheck 🩺" - name: "Category: Healthcheck 🩺"
color: "ffc7ea" color: "ffc7ea"
@@ -144,9 +138,3 @@
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"

View File

@@ -76,7 +76,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "^1.23" go-version: "^1.22"
- uses: github/codeql-action/init@v3 - uses: github/codeql-action/init@v3
with: with:
languages: go languages: go
@@ -138,7 +138,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@v6 uses: docker/build-push-action@v5
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 }}

View File

@@ -7,12 +7,27 @@ issues:
- path: _test\.go - path: _test\.go
linters: linters:
- dupl - dupl
- err113 - goerr113
- containedctx - containedctx
- goconst
- maintidx - maintidx
- path: "internal\\/server\\/.+\\.go" - path: "internal\\/server\\/.+\\.go"
linters: linters:
- dupl - 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\\)" - text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
linters: linters:
- ireturn - ireturn
@@ -20,6 +35,18 @@ issues:
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)" text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
linters: linters:
- ireturn - 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:
enable: enable:
@@ -29,19 +56,17 @@ linters:
- asciicheck - asciicheck
- bidichk - bidichk
- bodyclose - bodyclose
- canonicalheader
- containedctx - containedctx
- copyloopvar
- decorder - decorder
- dogsled - dogsled
- dupl - dupl
- dupword - dupword
- durationcheck - durationcheck
- err113
- errchkjson - errchkjson
- errname - errname
- execinquery
- exhaustive - exhaustive
- fatcontext - exportloopref
- forcetypeassert - forcetypeassert
- gci - gci
- gocheckcompilerdirectives - gocheckcompilerdirectives
@@ -52,9 +77,10 @@ linters:
- gocritic - gocritic
- gocyclo - gocyclo
- godot - godot
- gofumpt - goerr113
- goheader - goheader
- goimports - goimports
- gomnd
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
@@ -62,14 +88,12 @@ 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

View File

@@ -1,8 +1,8 @@
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.20
ARG GO_ALPINE_VERSION=3.20 ARG GO_ALPINE_VERSION=3.20
ARG GO_VERSION=1.23 ARG GO_VERSION=1.22
ARG XCPUTRANSLATE_VERSION=v0.6.0 ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.61.0 ARG GOLANGCI_LINT_VERSION=v1.56.2
ARG MOCKGEN_VERSION=v1.6.0 ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64 ARG BUILDPLATFORM=linux/amd64
@@ -91,7 +91,6 @@ 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= \
@@ -139,17 +138,15 @@ ENV VPN_SERVICE_PROVIDER=pia \
SERVER_NUMBER= \ SERVER_NUMBER= \
# # PIA only: # # PIA only:
SERVER_NAMES= \ SERVER_NAMES= \
# # VPNUnlimited and ProtonVPN only:
STREAM_ONLY= \
FREE_ONLY= \
# # ProtonVPN only: # # ProtonVPN only:
FREE_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 and ProtonVPN only: # # PIA 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 \
@@ -169,6 +166,9 @@ ENV VPN_SERVICE_PROVIDER=pia \
DOT=on \ DOT=on \
DOT_PROVIDERS=cloudflare \ DOT_PROVIDERS=cloudflare \
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \ DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
DOT_VERBOSITY=1 \
DOT_VERBOSITY_DETAILS=0 \
DOT_VALIDATION_LOGLEVEL=0 \
DOT_CACHING=on \ DOT_CACHING=on \
DOT_IPV6=off \ DOT_IPV6=off \
BLOCK_MALICIOUS=on \ BLOCK_MALICIOUS=on \
@@ -204,11 +204,9 @@ ENV VPN_SERVICE_PROVIDER=pia \
UPDATER_VPN_SERVICE_PROVIDERS= \ UPDATER_VPN_SERVICE_PROVIDERS= \
# Public IP # Public IP
PUBLICIP_FILE="/tmp/gluetun/ip" \ PUBLICIP_FILE="/tmp/gluetun/ip" \
PUBLICIP_ENABLED=on \ PUBLICIP_PERIOD=12h \
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \ PUBLICIP_API=ipinfo \
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 \
@@ -227,9 +225,10 @@ 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 tzdata && \ apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy unbound tzdata && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \ rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /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

View File

@@ -57,7 +57,7 @@ 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.20 for a small Docker image of 35.6MB
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - Supports: **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 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,8 +73,9 @@ 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), [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) - 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)
- 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

View File

@@ -4,10 +4,8 @@ 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"
@@ -15,9 +13,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"
@@ -34,6 +32,7 @@ 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"
@@ -43,6 +42,7 @@ 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,6 +51,7 @@ 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
@@ -79,7 +80,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.New() cmder := command.NewCmder()
reader := reader.New(reader.Settings{ reader := reader.New(reader.Settings{
Sources: []reader.Source{ Sources: []reader.Source{
@@ -139,14 +140,15 @@ func main() {
} }
} }
var errCommandUnknown = errors.New("command is unknown") var (
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 RunStarter, tun Tun, netLinker netLinker, cmder command.RunStarter,
cli clier, cli clier) error {
) error {
if len(args) > 1 { // cli operation if len(args) > 1 { // cli operation
switch args[1] { switch args[1] {
case "healthcheck": case "healthcheck":
@@ -188,7 +190,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
} }
var allSettings settings.Settings var allSettings settings.Settings
err = allSettings.Read(reader, logger) err = allSettings.Read(reader)
if err != nil { if err != nil {
return err return err
} }
@@ -239,7 +241,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, *allSettings.Storage.Filepath) storage, err := storage.New(storageLogger, constants.ServersData)
if err != nil { if err != nil {
return err return err
} }
@@ -249,7 +251,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, logger) err = allSettings.Validate(storage, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
@@ -269,11 +271,16 @@ 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 {
@@ -286,13 +293,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.Warn(warning) logger.Warn(warning)
} }
const permission = fs.FileMode(0o644) if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
err = os.MkdirAll("/tmp/gluetun", permission)
if err != nil {
return err return err
} }
err = os.MkdirAll("/gluetun", permission) if err := os.MkdirAll("/gluetun", 0644); err != nil {
if err != nil {
return err return err
} }
@@ -304,8 +308,15 @@ 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?")
@@ -364,8 +375,7 @@ 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...)
@@ -388,29 +398,28 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return fmt.Errorf("starting port forwarding loop: %w", err) return fmt.Errorf("starting port forwarding loop: %w", err)
} }
dnsLogger := logger.New(log.SetComponent("dns")) unboundLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient, unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
dnsLogger) unboundLogger)
if err != nil {
return fmt.Errorf("creating DNS loop: %w", err)
}
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler( dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
"dns", goroutine.OptionTimeout(defaultShutdownTimeout)) "unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker // wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
go dnsLooper.Run(dnsCtx, dnsDone) go unboundLooper.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 dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone) go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
controlGroupHandler.Add(dnsTickerHandler) controlGroupHandler.Add(dnsTickerHandler)
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient, publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API)
logger.New(log.SetComponent("ip getter"))) ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken)
if err != nil { if err != nil {
return fmt.Errorf("creating public ip loop: %w", err) return fmt.Errorf("creating public IP API client: %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)
@@ -422,12 +431,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
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, publicIPLooper.Fetcher(), openvpnFileExtractor) httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
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, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient, cmder, publicIPLooper, unboundLooper, 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,7 +477,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging, httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")), logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath, allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper, buildInfo, vpnLooper, portForwardLooper, unboundLooper, 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)
@@ -528,8 +537,7 @@ type infoer interface {
} }
func printVersions(ctx context.Context, logger infoer, func printVersions(ctx context.Context, logger infoer,
elements []printVersionElement, elements []printVersionElement) (err error) {
) (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()
@@ -597,9 +605,3 @@ 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)
}

55
go.mod
View File

@@ -1,62 +1,57 @@
module github.com/qdm12/gluetun module github.com/qdm12/gluetun
go 1.23 go 1.22
require ( require (
github.com/breml/rootcerts v0.2.18 github.com/breml/rootcerts v0.2.17
github.com/fatih/color v1.18.0 github.com/fatih/color v1.17.0
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.9 github.com/klauspost/compress v1.17.8
github.com/klauspost/pgzip v1.2.6 github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.2 github.com/pelletier/go-toml/v2 v2.2.2
github.com/qdm12/dns/v2 v2.0.0-rc7 github.com/qdm12/dns v1.11.0
github.com/qdm12/gosettings v0.4.3 github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/gosettings v0.4.2
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.3.0 github.com/qdm12/gotree v0.2.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.9.0 github.com/stretchr/testify v1.9.0
github.com/ulikunitz/xz v0.5.11 github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.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/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/net v0.25.0
golang.org/x/net v0.30.0 golang.org/x/sys v0.20.0
golang.org/x/sys v0.26.0 golang.org/x/text v0.15.0
golang.org/x/text v0.19.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/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.5.9 // 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.62 // indirect github.com/miekg/dns v1.1.40 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mr-tron/base58 v1.2.0 // 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.0 // 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.4 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
golang.org/x/crypto v0.28.0 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
golang.org/x/mod v0.21.0 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/tools v0.26.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/sync v0.1.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.70 // indirect kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
) )

220
go.sum
View File

@@ -1,32 +1,69 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I=
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.12.0/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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=
@@ -36,48 +73,55 @@ 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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/qdm12/gosettings v0.4.2 h1:Gb39NScPr7OQV+oy0o1OD7A121udITDJuUGa7ljDF58=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/qdm12/gosettings v0.4.2/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/dns/v2 v2.0.0-rc7 h1:noFoWunMfFuJeIpcyTJsdS9BU7jRxxKrBV5xN2qjPHI=
github.com/qdm12/dns/v2 v2.0.0-rc7/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/gosettings v0.4.3 h1:oGAjiKVtml9oHVlPQo6H3yk6TmtWpVYicNeGFcM7AP8=
github.com/qdm12/gosettings v0.4.3/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM= github.com/qdm12/goshutdown v0.3.0 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.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI= github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw= github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -85,55 +129,88 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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=
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
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/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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=
@@ -143,19 +220,26 @@ 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/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.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=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw= 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/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4= inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
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 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
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=

View File

@@ -3,13 +3,14 @@ package alpine
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/user" "os/user"
"strconv" "strconv"
) )
var ErrUserAlreadyExists = errors.New("user already exists") var (
ErrUserAlreadyExists = errors.New("user already exists")
)
// CreateUser creates a user in Alpine with the given UID. // CreateUser creates a user in Alpine with the given UID.
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) { func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
@@ -38,8 +39,7 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
ErrUserAlreadyExists, username, u.Uid, uid) ErrUserAlreadyExists, username, u.Uid, uid)
} }
const permission = fs.FileMode(0o644) file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -27,6 +27,9 @@ func (c *CLI) ClientKey(args []string) error {
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
return err return err
} }
if err != nil {
return err
}
s := string(data) s := string(data)
s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\r", "")

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -17,13 +16,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))
@@ -44,7 +43,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' or 'json'") flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
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 {
@@ -54,7 +53,9 @@ func (c *CLI) FormatServers(args []string) error {
return err return err
} }
// Note the format is validated by storage.Format if format != "markdown" {
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
@@ -86,14 +87,10 @@ func (c *CLI) FormatServers(args []string) error {
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }
formatted, err := storage.Format(providerToFormat, format) formatted := storage.FormatToMarkdown(providerToFormat)
if err != nil {
return fmt.Errorf("formatting servers: %w", err)
}
output = filepath.Clean(output) output = filepath.Clean(output)
const permission = fs.FileMode(0o644) file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
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)
} }

View File

@@ -34,7 +34,7 @@ func base58Encode(data []byte) string {
} }
// integer simplification of ceil(log(256)/log(58)) // integer simplification of ceil(log(256)/log(58))
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:gomnd
size := zcount + ceilLog256Div58 size := zcount + ceilLog256Div58
output := make([]byte, size) output := make([]byte, size)
@@ -43,7 +43,7 @@ func base58Encode(data []byte) string {
for _, b := range data { for _, b := range data {
i := size - 1 i := size - 1
for carry := uint32(b); i > high || carry != 0; i-- { for carry := uint32(b); i > high || carry != 0; i-- {
carry += 256 * uint32(output[i]) //nolint:mnd carry += 256 * uint32(output[i]) //nolint:gomnd
output[i] = byte(carry % radix) output[i] = byte(carry % radix)
carry /= radix carry /= radix
} }

View File

@@ -1,9 +1,16 @@
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) Info(string) {} func (l *noopLogger) Debug(string) {}
func (l *noopLogger) Info(string) {}
func (l *noopLogger) Warn(string) {}
func (l *noopLogger) Error(string) {}
func (l *noopLogger) PatchLevel(logging.Level) {}
func (l *noopLogger) PatchPrefix(string) {}

View File

@@ -34,8 +34,6 @@ 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)
} }
@@ -44,15 +42,14 @@ type IPv6Checker interface {
} }
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
ipv6Checker IPv6Checker, ipv6Checker IPv6Checker) error {
) 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, logger) err = allSettings.Read(reader)
if err != nil { if err != nil {
return err return err
} }
@@ -62,7 +59,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
return fmt.Errorf("checking for IPv6 support: %w", err) return fmt.Errorf("checking for IPv6 support: %w", err)
} }
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil { if err = allSettings.Validate(storage, ipv6Supported); err != nil {
return fmt.Errorf("validating settings: %w", err) return fmt.Errorf("validating settings: %w", err)
} }

View File

@@ -80,17 +80,10 @@ 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)
nameTokenPairs := []api.NameToken{ ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken)
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
}
fetchers, err := api.New(nameTokenPairs, httpClient)
if err != nil { if err != nil {
return fmt.Errorf("creating public IP fetchers: %w", err) return fmt.Errorf("creating public IP API client: %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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,10 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/netip" "net/netip"
"regexp" "regexp"
"github.com/qdm12/dns/v2/pkg/blockbuilder" "github.com/qdm12/dns/pkg/blacklist"
"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"
@@ -75,19 +74,16 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
} }
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) ( func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
settings blockbuilder.Settings, return blacklist.BuilderSettings{
) { BlockMalicious: *b.BlockMalicious,
return blockbuilder.Settings{ BlockAds: *b.BlockAds,
Client: client, BlockSurveillance: *b.BlockSurveillance,
BlockMalicious: b.BlockMalicious,
BlockAds: b.BlockAds,
BlockSurveillance: b.BlockSurveillance,
AllowedHosts: b.AllowedHosts, AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts, AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs, AddBlockedIPs: netipAddressesToNetaddrIPs(b.AddBlockedIPs),
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes, AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes),
} }, nil
} }
func (b DNSBlacklist) String() string { func (b DNSBlacklist) String() string {
@@ -102,30 +98,30 @@ 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.Append("Allowed hosts:") allowedHostsNode := node.Appendf("Allowed hosts:")
for _, host := range b.AllowedHosts { for _, host := range b.AllowedHosts {
allowedHostsNode.Append(host) allowedHostsNode.Appendf(host)
} }
} }
if len(b.AddBlockedHosts) > 0 { if len(b.AddBlockedHosts) > 0 {
blockedHostsNode := node.Append("Blocked hosts:") blockedHostsNode := node.Appendf("Blocked hosts:")
for _, host := range b.AddBlockedHosts { for _, host := range b.AddBlockedHosts {
blockedHostsNode.Append(host) blockedHostsNode.Appendf(host)
} }
} }
if len(b.AddBlockedIPs) > 0 { if len(b.AddBlockedIPs) > 0 {
blockedIPsNode := node.Append("Blocked IP addresses:") blockedIPsNode := node.Appendf("Blocked IP addresses:")
for _, ip := range b.AddBlockedIPs { for _, ip := range b.AddBlockedIPs {
blockedIPsNode.Append(ip.String()) blockedIPsNode.Appendf(ip.String())
} }
} }
if len(b.AddBlockedIPPrefixes) > 0 { if len(b.AddBlockedIPPrefixes) > 0 {
blockedIPPrefixesNode := node.Append("Blocked IP networks:") blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
for _, ipNetwork := range b.AddBlockedIPPrefixes { for _, ipNetwork := range b.AddBlockedIPPrefixes {
blockedIPPrefixesNode.Append(ipNetwork.String()) blockedIPPrefixesNode.Appendf(ipNetwork.String())
} }
} }
@@ -160,11 +156,12 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
return nil return nil
} }
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range") var (
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
)
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr, func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error, ipPrefixes []netip.Prefix, err error) {
) {
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS") privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
if len(privateAddresses) == 0 { if len(privateAddresses) == 0 {
return nil, nil, nil return nil, nil, nil

View File

@@ -3,10 +3,8 @@ package settings
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/netip"
"time" "time"
"github.com/qdm12/dns/v2/pkg/provider"
"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"
@@ -18,24 +16,22 @@ type DoT struct {
// and used. It defaults to true, and cannot be nil // and used. It defaults to true, and cannot be nil
// in the internal state. // in the internal state.
Enabled *bool Enabled *bool
// UpdatePeriod is the period to update DNS block lists. // 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 can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in // It defaults to 24h and cannot be nil in
// the internal state. // the internal state.
UpdatePeriod *time.Duration UpdatePeriod *time.Duration
// Providers is a list of DNS over TLS providers // Unbound contains settings to configure Unbound.
Providers []string `json:"providers"` Unbound Unbound
// Caching is true if the DoT server should cache
// DNS responses.
Caching *bool `json:"caching"`
// IPv6 is true if the DoT server should connect over IPv6.
IPv6 *bool `json:"ipv6"`
// Blacklist contains settings to configure the filter // Blacklist contains settings to configure the filter
// block lists. // block lists.
Blacklist DNSBlacklist Blacklist DNSBlacklist
} }
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short") var (
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DoT) validate() (err error) { func (d DoT) validate() (err error) {
const minUpdatePeriod = 30 * time.Second const minUpdatePeriod = 30 * time.Second
@@ -44,12 +40,9 @@ func (d DoT) validate() (err error) {
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
} }
providers := provider.NewProviders() err = d.Unbound.validate()
for _, providerName := range d.Providers { if err != nil {
_, err := providers.Get(providerName) return err
if err != nil {
return err
}
} }
err = d.Blacklist.validate() err = d.Blacklist.validate()
@@ -64,9 +57,7 @@ func (d *DoT) copy() (copied DoT) {
return DoT{ return DoT{
Enabled: gosettings.CopyPointer(d.Enabled), Enabled: gosettings.CopyPointer(d.Enabled),
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers), Unbound: d.Unbound.copy(),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(), Blacklist: d.Blacklist.copy(),
} }
} }
@@ -77,9 +68,7 @@ func (d *DoT) copy() (copied DoT) {
func (d *DoT) overrideWith(other DoT) { func (d *DoT) overrideWith(other DoT) {
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled) d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers) d.Unbound.overrideWith(other.Unbound)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist) d.Blacklist.overrideWith(other.Blacklist)
} }
@@ -87,26 +76,10 @@ func (d *DoT) setDefaults() {
d.Enabled = gosettings.DefaultPointer(d.Enabled, true) d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
const defaultUpdatePeriod = 24 * time.Hour const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{ d.Unbound.setDefaults()
provider.Cloudflare().Name,
})
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults() d.Blacklist.setDefaults()
} }
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.DoT.IPv4[0].Addr()
}
func (d DoT) String() string { func (d DoT) String() string {
return d.toLinesNode().String() return d.toLinesNode().String()
} }
@@ -125,14 +98,7 @@ func (d DoT) toLinesNode() (node *gotree.Node) {
} }
node.Appendf("Update period: %s", update) node.Appendf("Update period: %s", update)
upstreamResolvers := node.Append("Upstream resolvers:") node.AppendNode(d.Unbound.toLinesNode())
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
node.AppendNode(d.Blacklist.toLinesNode()) node.AppendNode(d.Blacklist.toLinesNode())
return node return node
@@ -149,14 +115,7 @@ func (d *DoT) read(reader *reader.Reader) (err error) {
return err return err
} }
d.Providers = reader.CSV("DOT_PROVIDERS") err = d.Unbound.read(reader)
d.Caching, err = reader.BoolPtr("DOT_CACHING")
if err != nil {
return err
}
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
if err != nil { if err != nil {
return err return err
} }

View File

@@ -30,6 +30,7 @@ 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")

View File

@@ -104,6 +104,7 @@ 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)
} }
} }

View File

@@ -59,6 +59,7 @@ 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()

View File

@@ -1,30 +1,4 @@
package settings package settings
import gomock "github.com/golang/mock/gomock" func boolPtr(b bool) *bool { return &b }
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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
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
}

View File

@@ -4,8 +4,7 @@ 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)

View File

@@ -155,8 +155,7 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
} }
func validateOpenVPNConfigFilepath(isCustom bool, func validateOpenVPNConfigFilepath(isCustom bool,
confFile string, confFile string) (err error) {
) (err error) {
if !isCustom { if !isCustom {
return nil return nil
} }
@@ -180,8 +179,7 @@ func validateOpenVPNConfigFilepath(isCustom bool,
} }
func validateOpenVPNClientCertificate(vpnProvider, func validateOpenVPNClientCertificate(vpnProvider,
clientCert string, clientCert string) (err error) {
) (err error) {
switch vpnProvider { switch vpnProvider {
case case
providers.Airvpn, providers.Airvpn,
@@ -228,8 +226,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
} }
func validateOpenVPNEncryptedKey(vpnProvider, func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string, encryptedPrivateKey string) (err error) {
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" { if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue) return fmt.Errorf("%w", ErrMissingValue)
} }

View File

@@ -32,6 +32,7 @@ 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()

View File

@@ -50,7 +50,6 @@ 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,
@@ -68,7 +67,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.Giganews, providers.Ipvanish, providers.Nordvpn, 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:
@@ -193,6 +192,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"))
if err != nil {
return err
}
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT", o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT")) reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))

View File

@@ -52,9 +52,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,
providers.Mullvad,
} }
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil { if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err) return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)

View File

@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
t.Parallel() t.Parallel()
settings := PortForwarding{ settings := PortForwarding{
Enabled: ptrTo(false), Enabled: boolPtr(false),
} }
s := settings.String() s := settings.String()

View File

@@ -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, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) { func (p *Provider) validate(vpnType string, storage Storage) (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, filterChoicesGetter FilterChoicesGet
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err) return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
} }
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner) err = p.ServerSelection.validate(p.Name, storage)
if err != nil { if err != nil {
return fmt.Errorf("server selection: %w", err) return fmt.Errorf("server selection: %w", err)
} }

View File

@@ -3,6 +3,7 @@ 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"
@@ -12,28 +13,24 @@ import (
// PublicIP contains settings for port forwarding. // PublicIP contains settings for port forwarding.
type PublicIP struct { type PublicIP struct {
// Enabled is set to true to fetch the public ip address // Period is the period to get the public IP address.
// information on VPN connection. It defaults to true. // It can be set to 0 to disable periodic checking.
Enabled *bool // It cannot be nil for the internal state.
// 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
// APIs is the list of public ip APIs to use to fetch public IP information. // API is the API name to use to fetch public IP information.
// If there is more than one API, the first one is used // It can be ipinfo or ip2location. It defaults to ipinfo.
// by default and the others are used as fallbacks in case of API string
// the service rate limiting us. It defaults to use all services, // APIToken is the token to use for the IP data service
// with the first one being ipinfo.io for historical reasons. // such as ipinfo.io. It can be the empty string to
APIs []PublicIPAPI // indicate not to use a token. It cannot be nil for the
} // 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
@@ -51,6 +48,12 @@ 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 {
@@ -58,11 +61,9 @@ func (p PublicIP) validate() (err error) {
} }
} }
for _, publicIPAPI := range p.APIs { _, err = api.ParseProvider(p.API)
_, err = api.ParseProvider(publicIPAPI.Name) if err != nil {
if err != nil { return fmt.Errorf("API name: %w", err)
return fmt.Errorf("API name: %w", err)
}
} }
return nil return nil
@@ -70,27 +71,26 @@ func (p PublicIP) validate() (err error) {
func (p *PublicIP) copy() (copied PublicIP) { func (p *PublicIP) copy() (copied PublicIP) {
return PublicIP{ return PublicIP{
Enabled: gosettings.CopyPointer(p.Enabled), Period: gosettings.CopyPointer(p.Period),
IPFilepath: gosettings.CopyPointer(p.IPFilepath), IPFilepath: gosettings.CopyPointer(p.IPFilepath),
APIs: gosettings.CopySlice(p.APIs), API: p.API,
APIToken: gosettings.CopyPointer(p.APIToken),
} }
} }
func (p *PublicIP) overrideWith(other PublicIP) { func (p *PublicIP) overrideWith(other PublicIP) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled) p.Period = gosettings.OverrideWithPointer(p.Period, other.Period)
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath) p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs) p.API = gosettings.OverrideWithComparable(p.API, other.API)
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
} }
func (p *PublicIP) setDefaults() { func (p *PublicIP) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, true) const defaultPeriod = 12 * time.Hour
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.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{ p.API = gosettings.DefaultComparable(p.API, "ipinfo")
{Name: string(api.IPInfo)}, p.APIToken = gosettings.DefaultPointer(p.APIToken, "")
{Name: string(api.Cloudflare)},
{Name: string(api.IfConfigCo)},
{Name: string(api.IP2Location)},
})
} }
func (p PublicIP) String() string { func (p PublicIP) String() string {
@@ -98,78 +98,41 @@ func (p PublicIP) String() string {
} }
func (p PublicIP) toLinesNode() (node *gotree.Node) { func (p PublicIP) toLinesNode() (node *gotree.Node) {
if !*p.Enabled { node = gotree.New("Public IP settings:")
return gotree.New("Public IP settings: disabled")
if *p.Period == 0 {
node.Appendf("Enabled: no")
return node
} }
node = gotree.New("Public IP settings:") 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)
} }
baseAPIString := "Public IP data base API: " + p.APIs[0].Name node.Appendf("Public IP data API: %s", p.API)
if p.APIs[0].Token != "" {
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")" if *p.APIToken != "" {
} 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, warner Warner) (err error) { func (p *PublicIP) read(r *reader.Reader) (err error) {
p.Enabled, err = readPublicIPEnabled(r, warner) p.Period, err = r.DurationPtr("PUBLICIP_PERIOD")
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")
apiNames := r.CSV("PUBLICIP_API") p.APIToken = r.Get("PUBLICIP_API_TOKEN")
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
}

View File

@@ -1,161 +0,0 @@
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)
}
})
}
}

View File

@@ -91,15 +91,14 @@ var (
) )
func (ss *ServerSelection) validate(vpnServiceProvider string, func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner, storage Storage) (err error) {
) (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, filterChoicesGetter, warner) filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
if err != nil { if err != nil {
return err // already wrapped error return err // already wrapped error
} }
@@ -112,7 +111,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
*ss = surfsharkRetroRegion(*ss) *ss = surfsharkRetroRegion(*ss)
} }
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner) err = validateServerFilters(*ss, filterChoices, vpnServiceProvider)
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)
} }
@@ -143,20 +142,19 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
} }
func getLocationFilterChoices(vpnServiceProvider string, func getLocationFilterChoices(vpnServiceProvider string,
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) ( ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
filterChoices models.FilterChoices, err error, 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 := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner) err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions)
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 = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner) err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err) return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
} }
@@ -167,29 +165,28 @@ 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, warner Warner, vpnServiceProvider string) (err error) {
) (err error) { err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries)
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 = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner) err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrRegionNotValid, err) return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner) err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCityNotValid, err) return fmt.Errorf("%w: %w", ErrCityNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner) err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrISPNotValid, err) return fmt.Errorf("%w: %w", ErrISPNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner) err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err) return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
} }
@@ -208,12 +205,12 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
ErrNameNotValid, len(settings.Names)) ErrNameNotValid, len(settings.Names))
} }
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner) err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err) return fmt.Errorf("%w: %w", ErrNameNotValid, err)
} }
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner) err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err) return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
} }
@@ -221,43 +218,6 @@ 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 &&
@@ -460,8 +420,7 @@ func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
} }
func (ss *ServerSelection) read(r *reader.Reader, func (ss *ServerSelection) read(r *reader.Reader,
vpnProvider, vpnType string, vpnProvider, vpnType string) (err error) {
) (err error) {
ss.VPN = vpnType ss.VPN = vpnType
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP", ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",

View File

@@ -22,7 +22,6 @@ 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
@@ -30,16 +29,14 @@ type Settings struct {
Pprof pprof.Settings Pprof pprof.Settings
} }
type FilterChoicesGetter interface { type Storage 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(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
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,
@@ -49,13 +46,12 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"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(filterChoicesGetter, ipv6Supported, warner) return s.VPN.Validate(storage, ipv6Supported)
}, },
} }
@@ -79,7 +75,6 @@ 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(),
@@ -89,8 +84,7 @@ func (s *Settings) copy() (copied Settings) {
} }
func (s *Settings) OverrideWith(other Settings, func (s *Settings) OverrideWith(other Settings,
filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner, storage Storage, ipv6Supported bool) (err error) {
) (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)
@@ -100,13 +94,12 @@ 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(filterChoicesGetter, ipv6Supported, warner) err = patchedSettings.Validate(storage, ipv6Supported)
if err != nil { if err != nil {
return err return err
} }
@@ -123,7 +116,6 @@ 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()
@@ -146,7 +138,6 @@ 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())
@@ -186,12 +177,7 @@ func (s Settings) Warnings() (warnings []string) {
return warnings return warnings
} }
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) { func (s *Settings) Read(r *reader.Reader) (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,
@@ -199,16 +185,13 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (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": func(r *reader.Reader) error { "public ip": s.PublicIP.read,
return s.PublicIP.read(r, warner) "shadowsocks": s.Shadowsocks.read,
}, "system": s.System.read,
"shadowsocks": s.Shadowsocks.read, "updater": s.Updater.read,
"storage": s.Storage.read, "version": s.Version.read,
"system": s.System.read, "VPN": s.VPN.read,
"updater": s.Updater.read, "profiling": s.Pprof.Read,
"version": s.Version.read,
"VPN": s.VPN.read,
"profiling": s.Pprof.Read,
} }
for name, read := range readFunctions { for name, read := range readFunctions {

View File

@@ -43,10 +43,18 @@ func Test_Settings_String(t *testing.T) {
| └── DNS over TLS settings: | └── DNS over TLS settings:
| ├── Enabled: yes | ├── Enabled: yes
| ├── Update period: every 24h0m0s | ├── Update period: every 24h0m0s
| ├── Upstream resolvers: | ├── Unbound settings:
| | ── Cloudflare | | ── Authoritative servers:
| ├── Caching: yes | | | └── Cloudflare
| ├── IPv6: no | | ├── Caching: yes
| | ├── IPv6: no
| | ├── Verbosity level: 1
| | ├── Verbosity details level: 0
| | ├── Validation log level: 0
| | ├── System user: root
| | └── Allowed networks:
| | ├── 0.0.0.0/0
| | └── ::/0
| └── DNS filtering settings: | └── DNS filtering settings:
| ├── Block malicious: yes | ├── Block malicious: yes
| ├── Block ads: no | ├── Block ads: no
@@ -72,24 +80,20 @@ func Test_Settings_String(t *testing.T) {
| ├── Listening address: :8000 | ├── Listening address: :8000
| ├── Logging: yes | ├── Logging: yes
| └── Authentication file path: /gluetun/auth/config.toml | └── 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 base API: ipinfo | ── Public IP data 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()

View File

@@ -1,59 +0,0 @@
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
}

View File

@@ -7,8 +7,7 @@ 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))

View File

@@ -0,0 +1,223 @@
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
}

View File

@@ -0,0 +1,43 @@
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)
}

View File

@@ -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(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) { func (v *VPN) Validate(storage Storage, ipv6Supported bool) (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, filterChoicesGetter, warner) err = v.Provider.validate(v.Type, storage)
if err != nil { if err != nil {
return fmt.Errorf("provider settings: %w", err) return fmt.Errorf("provider settings: %w", err)
} }

View File

@@ -215,12 +215,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.Append(address.String()) addressesNode.Appendf(address.String())
} }
allowedIPsNode := node.Appendf("Allowed IPs:") allowedIPsNode := node.Appendf("Allowed IPs:")
for _, allowedIP := range w.AllowedIPs { for _, allowedIP := range w.AllowedIPs {
allowedIPsNode.Append(allowedIP.String()) allowedIPsNode.Appendf(allowedIP.String())
} }
if *w.PersistentKeepaliveInterval > 0 { if *w.PersistentKeepaliveInterval > 0 {

View File

@@ -34,7 +34,9 @@ type WireguardConfig struct {
EndpointPort *string EndpointPort *string
} }
var regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`) var (
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)
@@ -66,18 +68,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 ErrEndpointHostNotIP = errors.New("endpoint host is not an IP") var (
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")
@@ -94,7 +96,9 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
return preSharedKey, publicKey, endpointIP, endpointPort return preSharedKey, publicKey, endpointIP, endpointPort
} }
var regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`) var (
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)

View File

@@ -1,7 +1,6 @@
package files package files
import ( import (
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -73,12 +72,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")
const permission = fs.FileMode(0o600) err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
require.NoError(t, err) require.NoError(t, err)
wireguard, err := ParseWireguardConf(configFile) wireguard, err := ParseWireguardConf(configFile)
@@ -122,6 +121,7 @@ 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,6 +182,7 @@ 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()

View File

@@ -1,7 +1,6 @@
package secrets package secrets
import ( import (
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -39,8 +38,7 @@ 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")
const permission = fs.FileMode(0o600) err = os.WriteFile(secretFilepath, nil, os.ModePerm)
err = os.WriteFile(secretFilepath, nil, permission)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -55,8 +53,7 @@ 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")
const permission = fs.FileMode(0o600) err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -72,8 +69,7 @@ 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")
const permission = fs.FileMode(0o600) err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -91,6 +87,7 @@ 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()

View File

@@ -2,6 +2,10 @@ 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)
} }

View File

@@ -9,7 +9,6 @@ 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"
@@ -38,7 +37,6 @@ func All() []string {
Cyberghost, Cyberghost,
Expressvpn, Expressvpn,
Fastestvpn, Fastestvpn,
Giganews,
HideMyAss, HideMyAss,
Ipvanish, Ipvanish,
Ivpn, Ivpn,

View File

@@ -0,0 +1,15 @@
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)
}

75
internal/dns/logs.go Normal file
View File

@@ -0,0 +1,75 @@
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
}

48
internal/dns/logs_test.go Normal file
View File

@@ -0,0 +1,48 @@
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)
})
}
}

View File

@@ -2,12 +2,10 @@ package dns
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter" "github.com/qdm12/dns/pkg/blacklist"
"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"
@@ -18,9 +16,9 @@ import (
type Loop struct { type Loop struct {
statusManager *loopstate.State statusManager *loopstate.State
state *state.State state *state.State
server *server.Server conf Configurator
filter *mapfilter.Filter
resolvConf string resolvConf string
blockBuilder blacklist.Builder
client *http.Client client *http.Client
logger Logger logger Logger
userTrigger bool userTrigger bool
@@ -36,9 +34,8 @@ type Loop struct {
const defaultBackoffTime = 10 * time.Second const defaultBackoffTime = 10 * time.Second
func NewLoop(settings settings.DNS, func NewLoop(conf Configurator, settings settings.DNS,
client *http.Client, logger Logger, client *http.Client, logger Logger) *Loop {
) (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{})
@@ -48,17 +45,12 @@ func NewLoop(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{})
if err != nil {
return nil, fmt.Errorf("creating map filter: %w", err)
}
return &Loop{ return &Loop{
statusManager: statusManager, statusManager: statusManager,
state: state, state: state,
server: nil, conf: conf,
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,
@@ -70,7 +62,7 @@ func NewLoop(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) {

View File

@@ -2,22 +2,36 @@ package dns
import ( import (
"net/netip" "net/netip"
"time"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/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 // Try with user provided plaintext ip address
// if it's not 127.0.0.1 (default for DoT), otherwise // if it's not 127.0.0.1 (default for DoT)
// use the first DoT provider ipv4 address found. targetIP := settings.ServerAddress
var targetIP netip.Addr if targetIP.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 { if fallback {
targetIP = settings.ServerAddress l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
} else { } else {
targetIP = settings.DoT.GetFirstPlaintextIPv4() 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 {
@@ -25,19 +39,9 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
} 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 dialTimeout = 3 * time.Second const keepNameserver = false
settingsInternalDNS := nameserver.SettingsInternalDNS{ err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
IP: targetIP,
Timeout: dialTimeout,
}
nameserver.UseDNSInternally(settingsInternalDNS)
settingsSystemWide := nameserver.SettingsSystemDNS{
IP: targetIP,
ResolvPath: l.resolvConf,
}
err := nameserver.UseDNSSystemWide(settingsSystemWide)
if err != nil { if err != nil {
l.logger.Error(err.Error()) l.logger.Error(err.Error())
} }

View File

@@ -26,14 +26,16 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
} }
for ctx.Err() == nil { for ctx.Err() == nil {
// Upper scope variables for the DNS over TLS server only // Upper scope variables for Unbound only
// Their values are to be used if DOT=off // Their values are to be used if DOT=off
var runError <-chan error waitError := make(chan error)
unboundCancel := func() { waitError <- nil }
closeStreams := func() {}
settings := l.GetSettings() settings := l.GetSettings()
for !*settings.KeepNameserver && *settings.DoT.Enabled { for !*settings.KeepNameserver && *settings.DoT.Enabled {
var err error var err error
runError, err = l.setupServer(ctx) unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
if err == nil { if err == nil {
l.backoffTime = defaultBackoffTime l.backoffTime = defaultBackoffTime
l.logger.Info("ready") l.logger.Info("ready")
@@ -47,12 +49,11 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
return return
} }
if !errors.Is(err, errUpdateBlockLists) { if !errors.Is(err, errUpdateFiles) {
const fallback = true const fallback = true
l.useUnencryptedDNS(fallback) l.useUnencryptedDNS(fallback)
} }
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
settings = l.GetSettings()
} }
settings = l.GetSettings() settings = l.GetSettings()
@@ -63,44 +64,40 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.userTrigger = false l.userTrigger = false
exitLoop := l.runWait(ctx, runError) stayHere := true
if exitLoop { for stayHere {
return select {
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():
l.stopServer()
// TODO revert OS and Go nameserver when exiting
return true
case <-l.stop:
l.userTrigger = true
l.logger.Info("stopping")
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 DoT server: " + stopErr.Error())
}
}

View File

@@ -2,74 +2,13 @@ package dns
import ( import (
"context" "context"
"fmt"
"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/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 buildDoTSettings(settings settings.DNS,
filter *mapfilter.Filter, logger Logger) (
serverSettings server.Settings, err error,
) {
serverSettings.Logger = logger
var dotSettings dot.Settings
dotSettings.Warner = logger
providersData := provider.NewProviders()
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
for i := range settings.DoT.Providers {
var err error
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
if err != nil {
panic(err) // this should already had been checked
}
}
dotSettings.IPVersion = "ipv4"
if *settings.DoT.IPv6 {
dotSettings.IPVersion = "ipv6"
}
serverSettings.Dialer, err = dot.New(dotSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
}
if *settings.DoT.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)
return serverSettings, nil
}

View File

@@ -4,55 +4,59 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"github.com/qdm12/dns/v2/pkg/check" "github.com/qdm12/dns/pkg/check"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/pkg/nameserver"
"github.com/qdm12/dns/v2/pkg/server"
) )
var errUpdateBlockLists = errors.New("cannot update filter block lists") var errUpdateFiles = errors.New("cannot update files")
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) { // Returning cancel == nil signals we want to re-run setupUnbound
// Returning err == errUpdateFiles signals we should not fall back
// on the plaintext DNS as DOT is still up and running.
func (l *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, fmt.Errorf("%w: %w", errUpdateBlockLists, err) return nil, nil, nil,
fmt.Errorf("%w: %s", errUpdateFiles, err)
} }
settings := l.GetSettings() settings := l.GetSettings()
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger) unboundCtx, cancel := context.WithCancel(context.Background())
stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx,
*settings.DoT.Unbound.VerbosityDetailsLevel)
if err != nil { if err != nil {
return nil, fmt.Errorf("building DoT settings: %w", err) cancel()
return nil, nil, nil, err
} }
server, err := server.New(dotSettings) linesCollectionCtx, linesCollectionCancel := context.WithCancel(context.Background())
if err != nil { lineCollectionDone := make(chan struct{})
return nil, fmt.Errorf("creating DoT server: %w", err) go l.collectLines(linesCollectionCtx, lineCollectionDone,
stdoutLines, stderrLines)
closeStreams = func() {
linesCollectionCancel()
<-lineCollectionDone
} }
runError, err = server.Start(ctx) // use Unbound
if err != nil { nameserver.UseDNSInternally(settings.ServerAddress.AsSlice())
return nil, fmt.Errorf("starting server: %w", err) err = nameserver.UseDNSSystemWide(l.resolvConf, settings.ServerAddress.AsSlice(),
} *settings.KeepNameserver)
l.server = server
// use internal DNS server
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
IP: settings.ServerAddress,
})
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
IP: settings.ServerAddress,
ResolvPath: l.resolvConf,
})
if err != nil { if err != nil {
l.logger.Error(err.Error()) l.logger.Error(err.Error())
} }
err = check.WaitForDNS(ctx, check.Settings{}) if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil {
if err != nil { cancel()
l.stopServer() <-waitError
return nil, err close(waitError)
closeStreams()
return nil, nil, nil, err
} }
return runError, nil return cancel, waitError, closeStreams, nil
} }

View File

@@ -15,8 +15,7 @@ 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)

View File

@@ -10,8 +10,7 @@ import (
func New(statusApplier StatusApplier, func New(statusApplier StatusApplier,
settings settings.DNS, settings settings.DNS,
updateTicker chan<- struct{}, updateTicker chan<- struct{}) *State {
) *State {
return &State{ return &State{
statusApplier: statusApplier, statusApplier: statusApplier,
settings: settings, settings: settings,

View File

@@ -11,7 +11,6 @@ func (l *Loop) GetStatus() (status models.LoopStatus) {
} }
func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) ( func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) (
outcome string, err error, outcome string, err error) {
) {
return l.statusManager.ApplyStatus(ctx, status) return l.statusManager.ApplyStatus(ctx, status)
} }

View File

@@ -34,7 +34,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
if err := l.updateFiles(ctx); err != nil { if err := l.updateFiles(ctx); err != nil {
l.statusManager.SetStatus(constants.Crashed) l.statusManager.SetStatus(constants.Crashed)
l.logger.Error(err.Error()) l.logger.Error(err.Error())
l.logger.Warn("skipping DNS server restart due to failed files update") l.logger.Warn("skipping Unbound restart due to failed files update")
continue continue
} }
} }

View File

@@ -1,46 +1,35 @@
package dns package dns
import ( import "context"
"context"
"fmt"
"github.com/qdm12/dns/v2/pkg/blockbuilder"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
)
func (l *Loop) updateFiles(ctx context.Context) (err error) { func (l *Loop) updateFiles(ctx context.Context) (err error) {
l.logger.Info("downloading DNS over TLS cryptographic files")
if err := l.conf.SetupFiles(ctx); err != nil {
return err
}
settings := l.GetSettings() settings := l.GetSettings()
l.logger.Info("downloading hostnames and IP block lists") unboundSettings, err := settings.DoT.Unbound.ToUnboundFormat()
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
blockBuilder, err := blockbuilder.New(blacklistSettings)
if err != nil {
return fmt.Errorf("creating block builder: %w", err)
}
result := blockBuilder.BuildAll(ctx)
for _, resultErr := range result.Errors {
if err != nil {
err = fmt.Errorf("%w, %w", err, resultErr)
continue
}
err = resultErr
}
if err != nil { if err != nil {
return err return err
} }
updateSettings := update.Settings{ l.logger.Info("downloading hostnames and IP block lists")
IPs: result.BlockedIPs, blacklistSettings, err := settings.DoT.Blacklist.ToBlacklistFormat()
IPPrefixes: result.BlockedIPPrefixes,
}
updateSettings.BlockHostnames(result.BlockedHostnames)
err = l.filter.Update(updateSettings)
if err != nil { if err != nil {
return fmt.Errorf("updating filter: %w", err) return err
} }
return nil blockedHostnames, blockedIPs, blockedIPPrefixes, errs :=
l.blockBuilder.All(ctx, blacklistSettings)
for _, err := range errs {
l.logger.Warn(err.Error())
}
// TODO change to BlockHostnames() when migrating to qdm12/dns v2
unboundSettings.Blacklist.FqdnHostnames = blockedHostnames
unboundSettings.Blacklist.IPs = blockedIPs
unboundSettings.Blacklist.IPPrefixes = blockedIPPrefixes
return l.conf.MakeUnboundConf(unboundSettings)
} }

View File

@@ -33,8 +33,7 @@ func isDeleteMatchInstruction(instruction string) bool {
} }
func deleteIPTablesRule(ctx context.Context, iptablesBinary, instruction string, func deleteIPTablesRule(ctx context.Context, iptablesBinary, instruction string,
runner CmdRunner, logger Logger, runner Runner, logger Logger) (err error) {
) (err error) {
targetRule, err := parseIptablesInstruction(instruction) targetRule, err := parseIptablesInstruction(instruction)
if err != nil { if err != nil {
return fmt.Errorf("parsing iptables command: %w", err) return fmt.Errorf("parsing iptables command: %w", err)
@@ -69,13 +68,10 @@ func deleteIPTablesRule(ctx context.Context, iptablesBinary, instruction string,
// findLineNumber finds the line number of an iptables rule. // findLineNumber finds the line number of an iptables rule.
// It returns 0 if the rule is not found. // It returns 0 if the rule is not found.
func findLineNumber(ctx context.Context, iptablesBinary string, func findLineNumber(ctx context.Context, iptablesBinary string,
instruction iptablesInstruction, runner CmdRunner, logger Logger) ( instruction iptablesInstruction, runner Runner, logger Logger) (
lineNumber uint16, err error, lineNumber uint16, err error) {
) { listFlags := []string{"-t", instruction.table, "-L", instruction.chain,
listFlags := []string{ "--line-numbers", "-n", "-v"}
"-t", instruction.table, "-L", instruction.chain,
"--line-numbers", "-n", "-v",
}
cmd := exec.CommandContext(ctx, iptablesBinary, listFlags...) // #nosec G204 cmd := exec.CommandContext(ctx, iptablesBinary, listFlags...) // #nosec G204
logger.Debug(cmd.String()) logger.Debug(cmd.String())
output, err := runner.Run(cmd) output, err := runner.Run(cmd)

View File

@@ -38,6 +38,7 @@ func Test_isDeleteMatchInstruction(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()
@@ -61,7 +62,7 @@ func Test_deleteIPTablesRule(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
instruction string instruction string
makeRunner func(ctrl *gomock.Controller) *MockCmdRunner makeRunner func(ctrl *gomock.Controller) *MockRunner
makeLogger func(ctrl *gomock.Controller) *MockLogger makeLogger func(ctrl *gomock.Controller) *MockLogger
errWrapped error errWrapped error
errMessage string errMessage string
@@ -74,8 +75,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
}, },
"list_error": { "list_error": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner { makeRunner: func(ctrl *gomock.Controller) *MockRunner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT(). runner.EXPECT().
Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
Return("", errTest) Return("", errTest)
@@ -92,8 +93,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
}, },
"rule_not_found": { "rule_not_found": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner { makeRunner: func(ctrl *gomock.Controller) *MockRunner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
Return(`Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) Return(`Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination num pkts bytes target prot opt in out source destination
@@ -111,8 +112,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
}, },
"rule_found_delete_error": { "rule_found_delete_error": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner { makeRunner: func(ctrl *gomock.Controller) *MockRunner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+ Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+
"num pkts bytes target prot opt in out source destination \n"+ "num pkts bytes target prot opt in out source destination \n"+
@@ -136,8 +137,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
}, },
"rule_found_delete_success": { "rule_found_delete_success": {
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner { makeRunner: func(ctrl *gomock.Controller) *MockRunner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+ Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+
"num pkts bytes target prot opt in out source destination \n"+ "num pkts bytes target prot opt in out source destination \n"+
@@ -160,13 +161,14 @@ func Test_deleteIPTablesRule(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()
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
ctx := context.Background() ctx := context.Background()
instruction := testCase.instruction instruction := testCase.instruction
var runner *MockCmdRunner var runner *MockRunner
if testCase.makeRunner != nil { if testCase.makeRunner != nil {
runner = testCase.makeRunner(ctrl) runner = testCase.makeRunner(ctrl)
} }

View File

@@ -106,20 +106,12 @@ func (c *Config) enable(ctx context.Context) (err error) {
return err return err
} }
localInterfaces := make(map[string]struct{}, len(c.localNetworks))
for _, network := range c.localNetworks { for _, network := range c.localNetworks {
if err := c.acceptOutputFromIPToSubnet(ctx, network.InterfaceName, network.IP, network.IPNet, remove); err != nil { if err := c.acceptOutputFromIPToSubnet(ctx, network.InterfaceName, network.IP, network.IPNet, remove); err != nil {
return err return err
} }
if err = c.acceptIpv6MulticastOutput(ctx, network.InterfaceName, remove); err != nil {
_, localInterfaceSeen := localInterfaces[network.InterfaceName] return err
if localInterfaceSeen {
continue
}
localInterfaces[network.InterfaceName] = struct{}{}
err = c.acceptIpv6MulticastOutput(ctx, network.InterfaceName, remove)
if err != nil {
return fmt.Errorf("accepting IPv6 multicast output: %w", err)
} }
} }
@@ -157,13 +149,7 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
} }
const remove = false const remove = false
interfacesSeen := make(map[string]struct{}, len(c.defaultRoutes))
for _, defaultRoute := range c.defaultRoutes { for _, defaultRoute := range c.defaultRoutes {
_, seen := interfacesSeen[defaultRoute.NetInterface]
if seen {
continue
}
interfacesSeen[defaultRoute.NetInterface] = struct{}{}
err = c.acceptOutputTrafficToVPN(ctx, defaultRoute.NetInterface, c.vpnConnection, remove) err = c.acceptOutputTrafficToVPN(ctx, defaultRoute.NetInterface, c.vpnConnection, remove)
if err != nil { if err != nil {
return fmt.Errorf("accepting output traffic through VPN: %w", err) return fmt.Errorf("accepting output traffic through VPN: %w", err)

View File

@@ -7,10 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/routing" "github.com/qdm12/gluetun/internal/routing"
"github.com/qdm12/golibs/command"
) )
type Config struct { //nolint:maligned type Config struct { //nolint:maligned
runner CmdRunner runner command.Runner
logger Logger logger Logger
iptablesMutex sync.Mutex iptablesMutex sync.Mutex
ip6tablesMutex sync.Mutex ip6tablesMutex sync.Mutex
@@ -35,9 +36,8 @@ type Config struct { //nolint:maligned
// NewConfig creates a new Config instance and returns an error // NewConfig creates a new Config instance and returns an error
// if no iptables implementation is available. // if no iptables implementation is available.
func NewConfig(ctx context.Context, logger Logger, func NewConfig(ctx context.Context, logger Logger,
runner CmdRunner, defaultRoutes []routing.DefaultRoute, runner command.Runner, defaultRoutes []routing.DefaultRoute,
localNetworks []routing.LocalNetwork, localNetworks []routing.LocalNetwork) (config *Config, err error) {
) (config *Config, err error) {
iptables, err := checkIptablesSupport(ctx, runner, "iptables", "iptables-nft", "iptables-legacy") iptables, err := checkIptablesSupport(ctx, runner, "iptables", "iptables-nft", "iptables-legacy")
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,14 +1,13 @@
package firewall package firewall
import "os/exec" import "github.com/qdm12/golibs/command"
type CmdRunner interface { type Runner interface {
Run(cmd *exec.Cmd) (output string, err error) Run(cmd command.ExecCmd) (output string, err error)
} }
type Logger interface { type Logger interface {
Debug(s string) Debug(s string)
Info(s string) Info(s string)
Warn(s string)
Error(s string) Error(s string)
} }

View File

@@ -6,14 +6,15 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"strings" "strings"
"github.com/qdm12/golibs/command"
) )
// findIP6tablesSupported checks for multiple iptables implementations // findIP6tablesSupported checks for multiple iptables implementations
// and returns the iptables path that is supported. If none work, an // and returns the iptables path that is supported. If none work, an
// empty string path is returned. // empty string path is returned.
func findIP6tablesSupported(ctx context.Context, runner CmdRunner) ( func findIP6tablesSupported(ctx context.Context, runner command.Runner) (
ip6tablesPath string, err error, ip6tablesPath string, err error) {
) {
ip6tablesPath, err = checkIptablesSupport(ctx, runner, "ip6tables", "ip6tables-nft", "ip6tables-legacy") ip6tablesPath, err = checkIptablesSupport(ctx, runner, "ip6tables", "ip6tables-nft", "ip6tables-legacy")
if errors.Is(err, ErrIPTablesNotSupported) { if errors.Is(err, ErrIPTablesNotSupported) {
return "", nil return "", nil

View File

@@ -112,8 +112,7 @@ func (c *Config) acceptInputThroughInterface(ctx context.Context, intf string, r
} }
func (c *Config) acceptInputToSubnet(ctx context.Context, intf string, func (c *Config) acceptInputToSubnet(ctx context.Context, intf string,
destination netip.Prefix, remove bool, destination netip.Prefix, remove bool) error {
) error {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
@@ -145,8 +144,7 @@ func (c *Config) acceptEstablishedRelatedTraffic(ctx context.Context, remove boo
} }
func (c *Config) acceptOutputTrafficToVPN(ctx context.Context, func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
defaultInterface string, connection models.Connection, remove bool, defaultInterface string, connection models.Connection, remove bool) error {
) error {
protocol := connection.Protocol protocol := connection.Protocol
if protocol == "tcp-client" { if protocol == "tcp-client" {
protocol = "tcp" //nolint:goconst protocol = "tcp" //nolint:goconst
@@ -164,8 +162,7 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
// Thanks to @npawelek. // Thanks to @npawelek.
func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context, func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool, intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool) error {
) error {
doIPv4 := sourceIP.Is4() && destinationSubnet.Addr().Is4() doIPv4 := sourceIP.Is4() && destinationSubnet.Addr().Is4()
interfaceFlag := "-o " + intf interfaceFlag := "-o " + intf
@@ -186,13 +183,12 @@ func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
// NDP uses multicast address (theres no broadcast in IPv6 like ARP uses in IPv4). // NDP uses multicast address (theres no broadcast in IPv6 like ARP uses in IPv4).
func (c *Config) acceptIpv6MulticastOutput(ctx context.Context, func (c *Config) acceptIpv6MulticastOutput(ctx context.Context,
intf string, remove bool, intf string, remove bool) error {
) error {
interfaceFlag := "-o " + intf interfaceFlag := "-o " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
} }
instruction := fmt.Sprintf("%s OUTPUT %s -d ff02::1:ff00:0/104 -j ACCEPT", instruction := fmt.Sprintf("%s OUTPUT %s -d ff02::1:ff/104 -j ACCEPT",
appendOrDelete(remove), interfaceFlag) appendOrDelete(remove), interfaceFlag)
return c.runIP6tablesInstruction(ctx, instruction) return c.runIP6tablesInstruction(ctx, instruction)
} }
@@ -211,8 +207,7 @@ func (c *Config) acceptInputToPort(ctx context.Context, intf string, port uint16
// Used for VPN server side port forwarding, with intf set to the VPN tunnel interface. // Used for VPN server side port forwarding, with intf set to the VPN tunnel interface.
func (c *Config) redirectPort(ctx context.Context, intf string, func (c *Config) redirectPort(ctx context.Context, intf string,
sourcePort, destinationPort uint16, remove bool, sourcePort, destinationPort uint16, remove bool) (err error) {
) (err error) {
interfaceFlag := "-i " + intf interfaceFlag := "-i " + intf
if intf == "*" { // all interfaces if intf == "*" { // all interfaces
interfaceFlag = "" interfaceFlag = ""
@@ -244,13 +239,6 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
appendOrDelete(remove), interfaceFlag, destinationPort), appendOrDelete(remove), interfaceFlag, destinationPort),
}) })
if err != nil { if err != nil {
errMessage := err.Error()
if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") {
if !remove {
c.logger.Warn("IPv6 port redirection disabled because your kernel does not support IPv6 NAT: " + errMessage)
}
return nil
}
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w", return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
sourcePort, destinationPort, intf, err) sourcePort, destinationPort, intf, err)
} }

View File

@@ -32,7 +32,9 @@ type chainRule struct {
ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty. ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty.
} }
var ErrChainListMalformed = errors.New("iptables chain list output is malformed") var (
ErrChainListMalformed = errors.New("iptables chain list output is malformed")
)
func parseChain(iptablesOutput string) (c chain, err error) { func parseChain(iptablesOutput string) (c chain, err error) {
// Text example: // Text example:
@@ -144,7 +146,9 @@ func parseChainGeneralDataLine(line string) (base chain, err error) {
return base, nil return base, nil
} }
var ErrChainRuleMalformed = errors.New("chain rule is malformed") var (
ErrChainRuleMalformed = errors.New("chain rule is malformed")
)
func parseChainRuleLine(line string) (rule chainRule, err error) { func parseChainRuleLine(line string) (rule chainRule, err error) {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
@@ -296,7 +300,9 @@ func parsePortsCSV(s string) (ports []uint16, err error) {
return ports, nil return ports, nil
} }
var ErrLineNumberIsZero = errors.New("line number is zero") var (
ErrLineNumberIsZero = errors.New("line number is zero")
)
func parseLineNumber(s string) (n uint16, err error) { func parseLineNumber(s string) (n uint16, err error) {
const base, bitLength = 10, 16 const base, bitLength = 10, 16
@@ -309,7 +315,9 @@ func parseLineNumber(s string) (n uint16, err error) {
return uint16(lineNumber), nil return uint16(lineNumber), nil
} }
var ErrTargetUnknown = errors.New("unknown target") var (
ErrTargetUnknown = errors.New("unknown target")
)
func checkTarget(target string) (err error) { func checkTarget(target string) (err error) {
switch target { switch target {
@@ -319,7 +327,9 @@ func checkTarget(target string) (err error) {
return fmt.Errorf("%w: %s", ErrTargetUnknown, target) return fmt.Errorf("%w: %s", ErrTargetUnknown, target)
} }
var ErrProtocolUnknown = errors.New("unknown protocol") var (
ErrProtocolUnknown = errors.New("unknown protocol")
)
func parseProtocol(s string) (protocol string, err error) { func parseProtocol(s string) (protocol string, err error) {
switch s { switch s {
@@ -334,7 +344,9 @@ func parseProtocol(s string) (protocol string, err error) {
return protocol, nil return protocol, nil
} }
var ErrMetricSizeMalformed = errors.New("metric size is malformed") var (
ErrMetricSizeMalformed = errors.New("metric size is malformed")
)
// parseMetricSize parses a metric size string like 140K or 226M and // parseMetricSize parses a metric size string like 140K or 226M and
// returns the raw integer matching it. // returns the raw integer matching it.
@@ -343,7 +355,7 @@ func parseMetricSize(size string) (n uint64, err error) {
return n, fmt.Errorf("%w: empty string", ErrMetricSizeMalformed) return n, fmt.Errorf("%w: empty string", ErrMetricSizeMalformed)
} }
//nolint:mnd //nolint:gomnd
multiplerLetterToValue := map[byte]uint64{ multiplerLetterToValue := map[byte]uint64{
'K': 1000, 'K': 1000,
'M': 1000000, 'M': 1000000,

View File

@@ -105,6 +105,7 @@ num pkts bytes target prot opt in out source destinati
} }
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()

View File

@@ -1,3 +1,3 @@
package firewall package firewall
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . CmdRunner,Logger //go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Runner,Logger

View File

@@ -1,41 +1,41 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/firewall (interfaces: CmdRunner,Logger) // Source: github.com/qdm12/gluetun/internal/firewall (interfaces: Runner,Logger)
// Package firewall is a generated GoMock package. // Package firewall is a generated GoMock package.
package firewall package firewall
import ( import (
exec "os/exec"
reflect "reflect" reflect "reflect"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
command "github.com/qdm12/golibs/command"
) )
// MockCmdRunner is a mock of CmdRunner interface. // MockRunner is a mock of Runner interface.
type MockCmdRunner struct { type MockRunner struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockCmdRunnerMockRecorder recorder *MockRunnerMockRecorder
} }
// MockCmdRunnerMockRecorder is the mock recorder for MockCmdRunner. // MockRunnerMockRecorder is the mock recorder for MockRunner.
type MockCmdRunnerMockRecorder struct { type MockRunnerMockRecorder struct {
mock *MockCmdRunner mock *MockRunner
} }
// NewMockCmdRunner creates a new mock instance. // NewMockRunner creates a new mock instance.
func NewMockCmdRunner(ctrl *gomock.Controller) *MockCmdRunner { func NewMockRunner(ctrl *gomock.Controller) *MockRunner {
mock := &MockCmdRunner{ctrl: ctrl} mock := &MockRunner{ctrl: ctrl}
mock.recorder = &MockCmdRunnerMockRecorder{mock} mock.recorder = &MockRunnerMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCmdRunner) EXPECT() *MockCmdRunnerMockRecorder { func (m *MockRunner) EXPECT() *MockRunnerMockRecorder {
return m.recorder return m.recorder
} }
// Run mocks base method. // Run mocks base method.
func (m *MockCmdRunner) Run(arg0 *exec.Cmd) (string, error) { func (m *MockRunner) Run(arg0 command.ExecCmd) (string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Run", arg0) ret := m.ctrl.Call(m, "Run", arg0)
ret0, _ := ret[0].(string) ret0, _ := ret[0].(string)
@@ -44,9 +44,9 @@ func (m *MockCmdRunner) Run(arg0 *exec.Cmd) (string, error) {
} }
// Run indicates an expected call of Run. // Run indicates an expected call of Run.
func (mr *MockCmdRunnerMockRecorder) Run(arg0 interface{}) *gomock.Call { func (mr *MockRunnerMockRecorder) Run(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockCmdRunner)(nil).Run), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockRunner)(nil).Run), arg0)
} }
// MockLogger is a mock of Logger interface. // MockLogger is a mock of Logger interface.
@@ -107,15 +107,3 @@ func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
} }
// Warn mocks base method.
func (m *MockLogger) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
}

View File

@@ -70,7 +70,9 @@ func ipPrefixesEqual(instruction, chainRule netip.Prefix) bool {
(!instruction.IsValid() && chainRule.Bits() == 0 && chainRule.Addr().IsUnspecified()) (!instruction.IsValid() && chainRule.Bits() == 0 && chainRule.Addr().IsUnspecified())
} }
var ErrIptablesCommandMalformed = errors.New("iptables command is malformed") var (
ErrIptablesCommandMalformed = errors.New("iptables command is malformed")
)
func parseIptablesInstruction(s string) (instruction iptablesInstruction, err error) { func parseIptablesInstruction(s string) (instruction iptablesInstruction, err error) {
if s == "" { if s == "" {

View File

@@ -68,6 +68,7 @@ func Test_parseIptablesInstruction(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()
@@ -120,6 +121,7 @@ func Test_parseIPPrefix(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()

View File

@@ -11,8 +11,7 @@ import (
// If the destination port is zero, the redirection for the source port is removed // If the destination port is zero, the redirection for the source port is removed
// and no new redirection is added. // and no new redirection is added.
func (c *Config) RedirectPort(ctx context.Context, intf string, sourcePort, func (c *Config) RedirectPort(ctx context.Context, intf string, sourcePort,
destinationPort uint16, destinationPort uint16) (err error) {
) (err error) {
c.stateMutex.Lock() c.stateMutex.Lock()
defer c.stateMutex.Unlock() defer c.stateMutex.Unlock()
@@ -91,8 +90,7 @@ func (p *portRedirections) remove(intf string, sourcePort uint16) {
} }
func (p *portRedirections) check(dryRun portRedirection) (alreadyExists bool, func (p *portRedirections) check(dryRun portRedirection) (alreadyExists bool,
conflict *portRedirection, conflict *portRedirection) {
) {
slice := *p slice := *p
for _, redirection := range slice { for _, redirection := range slice {
interfaceMatch := redirection.interfaceName == "" || interfaceMatch := redirection.interfaceName == "" ||

View File

@@ -8,6 +8,8 @@ import (
"os/exec" "os/exec"
"sort" "sort"
"strings" "strings"
"github.com/qdm12/golibs/command"
) )
var ( var (
@@ -17,9 +19,8 @@ var (
ErrIPTablesNotSupported = errors.New("no iptables supported found") ErrIPTablesNotSupported = errors.New("no iptables supported found")
) )
func checkIptablesSupport(ctx context.Context, runner CmdRunner, func checkIptablesSupport(ctx context.Context, runner command.Runner,
iptablesPathsToTry ...string, iptablesPathsToTry ...string) (iptablesPath string, err error) {
) (iptablesPath string, err error) {
iptablesPathToUnsupportedMessage := make(map[string]string, len(iptablesPathsToTry)) iptablesPathToUnsupportedMessage := make(map[string]string, len(iptablesPathsToTry))
for _, pathToTest := range iptablesPathsToTry { for _, pathToTest := range iptablesPathsToTry {
ok, unsupportedMessage, err := testIptablesPath(ctx, pathToTest, runner) ok, unsupportedMessage, err := testIptablesPath(ctx, pathToTest, runner)
@@ -61,9 +62,8 @@ func checkIptablesSupport(ctx context.Context, runner CmdRunner,
} }
func testIptablesPath(ctx context.Context, path string, func testIptablesPath(ctx context.Context, path string,
runner CmdRunner) (ok bool, unsupportedMessage string, runner command.Runner) (ok bool, unsupportedMessage string,
criticalErr error, criticalErr error) {
) {
// Just listing iptables rules often work but we need // Just listing iptables rules often work but we need
// to modify them to ensure we can support the iptables // to modify them to ensure we can support the iptables
// being tested. // being tested.

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/qdm12/golibs/command"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -40,15 +41,15 @@ func Test_checkIptablesSupport(t *testing.T) {
const inputPolicy = "ACCEPT" const inputPolicy = "ACCEPT"
testCases := map[string]struct { testCases := map[string]struct {
buildRunner func(ctrl *gomock.Controller) CmdRunner buildRunner func(ctrl *gomock.Controller) command.Runner
iptablesPathsToTry []string iptablesPathsToTry []string
iptablesPath string iptablesPath string
errSentinel error errSentinel error
errMessage string errMessage string
}{ }{
"critical error when checking": { "critical error when checking": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")). runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("", nil) Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")). runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
@@ -61,8 +62,8 @@ func Test_checkIptablesSupport(t *testing.T) {
"output (exit code 4)", "output (exit code 4)",
}, },
"found valid path": { "found valid path": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")). runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("", nil) Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")). runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
@@ -77,8 +78,8 @@ func Test_checkIptablesSupport(t *testing.T) {
iptablesPath: "path1", iptablesPath: "path1",
}, },
"all permission denied": { "all permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")). runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("Permission denied (you must be root) more context", errDummy) Return("Permission denied (you must be root) more context", errDummy)
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")). runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
@@ -92,8 +93,8 @@ func Test_checkIptablesSupport(t *testing.T) {
"path2: context: Permission denied (you must be root) (exit code 4)", "path2: context: Permission denied (you must be root) (exit code 4)",
}, },
"no valid path": { "no valid path": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")). runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("output 1", errDummy) Return("output 1", errDummy)
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")). runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
@@ -110,13 +111,15 @@ func Test_checkIptablesSupport(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()
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
runner := testCase.buildRunner(ctrl) runner := testCase.buildRunner(ctrl)
iptablesPath, err := checkIptablesSupport(ctx, runner, testCase.iptablesPathsToTry...) iptablesPath, err :=
checkIptablesSupport(ctx, runner, testCase.iptablesPathsToTry...)
require.ErrorIs(t, err, testCase.errSentinel) require.ErrorIs(t, err, testCase.errSentinel)
if testCase.errSentinel != nil { if testCase.errSentinel != nil {
@@ -136,15 +139,15 @@ func Test_testIptablesPath(t *testing.T) {
const inputPolicy = "ACCEPT" const inputPolicy = "ACCEPT"
testCases := map[string]struct { testCases := map[string]struct {
buildRunner func(ctrl *gomock.Controller) CmdRunner buildRunner func(ctrl *gomock.Controller) command.Runner
ok bool ok bool
unsupportedMessage string unsupportedMessage string
criticalErrWrapped error criticalErrWrapped error
criticalErrMessage string criticalErrMessage string
}{ }{
"append test rule permission denied": { "append test rule permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)). runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("Permission denied (you must be root)", errDummy) Return("Permission denied (you must be root)", errDummy)
return runner return runner
@@ -152,8 +155,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "Permission denied (you must be root) (exit code 4)", unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
}, },
"append test rule unsupported": { "append test rule unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)). runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("some output", errDummy) Return("some output", errDummy)
return runner return runner
@@ -161,8 +164,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "some output (exit code 4)", unsupportedMessage: "some output (exit code 4)",
}, },
"remove test rule error": { "remove test rule error": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)). runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).
Return("some output", errDummy) Return("some output", errDummy)
@@ -172,8 +175,8 @@ func Test_testIptablesPath(t *testing.T) {
criticalErrMessage: "failed cleaning up test rule: some output (exit code 4)", criticalErrMessage: "failed cleaning up test rule: some output (exit code 4)",
}, },
"list input rules permission denied": { "list input rules permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -183,8 +186,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "Permission denied (you must be root) (exit code 4)", unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
}, },
"list input rules unsupported": { "list input rules unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -194,8 +197,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "some output (exit code 4)", unsupportedMessage: "some output (exit code 4)",
}, },
"list input rules no policy": { "list input rules no policy": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -206,8 +209,8 @@ func Test_testIptablesPath(t *testing.T) {
criticalErrMessage: "input policy not found: in INPUT rules: some\noutput", criticalErrMessage: "input policy not found: in INPUT rules: some\noutput",
}, },
"set policy permission denied": { "set policy permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -219,8 +222,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "Permission denied (you must be root) (exit code 4)", unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
}, },
"set policy unsupported": { "set policy unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -232,8 +235,8 @@ func Test_testIptablesPath(t *testing.T) {
unsupportedMessage: "some output (exit code 4)", unsupportedMessage: "some output (exit code 4)",
}, },
"success": { "success": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockCmdRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)). runner.EXPECT().Run(newListInputRulesMatcher(path)).
@@ -247,13 +250,15 @@ func Test_testIptablesPath(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()
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
runner := testCase.buildRunner(ctrl) runner := testCase.buildRunner(ctrl)
ok, unsupportedMessage, criticalErr := testIptablesPath(ctx, path, runner) ok, unsupportedMessage, criticalErr :=
testIptablesPath(ctx, path, runner)
assert.Equal(t, testCase.ok, ok) assert.Equal(t, testCase.ok, ok)
assert.Equal(t, testCase.unsupportedMessage, unsupportedMessage) assert.Equal(t, testCase.unsupportedMessage, unsupportedMessage)
@@ -283,6 +288,7 @@ func Test_isPermissionDenied(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()
@@ -325,6 +331,7 @@ func Test_extractInputPolicy(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()

View File

@@ -8,8 +8,7 @@ import (
) )
func (c *Config) SetVPNConnection(ctx context.Context, func (c *Config) SetVPNConnection(ctx context.Context,
connection models.Connection, vpnIntf string, connection models.Connection, vpnIntf string) (err error) {
) (err error) {
c.stateMutex.Lock() c.stateMutex.Lock()
defer c.stateMutex.Unlock() defer c.stateMutex.Unlock()

View File

@@ -1,34 +0,0 @@
package format
import (
"fmt"
"time"
)
// FriendlyDuration formats a duration in an approximate, human friendly duration.
// For example 55 hours will result in "2 days".
func FriendlyDuration(duration time.Duration) string {
const twoDays = 48 * time.Hour
switch {
case duration < time.Minute:
seconds := int(duration.Round(time.Second).Seconds())
const two = 2
if seconds < two {
return fmt.Sprintf("%d second", seconds)
}
return fmt.Sprintf("%d seconds", seconds)
case duration <= time.Hour:
minutes := int(duration.Round(time.Minute).Minutes())
if minutes == 1 {
return "1 minute"
}
return fmt.Sprintf("%d minutes", minutes)
case duration < twoDays:
hours := int(duration.Truncate(time.Hour).Hours())
return fmt.Sprintf("%d hours", hours)
default:
const hoursInDay = 24
days := int(duration.Truncate(time.Hour).Hours() / hoursInDay)
return fmt.Sprintf("%d days", days)
}
}

View File

@@ -1,65 +0,0 @@
package format
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_FriendlyDuration(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
duration time.Duration
friendly string
}{
"zero": {
friendly: "0 second",
},
"one_second": {
duration: time.Second,
friendly: "1 second",
},
"59_seconds": {
duration: 59 * time.Second,
friendly: "59 seconds",
},
"1_minute": {
duration: time.Minute,
friendly: "1 minute",
},
"2_minutes": {
duration: 2 * time.Minute,
friendly: "2 minutes",
},
"1_hour": {
duration: time.Hour,
friendly: "60 minutes",
},
"2_hours": {
duration: 2 * time.Hour,
friendly: "2 hours",
},
"26_hours": {
duration: 26 * time.Hour,
friendly: "26 hours",
},
"28_hours": {
duration: 28 * time.Hour,
friendly: "28 hours",
},
"55_hours": {
duration: 55 * time.Hour,
friendly: "2 days",
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
s := FriendlyDuration(testCase.duration)
assert.Equal(t, testCase.friendly, s)
})
}
}

View File

@@ -8,7 +8,9 @@ import (
"net/http" "net/http"
) )
var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK") var (
ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK")
)
type Client struct { type Client struct {
httpClient *http.Client httpClient *http.Client

View File

@@ -90,6 +90,7 @@ func Test_makeAddressToDial(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()

View File

@@ -17,8 +17,7 @@ type Server struct {
} }
func NewServer(config settings.Health, func NewServer(config settings.Health,
logger Logger, vpnLoop StatusApplier, logger Logger, vpnLoop StatusApplier) *Server {
) *Server {
return &Server{ return &Server{
logger: logger, logger: logger,
handler: newHandler(), handler: newHandler(),

View File

@@ -8,7 +8,7 @@ import (
) )
func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http.Request) (authorized bool) { func (h *handler) isAuthorized(responseWriter http.ResponseWriter, request *http.Request) (authorized bool) {
if h.username == "" || (request.Method != http.MethodConnect && !request.URL.IsAbs()) { if h.username == "" || (request.Method != "CONNECT" && !request.URL.IsAbs()) {
return true return true
} }
basicAuth := request.Header.Get("Proxy-Authorization") basicAuth := request.Header.Get("Proxy-Authorization")

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