Compare commits
60 Commits
v3.39.0
...
port-forwa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1114981914 | ||
|
|
4ef0df04aa | ||
|
|
937c667ca8 | ||
|
|
3c45f57aaa | ||
|
|
30640eefe2 | ||
|
|
8567522594 | ||
|
|
bd8214e648 | ||
|
|
a61302f135 | ||
|
|
3dfb43e117 | ||
|
|
2388e0550b | ||
|
|
a7d70dd9a3 | ||
|
|
76a4bb5dc3 | ||
|
|
3daf15a612 | ||
|
|
81ffbaf057 | ||
|
|
abe9dcbe33 | ||
|
|
3c8e80a1a4 | ||
|
|
694988b32f | ||
|
|
ea31886299 | ||
|
|
5b2923ca65 | ||
|
|
432eaa6c04 | ||
|
|
5fd0af9395 | ||
|
|
03deb9aed0 | ||
|
|
cbdd1a933c | ||
|
|
99e9bc87cf | ||
|
|
9ef14ee070 | ||
|
|
7842ff4cdc | ||
|
|
3d6d03b327 | ||
|
|
7ebbaf4351 | ||
|
|
c665b13cec | ||
|
|
970b21a6eb | ||
|
|
62747f1eb8 | ||
|
|
a2e76e1683 | ||
|
|
07651683f9 | ||
|
|
429aea8e0f | ||
|
|
01fa9934bc | ||
|
|
ff7cadb43b | ||
|
|
540acc915d | ||
|
|
703a546c1d | ||
|
|
4851bd70da | ||
|
|
a2b3d7e30c | ||
|
|
4d60b71583 | ||
|
|
3f130931d2 | ||
|
|
946f055fed | ||
|
|
eaece0cb8e | ||
|
|
4203f4fabf | ||
|
|
c39edb6378 | ||
|
|
b3cc2781ff | ||
|
|
12c411e203 | ||
|
|
3bf937d705 | ||
|
|
bc55c25e73 | ||
|
|
897a9d7f57 | ||
|
|
4a128677dd | ||
|
|
9233f3f5ba | ||
|
|
11c2354408 | ||
|
|
1f2882434a | ||
|
|
01aaf2c86a | ||
|
|
d260ac7a49 | ||
|
|
0bea0d4ecd | ||
|
|
59994bd6e7 | ||
|
|
62799d2449 |
@@ -1,5 +1,4 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
FROM qmcgaw/godevcontainer
|
||||
FROM qmcgaw/godevcontainer:v0.20-alpine
|
||||
RUN apk add wireguard-tools htop openssl
|
||||
|
||||
@@ -2,68 +2,47 @@
|
||||
|
||||
Development container that can be used with VSCode.
|
||||
|
||||
It works on Linux, Windows and OSX.
|
||||
It works on Linux, Windows (WSL2) and OSX.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the following files on your host if you don't have them:
|
||||
1. Create the following files and directory on your host if you don't have them:
|
||||
|
||||
```sh
|
||||
touch ~/.gitconfig ~/.zsh_history
|
||||
mkdir -p ~/.ssh
|
||||
```
|
||||
|
||||
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
|
||||
|
||||
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
|
||||
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||
1. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
### Customize the image
|
||||
For any customization to take effect, you should "rebuild and reopen":
|
||||
|
||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||
2. Select `Dev Containers: Rebuild Container`
|
||||
|
||||
```Dockerfile
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add curl
|
||||
```
|
||||
Changes you can make are notably:
|
||||
|
||||
To rebuild the image, either:
|
||||
- Changes to the Docker image in [Dockerfile](Dockerfile)
|
||||
- Changes to VSCode **settings** and **extensions** in [devcontainer.json](devcontainer.json).
|
||||
- Change the entrypoint script by adding a bind mount in [devcontainer.json](devcontainer.json) of a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh). For example:
|
||||
|
||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
||||
- With a terminal, go to this directory and `docker-compose build`
|
||||
|
||||
### Customize VS code settings
|
||||
|
||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/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
|
||||
```json
|
||||
// Welcome script
|
||||
{
|
||||
"source": "/yourpath/.welcome.sh",
|
||||
"target": "/root/.welcome.sh",
|
||||
"type": "bind"
|
||||
},
|
||||
```
|
||||
|
||||
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.
|
||||
- More options are documented in the [devcontainer.json reference](https://containers.dev/implementors/json_reference/).
|
||||
|
||||
@@ -1,16 +1,50 @@
|
||||
{
|
||||
"name": "gluetun-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
// User defined settings
|
||||
"containerEnv": {
|
||||
"TZ": ""
|
||||
},
|
||||
// Fixed settings
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile"
|
||||
},
|
||||
"postCreateCommand": "~/.windows.sh && go mod download",
|
||||
"capAdd": [
|
||||
"NET_ADMIN", // Gluetun specific
|
||||
"SYS_PTRACE" // for dlv Go debugging
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
"securityOpt": [
|
||||
"seccomp=unconfined" // for dlv Go debugging
|
||||
],
|
||||
"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": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
@@ -47,7 +81,8 @@
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true
|
||||
"staticcheck": true,
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
build: .
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
# created in the container. On Windows, files are copied
|
||||
# from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
- ~/.ssh:/mnt/ssh
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
environment:
|
||||
- TZ=
|
||||
cap_add:
|
||||
# For debugging with dlv
|
||||
- SYS_PTRACE
|
||||
- NET_ADMIN
|
||||
security_opt:
|
||||
# For debugging with dlv
|
||||
- seccomp:unconfined
|
||||
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
|
||||
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -50,6 +50,7 @@ body:
|
||||
- Cyberghost
|
||||
- ExpressVPN
|
||||
- FastestVPN
|
||||
- Giganews
|
||||
- HideMyAss
|
||||
- IPVanish
|
||||
- IVPN
|
||||
|
||||
14
.github/labels.yml
vendored
14
.github/labels.yml
vendored
@@ -9,6 +9,9 @@
|
||||
- name: "Status: 🔒 After next release"
|
||||
color: "f7d692"
|
||||
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"
|
||||
color: "959a9c"
|
||||
@@ -43,6 +46,8 @@
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Cyberghost"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Giganews"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ HideMyAss"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ IPVanish"
|
||||
@@ -86,7 +91,8 @@
|
||||
- name: "☁️ Windscribe"
|
||||
color: "cfe8d4"
|
||||
|
||||
- name: "Category: Config problem 📝"
|
||||
- name: "Category: User error 🤦"
|
||||
from_name: "Category: Config problem 📝"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Healthcheck 🩺"
|
||||
color: "ffc7ea"
|
||||
@@ -138,3 +144,9 @@
|
||||
color: "ffc7ea"
|
||||
- name: "Category: public IP service 💬"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: servers storage 📦"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Performance 🚀"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Investigation 🔍"
|
||||
color: "ffc7ea"
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
go-version: "^1.23"
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -7,27 +7,12 @@ issues:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- goerr113
|
||||
- err113
|
||||
- containedctx
|
||||
- goconst
|
||||
- maintidx
|
||||
- path: "internal\\/server\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- path: "internal\\/configuration\\/settings\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
||||
source: "^.+= os\\.OpenFile\\(.+, .+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
- gomnd
|
||||
- text: "^mnd: Magic number: 0[0-9]{3}, in <argument> detected$"
|
||||
source: "^.+= os\\.MkdirAll\\(.+, 0[0-9]{3}\\)"
|
||||
linters:
|
||||
- gomnd
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate .+$"
|
||||
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
@@ -35,18 +20,6 @@ issues:
|
||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
- path: "internal\\/firewall\\/.*\\.go"
|
||||
text: "string `-i ` has [1-9][0-9]* occurrences, make it a constant"
|
||||
linters:
|
||||
- goconst
|
||||
- path: "internal\\/provider\\/ipvanish\\/updater\\/servers.go"
|
||||
text: "string ` in ` has 3 occurrences, make it a constant"
|
||||
linters:
|
||||
- goconst
|
||||
- path: "internal\\/vpn\\/portforward.go"
|
||||
text: 'directive `//nolint:ireturn` is unused for linter "ireturn"'
|
||||
linters:
|
||||
- nolintlint
|
||||
|
||||
linters:
|
||||
enable:
|
||||
@@ -56,17 +29,19 @@ linters:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- containedctx
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- err113
|
||||
- errchkjson
|
||||
- errname
|
||||
- execinquery
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- fatcontext
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
@@ -77,10 +52,9 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
@@ -88,12 +62,14 @@ linters:
|
||||
- grouper
|
||||
- importas
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@@ -1,8 +1,8 @@
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GO_ALPINE_VERSION=3.20
|
||||
ARG GO_VERSION=1.22
|
||||
ARG GO_VERSION=1.23
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.56.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.61.0
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
@@ -91,6 +91,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
OPENVPN_CIPHERS= \
|
||||
OPENVPN_AUTH= \
|
||||
OPENVPN_PROCESS_USER=root \
|
||||
OPENVPN_MSSFIX= \
|
||||
OPENVPN_CUSTOM_CONFIG= \
|
||||
# Wireguard
|
||||
WIREGUARD_ENDPOINT_IP= \
|
||||
@@ -138,15 +139,17 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
SERVER_NUMBER= \
|
||||
# # PIA only:
|
||||
SERVER_NAMES= \
|
||||
# # ProtonVPN only:
|
||||
# # VPNUnlimited and ProtonVPN only:
|
||||
STREAM_ONLY= \
|
||||
FREE_ONLY= \
|
||||
# # ProtonVPN only:
|
||||
SECURE_CORE_ONLY= \
|
||||
TOR_ONLY= \
|
||||
# # Surfshark only:
|
||||
MULTIHOP_ONLY= \
|
||||
# # VPN Secure only:
|
||||
PREMIUM_ONLY= \
|
||||
# # PIA only:
|
||||
# # PIA and ProtonVPN only:
|
||||
PORT_FORWARD_ONLY= \
|
||||
# Firewall
|
||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||
@@ -166,9 +169,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
DOT=on \
|
||||
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_VERBOSITY=1 \
|
||||
DOT_VERBOSITY_DETAILS=0 \
|
||||
DOT_VALIDATION_LOGLEVEL=0 \
|
||||
DOT_CACHING=on \
|
||||
DOT_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
@@ -197,15 +197,18 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# Control server
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_PERIOD=12h \
|
||||
PUBLICIP_API=ipinfo \
|
||||
PUBLICIP_ENABLED=on \
|
||||
PUBLICIP_API=ipinfo,ifconfigco,ip2location,cloudflare \
|
||||
PUBLICIP_API_TOKEN= \
|
||||
# Storage
|
||||
STORAGE_FILEPATH=/gluetun/servers.json \
|
||||
# Pprof
|
||||
PPROF_ENABLED=no \
|
||||
PPROF_BLOCK_PROFILE_RATE=0 \
|
||||
@@ -224,10 +227,9 @@ RUN apk add --no-cache --update -l wget && \
|
||||
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
|
||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
|
||||
apk del openvpn && \
|
||||
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy unbound tzdata && \
|
||||
apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \
|
||||
mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \
|
||||
rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||
rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \
|
||||
deluser openvpn && \
|
||||
deluser unbound && \
|
||||
mkdir /gluetun
|
||||
COPY --from=build /tmp/gobuild/entrypoint /gluetun-entrypoint
|
||||
|
||||
@@ -57,7 +57,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
## Features
|
||||
|
||||
- Based on Alpine 3.20 for a small Docker image of 35.6MB
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||
@@ -73,9 +73,8 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||
- [Connect 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 🎆
|
||||
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
||||
- Custom VPN server side port forwarding for [Perfect Privacy](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/perfect-privacy.md#vpn-server-port-forwarding), [Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding), [PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/privatevpn.md#vpn-server-port-forwarding) and [ProtonVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/protonvpn.md#vpn-server-port-forwarding)
|
||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||
- Unbound subprogram drops root privileges once launched
|
||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -13,9 +15,9 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/gluetun/internal/alpine"
|
||||
"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/sources/files"
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/secrets"
|
||||
@@ -32,7 +34,6 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/pprof"
|
||||
"github.com/qdm12/gluetun/internal/provider"
|
||||
"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/server"
|
||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||
@@ -42,7 +43,6 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
"github.com/qdm12/gluetun/internal/vpn"
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gosettings/reader/sources/env"
|
||||
"github.com/qdm12/goshutdown"
|
||||
@@ -51,7 +51,6 @@ import (
|
||||
"github.com/qdm12/goshutdown/order"
|
||||
"github.com/qdm12/gosplash"
|
||||
"github.com/qdm12/log"
|
||||
"github.com/qdm12/updated/pkg/dnscrypto"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
@@ -80,7 +79,7 @@ func main() {
|
||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||
netLinker := netlink.New(netLinkDebugLogger)
|
||||
cli := cli.New()
|
||||
cmder := command.NewCmder()
|
||||
cmder := command.New()
|
||||
|
||||
reader := reader.New(reader.Settings{
|
||||
Sources: []reader.Source{
|
||||
@@ -140,15 +139,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errCommandUnknown = errors.New("command is unknown")
|
||||
)
|
||||
var errCommandUnknown = errors.New("command is unknown")
|
||||
|
||||
//nolint:gocognit,gocyclo,maintidx
|
||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||
tun Tun, netLinker netLinker, cmder command.RunStarter,
|
||||
cli clier) error {
|
||||
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||
cli clier,
|
||||
) error {
|
||||
if len(args) > 1 { // cli operation
|
||||
switch args[1] {
|
||||
case "healthcheck":
|
||||
@@ -161,12 +159,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return cli.Update(ctx, args[2:], logger)
|
||||
case "format-servers":
|
||||
return cli.FormatServers(args[2:])
|
||||
case "genkey":
|
||||
return cli.GenKey(args[2:])
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", errCommandUnknown, args[1])
|
||||
}
|
||||
}
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z")
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
Created: buildInfo.Created,
|
||||
Announcement: "Wiki moved to https://github.com/qdm12/gluetun-wiki",
|
||||
Announcement: "All control server routes will become private by default after the v3.41.0 release",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
@@ -188,7 +188,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
|
||||
var allSettings settings.Settings
|
||||
err = allSettings.Read(reader)
|
||||
err = allSettings.Read(reader, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||
storageLogger := logger.New(log.SetComponent("storage"))
|
||||
storage, err := storage.New(storageLogger, constants.ServersData)
|
||||
storage, err := storage.New(storageLogger, *allSettings.Storage.Filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
err = allSettings.Validate(storage, ipv6Supported)
|
||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -269,16 +269,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
ovpnConf := openvpn.New(
|
||||
logger.New(log.SetComponent("openvpn configurator")),
|
||||
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{
|
||||
{name: "Alpine", getVersion: alpineConf.Version},
|
||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||
{name: "Unbound", getVersion: dnsConf.Version},
|
||||
{name: "IPtables", getVersion: firewallConf.Version},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -291,10 +286,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
logger.Warn(warning)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("/tmp/gluetun", 0644); err != nil {
|
||||
const permission = fs.FileMode(0o644)
|
||||
err = os.MkdirAll("/tmp/gluetun", permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll("/gluetun", 0644); err != nil {
|
||||
err = os.MkdirAll("/gluetun", permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -306,15 +304,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
if nonRootUsername != defaultUsername {
|
||||
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
|
||||
|
||||
if err := os.Chown("/etc/unbound", puid, pgid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := routingConf.Setup(); err != nil {
|
||||
if strings.Contains(err.Error(), "operation not permitted") {
|
||||
logger.Warn("💡 Tip: Are you passing NET_ADMIN capability to gluetun?")
|
||||
@@ -373,7 +364,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
defaultGroupOptions := []group.Option{
|
||||
group.OptionTimeout(defaultShutdownTimeout),
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess)}
|
||||
group.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
}
|
||||
|
||||
controlGroupHandler := goshutdown.NewGroupHandler("control", defaultGroupOptions...)
|
||||
tickersGroupHandler := goshutdown.NewGroupHandler("tickers", defaultGroupOptions...)
|
||||
@@ -396,28 +388,29 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||
}
|
||||
|
||||
unboundLogger := logger.New(log.SetComponent("dns"))
|
||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
||||
unboundLogger)
|
||||
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||
dnsLogger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DNS loop: %w", err)
|
||||
}
|
||||
|
||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||
"unbound", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
// wait for unboundLooper.Restart or its ticker launched with RunRestartTicker
|
||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||
"dns", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
// wait for dnsLooper.Restart or its ticker launched with RunRestartTicker
|
||||
go dnsLooper.Run(dnsCtx, dnsDone)
|
||||
otherGroupHandler.Add(dnsHandler)
|
||||
|
||||
dnsTickerHandler, dnsTickerCtx, dnsTickerDone := goshutdown.NewGoRoutineHandler(
|
||||
"dns ticker", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go unboundLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
go dnsLooper.RunRestartTicker(dnsTickerCtx, dnsTickerDone)
|
||||
controlGroupHandler.Add(dnsTickerHandler)
|
||||
|
||||
publicipAPI, _ := pubipapi.ParseProvider(allSettings.PublicIP.API)
|
||||
ipFetcher, err := pubipapi.New(publicipAPI, httpClient, *allSettings.PublicIP.APIToken)
|
||||
publicIPLooper, err := publicip.NewLoop(allSettings.PublicIP, puid, pgid, httpClient,
|
||||
logger.New(log.SetComponent("ip getter")))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating public IP API client: %w", err)
|
||||
return fmt.Errorf("creating public ip loop: %w", err)
|
||||
}
|
||||
publicIPLooper := publicip.NewLoop(ipFetcher,
|
||||
logger.New(log.SetComponent("ip getter")),
|
||||
allSettings.PublicIP, puid, pgid)
|
||||
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting public ip loop: %w", err)
|
||||
@@ -429,12 +422,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
||||
|
||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||
cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient,
|
||||
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||
buildInfo, *allSettings.Version.Enabled)
|
||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||
"vpn", goroutine.OptionTimeout(time.Second))
|
||||
@@ -474,7 +467,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
buildInfo, vpnLooper, portForwardLooper, unboundLooper, updaterLooper, publicIPLooper,
|
||||
allSettings.ControlServer.AuthFilePath,
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up control server: %w", err)
|
||||
@@ -534,7 +528,8 @@ type infoer interface {
|
||||
}
|
||||
|
||||
func printVersions(ctx context.Context, logger infoer,
|
||||
elements []printVersionElement) (err error) {
|
||||
elements []printVersionElement,
|
||||
) (err error) {
|
||||
const timeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -595,9 +590,16 @@ type clier interface {
|
||||
OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error
|
||||
HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error
|
||||
Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error
|
||||
GenKey(args []string) error
|
||||
}
|
||||
|
||||
type Tun interface {
|
||||
Check(tunDevice string) error
|
||||
Create(tunDevice string) error
|
||||
}
|
||||
|
||||
type RunStarter interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, err error)
|
||||
}
|
||||
|
||||
56
go.mod
56
go.mod
@@ -1,56 +1,62 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.22
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.17
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/breml/rootcerts v0.2.18
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/qdm12/dns v1.11.0
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/qdm12/gosettings v0.4.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc7
|
||||
github.com/qdm12/gosettings v0.4.3
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
github.com/qdm12/gotree v0.2.0
|
||||
github.com/qdm12/gotree v0.3.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.6.0
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
github.com/vishvananda/netlink v1.2.1
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
)
|
||||
|
||||
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/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/miekg/dns v1.1.40 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
||||
)
|
||||
|
||||
228
go.sum
228
go.sum
@@ -1,69 +1,32 @@
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
|
||||
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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/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/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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-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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
@@ -73,136 +36,104 @@ 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/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/dns v1.11.0 h1:jpcD5DZXXQSQe5a263PL09ghukiIdptvXFOZvyKEm6Q=
|
||||
github.com/qdm12/dns v1.11.0/go.mod h1:FmQsNOUcrrZ4UFzWAiED56AKXeNgaX3ySbmPwEfNjjE=
|
||||
github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8XB7ADIoLJWp9ITRgsz3LroEI2FiOXLRg=
|
||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/gosettings v0.4.2 h1:Gb39NScPr7OQV+oy0o1OD7A121udITDJuUGa7ljDF58=
|
||||
github.com/qdm12/gosettings v0.4.2/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
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/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
||||
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
github.com/qdm12/ss-server v0.6.0 h1:OaOdCIBXx0z3DGHPT6Th0v88vGa3MtAS4oRgUsDHGZE=
|
||||
github.com/qdm12/ss-server v0.6.0/go.mod h1:0BO/zEmtTiLDlmQEcjtoHTC+w+cWxwItjBuGP6TWM78=
|
||||
github.com/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/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
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/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.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
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/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/vishvananda/netlink v1.2.1 h1:pfLv/qlJUwOTPvtWREA7c3PI4u81YkqZw1DYhI2HmLA=
|
||||
github.com/vishvananda/netlink v1.2.1/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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-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-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
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-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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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-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-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-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
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/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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
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/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-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.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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -212,26 +143,19 @@ 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/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
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=
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70 h1:QnLPkuDWWbD5C+3DUA2IUXai5TK6w2zff+MAGccqdsw=
|
||||
kernel.org/pub/linux/libs/security/libcap/cap v1.2.70/go.mod h1:/iBwcj9nbLejQitYvUm9caurITQ6WyNHibJk6Q9fiS4=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||
|
||||
@@ -3,14 +3,13 @@ package alpine
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
"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.
|
||||
func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, err error) {
|
||||
@@ -39,7 +38,8 @@ func (a *Alpine) CreateUser(username string, uid int) (createdUsername string, e
|
||||
ErrUserAlreadyExists, username, u.Uid, uid)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(a.passwdPath, os.O_APPEND|os.O_WRONLY, permission)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ func (c *CLI) ClientKey(args []string) error {
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := string(data)
|
||||
s = strings.ReplaceAll(s, "\n", "")
|
||||
s = strings.ReplaceAll(s, "\r", "")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -16,13 +17,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFormatNotRecognized = errors.New("format is not recognized")
|
||||
ErrProviderUnspecified = errors.New("VPN provider to format was not specified")
|
||||
ErrMultipleProvidersToFormat = errors.New("more than one VPN provider to format were specified")
|
||||
)
|
||||
|
||||
func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
||||
provider string, titleCaser cases.Caser) {
|
||||
provider string, titleCaser cases.Caser,
|
||||
) {
|
||||
boolPtr, ok := providerToFormat[provider]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown provider in format map: %s", provider))
|
||||
@@ -43,7 +44,7 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
providersToFormat[provider] = new(bool)
|
||||
}
|
||||
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown' or 'json'")
|
||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||
titleCaser := cases.Title(language.English)
|
||||
for _, provider := range allProviderFlags {
|
||||
@@ -53,9 +54,7 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if format != "markdown" {
|
||||
return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format)
|
||||
}
|
||||
// Note the format is validated by storage.Format
|
||||
|
||||
// Verify only one provider is set to be formatted.
|
||||
var providers []string
|
||||
@@ -87,10 +86,14 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
|
||||
formatted := storage.FormatToMarkdown(providerToFormat)
|
||||
formatted, err := storage.Format(providerToFormat, format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting servers: %w", err)
|
||||
}
|
||||
|
||||
output = filepath.Clean(output)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
const permission = fs.FileMode(0o644)
|
||||
file, err := os.OpenFile(output, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, permission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening output file: %w", err)
|
||||
}
|
||||
|
||||
66
internal/cli/genkey.go
Normal file
66
internal/cli/genkey.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *CLI) GenKey(args []string) (err error) {
|
||||
flagSet := flag.NewFlagSet("genkey", flag.ExitOnError)
|
||||
err = flagSet.Parse(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
const keyLength = 128 / 8
|
||||
keyBytes := make([]byte, keyLength)
|
||||
|
||||
_, _ = rand.Read(keyBytes)
|
||||
|
||||
key := base58Encode(keyBytes)
|
||||
fmt.Println(key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func base58Encode(data []byte) string {
|
||||
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
const radix = 58
|
||||
|
||||
zcount := 0
|
||||
for zcount < len(data) && data[zcount] == 0 {
|
||||
zcount++
|
||||
}
|
||||
|
||||
// integer simplification of ceil(log(256)/log(58))
|
||||
ceilLog256Div58 := (len(data)-zcount)*555/406 + 1 //nolint:mnd
|
||||
size := zcount + ceilLog256Div58
|
||||
|
||||
output := make([]byte, size)
|
||||
|
||||
high := size - 1
|
||||
for _, b := range data {
|
||||
i := size - 1
|
||||
for carry := uint32(b); i > high || carry != 0; i-- {
|
||||
carry += 256 * uint32(output[i]) //nolint:mnd
|
||||
output[i] = byte(carry % radix)
|
||||
carry /= radix
|
||||
}
|
||||
high = i
|
||||
}
|
||||
|
||||
// Determine the additional "zero-gap" in the output buffer
|
||||
additionalZeroGapEnd := zcount
|
||||
for additionalZeroGapEnd < size && output[additionalZeroGapEnd] == 0 {
|
||||
additionalZeroGapEnd++
|
||||
}
|
||||
|
||||
val := output[additionalZeroGapEnd-zcount:]
|
||||
size = len(val)
|
||||
for i := range val {
|
||||
output[i] = alphabet[val[i]]
|
||||
}
|
||||
|
||||
return string(output[:size])
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
package cli
|
||||
|
||||
import "github.com/qdm12/golibs/logging"
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func newNoopLogger() *noopLogger {
|
||||
return new(noopLogger)
|
||||
}
|
||||
|
||||
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) {}
|
||||
func (l *noopLogger) Info(string) {}
|
||||
|
||||
@@ -34,6 +34,8 @@ type ParallelResolver interface {
|
||||
}
|
||||
|
||||
type IPFetcher interface {
|
||||
String() string
|
||||
CanFetchAnyIP() bool
|
||||
FetchInfo(ctx context.Context, ip netip.Addr) (data models.PublicIP, err error)
|
||||
}
|
||||
|
||||
@@ -42,14 +44,15 @@ type IPv6Checker interface {
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
ipv6Checker IPv6Checker) error {
|
||||
ipv6Checker IPv6Checker,
|
||||
) error {
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var allSettings settings.Settings
|
||||
err = allSettings.Read(reader)
|
||||
err = allSettings.Read(reader, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -59,7 +62,7 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
if err = allSettings.Validate(storage, ipv6Supported); err != nil {
|
||||
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||
return fmt.Errorf("validating settings: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -80,10 +80,17 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||
ipFetcher, err := api.New(api.IPInfo, httpClient, ipToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating public IP API client: %w", err)
|
||||
nameTokenPairs := []api.NameToken{
|
||||
{Name: string(api.IPInfo), Token: ipToken},
|
||||
{Name: string(api.IP2Location)},
|
||||
{Name: string(api.IfConfigCo)},
|
||||
}
|
||||
fetchers, err := api.New(nameTokenPairs, httpClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating public IP fetchers: %w", err)
|
||||
}
|
||||
ipFetcher := api.NewResilient(fetchers, logger)
|
||||
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
|
||||
8
internal/command/cmder.go
Normal file
8
internal/command/cmder.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package command
|
||||
|
||||
// Cmder handles running subprograms synchronously and asynchronously.
|
||||
type Cmder struct{}
|
||||
|
||||
func New() *Cmder {
|
||||
return &Cmder{}
|
||||
}
|
||||
11
internal/command/interfaces_local.go
Normal file
11
internal/command/interfaces_local.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package command
|
||||
|
||||
import "io"
|
||||
|
||||
type execCmd interface {
|
||||
CombinedOutput() ([]byte, error)
|
||||
StdoutPipe() (io.ReadCloser, error)
|
||||
StderrPipe() (io.ReadCloser, error)
|
||||
Start() error
|
||||
Wait() error
|
||||
}
|
||||
3
internal/command/mocks_generate_test.go
Normal file
3
internal/command/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package command
|
||||
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package=$GOPACKAGE -source=interfaces_local.go
|
||||
108
internal/command/mocks_local_test.go
Normal file
108
internal/command/mocks_local_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interfaces_local.go
|
||||
|
||||
// Package command is a generated GoMock package.
|
||||
package command
|
||||
|
||||
import (
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockexecCmd is a mock of execCmd interface.
|
||||
type MockexecCmd struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockexecCmdMockRecorder
|
||||
}
|
||||
|
||||
// MockexecCmdMockRecorder is the mock recorder for MockexecCmd.
|
||||
type MockexecCmdMockRecorder struct {
|
||||
mock *MockexecCmd
|
||||
}
|
||||
|
||||
// NewMockexecCmd creates a new mock instance.
|
||||
func NewMockexecCmd(ctrl *gomock.Controller) *MockexecCmd {
|
||||
mock := &MockexecCmd{ctrl: ctrl}
|
||||
mock.recorder = &MockexecCmdMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockexecCmd) EXPECT() *MockexecCmdMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CombinedOutput mocks base method.
|
||||
func (m *MockexecCmd) CombinedOutput() ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CombinedOutput")
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CombinedOutput indicates an expected call of CombinedOutput.
|
||||
func (mr *MockexecCmdMockRecorder) CombinedOutput() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CombinedOutput", reflect.TypeOf((*MockexecCmd)(nil).CombinedOutput))
|
||||
}
|
||||
|
||||
// Start mocks base method.
|
||||
func (m *MockexecCmd) Start() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Start")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start.
|
||||
func (mr *MockexecCmdMockRecorder) Start() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockexecCmd)(nil).Start))
|
||||
}
|
||||
|
||||
// StderrPipe mocks base method.
|
||||
func (m *MockexecCmd) StderrPipe() (io.ReadCloser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "StderrPipe")
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// StderrPipe indicates an expected call of StderrPipe.
|
||||
func (mr *MockexecCmdMockRecorder) StderrPipe() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StderrPipe", reflect.TypeOf((*MockexecCmd)(nil).StderrPipe))
|
||||
}
|
||||
|
||||
// StdoutPipe mocks base method.
|
||||
func (m *MockexecCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "StdoutPipe")
|
||||
ret0, _ := ret[0].(io.ReadCloser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// StdoutPipe indicates an expected call of StdoutPipe.
|
||||
func (mr *MockexecCmdMockRecorder) StdoutPipe() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StdoutPipe", reflect.TypeOf((*MockexecCmd)(nil).StdoutPipe))
|
||||
}
|
||||
|
||||
// Wait mocks base method.
|
||||
func (m *MockexecCmd) Wait() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Wait")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Wait indicates an expected call of Wait.
|
||||
func (mr *MockexecCmdMockRecorder) Wait() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockexecCmd)(nil).Wait))
|
||||
}
|
||||
30
internal/command/run.go
Normal file
30
internal/command/run.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Run runs a command in a blocking manner, returning its output and
|
||||
// an error if it failed.
|
||||
func (c *Cmder) Run(cmd *exec.Cmd) (output string, err error) {
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func run(cmd execCmd) (output string, err error) {
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
output = string(stdout)
|
||||
output = strings.TrimSuffix(output, "\n")
|
||||
lines := stringToLines(output)
|
||||
for i := range lines {
|
||||
lines[i] = strings.TrimPrefix(lines[i], "'")
|
||||
lines[i] = strings.TrimSuffix(lines[i], "'")
|
||||
}
|
||||
output = strings.Join(lines, "\n")
|
||||
return output, err
|
||||
}
|
||||
|
||||
func stringToLines(s string) (lines []string) {
|
||||
s = strings.TrimSuffix(s, "\n")
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
54
internal/command/run_test.go
Normal file
54
internal/command/run_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errDummy := errors.New("dummy")
|
||||
|
||||
testCases := map[string]struct {
|
||||
stdout []byte
|
||||
cmdErr error
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
"no output": {},
|
||||
"cmd error": {
|
||||
stdout: []byte("'hello \nworld'\n"),
|
||||
cmdErr: errDummy,
|
||||
output: "hello \nworld",
|
||||
err: errDummy,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockCmd := NewMockexecCmd(ctrl)
|
||||
|
||||
mockCmd.EXPECT().CombinedOutput().Return(testCase.stdout, testCase.cmdErr)
|
||||
|
||||
output, err := run(mockCmd)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
100
internal/command/start.go
Normal file
100
internal/command/start.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Start launches a command and streams stdout and stderr to channels.
|
||||
// All the channels returned are ready only and won't be closed
|
||||
// if the command fails later.
|
||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||
stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
) {
|
||||
return start(cmd)
|
||||
}
|
||||
|
||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
) {
|
||||
stop := make(chan struct{})
|
||||
stdoutReady := make(chan struct{})
|
||||
stdoutLinesCh := make(chan string)
|
||||
stdoutDone := make(chan struct{})
|
||||
stderrReady := make(chan struct{})
|
||||
stderrLinesCh := make(chan string)
|
||||
stderrDone := make(chan struct{})
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
waitErrorCh := make(chan error)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
waitErrorCh <- err
|
||||
}()
|
||||
|
||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||
}
|
||||
|
||||
func streamToChannel(ready chan<- struct{},
|
||||
stop <-chan struct{}, done chan<- struct{},
|
||||
stream io.Reader, lines chan<- string,
|
||||
) {
|
||||
defer close(done)
|
||||
close(ready)
|
||||
scanner := bufio.NewScanner(stream)
|
||||
lineBuffer := make([]byte, bufio.MaxScanTokenSize) // 64KB
|
||||
const maxCapacity = 20 * 1024 * 1024 // 20MB
|
||||
scanner.Buffer(lineBuffer, maxCapacity)
|
||||
|
||||
for scanner.Scan() {
|
||||
// scanner is closed if the context is canceled
|
||||
// or if the command failed starting because the
|
||||
// stream is closed (io.EOF error).
|
||||
lines <- scanner.Text()
|
||||
}
|
||||
err := scanner.Err()
|
||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore the error if it is stopped.
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
lines <- "stream error: " + err.Error()
|
||||
}
|
||||
}
|
||||
118
internal/command/start_test.go
Normal file
118
internal/command/start_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func linesToReadCloser(lines []string) io.ReadCloser {
|
||||
s := strings.Join(lines, "\n")
|
||||
return io.NopCloser(bytes.NewBufferString(s))
|
||||
}
|
||||
|
||||
func Test_start(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errDummy := errors.New("dummy")
|
||||
|
||||
testCases := map[string]struct {
|
||||
stdout []string
|
||||
stdoutPipeErr error
|
||||
stderr []string
|
||||
stderrPipeErr error
|
||||
startErr error
|
||||
waitErr error
|
||||
err error
|
||||
}{
|
||||
"no output": {},
|
||||
"success": {
|
||||
stdout: []string{"hello", "world"},
|
||||
stderr: []string{"some", "error"},
|
||||
},
|
||||
"stdout pipe error": {
|
||||
stdoutPipeErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"stderr pipe error": {
|
||||
stderrPipeErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"start error": {
|
||||
startErr: errDummy,
|
||||
err: errDummy,
|
||||
},
|
||||
"wait error": {
|
||||
waitErr: errDummy,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
stdout := linesToReadCloser(testCase.stdout)
|
||||
stderr := linesToReadCloser(testCase.stderr)
|
||||
|
||||
mockCmd := NewMockexecCmd(ctrl)
|
||||
|
||||
mockCmd.EXPECT().StdoutPipe().
|
||||
Return(stdout, testCase.stdoutPipeErr)
|
||||
if testCase.stdoutPipeErr == nil {
|
||||
mockCmd.EXPECT().StderrPipe().Return(stderr, testCase.stderrPipeErr)
|
||||
if testCase.stderrPipeErr == nil {
|
||||
mockCmd.EXPECT().Start().Return(testCase.startErr)
|
||||
if testCase.startErr == nil {
|
||||
mockCmd.EXPECT().Wait().Return(testCase.waitErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdoutLines, stderrLines, waitError, err := start(mockCmd)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
assert.Nil(t, stdoutLines)
|
||||
assert.Nil(t, stderrLines)
|
||||
assert.Nil(t, waitError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var stdoutIndex, stderrIndex int
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case line := <-stdoutLines:
|
||||
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
||||
stdoutIndex++
|
||||
case line := <-stderrLines:
|
||||
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
||||
stderrIndex++
|
||||
case err := <-waitError:
|
||||
if testCase.waitErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
||||
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
25
internal/configuration/settings/deprecated.go
Normal file
25
internal/configuration/settings/deprecated.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
@@ -3,10 +3,11 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/dns/pkg/blacklist"
|
||||
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -74,16 +75,19 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
|
||||
return blacklist.BuilderSettings{
|
||||
BlockMalicious: *b.BlockMalicious,
|
||||
BlockAds: *b.BlockAds,
|
||||
BlockSurveillance: *b.BlockSurveillance,
|
||||
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||
settings blockbuilder.Settings,
|
||||
) {
|
||||
return blockbuilder.Settings{
|
||||
Client: client,
|
||||
BlockMalicious: b.BlockMalicious,
|
||||
BlockAds: b.BlockAds,
|
||||
BlockSurveillance: b.BlockSurveillance,
|
||||
AllowedHosts: b.AllowedHosts,
|
||||
AddBlockedHosts: b.AddBlockedHosts,
|
||||
AddBlockedIPs: netipAddressesToNetaddrIPs(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: netipPrefixesToNetaddrIPPrefixes(b.AddBlockedIPPrefixes),
|
||||
}, nil
|
||||
AddBlockedIPs: b.AddBlockedIPs,
|
||||
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||
}
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) String() string {
|
||||
@@ -98,30 +102,30 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
||||
|
||||
if len(b.AllowedHosts) > 0 {
|
||||
allowedHostsNode := node.Appendf("Allowed hosts:")
|
||||
allowedHostsNode := node.Append("Allowed hosts:")
|
||||
for _, host := range b.AllowedHosts {
|
||||
allowedHostsNode.Appendf(host)
|
||||
allowedHostsNode.Append(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedHosts) > 0 {
|
||||
blockedHostsNode := node.Appendf("Blocked hosts:")
|
||||
blockedHostsNode := node.Append("Blocked hosts:")
|
||||
for _, host := range b.AddBlockedHosts {
|
||||
blockedHostsNode.Appendf(host)
|
||||
blockedHostsNode.Append(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPs) > 0 {
|
||||
blockedIPsNode := node.Appendf("Blocked IP addresses:")
|
||||
blockedIPsNode := node.Append("Blocked IP addresses:")
|
||||
for _, ip := range b.AddBlockedIPs {
|
||||
blockedIPsNode.Appendf(ip.String())
|
||||
blockedIPsNode.Append(ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
|
||||
blockedIPPrefixesNode := node.Append("Blocked IP networks:")
|
||||
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||
blockedIPPrefixesNode.Appendf(ipNetwork.String())
|
||||
blockedIPPrefixesNode.Append(ipNetwork.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,12 +160,11 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||
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,
|
||||
ipPrefixes []netip.Prefix, err error) {
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||
if len(privateAddresses) == 0 {
|
||||
return nil, nil, nil
|
||||
|
||||
@@ -3,8 +3,10 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -16,22 +18,24 @@ type DoT struct {
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
Enabled *bool
|
||||
// UpdatePeriod is the period to update DNS block
|
||||
// lists and cryptographic files for DNSSEC validation.
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Unbound contains settings to configure Unbound.
|
||||
Unbound Unbound
|
||||
// Providers is a list of DNS over TLS providers
|
||||
Providers []string `json:"providers"`
|
||||
// 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
|
||||
// block lists.
|
||||
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) {
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
@@ -40,9 +44,12 @@ func (d DoT) validate() (err error) {
|
||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
err = d.Unbound.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
@@ -57,7 +64,9 @@ func (d *DoT) copy() (copied DoT) {
|
||||
return DoT{
|
||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Unbound: d.Unbound.copy(),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
}
|
||||
}
|
||||
@@ -68,7 +77,9 @@ func (d *DoT) copy() (copied DoT) {
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Unbound.overrideWith(other.Unbound)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
}
|
||||
|
||||
@@ -76,10 +87,26 @@ func (d *DoT) setDefaults() {
|
||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Unbound.setDefaults()
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
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 {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
@@ -98,7 +125,14 @@ func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
node.AppendNode(d.Unbound.toLinesNode())
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
@@ -115,7 +149,14 @@ func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Unbound.read(reader)
|
||||
d.Providers = reader.CSV("DOT_PROVIDERS")
|
||||
|
||||
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ var (
|
||||
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||
ErrPortForwardingUserEmpty = errors.New("port forwarding username 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")
|
||||
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||
|
||||
@@ -104,7 +104,6 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||
if len(f.OutboundSubnets) > 0 {
|
||||
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||
for _, subnet := range f.OutboundSubnets {
|
||||
subnet := subnet
|
||||
outboundSubnets.Appendf("%s", &subnet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ func Test_Firewall_validate(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
package settings
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func uint8Ptr(n uint8) *uint8 { return &n }
|
||||
import gomock "github.com/golang/mock/gomock"
|
||||
|
||||
type sourceKeyValue struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func newMockSource(ctrl *gomock.Controller, keyValues []sourceKeyValue) *MockSource {
|
||||
source := NewMockSource(ctrl)
|
||||
var previousCall *gomock.Call
|
||||
for _, keyValue := range keyValues {
|
||||
transformedKey := keyValue.key
|
||||
keyTransformCall := source.EXPECT().KeyTransform(keyValue.key).Return(transformedKey)
|
||||
if previousCall != nil {
|
||||
keyTransformCall.After(previousCall)
|
||||
}
|
||||
isSet := keyValue.value != ""
|
||||
previousCall = source.EXPECT().Get(transformedKey).
|
||||
Return(keyValue.value, isSet).After(keyTransformCall)
|
||||
if isSet {
|
||||
previousCall = source.EXPECT().KeyTransform(keyValue.key).
|
||||
Return(transformedKey).After(previousCall)
|
||||
previousCall = source.EXPECT().String().
|
||||
Return("mock source").After(previousCall)
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
5
internal/configuration/settings/interfaces.go
Normal file
5
internal/configuration/settings/interfaces.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package settings
|
||||
|
||||
type Warner interface {
|
||||
Warn(message string)
|
||||
}
|
||||
4
internal/configuration/settings/mocks_generate_test.go
Normal file
4
internal/configuration/settings/mocks_generate_test.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package settings
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Warner
|
||||
//go:generate mockgen -destination=mocks_reader_test.go -package=$GOPACKAGE github.com/qdm12/gosettings/reader Source
|
||||
77
internal/configuration/settings/mocks_reader_test.go
Normal file
77
internal/configuration/settings/mocks_reader_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
|
||||
|
||||
// Package settings is a generated GoMock package.
|
||||
package settings
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockSource is a mock of Source interface.
|
||||
type MockSource struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSourceMockRecorder
|
||||
}
|
||||
|
||||
// MockSourceMockRecorder is the mock recorder for MockSource.
|
||||
type MockSourceMockRecorder struct {
|
||||
mock *MockSource
|
||||
}
|
||||
|
||||
// NewMockSource creates a new mock instance.
|
||||
func NewMockSource(ctrl *gomock.Controller) *MockSource {
|
||||
mock := &MockSource{ctrl: ctrl}
|
||||
mock.recorder = &MockSourceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSource) EXPECT() *MockSourceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockSource) Get(arg0 string) (string, bool) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(bool)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
|
||||
}
|
||||
|
||||
// KeyTransform mocks base method.
|
||||
func (m *MockSource) KeyTransform(arg0 string) string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "KeyTransform", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// KeyTransform indicates an expected call of KeyTransform.
|
||||
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
|
||||
}
|
||||
|
||||
// String mocks base method.
|
||||
func (m *MockSource) String() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "String")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// String indicates an expected call of String.
|
||||
func (mr *MockSourceMockRecorder) String() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockSource)(nil).String))
|
||||
}
|
||||
46
internal/configuration/settings/mocks_test.go
Normal file
46
internal/configuration/settings/mocks_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
|
||||
|
||||
// Package settings is a generated GoMock package.
|
||||
package settings
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockWarner is a mock of Warner interface.
|
||||
type MockWarner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockWarnerMockRecorder
|
||||
}
|
||||
|
||||
// MockWarnerMockRecorder is the mock recorder for MockWarner.
|
||||
type MockWarnerMockRecorder struct {
|
||||
mock *MockWarner
|
||||
}
|
||||
|
||||
// NewMockWarner creates a new mock instance.
|
||||
func NewMockWarner(ctrl *gomock.Controller) *MockWarner {
|
||||
mock := &MockWarner{ctrl: ctrl}
|
||||
mock.recorder = &MockWarnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockWarner) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Warn", arg0)
|
||||
}
|
||||
|
||||
// Warn indicates an expected call of Warn.
|
||||
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func netipAddressToNetaddrIP(address netip.Addr) (ip netaddr.IP) {
|
||||
if address.Is4() {
|
||||
return netaddr.IPFrom4(address.As4())
|
||||
}
|
||||
return netaddr.IPFrom16(address.As16())
|
||||
}
|
||||
|
||||
func netipAddressesToNetaddrIPs(addresses []netip.Addr) (ips []netaddr.IP) {
|
||||
ips = make([]netaddr.IP, len(addresses))
|
||||
for i := range addresses {
|
||||
ips[i] = netipAddressToNetaddrIP(addresses[i])
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func netipPrefixToNetaddrIPPrefix(prefix netip.Prefix) (ipPrefix netaddr.IPPrefix) {
|
||||
netaddrIP := netipAddressToNetaddrIP(prefix.Addr())
|
||||
bits := prefix.Bits()
|
||||
return netaddr.IPPrefixFrom(netaddrIP, uint8(bits))
|
||||
}
|
||||
|
||||
func netipPrefixesToNetaddrIPPrefixes(prefixes []netip.Prefix) (ipPrefixes []netaddr.IPPrefix) {
|
||||
ipPrefixes = make([]netaddr.IPPrefix, len(prefixes))
|
||||
for i := range ipPrefixes {
|
||||
ipPrefixes[i] = netipPrefixToNetaddrIPPrefix(prefixes[i])
|
||||
}
|
||||
return ipPrefixes
|
||||
}
|
||||
@@ -4,7 +4,8 @@ package settings
|
||||
// and SERVER_REGIONS is now the continent field for servers.
|
||||
// TODO v4 remove.
|
||||
func nordvpnRetroRegion(selection ServerSelection, validRegions, validCountries []string) (
|
||||
updatedSelection ServerSelection) {
|
||||
updatedSelection ServerSelection,
|
||||
) {
|
||||
validRegionsMap := stringSliceToMap(validRegions)
|
||||
validCountriesMap := stringSliceToMap(validCountries)
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||
}
|
||||
|
||||
func validateOpenVPNConfigFilepath(isCustom bool,
|
||||
confFile string) (err error) {
|
||||
confFile string,
|
||||
) (err error) {
|
||||
if !isCustom {
|
||||
return nil
|
||||
}
|
||||
@@ -179,7 +180,8 @@ func validateOpenVPNConfigFilepath(isCustom bool,
|
||||
}
|
||||
|
||||
func validateOpenVPNClientCertificate(vpnProvider,
|
||||
clientCert string) (err error) {
|
||||
clientCert string,
|
||||
) (err error) {
|
||||
switch vpnProvider {
|
||||
case
|
||||
providers.Airvpn,
|
||||
@@ -226,7 +228,8 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
||||
}
|
||||
|
||||
func validateOpenVPNEncryptedKey(vpnProvider,
|
||||
encryptedPrivateKey string) (err error) {
|
||||
encryptedPrivateKey string,
|
||||
) (err error) {
|
||||
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
|
||||
return fmt.Errorf("%w", ErrMissingValue)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ func Test_ivpnAccountID(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
|
||||
// Validate TCP
|
||||
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
|
||||
providers.Giganews,
|
||||
providers.Ipvanish,
|
||||
providers.Perfectprivacy,
|
||||
providers.Privado,
|
||||
@@ -67,7 +68,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Privatevpn, providers.Torguard:
|
||||
// no custom port allowed
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Privado, providers.Purevpn,
|
||||
providers.Surfshark, providers.VPNSecure,
|
||||
providers.VPNUnlimited, providers.Vyprvpn:
|
||||
@@ -192,9 +193,6 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
||||
o.ConfFile = r.Get("OPENVPN_CUSTOM_CONFIG", reader.ForceLowercase(false))
|
||||
|
||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||
|
||||
@@ -52,7 +52,9 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
validProviders := []string{
|
||||
providers.Perfectprivacy,
|
||||
providers.PrivateInternetAccess,
|
||||
providers.Privatevpn,
|
||||
providers.Protonvpn,
|
||||
providers.Mullvad,
|
||||
}
|
||||
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
|
||||
|
||||
@@ -10,7 +10,7 @@ func Test_PortForwarding_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := PortForwarding{
|
||||
Enabled: boolPtr(false),
|
||||
Enabled: ptrTo(false),
|
||||
}
|
||||
|
||||
s := settings.String()
|
||||
|
||||
@@ -25,7 +25,7 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
||||
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||
// Validate Name
|
||||
var validNames []string
|
||||
if vpnType == vpn.OpenVPN {
|
||||
@@ -48,7 +48,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
||||
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
err = p.ServerSelection.validate(p.Name, storage)
|
||||
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server selection: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package settings
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -13,24 +12,28 @@ import (
|
||||
|
||||
// PublicIP contains settings for port forwarding.
|
||||
type PublicIP struct {
|
||||
// Period is the period to get the public IP address.
|
||||
// It can be set to 0 to disable periodic checking.
|
||||
// It cannot be nil for the internal state.
|
||||
// TODO change to value and add enabled field
|
||||
Period *time.Duration
|
||||
// Enabled is set to true to fetch the public ip address
|
||||
// information on VPN connection. It defaults to true.
|
||||
Enabled *bool
|
||||
// IPFilepath is the public IP address status file path
|
||||
// to use. It can be the empty string to indicate not
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
IPFilepath *string
|
||||
// API is the API name to use to fetch public IP information.
|
||||
// It can be ipinfo or ip2location. It defaults to ipinfo.
|
||||
API string
|
||||
// APIToken is the token to use for the IP data service
|
||||
// such as ipinfo.io. It can be the empty string to
|
||||
// indicate not to use a token. It cannot be nil for the
|
||||
// internal state.
|
||||
APIToken *string
|
||||
// APIs is the list of public ip APIs to use to fetch public IP information.
|
||||
// If there is more than one API, the first one is used
|
||||
// by default and the others are used as fallbacks in case of
|
||||
// the service rate limiting us. It defaults to use all services,
|
||||
// with the first one being ipinfo.io for historical reasons.
|
||||
APIs []PublicIPAPI
|
||||
}
|
||||
|
||||
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
|
||||
@@ -48,12 +51,6 @@ func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP,
|
||||
}
|
||||
|
||||
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
|
||||
_, err := filepath.Abs(*p.IPFilepath)
|
||||
if err != nil {
|
||||
@@ -61,9 +58,11 @@ func (p PublicIP) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = api.ParseProvider(p.API)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API name: %w", err)
|
||||
for _, publicIPAPI := range p.APIs {
|
||||
_, err = api.ParseProvider(publicIPAPI.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -71,26 +70,27 @@ func (p PublicIP) validate() (err error) {
|
||||
|
||||
func (p *PublicIP) copy() (copied PublicIP) {
|
||||
return PublicIP{
|
||||
Period: gosettings.CopyPointer(p.Period),
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
IPFilepath: gosettings.CopyPointer(p.IPFilepath),
|
||||
API: p.API,
|
||||
APIToken: gosettings.CopyPointer(p.APIToken),
|
||||
APIs: gosettings.CopySlice(p.APIs),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||
p.Period = gosettings.OverrideWithPointer(p.Period, other.Period)
|
||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||
p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
|
||||
p.API = gosettings.OverrideWithComparable(p.API, other.API)
|
||||
p.APIToken = gosettings.OverrideWithPointer(p.APIToken, other.APIToken)
|
||||
p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
|
||||
}
|
||||
|
||||
func (p *PublicIP) setDefaults() {
|
||||
const defaultPeriod = 12 * time.Hour
|
||||
p.Period = gosettings.DefaultPointer(p.Period, defaultPeriod)
|
||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
|
||||
p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
|
||||
p.API = gosettings.DefaultComparable(p.API, "ipinfo")
|
||||
p.APIToken = gosettings.DefaultPointer(p.APIToken, "")
|
||||
p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
|
||||
{Name: string(api.IPInfo)},
|
||||
{Name: string(api.Cloudflare)},
|
||||
{Name: string(api.IfConfigCo)},
|
||||
{Name: string(api.IP2Location)},
|
||||
})
|
||||
}
|
||||
|
||||
func (p PublicIP) String() string {
|
||||
@@ -98,41 +98,78 @@ func (p PublicIP) String() string {
|
||||
}
|
||||
|
||||
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||
if !*p.Enabled {
|
||||
return gotree.New("Public IP settings: disabled")
|
||||
}
|
||||
|
||||
node = gotree.New("Public IP settings:")
|
||||
|
||||
if *p.Period == 0 {
|
||||
node.Appendf("Enabled: no")
|
||||
return node
|
||||
}
|
||||
|
||||
updatePeriod := "disabled"
|
||||
if *p.Period > 0 {
|
||||
updatePeriod = "every " + p.Period.String()
|
||||
}
|
||||
node.Appendf("Fetching: %s", updatePeriod)
|
||||
|
||||
if *p.IPFilepath != "" {
|
||||
node.Appendf("IP file path: %s", *p.IPFilepath)
|
||||
}
|
||||
|
||||
node.Appendf("Public IP data API: %s", p.API)
|
||||
|
||||
if *p.APIToken != "" {
|
||||
node.Appendf("API token: %s", gosettings.ObfuscateKey(*p.APIToken))
|
||||
baseAPIString := "Public IP data base API: " + p.APIs[0].Name
|
||||
if p.APIs[0].Token != "" {
|
||||
baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (p *PublicIP) read(r *reader.Reader) (err error) {
|
||||
p.Period, err = r.DurationPtr("PUBLICIP_PERIOD")
|
||||
func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
|
||||
p.Enabled, err = readPublicIPEnabled(r, warner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.IPFilepath = r.Get("PUBLICIP_FILE",
|
||||
reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))
|
||||
p.API = r.String("PUBLICIP_API")
|
||||
p.APIToken = r.Get("PUBLICIP_API_TOKEN")
|
||||
|
||||
apiNames := r.CSV("PUBLICIP_API")
|
||||
if len(apiNames) > 0 {
|
||||
apiTokens := r.CSV("PUBLICIP_API_TOKEN")
|
||||
p.APIs = make([]PublicIPAPI, len(apiNames))
|
||||
for i := range apiNames {
|
||||
p.APIs[i].Name = apiNames[i]
|
||||
var token string
|
||||
if i < len(apiTokens) { // only set token if it exists
|
||||
token = apiTokens[i]
|
||||
}
|
||||
p.APIs[i].Token = token
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPublicIPEnabled(r *reader.Reader, warner Warner) (
|
||||
enabled *bool, err error,
|
||||
) {
|
||||
periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if periodPtr == nil {
|
||||
return r.BoolPtr("PUBLICIP_ENABLED")
|
||||
}
|
||||
|
||||
if *periodPtr == 0 {
|
||||
warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
|
||||
return ptrTo(false), nil
|
||||
}
|
||||
|
||||
warner.Warn("PUBLICIP_PERIOD is no longer used. " +
|
||||
"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
|
||||
"Please migrate to use PUBLICIP_ENABLED only in the future.")
|
||||
return ptrTo(true), nil
|
||||
}
|
||||
|
||||
161
internal/configuration/settings/publicip_test.go
Normal file
161
internal/configuration/settings/publicip_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PublicIP_read(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
makeReader func(ctrl *gomock.Controller) *reader.Reader
|
||||
makeWarner func(ctrl *gomock.Controller) Warner
|
||||
settings PublicIP
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"nothing_read": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
},
|
||||
"single_api_no_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||
{key: "PUBLICIP_API_TOKEN"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"single_api_with_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_no_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo"},
|
||||
{Name: "ip2location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_with_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz,abc"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
{Name: "ip2location", Token: "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_apis_with_and_without_token": {
|
||||
makeReader: func(ctrl *gomock.Controller) *reader.Reader {
|
||||
source := newMockSource(ctrl, []sourceKeyValue{
|
||||
{key: "PUBLICIP_PERIOD"},
|
||||
{key: "PUBLICIP_ENABLED"},
|
||||
{key: "IP_STATUS_FILE"},
|
||||
{key: "PUBLICIP_FILE"},
|
||||
{key: "PUBLICIP_API", value: "ipinfo,ip2location"},
|
||||
{key: "PUBLICIP_API_TOKEN", value: "xyz"},
|
||||
})
|
||||
return reader.New(reader.Settings{
|
||||
Sources: []reader.Source{source},
|
||||
})
|
||||
},
|
||||
settings: PublicIP{
|
||||
APIs: []PublicIPAPI{
|
||||
{Name: "ipinfo", Token: "xyz"},
|
||||
{Name: "ip2location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
reader := testCase.makeReader(ctrl)
|
||||
var warner Warner
|
||||
if testCase.makeWarner != nil {
|
||||
warner = testCase.makeWarner(ctrl)
|
||||
}
|
||||
|
||||
var settings PublicIP
|
||||
err := settings.read(reader, warner)
|
||||
|
||||
assert.Equal(t, testCase.settings, settings)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ type ControlServer struct {
|
||||
// Log can be true or false to enable logging on requests.
|
||||
// It cannot be nil in the internal state.
|
||||
Log *bool
|
||||
// AuthFilePath is the path to the file containing the authentication
|
||||
// configuration for the middleware.
|
||||
// It cannot be empty in the internal state and defaults to
|
||||
// /gluetun/auth/config.toml.
|
||||
AuthFilePath string
|
||||
}
|
||||
|
||||
func (c ControlServer) validate() (err error) {
|
||||
@@ -44,8 +49,9 @@ func (c ControlServer) validate() (err error) {
|
||||
|
||||
func (c *ControlServer) copy() (copied ControlServer) {
|
||||
return ControlServer{
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +61,13 @@ func (c *ControlServer) copy() (copied ControlServer) {
|
||||
func (c *ControlServer) overrideWith(other ControlServer) {
|
||||
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
|
||||
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
|
||||
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
|
||||
}
|
||||
|
||||
func (c *ControlServer) setDefaults() {
|
||||
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
|
||||
c.Log = gosettings.DefaultPointer(c.Log, true)
|
||||
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
|
||||
}
|
||||
|
||||
func (c ControlServer) String() string {
|
||||
@@ -70,6 +78,7 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Control server settings:")
|
||||
node.Appendf("Listening address: %s", *c.Address)
|
||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -78,6 +87,10 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||
|
||||
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,14 +91,15 @@ var (
|
||||
)
|
||||
|
||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
storage Storage) (err error) {
|
||||
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||
) (err error) {
|
||||
switch ss.VPN {
|
||||
case vpn.OpenVPN, vpn.Wireguard:
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||
}
|
||||
|
||||
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, storage)
|
||||
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
|
||||
if err != nil {
|
||||
return err // already wrapped error
|
||||
}
|
||||
@@ -111,7 +112,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
*ss = surfsharkRetroRegion(*ss)
|
||||
}
|
||||
|
||||
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider)
|
||||
err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
|
||||
}
|
||||
@@ -142,19 +143,20 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
}
|
||||
|
||||
func getLocationFilterChoices(vpnServiceProvider string,
|
||||
ss *ServerSelection, storage Storage) (filterChoices models.FilterChoices,
|
||||
err error) {
|
||||
filterChoices = storage.GetFilterChoices(vpnServiceProvider)
|
||||
ss *ServerSelection, filterChoicesGetter FilterChoicesGetter, warner Warner) (
|
||||
filterChoices models.FilterChoices, err error,
|
||||
) {
|
||||
filterChoices = filterChoicesGetter.GetFilterChoices(vpnServiceProvider)
|
||||
|
||||
if vpnServiceProvider == providers.Surfshark {
|
||||
// // Retro compatibility
|
||||
// TODO v4 remove
|
||||
newAndRetroRegions := append(filterChoices.Regions, validation.SurfsharkRetroLocChoices()...) //nolint:gocritic
|
||||
err := validate.AreAllOneOfCaseInsensitive(ss.Regions, newAndRetroRegions)
|
||||
err := atLeastOneIsOneOfCaseInsensitive(ss.Regions, newAndRetroRegions, warner)
|
||||
if err != nil {
|
||||
// Only return error comparing with newer regions, we don't want to confuse the user
|
||||
// with the retro regions in the error message.
|
||||
err = validate.AreAllOneOfCaseInsensitive(ss.Regions, filterChoices.Regions)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
|
||||
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||
}
|
||||
}
|
||||
@@ -165,44 +167,53 @@ func getLocationFilterChoices(vpnServiceProvider string,
|
||||
// validateServerFilters validates filters against the choices given as arguments.
|
||||
// Set an argument to nil to pass the check for a particular filter.
|
||||
func validateServerFilters(settings ServerSelection, filterChoices models.FilterChoices,
|
||||
vpnServiceProvider string) (err error) {
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Countries, filterChoices.Countries)
|
||||
vpnServiceProvider string, warner Warner,
|
||||
) (err error) {
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
|
||||
}
|
||||
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Regions, filterChoices.Regions)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
|
||||
}
|
||||
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Cities, filterChoices.Cities)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
|
||||
}
|
||||
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
|
||||
}
|
||||
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
|
||||
}
|
||||
|
||||
if vpnServiceProvider == providers.Custom && len(settings.Names) == 1 {
|
||||
// Allow a single name to be specified for the custom provider in case
|
||||
// the user wants to use VPN server side port forwarding with PIA
|
||||
// which requires a server name for TLS verification.
|
||||
filterChoices.Names = settings.Names
|
||||
if vpnServiceProvider == providers.Custom {
|
||||
switch len(settings.Names) {
|
||||
case 0:
|
||||
case 1:
|
||||
// Allow a single name to be specified for the custom provider in case
|
||||
// the user wants to use VPN server side port forwarding with PIA
|
||||
// which requires a server name for TLS verification.
|
||||
filterChoices.Names = settings.Names
|
||||
default:
|
||||
return fmt.Errorf("%w: %d names specified instead of "+
|
||||
"0 or 1 for the custom provider",
|
||||
ErrNameNotValid, len(settings.Names))
|
||||
}
|
||||
}
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Names, filterChoices.Names)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
|
||||
}
|
||||
|
||||
err = validate.AreAllOneOfCaseInsensitive(settings.Categories, filterChoices.Categories)
|
||||
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
|
||||
}
|
||||
@@ -210,6 +221,43 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
|
||||
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 {
|
||||
switch {
|
||||
case *settings.FreeOnly &&
|
||||
@@ -229,6 +277,8 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
||||
switch {
|
||||
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
|
||||
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
|
||||
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
|
||||
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
|
||||
case *settings.StreamOnly &&
|
||||
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
|
||||
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
|
||||
@@ -410,7 +460,8 @@ func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) read(r *reader.Reader,
|
||||
vpnProvider, vpnType string) (err error) {
|
||||
vpnProvider, vpnType string,
|
||||
) (err error) {
|
||||
ss.VPN = vpnType
|
||||
|
||||
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
|
||||
@@ -22,6 +22,7 @@ type Settings struct {
|
||||
Log Log
|
||||
PublicIP PublicIP
|
||||
Shadowsocks Shadowsocks
|
||||
Storage Storage
|
||||
System System
|
||||
Updater Updater
|
||||
Version Version
|
||||
@@ -29,14 +30,16 @@ type Settings struct {
|
||||
Pprof pprof.Settings
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
type FilterChoicesGetter interface {
|
||||
GetFilterChoices(provider string) models.FilterChoices
|
||||
}
|
||||
|
||||
// Validate validates all the settings and returns an error
|
||||
// if one of them is not valid.
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
|
||||
func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool,
|
||||
warner Warner,
|
||||
) (err error) {
|
||||
nameToValidation := map[string]func() error{
|
||||
"control server": s.ControlServer.validate,
|
||||
"dns": s.DNS.validate,
|
||||
@@ -46,12 +49,13 @@ func (s *Settings) Validate(storage Storage, ipv6Supported bool) (err error) {
|
||||
"log": s.Log.validate,
|
||||
"public ip check": s.PublicIP.validate,
|
||||
"shadowsocks": s.Shadowsocks.validate,
|
||||
"storage": s.Storage.validate,
|
||||
"system": s.System.validate,
|
||||
"updater": s.Updater.Validate,
|
||||
"version": s.Version.validate,
|
||||
// Pprof validation done in pprof constructor
|
||||
"VPN": func() error {
|
||||
return s.VPN.Validate(storage, ipv6Supported)
|
||||
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -75,6 +79,7 @@ func (s *Settings) copy() (copied Settings) {
|
||||
Log: s.Log.copy(),
|
||||
PublicIP: s.PublicIP.copy(),
|
||||
Shadowsocks: s.Shadowsocks.copy(),
|
||||
Storage: s.Storage.copy(),
|
||||
System: s.System.copy(),
|
||||
Updater: s.Updater.copy(),
|
||||
Version: s.Version.copy(),
|
||||
@@ -84,7 +89,8 @@ func (s *Settings) copy() (copied Settings) {
|
||||
}
|
||||
|
||||
func (s *Settings) OverrideWith(other Settings,
|
||||
storage Storage, ipv6Supported bool) (err error) {
|
||||
filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner,
|
||||
) (err error) {
|
||||
patchedSettings := s.copy()
|
||||
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
||||
patchedSettings.DNS.overrideWith(other.DNS)
|
||||
@@ -94,12 +100,13 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
patchedSettings.Log.overrideWith(other.Log)
|
||||
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
||||
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
||||
patchedSettings.Storage.overrideWith(other.Storage)
|
||||
patchedSettings.System.overrideWith(other.System)
|
||||
patchedSettings.Updater.overrideWith(other.Updater)
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||
err = patchedSettings.Validate(storage, ipv6Supported)
|
||||
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -116,6 +123,7 @@ func (s *Settings) SetDefaults() {
|
||||
s.Log.setDefaults()
|
||||
s.PublicIP.setDefaults()
|
||||
s.Shadowsocks.setDefaults()
|
||||
s.Storage.setDefaults()
|
||||
s.System.setDefaults()
|
||||
s.Version.setDefaults()
|
||||
s.VPN.setDefaults()
|
||||
@@ -138,6 +146,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||
node.AppendNode(s.ControlServer.toLinesNode())
|
||||
node.AppendNode(s.Storage.toLinesNode())
|
||||
node.AppendNode(s.System.toLinesNode())
|
||||
node.AppendNode(s.PublicIP.toLinesNode())
|
||||
node.AppendNode(s.Updater.toLinesNode())
|
||||
@@ -177,7 +186,12 @@ func (s Settings) Warnings() (warnings []string) {
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (s *Settings) Read(r *reader.Reader) (err error) {
|
||||
func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||
warnings := readObsolete(r)
|
||||
for _, warning := range warnings {
|
||||
warner.Warn(warning)
|
||||
}
|
||||
|
||||
readFunctions := map[string]func(r *reader.Reader) error{
|
||||
"control server": s.ControlServer.read,
|
||||
"DNS": s.DNS.read,
|
||||
@@ -185,13 +199,16 @@ func (s *Settings) Read(r *reader.Reader) (err error) {
|
||||
"health": s.Health.Read,
|
||||
"http proxy": s.HTTPProxy.read,
|
||||
"log": s.Log.read,
|
||||
"public ip": s.PublicIP.read,
|
||||
"shadowsocks": s.Shadowsocks.read,
|
||||
"system": s.System.read,
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
"public ip": func(r *reader.Reader) error {
|
||||
return s.PublicIP.read(r, warner)
|
||||
},
|
||||
"shadowsocks": s.Shadowsocks.read,
|
||||
"storage": s.Storage.read,
|
||||
"system": s.System.read,
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
}
|
||||
|
||||
for name, read := range readFunctions {
|
||||
|
||||
@@ -43,18 +43,10 @@ func Test_Settings_String(t *testing.T) {
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Unbound settings:
|
||||
| | ├── Authoritative servers:
|
||||
| | | └── Cloudflare
|
||||
| | ├── 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
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
@@ -78,21 +70,26 @@ func Test_Settings_String(t *testing.T) {
|
||||
| └── Enabled: no
|
||||
├── Control server settings:
|
||||
| ├── Listening address: :8000
|
||||
| └── Logging: yes
|
||||
| ├── Logging: yes
|
||||
| └── Authentication file path: /gluetun/auth/config.toml
|
||||
├── Storage settings:
|
||||
| └── Filepath: /gluetun/servers.json
|
||||
├── OS Alpine settings:
|
||||
| ├── Process UID: 1000
|
||||
| └── Process GID: 1000
|
||||
├── Public IP settings:
|
||||
| ├── Fetching: every 12h0m0s
|
||||
| ├── IP file path: /tmp/gluetun/ip
|
||||
| └── Public IP data API: ipinfo
|
||||
| ├── Public IP data base API: ipinfo
|
||||
| └── Public IP data backup APIs:
|
||||
| ├── cloudflare
|
||||
| ├── ifconfigco
|
||||
| └── ip2location
|
||||
└── Version settings:
|
||||
└── Enabled: yes`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
59
internal/configuration/settings/storage.go
Normal file
59
internal/configuration/settings/storage.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Storage contains settings to configure the storage.
|
||||
type Storage struct {
|
||||
// Filepath is the path to the servers.json file. An empty string disables on-disk storage.
|
||||
Filepath *string
|
||||
}
|
||||
|
||||
func (s Storage) validate() (err error) {
|
||||
if *s.Filepath != "" { // optional
|
||||
_, err := filepath.Abs(*s.Filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath is not valid: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) copy() (copied Storage) {
|
||||
return Storage{
|
||||
Filepath: gosettings.CopyPointer(s.Filepath),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) overrideWith(other Storage) {
|
||||
s.Filepath = gosettings.OverrideWithPointer(s.Filepath, other.Filepath)
|
||||
}
|
||||
|
||||
func (s *Storage) setDefaults() {
|
||||
const defaultFilepath = "/gluetun/servers.json"
|
||||
s.Filepath = gosettings.DefaultPointer(s.Filepath, defaultFilepath)
|
||||
}
|
||||
|
||||
func (s Storage) String() string {
|
||||
return s.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (s Storage) toLinesNode() (node *gotree.Node) {
|
||||
if *s.Filepath == "" {
|
||||
return gotree.New("Storage settings: disabled")
|
||||
}
|
||||
node = gotree.New("Storage settings:")
|
||||
node.Appendf("Filepath: %s", *s.Filepath)
|
||||
return node
|
||||
}
|
||||
|
||||
func (s *Storage) read(r *reader.Reader) (err error) {
|
||||
s.Filepath = r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true))
|
||||
return nil
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func surfsharkRetroRegion(selection ServerSelection) (
|
||||
updatedSelection ServerSelection) {
|
||||
updatedSelection ServerSelection,
|
||||
) {
|
||||
locationData := servers.LocationData()
|
||||
|
||||
retroToLocation := make(map[string]servers.ServerLocation, len(locationData))
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/pkg/provider"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Unbound is settings for the Unbound program.
|
||||
type Unbound struct {
|
||||
Providers []string `json:"providers"`
|
||||
Caching *bool `json:"caching"`
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
VerbosityLevel *uint8 `json:"verbosity_level"`
|
||||
VerbosityDetailsLevel *uint8 `json:"verbosity_details_level"`
|
||||
ValidationLogLevel *uint8 `json:"validation_log_level"`
|
||||
Username string `json:"username"`
|
||||
Allowed []netip.Prefix `json:"allowed"`
|
||||
}
|
||||
|
||||
func (u *Unbound) setDefaults() {
|
||||
if len(u.Providers) == 0 {
|
||||
u.Providers = []string{
|
||||
provider.Cloudflare().String(),
|
||||
}
|
||||
}
|
||||
|
||||
u.Caching = gosettings.DefaultPointer(u.Caching, true)
|
||||
u.IPv6 = gosettings.DefaultPointer(u.IPv6, false)
|
||||
|
||||
const defaultVerbosityLevel = 1
|
||||
u.VerbosityLevel = gosettings.DefaultPointer(u.VerbosityLevel, defaultVerbosityLevel)
|
||||
|
||||
const defaultVerbosityDetailsLevel = 0
|
||||
u.VerbosityDetailsLevel = gosettings.DefaultPointer(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
|
||||
|
||||
const defaultValidationLogLevel = 0
|
||||
u.ValidationLogLevel = gosettings.DefaultPointer(u.ValidationLogLevel, defaultValidationLogLevel)
|
||||
|
||||
if u.Allowed == nil {
|
||||
u.Allowed = []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
||||
}
|
||||
}
|
||||
|
||||
u.Username = gosettings.DefaultComparable(u.Username, "root")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
|
||||
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
|
||||
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
|
||||
)
|
||||
|
||||
func (u Unbound) validate() (err error) {
|
||||
for _, s := range u.Providers {
|
||||
_, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
const maxVerbosityLevel = 5
|
||||
if *u.VerbosityLevel > maxVerbosityLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityLevelNotValid,
|
||||
*u.VerbosityLevel,
|
||||
maxVerbosityLevel)
|
||||
}
|
||||
|
||||
const maxVerbosityDetailsLevel = 4
|
||||
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityDetailsLevelNotValid,
|
||||
*u.VerbosityDetailsLevel,
|
||||
maxVerbosityDetailsLevel)
|
||||
}
|
||||
|
||||
const maxValidationLogLevel = 2
|
||||
if *u.ValidationLogLevel > maxValidationLogLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundValidationLogLevelNotValid,
|
||||
*u.ValidationLogLevel, maxValidationLogLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u Unbound) copy() (copied Unbound) {
|
||||
return Unbound{
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
Caching: gosettings.CopyPointer(u.Caching),
|
||||
IPv6: gosettings.CopyPointer(u.IPv6),
|
||||
VerbosityLevel: gosettings.CopyPointer(u.VerbosityLevel),
|
||||
VerbosityDetailsLevel: gosettings.CopyPointer(u.VerbosityDetailsLevel),
|
||||
ValidationLogLevel: gosettings.CopyPointer(u.ValidationLogLevel),
|
||||
Username: u.Username,
|
||||
Allowed: gosettings.CopySlice(u.Allowed),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unbound) overrideWith(other Unbound) {
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.Caching = gosettings.OverrideWithPointer(u.Caching, other.Caching)
|
||||
u.IPv6 = gosettings.OverrideWithPointer(u.IPv6, other.IPv6)
|
||||
u.VerbosityLevel = gosettings.OverrideWithPointer(u.VerbosityLevel, other.VerbosityLevel)
|
||||
u.VerbosityDetailsLevel = gosettings.OverrideWithPointer(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
|
||||
u.ValidationLogLevel = gosettings.OverrideWithPointer(u.ValidationLogLevel, other.ValidationLogLevel)
|
||||
u.Username = gosettings.OverrideWithComparable(u.Username, other.Username)
|
||||
u.Allowed = gosettings.OverrideWithSlice(u.Allowed, other.Allowed)
|
||||
}
|
||||
|
||||
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
|
||||
providers := make([]provider.Provider, len(u.Providers))
|
||||
for i := range providers {
|
||||
providers[i], err = provider.Parse(u.Providers[i])
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
|
||||
const port = 53
|
||||
|
||||
return unbound.Settings{
|
||||
ListeningPort: port,
|
||||
IPv4: true,
|
||||
Providers: providers,
|
||||
Caching: *u.Caching,
|
||||
IPv6: *u.IPv6,
|
||||
VerbosityLevel: *u.VerbosityLevel,
|
||||
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
|
||||
ValidationLogLevel: *u.ValidationLogLevel,
|
||||
AccessControl: unbound.AccessControlSettings{
|
||||
Allowed: netipPrefixesToNetaddrIPPrefixes(u.Allowed),
|
||||
},
|
||||
Username: u.Username,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrConvertingNetip = errors.New("converting net.IP to netip.Addr failed")
|
||||
)
|
||||
|
||||
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 netip.Addr, err error) {
|
||||
s := u.Providers[0]
|
||||
provider, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return ipv4, err
|
||||
}
|
||||
|
||||
ip := provider.DNS().IPv4[0]
|
||||
ipv4, ok := netip.AddrFromSlice(ip)
|
||||
if !ok {
|
||||
return ipv4, fmt.Errorf("%w: for ip %s (%#v)",
|
||||
ErrConvertingNetip, ip, ip)
|
||||
}
|
||||
return ipv4.Unmap(), nil
|
||||
}
|
||||
|
||||
func (u Unbound) String() string {
|
||||
return u.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (u Unbound) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Unbound settings:")
|
||||
|
||||
authServers := node.Appendf("Authoritative servers:")
|
||||
for _, provider := range u.Providers {
|
||||
authServers.Appendf(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(u.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(u.IPv6))
|
||||
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
|
||||
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
|
||||
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
|
||||
node.Appendf("System user: %s", u.Username)
|
||||
|
||||
allowedNetworks := node.Appendf("Allowed networks:")
|
||||
for _, network := range u.Allowed {
|
||||
allowedNetworks.Appendf(network.String())
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (u *Unbound) read(reader *reader.Reader) (err error) {
|
||||
u.Providers = reader.CSV("DOT_PROVIDERS")
|
||||
|
||||
u.Caching, err = reader.BoolPtr("DOT_CACHING")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.VerbosityLevel, err = reader.Uint8Ptr("DOT_VERBOSITY")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.VerbosityDetailsLevel, err = reader.Uint8Ptr("DOT_VERBOSITY_DETAILS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.ValidationLogLevel, err = reader.Uint8Ptr("DOT_VALIDATION_LOGLEVEL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Unbound_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := Unbound{
|
||||
Providers: []string{"cloudflare"},
|
||||
Caching: boolPtr(true),
|
||||
IPv6: boolPtr(false),
|
||||
VerbosityLevel: uint8Ptr(1),
|
||||
VerbosityDetailsLevel: nil,
|
||||
ValidationLogLevel: uint8Ptr(0),
|
||||
Username: "user",
|
||||
Allowed: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
|
||||
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
const expected = `{"providers":["cloudflare"],"caching":true,"ipv6":false,` +
|
||||
`"verbosity_level":1,"verbosity_details_level":null,"validation_log_level":0,` +
|
||||
`"username":"user","allowed":["0.0.0.0/0","::/0"]}`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
|
||||
var resultSettings Unbound
|
||||
err = json.Unmarshal(b, &resultSettings)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, settings, resultSettings)
|
||||
}
|
||||
@@ -21,14 +21,14 @@ type VPN struct {
|
||||
}
|
||||
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (v *VPN) Validate(storage Storage, ipv6Supported bool) (err error) {
|
||||
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
|
||||
// Validate Type
|
||||
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
||||
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
|
||||
}
|
||||
|
||||
err = v.Provider.validate(v.Type, storage)
|
||||
err = v.Provider.validate(v.Type, filterChoicesGetter, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provider settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -215,12 +215,12 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
addressesNode := node.Appendf("Interface addresses:")
|
||||
for _, address := range w.Addresses {
|
||||
addressesNode.Appendf(address.String())
|
||||
addressesNode.Append(address.String())
|
||||
}
|
||||
|
||||
allowedIPsNode := node.Appendf("Allowed IPs:")
|
||||
for _, allowedIP := range w.AllowedIPs {
|
||||
allowedIPsNode.Appendf(allowedIP.String())
|
||||
allowedIPsNode.Append(allowedIP.String())
|
||||
}
|
||||
|
||||
if *w.PersistentKeepaliveInterval > 0 {
|
||||
|
||||
@@ -34,9 +34,7 @@ type WireguardConfig struct {
|
||||
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) {
|
||||
iniFile, err := ini.InsensitiveLoad(path)
|
||||
@@ -68,18 +66,18 @@ func ParseWireguardConf(path string) (config WireguardConfig, err error) {
|
||||
}
|
||||
|
||||
func parseWireguardInterfaceSection(interfaceSection *ini.Section) (
|
||||
privateKey, addresses *string) {
|
||||
privateKey, addresses *string,
|
||||
) {
|
||||
privateKey = getINIKeyFromSection(interfaceSection, "PrivateKey")
|
||||
addresses = getINIKeyFromSection(interfaceSection, "Address")
|
||||
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) (
|
||||
preSharedKey, publicKey, endpointIP, endpointPort *string) {
|
||||
preSharedKey, publicKey, endpointIP, endpointPort *string,
|
||||
) {
|
||||
preSharedKey = getINIKeyFromSection(peerSection, "PresharedKey")
|
||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||
@@ -96,9 +94,7 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||
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) {
|
||||
iniKey, err := section.GetKey(key)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -72,12 +73,12 @@ PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
|
||||
const permission = fs.FileMode(0o600)
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
|
||||
require.NoError(t, err)
|
||||
|
||||
wireguard, err := ParseWireguardConf(configFile)
|
||||
@@ -121,7 +122,6 @@ Address = 10.38.22.35/32
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -182,7 +182,6 @@ Endpoint = 1.2.3.4:51820`,
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -38,7 +39,8 @@ func Test_Source_Get(t *testing.T) {
|
||||
"empty_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||
err = os.WriteFile(secretFilepath, nil, os.ModePerm)
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, nil, permission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,7 +55,8 @@ func Test_Source_Get(t *testing.T) {
|
||||
"default_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file")
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -69,7 +72,8 @@ func Test_Source_Get(t *testing.T) {
|
||||
"env_specified_secret_file": {
|
||||
makeSource: func(tempDir string) (source *Source, err error) {
|
||||
secretFilepath := filepath.Join(tempDir, "test_file_custom")
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
|
||||
const permission = fs.FileMode(0o600)
|
||||
err = os.WriteFile(secretFilepath, []byte{'A'}, permission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,7 +91,6 @@ func Test_Source_Get(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ package constants
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
func ColorUnbound() *color.Color {
|
||||
return color.New(color.FgCyan)
|
||||
}
|
||||
|
||||
func ColorOpenvpn() *color.Color {
|
||||
return color.New(color.FgHiMagenta)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
Example = "example"
|
||||
Expressvpn = "expressvpn"
|
||||
Fastestvpn = "fastestvpn"
|
||||
Giganews = "giganews"
|
||||
HideMyAss = "hidemyass"
|
||||
Ipvanish = "ipvanish"
|
||||
Ivpn = "ivpn"
|
||||
@@ -37,6 +38,7 @@ func All() []string {
|
||||
Cyberghost,
|
||||
Expressvpn,
|
||||
Fastestvpn,
|
||||
Giganews,
|
||||
HideMyAss,
|
||||
Ipvanish,
|
||||
Ivpn,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
)
|
||||
|
||||
type Configurator interface {
|
||||
SetupFiles(ctx context.Context) error
|
||||
MakeUnboundConf(settings unbound.Settings) (err error)
|
||||
Start(ctx context.Context, verbosityDetailsLevel uint8) (
|
||||
stdoutLines, stderrLines chan string, waitError chan error, err error)
|
||||
Version(ctx context.Context) (version string, err error)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
type logLevel uint8
|
||||
|
||||
const (
|
||||
levelDebug logLevel = iota
|
||||
levelInfo
|
||||
levelWarn
|
||||
levelError
|
||||
)
|
||||
|
||||
func (l *Loop) collectLines(ctx context.Context, done chan<- struct{},
|
||||
stdout, stderr chan string) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context should only be canceled after stdout and stderr are done
|
||||
// being written to.
|
||||
close(stdout)
|
||||
close(stderr)
|
||||
return
|
||||
case line = <-stderr:
|
||||
case line = <-stdout:
|
||||
}
|
||||
|
||||
line, level := processLogLine(line)
|
||||
switch level {
|
||||
case levelDebug:
|
||||
l.logger.Debug(line)
|
||||
case levelInfo:
|
||||
l.logger.Info(line)
|
||||
case levelWarn:
|
||||
l.logger.Warn(line)
|
||||
case levelError:
|
||||
l.logger.Error(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unboundPrefix = regexp.MustCompile(`\[[0-9]{10}\] unbound\[[0-9]+:[0|1]\] `)
|
||||
|
||||
func processLogLine(s string) (filtered string, level logLevel) {
|
||||
prefix := unboundPrefix.FindString(s)
|
||||
filtered = s[len(prefix):]
|
||||
switch {
|
||||
case strings.HasPrefix(filtered, "notice: "):
|
||||
filtered = strings.TrimPrefix(filtered, "notice: ")
|
||||
level = levelInfo
|
||||
case strings.HasPrefix(filtered, "info: "):
|
||||
filtered = strings.TrimPrefix(filtered, "info: ")
|
||||
level = levelInfo
|
||||
case strings.HasPrefix(filtered, "warn: "):
|
||||
filtered = strings.TrimPrefix(filtered, "warn: ")
|
||||
level = levelWarn
|
||||
case strings.HasPrefix(filtered, "error: "):
|
||||
filtered = strings.TrimPrefix(filtered, "error: ")
|
||||
level = levelError
|
||||
default:
|
||||
level = levelInfo
|
||||
}
|
||||
filtered = constants.ColorUnbound().Sprintf(filtered)
|
||||
return filtered, level
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_processLogLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
s string
|
||||
filtered string
|
||||
level logLevel
|
||||
}{
|
||||
"empty string": {"", "", levelInfo},
|
||||
"random string": {"asdasqdb", "asdasqdb", levelInfo},
|
||||
"unbound notice": {
|
||||
"[1594595249] unbound[75:0] notice: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelInfo},
|
||||
"unbound info": {
|
||||
"[1594595249] unbound[75:0] info: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelInfo},
|
||||
"unbound warn": {
|
||||
"[1594595249] unbound[75:0] warn: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelWarn},
|
||||
"unbound error": {
|
||||
"[1594595249] unbound[75:0] error: init module 0: validator",
|
||||
"init module 0: validator",
|
||||
levelError},
|
||||
"unbound unknown": {
|
||||
"[1594595249] unbound[75:0] BLA: init module 0: validator",
|
||||
"BLA: init module 0: validator",
|
||||
levelInfo},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
filtered, level := processLogLine(tc.s)
|
||||
assert.Equal(t, tc.filtered, filtered)
|
||||
assert.Equal(t, tc.level, level)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/pkg/blacklist"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/dns/state"
|
||||
@@ -16,9 +18,9 @@ import (
|
||||
type Loop struct {
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
conf Configurator
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
resolvConf string
|
||||
blockBuilder blacklist.Builder
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
@@ -34,8 +36,9 @@ type Loop struct {
|
||||
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
|
||||
func NewLoop(conf Configurator, settings settings.DNS,
|
||||
client *http.Client, logger Logger) *Loop {
|
||||
func NewLoop(settings settings.DNS,
|
||||
client *http.Client, logger Logger,
|
||||
) (loop *Loop, err error) {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
stop := make(chan struct{})
|
||||
@@ -45,12 +48,17 @@ func NewLoop(conf Configurator, settings settings.DNS,
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
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{
|
||||
statusManager: statusManager,
|
||||
state: state,
|
||||
conf: conf,
|
||||
server: nil,
|
||||
filter: filter,
|
||||
resolvConf: "/etc/resolv.conf",
|
||||
blockBuilder: blacklist.NewBuilder(client),
|
||||
client: client,
|
||||
logger: logger,
|
||||
userTrigger: true,
|
||||
@@ -62,7 +70,7 @@ func NewLoop(conf Configurator, settings settings.DNS,
|
||||
backoffTime: defaultBackoffTime,
|
||||
timeNow: time.Now,
|
||||
timeSince: time.Since,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
||||
|
||||
@@ -2,36 +2,22 @@ package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/pkg/nameserver"
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
)
|
||||
|
||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
// Try with user provided plaintext ip address
|
||||
// if it's not 127.0.0.1 (default for DoT)
|
||||
targetIP := settings.ServerAddress
|
||||
if targetIP.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
if fallback {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
} else {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
const keepNameserver = false
|
||||
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use first plaintext DNS IPv4 address
|
||||
targetIP, err := settings.DoT.Unbound.GetFirstPlaintextIPv4()
|
||||
if err != nil {
|
||||
// Unbound should always have a default provider
|
||||
panic(err)
|
||||
// if it's not 127.0.0.1 (default for DoT), otherwise
|
||||
// use the first DoT provider ipv4 address found.
|
||||
var targetIP netip.Addr
|
||||
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
targetIP = settings.ServerAddress
|
||||
} else {
|
||||
targetIP = settings.DoT.GetFirstPlaintextIPv4()
|
||||
}
|
||||
|
||||
if fallback {
|
||||
@@ -39,9 +25,19 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
} else {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
const keepNameserver = false
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
|
||||
const dialTimeout = 3 * time.Second
|
||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||
IP: targetIP,
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||
|
||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||
IP: targetIP,
|
||||
ResolvPath: l.resolvConf,
|
||||
}
|
||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -26,16 +26,14 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
// Upper scope variables for Unbound only
|
||||
// Upper scope variables for the DNS over TLS server only
|
||||
// Their values are to be used if DOT=off
|
||||
waitError := make(chan error)
|
||||
unboundCancel := func() { waitError <- nil }
|
||||
closeStreams := func() {}
|
||||
var runError <-chan error
|
||||
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
var err error
|
||||
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
||||
runError, err = l.setupServer(ctx)
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
@@ -49,11 +47,12 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, errUpdateFiles) {
|
||||
if !errors.Is(err, errUpdateBlockLists) {
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
|
||||
settings = l.GetSettings()
|
||||
@@ -64,40 +63,44 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
|
||||
l.userTrigger = false
|
||||
|
||||
stayHere := true
|
||||
for stayHere {
|
||||
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
|
||||
}
|
||||
exitLoop := l.runWait(ctx, runError)
|
||||
if exitLoop {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,74 @@ package dns
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func (l *Loop) GetSettings() (settings settings.DNS) { return l.state.GetSettings() }
|
||||
|
||||
func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
outcome string) {
|
||||
outcome string,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,59 +4,55 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/dns/pkg/check"
|
||||
"github.com/qdm12/dns/pkg/nameserver"
|
||||
"github.com/qdm12/dns/v2/pkg/check"
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
)
|
||||
|
||||
var errUpdateFiles = errors.New("cannot update files")
|
||||
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||
|
||||
// Returning cancel == nil signals we want to re-run setupUnbound
|
||||
// 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) {
|
||||
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
||||
err = l.updateFiles(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil,
|
||||
fmt.Errorf("%w: %s", errUpdateFiles, err)
|
||||
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
|
||||
}
|
||||
|
||||
settings := l.GetSettings()
|
||||
|
||||
unboundCtx, cancel := context.WithCancel(context.Background())
|
||||
stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx,
|
||||
*settings.DoT.Unbound.VerbosityDetailsLevel)
|
||||
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
return nil, fmt.Errorf("building DoT settings: %w", err)
|
||||
}
|
||||
|
||||
linesCollectionCtx, linesCollectionCancel := context.WithCancel(context.Background())
|
||||
lineCollectionDone := make(chan struct{})
|
||||
go l.collectLines(linesCollectionCtx, lineCollectionDone,
|
||||
stdoutLines, stderrLines)
|
||||
closeStreams = func() {
|
||||
linesCollectionCancel()
|
||||
<-lineCollectionDone
|
||||
server, err := server.New(dotSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating DoT server: %w", err)
|
||||
}
|
||||
|
||||
// use Unbound
|
||||
nameserver.UseDNSInternally(settings.ServerAddress.AsSlice())
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, settings.ServerAddress.AsSlice(),
|
||||
*settings.KeepNameserver)
|
||||
runError, err = server.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting server: %w", err)
|
||||
}
|
||||
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 {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil {
|
||||
cancel()
|
||||
<-waitError
|
||||
close(waitError)
|
||||
closeStreams()
|
||||
return nil, nil, nil, err
|
||||
err = check.WaitForDNS(ctx, check.Settings{})
|
||||
if err != nil {
|
||||
l.stopServer()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cancel, waitError, closeStreams, nil
|
||||
return runError, nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ func (s *State) GetSettings() (settings settings.DNS) {
|
||||
}
|
||||
|
||||
func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
||||
outcome string) {
|
||||
outcome string,
|
||||
) {
|
||||
s.settingsMu.Lock()
|
||||
|
||||
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func New(statusApplier StatusApplier,
|
||||
settings settings.DNS,
|
||||
updateTicker chan<- struct{}) *State {
|
||||
updateTicker chan<- struct{},
|
||||
) *State {
|
||||
return &State{
|
||||
statusApplier: statusApplier,
|
||||
settings: settings,
|
||||
|
||||
@@ -11,6 +11,7 @@ func (l *Loop) GetStatus() (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)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
if err := l.updateFiles(ctx); err != nil {
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
l.logger.Error(err.Error())
|
||||
l.logger.Warn("skipping Unbound restart due to failed files update")
|
||||
l.logger.Warn("skipping DNS server restart due to failed files update")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
package dns
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"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) {
|
||||
l.logger.Info("downloading DNS over TLS cryptographic files")
|
||||
if err := l.conf.SetupFiles(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
settings := l.GetSettings()
|
||||
|
||||
unboundSettings, err := settings.DoT.Unbound.ToUnboundFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.logger.Info("downloading hostnames and IP block lists")
|
||||
blacklistSettings, err := settings.DoT.Blacklist.ToBlacklistFormat()
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
blockedHostnames, blockedIPs, blockedIPPrefixes, errs :=
|
||||
l.blockBuilder.All(ctx, blacklistSettings)
|
||||
for _, err := range errs {
|
||||
l.logger.Warn(err.Error())
|
||||
updateSettings := update.Settings{
|
||||
IPs: result.BlockedIPs,
|
||||
IPPrefixes: result.BlockedIPPrefixes,
|
||||
}
|
||||
updateSettings.BlockHostnames(result.BlockedHostnames)
|
||||
err = l.filter.Update(updateSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating filter: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
return nil
|
||||
}
|
||||
|
||||
102
internal/firewall/delete.go
Normal file
102
internal/firewall/delete.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isDeleteMatchInstruction returns true if the iptables instruction
|
||||
// is a delete instruction by rule matching. It returns false if the
|
||||
// instruction is a delete instruction by line number, or not a delete
|
||||
// instruction.
|
||||
func isDeleteMatchInstruction(instruction string) bool {
|
||||
fields := strings.Fields(instruction)
|
||||
for i, field := range fields {
|
||||
switch {
|
||||
case field != "-D" && field != "--delete": //nolint:goconst
|
||||
continue
|
||||
case i == len(fields)-1: // malformed: missing chain name
|
||||
return false
|
||||
case i == len(fields)-2: // chain name is last field
|
||||
return true
|
||||
default:
|
||||
// chain name is fields[i+1]
|
||||
const base, bitLength = 10, 16
|
||||
_, err := strconv.ParseUint(fields[i+2], base, bitLength)
|
||||
return err != nil // not a line number
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func deleteIPTablesRule(ctx context.Context, iptablesBinary, instruction string,
|
||||
runner CmdRunner, logger Logger,
|
||||
) (err error) {
|
||||
targetRule, err := parseIptablesInstruction(instruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing iptables command: %w", err)
|
||||
}
|
||||
|
||||
lineNumber, err := findLineNumber(ctx, iptablesBinary,
|
||||
targetRule, runner, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding iptables chain rule line number: %w", err)
|
||||
} else if lineNumber == 0 {
|
||||
logger.Debug("rule matching \"" + instruction + "\" not found")
|
||||
return nil
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("found iptables chain rule matching %q at line number %d",
|
||||
instruction, lineNumber))
|
||||
|
||||
cmd := exec.CommandContext(ctx, iptablesBinary, "-t", targetRule.table,
|
||||
"-D", targetRule.chain, fmt.Sprint(lineNumber)) // #nosec G204
|
||||
logger.Debug(cmd.String())
|
||||
output, err := runner.Run(cmd)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("command failed: %q: %w", cmd, err)
|
||||
if output != "" {
|
||||
err = fmt.Errorf("%w: %s", err, output)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findLineNumber finds the line number of an iptables rule.
|
||||
// It returns 0 if the rule is not found.
|
||||
func findLineNumber(ctx context.Context, iptablesBinary string,
|
||||
instruction iptablesInstruction, runner CmdRunner, logger Logger) (
|
||||
lineNumber uint16, err error,
|
||||
) {
|
||||
listFlags := []string{
|
||||
"-t", instruction.table, "-L", instruction.chain,
|
||||
"--line-numbers", "-n", "-v",
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, iptablesBinary, listFlags...) // #nosec G204
|
||||
logger.Debug(cmd.String())
|
||||
output, err := runner.Run(cmd)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("command failed: %q: %w", cmd, err)
|
||||
if output != "" {
|
||||
err = fmt.Errorf("%w: %s", err, output)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
chain, err := parseChain(output)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing chain list: %w", err)
|
||||
}
|
||||
|
||||
for _, rule := range chain.rules {
|
||||
if instruction.equalToRule(instruction.table, chain.name, rule) {
|
||||
return rule.lineNumber, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
186
internal/firewall/delete_test.go
Normal file
186
internal/firewall/delete_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_isDeleteMatchInstruction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
instruction string
|
||||
isDeleteMatch bool
|
||||
}{
|
||||
"not_delete": {
|
||||
instruction: "-t nat -A PREROUTING -i tun0 -j ACCEPT",
|
||||
},
|
||||
"malformed_missing_chain_name": {
|
||||
instruction: "-t nat -D",
|
||||
},
|
||||
"delete_chain_name_last_field": {
|
||||
instruction: "-t nat --delete PREROUTING",
|
||||
isDeleteMatch: true,
|
||||
},
|
||||
"delete_match": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -j ACCEPT",
|
||||
isDeleteMatch: true,
|
||||
},
|
||||
"delete_line_number_last_field": {
|
||||
instruction: "-t nat -D PREROUTING 2",
|
||||
},
|
||||
"delete_line_number": {
|
||||
instruction: "-t nat -D PREROUTING 2 -i tun0 -j ACCEPT",
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
isDeleteMatch := isDeleteMatchInstruction(testCase.instruction)
|
||||
|
||||
assert.Equal(t, testCase.isDeleteMatch, isDeleteMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCmdMatcherListRules(iptablesBinary, table, chain string) *cmdMatcher { //nolint:unparam
|
||||
return newCmdMatcher(iptablesBinary, "^-t$", "^"+table+"$", "^-L$", "^"+chain+"$",
|
||||
"^--line-numbers$", "^-n$", "^-v$")
|
||||
}
|
||||
|
||||
func Test_deleteIPTablesRule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const iptablesBinary = "/sbin/iptables"
|
||||
errTest := errors.New("test error")
|
||||
|
||||
testCases := map[string]struct {
|
||||
instruction string
|
||||
makeRunner func(ctrl *gomock.Controller) *MockCmdRunner
|
||||
makeLogger func(ctrl *gomock.Controller) *MockLogger
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"invalid_instruction": {
|
||||
instruction: "invalid",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "parsing iptables command: iptables command is malformed: " +
|
||||
"fields count 1 is not even: \"invalid\"",
|
||||
},
|
||||
"list_error": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().
|
||||
Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
|
||||
Return("", errTest)
|
||||
return runner
|
||||
},
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v")
|
||||
return logger
|
||||
},
|
||||
errWrapped: errTest,
|
||||
errMessage: `finding iptables chain rule line number: command failed: ` +
|
||||
`"/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v": test error`,
|
||||
},
|
||||
"rule_not_found": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
|
||||
Return(`Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999`, //nolint:lll
|
||||
nil)
|
||||
return runner
|
||||
},
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v")
|
||||
logger.EXPECT().Debug("rule matching \"-t nat --delete PREROUTING " +
|
||||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" not found")
|
||||
return logger
|
||||
},
|
||||
},
|
||||
"rule_found_delete_error": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
|
||||
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+
|
||||
"num pkts bytes target prot opt in out source destination \n"+
|
||||
"1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999\n"+ //nolint:lll
|
||||
"2 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43716 redir ports 5678\n", //nolint:lll
|
||||
nil)
|
||||
runner.EXPECT().Run(newCmdMatcher(iptablesBinary, "^-t$", "^nat$",
|
||||
"^-D$", "^PREROUTING$", "^2$")).Return("details", errTest)
|
||||
return runner
|
||||
},
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v")
|
||||
logger.EXPECT().Debug("found iptables chain rule matching \"-t nat --delete PREROUTING " +
|
||||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" at line number 2")
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -D PREROUTING 2")
|
||||
return logger
|
||||
},
|
||||
errWrapped: errTest,
|
||||
errMessage: "command failed: \"/sbin/iptables -t nat -D PREROUTING 2\": test error: details",
|
||||
},
|
||||
"rule_found_delete_success": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
makeRunner: func(ctrl *gomock.Controller) *MockCmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")).
|
||||
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+
|
||||
"num pkts bytes target prot opt in out source destination \n"+
|
||||
"1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999\n"+ //nolint:lll
|
||||
"2 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43716 redir ports 5678\n", //nolint:lll
|
||||
nil)
|
||||
runner.EXPECT().Run(newCmdMatcher(iptablesBinary, "^-t$", "^nat$",
|
||||
"^-D$", "^PREROUTING$", "^2$")).Return("", nil)
|
||||
return runner
|
||||
},
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v")
|
||||
logger.EXPECT().Debug("found iptables chain rule matching \"-t nat --delete PREROUTING " +
|
||||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" at line number 2")
|
||||
logger.EXPECT().Debug("/sbin/iptables -t nat -D PREROUTING 2")
|
||||
return logger
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
instruction := testCase.instruction
|
||||
var runner *MockCmdRunner
|
||||
if testCase.makeRunner != nil {
|
||||
runner = testCase.makeRunner(ctrl)
|
||||
}
|
||||
var logger *MockLogger
|
||||
if testCase.makeLogger != nil {
|
||||
logger = testCase.makeLogger(ctrl)
|
||||
}
|
||||
|
||||
err := deleteIPTablesRule(ctx, iptablesBinary, instruction, runner, logger)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -106,12 +106,20 @@ func (c *Config) enable(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
localInterfaces := make(map[string]struct{}, len(c.localNetworks))
|
||||
for _, network := range c.localNetworks {
|
||||
if err := c.acceptOutputFromIPToSubnet(ctx, network.InterfaceName, network.IP, network.IPNet, remove); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.acceptIpv6MulticastOutput(ctx, network.InterfaceName, remove); err != nil {
|
||||
return err
|
||||
|
||||
_, localInterfaceSeen := localInterfaces[network.InterfaceName]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +157,13 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
const remove = false
|
||||
interfacesSeen := make(map[string]struct{}, len(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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("accepting output traffic through VPN: %w", err)
|
||||
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/routing"
|
||||
"github.com/qdm12/golibs/command"
|
||||
)
|
||||
|
||||
type Config struct { //nolint:maligned
|
||||
runner command.Runner
|
||||
runner CmdRunner
|
||||
logger Logger
|
||||
iptablesMutex sync.Mutex
|
||||
ip6tablesMutex sync.Mutex
|
||||
@@ -36,8 +35,9 @@ type Config struct { //nolint:maligned
|
||||
// NewConfig creates a new Config instance and returns an error
|
||||
// if no iptables implementation is available.
|
||||
func NewConfig(ctx context.Context, logger Logger,
|
||||
runner command.Runner, defaultRoutes []routing.DefaultRoute,
|
||||
localNetworks []routing.LocalNetwork) (config *Config, err error) {
|
||||
runner CmdRunner, defaultRoutes []routing.DefaultRoute,
|
||||
localNetworks []routing.LocalNetwork,
|
||||
) (config *Config, err error) {
|
||||
iptables, err := checkIptablesSupport(ctx, runner, "iptables", "iptables-nft", "iptables-legacy")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
14
internal/firewall/interfaces.go
Normal file
14
internal/firewall/interfaces.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package firewall
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type CmdRunner interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
@@ -6,15 +6,14 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/command"
|
||||
)
|
||||
|
||||
// findIP6tablesSupported checks for multiple iptables implementations
|
||||
// and returns the iptables path that is supported. If none work, an
|
||||
// empty string path is returned.
|
||||
func findIP6tablesSupported(ctx context.Context, runner command.Runner) (
|
||||
ip6tablesPath string, err error) {
|
||||
func findIP6tablesSupported(ctx context.Context, runner CmdRunner) (
|
||||
ip6tablesPath string, err error,
|
||||
) {
|
||||
ip6tablesPath, err = checkIptablesSupport(ctx, runner, "ip6tables", "ip6tables-nft", "ip6tables-legacy")
|
||||
if errors.Is(err, ErrIPTablesNotSupported) {
|
||||
return "", nil
|
||||
@@ -40,10 +39,14 @@ func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string
|
||||
c.ip6tablesMutex.Lock() // only one ip6tables command at once
|
||||
defer c.ip6tablesMutex.Unlock()
|
||||
|
||||
c.logger.Debug(c.ip6Tables + " " + instruction)
|
||||
if isDeleteMatchInstruction(instruction) {
|
||||
return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
|
||||
c.runner, c.logger)
|
||||
}
|
||||
|
||||
flags := strings.Fields(instruction)
|
||||
cmd := exec.CommandContext(ctx, c.ip6Tables, flags...) // #nosec G204
|
||||
c.logger.Debug(cmd.String())
|
||||
if output, err := c.runner.Run(cmd); err != nil {
|
||||
return fmt.Errorf("command failed: \"%s %s\": %s: %w",
|
||||
c.ip6Tables, instruction, output, err)
|
||||
@@ -55,7 +58,7 @@ var ErrPolicyNotValid = errors.New("policy is not valid")
|
||||
|
||||
func (c *Config) setIPv6AllPolicies(ctx context.Context, policy string) error {
|
||||
switch policy {
|
||||
case "ACCEPT", "DROP":
|
||||
case "ACCEPT", "DROP": //nolint:goconst
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrPolicyNotValid, policy)
|
||||
}
|
||||
|
||||
@@ -70,10 +70,14 @@ func (c *Config) runIptablesInstruction(ctx context.Context, instruction string)
|
||||
c.iptablesMutex.Lock() // only one iptables command at once
|
||||
defer c.iptablesMutex.Unlock()
|
||||
|
||||
c.logger.Debug(c.ipTables + " " + instruction)
|
||||
if isDeleteMatchInstruction(instruction) {
|
||||
return deleteIPTablesRule(ctx, c.ipTables, instruction,
|
||||
c.runner, c.logger)
|
||||
}
|
||||
|
||||
flags := strings.Fields(instruction)
|
||||
cmd := exec.CommandContext(ctx, c.ipTables, flags...) // #nosec G204
|
||||
c.logger.Debug(cmd.String())
|
||||
if output, err := c.runner.Run(cmd); err != nil {
|
||||
return fmt.Errorf("command failed: \"%s %s\": %s: %w",
|
||||
c.ipTables, instruction, output, err)
|
||||
@@ -108,7 +112,8 @@ func (c *Config) acceptInputThroughInterface(ctx context.Context, intf string, r
|
||||
}
|
||||
|
||||
func (c *Config) acceptInputToSubnet(ctx context.Context, intf string,
|
||||
destination netip.Prefix, remove bool) error {
|
||||
destination netip.Prefix, remove bool,
|
||||
) error {
|
||||
interfaceFlag := "-i " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
@@ -140,10 +145,11 @@ func (c *Config) acceptEstablishedRelatedTraffic(ctx context.Context, remove boo
|
||||
}
|
||||
|
||||
func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
||||
defaultInterface string, connection models.Connection, remove bool) error {
|
||||
defaultInterface string, connection models.Connection, remove bool,
|
||||
) error {
|
||||
protocol := connection.Protocol
|
||||
if protocol == "tcp-client" {
|
||||
protocol = "tcp"
|
||||
protocol = "tcp" //nolint:goconst
|
||||
}
|
||||
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
|
||||
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
|
||||
@@ -158,7 +164,8 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
||||
|
||||
// Thanks to @npawelek.
|
||||
func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
|
||||
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool) error {
|
||||
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool,
|
||||
) error {
|
||||
doIPv4 := sourceIP.Is4() && destinationSubnet.Addr().Is4()
|
||||
|
||||
interfaceFlag := "-o " + intf
|
||||
@@ -179,12 +186,13 @@ func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
|
||||
|
||||
// NDP uses multicast address (theres no broadcast in IPv6 like ARP uses in IPv4).
|
||||
func (c *Config) acceptIpv6MulticastOutput(ctx context.Context,
|
||||
intf string, remove bool) error {
|
||||
intf string, remove bool,
|
||||
) error {
|
||||
interfaceFlag := "-o " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
}
|
||||
instruction := fmt.Sprintf("%s OUTPUT %s -d ff02::1:ff/104 -j ACCEPT",
|
||||
instruction := fmt.Sprintf("%s OUTPUT %s -d ff02::1:ff00:0/104 -j ACCEPT",
|
||||
appendOrDelete(remove), interfaceFlag)
|
||||
return c.runIP6tablesInstruction(ctx, instruction)
|
||||
}
|
||||
@@ -203,7 +211,8 @@ 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.
|
||||
func (c *Config) redirectPort(ctx context.Context, intf string,
|
||||
sourcePort, destinationPort uint16, remove bool) (err error) {
|
||||
sourcePort, destinationPort uint16, remove bool,
|
||||
) (err error) {
|
||||
interfaceFlag := "-i " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
@@ -235,6 +244,13 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
|
||||
appendOrDelete(remove), interfaceFlag, destinationPort),
|
||||
})
|
||||
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",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
|
||||
369
internal/firewall/list.go
Normal file
369
internal/firewall/list.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type chain struct {
|
||||
name string
|
||||
policy string
|
||||
packets uint64
|
||||
bytes uint64
|
||||
rules []chainRule
|
||||
}
|
||||
|
||||
type chainRule struct {
|
||||
lineNumber uint16 // starts from 1 and cannot be zero.
|
||||
packets uint64
|
||||
bytes uint64
|
||||
target string // "ACCEPT", "DROP", "REJECT" or "REDIRECT"
|
||||
protocol string // "tcp", "udp" or "" for all protocols.
|
||||
inputInterface string // input interface, for example "tun0" or "*""
|
||||
outputInterface string // output interface, for example "eth0" or "*""
|
||||
source netip.Prefix // source IP CIDR, for example 0.0.0.0/0. Must be valid.
|
||||
destination netip.Prefix // destination IP CIDR, for example 0.0.0.0/0. Must be valid.
|
||||
destinationPort uint16 // Not specified if set to zero.
|
||||
redirPorts []uint16 // Not specified if empty.
|
||||
ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty.
|
||||
}
|
||||
|
||||
var ErrChainListMalformed = errors.New("iptables chain list output is malformed")
|
||||
|
||||
func parseChain(iptablesOutput string) (c chain, err error) {
|
||||
// Text example:
|
||||
// Chain INPUT (policy ACCEPT 140K packets, 226M bytes)
|
||||
// pkts bytes target prot opt in out source destination
|
||||
// 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
||||
// 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||
// 0 0 DROP 0 -- tun0 * 0.0.0.0/0 0.0.0.0/0
|
||||
iptablesOutput = strings.TrimSpace(iptablesOutput)
|
||||
linesWithComments := strings.Split(iptablesOutput, "\n")
|
||||
|
||||
// Filter out lines starting with a '#' character
|
||||
lines := make([]string, 0, len(linesWithComments))
|
||||
for _, line := range linesWithComments {
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
const minLines = 2 // chain general information line + legend line
|
||||
if len(lines) < minLines {
|
||||
return chain{}, fmt.Errorf("%w: not enough lines to process in: %s",
|
||||
ErrChainListMalformed, iptablesOutput)
|
||||
}
|
||||
|
||||
c, err = parseChainGeneralDataLine(lines[0])
|
||||
if err != nil {
|
||||
return chain{}, fmt.Errorf("parsing chain general data line: %w", err)
|
||||
}
|
||||
|
||||
// Sanity check for the legend line
|
||||
expectedLegendFields := []string{"num", "pkts", "bytes", "target", "prot", "opt", "in", "out", "source", "destination"}
|
||||
legendLine := strings.TrimSpace(lines[1])
|
||||
legendFields := strings.Fields(legendLine)
|
||||
if !slices.Equal(expectedLegendFields, legendFields) {
|
||||
return chain{}, fmt.Errorf("%w: legend %q is not the expected %q",
|
||||
ErrChainListMalformed, legendLine, strings.Join(expectedLegendFields, " "))
|
||||
}
|
||||
|
||||
lines = lines[2:] // remove chain general information line and legend line
|
||||
if len(lines) == 0 {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
c.rules = make([]chainRule, len(lines))
|
||||
for i, line := range lines {
|
||||
c.rules[i], err = parseChainRuleLine(line)
|
||||
if err != nil {
|
||||
return chain{}, fmt.Errorf("parsing chain rule %q: %w", line, err)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// parseChainGeneralDataLine parses the first line of iptables chain list output.
|
||||
// For example, it can parse the following line:
|
||||
// Chain INPUT (policy ACCEPT 140K packets, 226M bytes)
|
||||
// It returns a chain struct with the parsed data.
|
||||
func parseChainGeneralDataLine(line string) (base chain, err error) {
|
||||
line = strings.TrimSpace(line)
|
||||
runesToRemove := []rune{'(', ')', ','}
|
||||
for _, r := range runesToRemove {
|
||||
line = strings.ReplaceAll(line, string(r), "")
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
const expectedNumberOfFields = 8
|
||||
if len(fields) != expectedNumberOfFields {
|
||||
return chain{}, fmt.Errorf("%w: expected %d fields in %q",
|
||||
ErrChainListMalformed, expectedNumberOfFields, line)
|
||||
}
|
||||
|
||||
// Sanity checks
|
||||
indexToExpectedValue := map[int]string{
|
||||
0: "Chain",
|
||||
2: "policy",
|
||||
5: "packets",
|
||||
7: "bytes",
|
||||
}
|
||||
for index, expectedValue := range indexToExpectedValue {
|
||||
if fields[index] == expectedValue {
|
||||
continue
|
||||
}
|
||||
return chain{}, fmt.Errorf("%w: expected %q for field %d in %q",
|
||||
ErrChainListMalformed, expectedValue, index, line)
|
||||
}
|
||||
|
||||
base.name = fields[1] // chain name could be custom
|
||||
base.policy = fields[3]
|
||||
err = checkTarget(base.policy)
|
||||
if err != nil {
|
||||
return chain{}, fmt.Errorf("policy target in %q: %w", line, err)
|
||||
}
|
||||
|
||||
packets, err := parseMetricSize(fields[4])
|
||||
if err != nil {
|
||||
return chain{}, fmt.Errorf("parsing packets: %w", err)
|
||||
}
|
||||
base.packets = packets
|
||||
|
||||
bytes, err := parseMetricSize(fields[6])
|
||||
if err != nil {
|
||||
return chain{}, fmt.Errorf("parsing bytes: %w", err)
|
||||
}
|
||||
base.bytes = bytes
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
var ErrChainRuleMalformed = errors.New("chain rule is malformed")
|
||||
|
||||
func parseChainRuleLine(line string) (rule chainRule, err error) {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return chainRule{}, fmt.Errorf("%w: empty line", ErrChainRuleMalformed)
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
|
||||
const minFields = 10
|
||||
if len(fields) < minFields {
|
||||
return chainRule{}, fmt.Errorf("%w: not enough fields", ErrChainRuleMalformed)
|
||||
}
|
||||
|
||||
for fieldIndex, field := range fields[:minFields] {
|
||||
err = parseChainRuleField(fieldIndex, field, &rule)
|
||||
if err != nil {
|
||||
return chainRule{}, fmt.Errorf("parsing chain rule field: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) > minFields {
|
||||
err = parseChainRuleOptionalFields(fields[minFields:], &rule)
|
||||
if err != nil {
|
||||
return chainRule{}, fmt.Errorf("parsing optional fields: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func parseChainRuleField(fieldIndex int, field string, rule *chainRule) (err error) {
|
||||
if field == "" {
|
||||
return fmt.Errorf("%w: empty field at index %d", ErrChainRuleMalformed, fieldIndex)
|
||||
}
|
||||
|
||||
const (
|
||||
numIndex = iota
|
||||
packetsIndex
|
||||
bytesIndex
|
||||
targetIndex
|
||||
protocolIndex
|
||||
optIndex
|
||||
inputInterfaceIndex
|
||||
outputInterfaceIndex
|
||||
sourceIndex
|
||||
destinationIndex
|
||||
)
|
||||
|
||||
switch fieldIndex {
|
||||
case numIndex:
|
||||
rule.lineNumber, err = parseLineNumber(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing line number: %w", err)
|
||||
}
|
||||
case packetsIndex:
|
||||
rule.packets, err = parseMetricSize(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing packets: %w", err)
|
||||
}
|
||||
case bytesIndex:
|
||||
rule.bytes, err = parseMetricSize(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing bytes: %w", err)
|
||||
}
|
||||
case targetIndex:
|
||||
err = checkTarget(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking target: %w", err)
|
||||
}
|
||||
rule.target = field
|
||||
case protocolIndex:
|
||||
rule.protocol, err = parseProtocol(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing protocol: %w", err)
|
||||
}
|
||||
case optIndex: // ignored
|
||||
case inputInterfaceIndex:
|
||||
rule.inputInterface = field
|
||||
case outputInterfaceIndex:
|
||||
rule.outputInterface = field
|
||||
case sourceIndex:
|
||||
rule.source, err = parseIPPrefix(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing source IP CIDR: %w", err)
|
||||
}
|
||||
case destinationIndex:
|
||||
rule.destination, err = parseIPPrefix(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing destination IP CIDR: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err error) {
|
||||
for i := 0; i < len(optionalFields); i++ {
|
||||
key := optionalFields[i]
|
||||
switch key {
|
||||
case "tcp", "udp":
|
||||
i++
|
||||
value := optionalFields[i]
|
||||
value = strings.TrimPrefix(value, "dpt:")
|
||||
const base, bitLength = 10, 16
|
||||
destinationPort, err := strconv.ParseUint(value, base, bitLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing destination port %q: %w", value, err)
|
||||
}
|
||||
rule.destinationPort = uint16(destinationPort)
|
||||
case "redir":
|
||||
i++
|
||||
switch optionalFields[i] {
|
||||
case "ports":
|
||||
i++
|
||||
ports, err := parsePortsCSV(optionalFields[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing redirection ports: %w", err)
|
||||
}
|
||||
rule.redirPorts = ports
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected optional field: %s",
|
||||
ErrChainRuleMalformed, optionalFields[i])
|
||||
}
|
||||
case "ctstate":
|
||||
i++
|
||||
rule.ctstate = strings.Split(optionalFields[i], ",")
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected optional field: %s", ErrChainRuleMalformed, key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePortsCSV(s string) (ports []uint16, err error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
ports = make([]uint16, len(fields))
|
||||
for i, field := range fields {
|
||||
const base, bitLength = 10, 16
|
||||
port, err := strconv.ParseUint(field, base, bitLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing port %q: %w", field, err)
|
||||
}
|
||||
ports[i] = uint16(port)
|
||||
}
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
var ErrLineNumberIsZero = errors.New("line number is zero")
|
||||
|
||||
func parseLineNumber(s string) (n uint16, err error) {
|
||||
const base, bitLength = 10, 16
|
||||
lineNumber, err := strconv.ParseUint(s, base, bitLength)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if lineNumber == 0 {
|
||||
return 0, fmt.Errorf("%w", ErrLineNumberIsZero)
|
||||
}
|
||||
return uint16(lineNumber), nil
|
||||
}
|
||||
|
||||
var ErrTargetUnknown = errors.New("unknown target")
|
||||
|
||||
func checkTarget(target string) (err error) {
|
||||
switch target {
|
||||
case "ACCEPT", "DROP", "REJECT", "REDIRECT":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrTargetUnknown, target)
|
||||
}
|
||||
|
||||
var ErrProtocolUnknown = errors.New("unknown protocol")
|
||||
|
||||
func parseProtocol(s string) (protocol string, err error) {
|
||||
switch s {
|
||||
case "0":
|
||||
case "6":
|
||||
protocol = "tcp"
|
||||
case "17":
|
||||
protocol = "udp"
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
||||
}
|
||||
return protocol, nil
|
||||
}
|
||||
|
||||
var ErrMetricSizeMalformed = errors.New("metric size is malformed")
|
||||
|
||||
// parseMetricSize parses a metric size string like 140K or 226M and
|
||||
// returns the raw integer matching it.
|
||||
func parseMetricSize(size string) (n uint64, err error) {
|
||||
if size == "" {
|
||||
return n, fmt.Errorf("%w: empty string", ErrMetricSizeMalformed)
|
||||
}
|
||||
|
||||
//nolint:mnd
|
||||
multiplerLetterToValue := map[byte]uint64{
|
||||
'K': 1000,
|
||||
'M': 1000000,
|
||||
'G': 1000000000,
|
||||
'T': 1000000000000,
|
||||
}
|
||||
|
||||
lastCharacter := size[len(size)-1]
|
||||
multiplier, ok := multiplerLetterToValue[lastCharacter]
|
||||
if ok { // multiplier present
|
||||
size = size[:len(size)-1]
|
||||
} else {
|
||||
multiplier = 1
|
||||
}
|
||||
|
||||
const base, bitLength = 10, 64
|
||||
n, err = strconv.ParseUint(size, base, bitLength)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("%w: %w", ErrMetricSizeMalformed, err)
|
||||
}
|
||||
n *= multiplier
|
||||
return n, nil
|
||||
}
|
||||
120
internal/firewall/list_test.go
Normal file
120
internal/firewall/list_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
iptablesOutput string
|
||||
table chain
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"no_output": {
|
||||
errWrapped: ErrChainListMalformed,
|
||||
errMessage: "iptables chain list output is malformed: not enough lines to process in: ",
|
||||
},
|
||||
"single_line_only": {
|
||||
iptablesOutput: `Chain INPUT (policy ACCEPT 140K packets, 226M bytes)`,
|
||||
errWrapped: ErrChainListMalformed,
|
||||
errMessage: "iptables chain list output is malformed: not enough lines to process in: " +
|
||||
"Chain INPUT (policy ACCEPT 140K packets, 226M bytes)",
|
||||
},
|
||||
"malformed_general_data_line": {
|
||||
iptablesOutput: `Chain INPUT
|
||||
num pkts bytes target prot opt in out source destination`,
|
||||
errWrapped: ErrChainListMalformed,
|
||||
errMessage: "parsing chain general data line: iptables chain list output is malformed: " +
|
||||
"expected 8 fields in \"Chain INPUT\"",
|
||||
},
|
||||
"malformed_legend": {
|
||||
iptablesOutput: `Chain INPUT (policy ACCEPT 140K packets, 226M bytes)
|
||||
num pkts bytes target prot opt in out source`,
|
||||
errWrapped: ErrChainListMalformed,
|
||||
errMessage: "iptables chain list output is malformed: legend " +
|
||||
"\"num pkts bytes target prot opt in out source\" " +
|
||||
"is not the expected \"num pkts bytes target prot opt in out source destination\"",
|
||||
},
|
||||
"no_rule": {
|
||||
iptablesOutput: `Chain INPUT (policy ACCEPT 140K packets, 226M bytes)
|
||||
num pkts bytes target prot opt in out source destination`,
|
||||
table: chain{
|
||||
name: "INPUT",
|
||||
policy: "ACCEPT",
|
||||
packets: 140000,
|
||||
bytes: 226000000,
|
||||
},
|
||||
},
|
||||
"some_rules": {
|
||||
iptablesOutput: `Chain INPUT (policy ACCEPT 140K packets, 226M bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 ACCEPT 17 -- tun0 * 0.0.0.0/0 0.0.0.0/0 udp dpt:55405
|
||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||
3 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||
`,
|
||||
table: chain{
|
||||
name: "INPUT",
|
||||
policy: "ACCEPT",
|
||||
packets: 140000,
|
||||
bytes: 226000000,
|
||||
rules: []chainRule{
|
||||
{
|
||||
lineNumber: 1,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "ACCEPT",
|
||||
protocol: "udp",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
destinationPort: 55405,
|
||||
},
|
||||
{
|
||||
lineNumber: 2,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "ACCEPT",
|
||||
protocol: "tcp",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
destinationPort: 55405,
|
||||
},
|
||||
{
|
||||
lineNumber: 3,
|
||||
packets: 0,
|
||||
bytes: 0,
|
||||
target: "DROP",
|
||||
protocol: "",
|
||||
inputInterface: "tun0",
|
||||
outputInterface: "*",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
table, err := parseChain(testCase.iptablesOutput)
|
||||
|
||||
assert.Equal(t, testCase.table, table)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,6 @@ import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
func (c *Config) logIgnoredSubnetFamily(subnet netip.Prefix) {
|
||||
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
|
||||
"no default route matching its family", subnet))
|
||||
|
||||
3
internal/firewall/mocks_generate_test.go
Normal file
3
internal/firewall/mocks_generate_test.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package firewall
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . CmdRunner,Logger
|
||||
@@ -1,15 +1,54 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/routing (interfaces: Logger)
|
||||
// Source: github.com/qdm12/gluetun/internal/firewall (interfaces: CmdRunner,Logger)
|
||||
|
||||
// Package routing is a generated GoMock package.
|
||||
package routing
|
||||
// Package firewall is a generated GoMock package.
|
||||
package firewall
|
||||
|
||||
import (
|
||||
exec "os/exec"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockCmdRunner is a mock of CmdRunner interface.
|
||||
type MockCmdRunner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCmdRunnerMockRecorder
|
||||
}
|
||||
|
||||
// MockCmdRunnerMockRecorder is the mock recorder for MockCmdRunner.
|
||||
type MockCmdRunnerMockRecorder struct {
|
||||
mock *MockCmdRunner
|
||||
}
|
||||
|
||||
// NewMockCmdRunner creates a new mock instance.
|
||||
func NewMockCmdRunner(ctrl *gomock.Controller) *MockCmdRunner {
|
||||
mock := &MockCmdRunner{ctrl: ctrl}
|
||||
mock.recorder = &MockCmdRunnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockCmdRunner) EXPECT() *MockCmdRunnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Run mocks base method.
|
||||
func (m *MockCmdRunner) Run(arg0 *exec.Cmd) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Run", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run.
|
||||
func (mr *MockCmdRunnerMockRecorder) Run(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockCmdRunner)(nil).Run), arg0)
|
||||
}
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
164
internal/firewall/parse.go
Normal file
164
internal/firewall/parse.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type iptablesInstruction struct {
|
||||
table string // defaults to "filter", and can be "nat" for example.
|
||||
append bool
|
||||
chain string // for example INPUT, PREROUTING. Cannot be empty.
|
||||
target string // for example ACCEPT. Can be empty.
|
||||
protocol string // "tcp" or "udp" or "" for all protocols.
|
||||
inputInterface string // for example "tun0" or "" for any interface.
|
||||
outputInterface string // for example "tun0" or "" for any interface.
|
||||
source netip.Prefix // if not valid, then it is unspecified.
|
||||
destination netip.Prefix // if not valid, then it is unspecified.
|
||||
destinationPort uint16 // if zero, there is no destination port
|
||||
toPorts []uint16 // if empty, there is no redirection
|
||||
ctstate []string // if empty, there is no ctstate
|
||||
}
|
||||
|
||||
func (i *iptablesInstruction) setDefaults() {
|
||||
if i.table == "" {
|
||||
i.table = "filter"
|
||||
}
|
||||
}
|
||||
|
||||
// equalToRule ignores the append boolean flag of the instruction to compare against the rule.
|
||||
func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (equal bool) {
|
||||
switch {
|
||||
case i.table != table:
|
||||
return false
|
||||
case i.chain != chain:
|
||||
return false
|
||||
case i.target != rule.target:
|
||||
return false
|
||||
case i.protocol != rule.protocol:
|
||||
return false
|
||||
case i.destinationPort != rule.destinationPort:
|
||||
return false
|
||||
case !slices.Equal(i.toPorts, rule.redirPorts):
|
||||
return false
|
||||
case !slices.Equal(i.ctstate, rule.ctstate):
|
||||
return false
|
||||
case !networkInterfacesEqual(i.inputInterface, rule.inputInterface):
|
||||
return false
|
||||
case !networkInterfacesEqual(i.outputInterface, rule.outputInterface):
|
||||
return false
|
||||
case !ipPrefixesEqual(i.source, rule.source):
|
||||
return false
|
||||
case !ipPrefixesEqual(i.destination, rule.destination):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// instruction can be "" which equivalent to the "*" chain rule interface.
|
||||
func networkInterfacesEqual(instruction, chainRule string) bool {
|
||||
return instruction == chainRule || (instruction == "" && chainRule == "*")
|
||||
}
|
||||
|
||||
func ipPrefixesEqual(instruction, chainRule netip.Prefix) bool {
|
||||
return instruction == chainRule ||
|
||||
(!instruction.IsValid() && chainRule.Bits() == 0 && chainRule.Addr().IsUnspecified())
|
||||
}
|
||||
|
||||
var ErrIptablesCommandMalformed = errors.New("iptables command is malformed")
|
||||
|
||||
func parseIptablesInstruction(s string) (instruction iptablesInstruction, err error) {
|
||||
if s == "" {
|
||||
return iptablesInstruction{}, fmt.Errorf("%w: empty instruction", ErrIptablesCommandMalformed)
|
||||
}
|
||||
fields := strings.Fields(s)
|
||||
if len(fields)%2 != 0 {
|
||||
return iptablesInstruction{}, fmt.Errorf("%w: fields count %d is not even: %q",
|
||||
ErrIptablesCommandMalformed, len(fields), s)
|
||||
}
|
||||
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
key := fields[i]
|
||||
value := fields[i+1]
|
||||
err = parseInstructionFlag(key, value, &instruction)
|
||||
if err != nil {
|
||||
return iptablesInstruction{}, fmt.Errorf("parsing %q: %w", s, err)
|
||||
}
|
||||
}
|
||||
|
||||
instruction.setDefaults()
|
||||
return instruction, nil
|
||||
}
|
||||
|
||||
func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (err error) {
|
||||
switch key {
|
||||
case "-t", "--table":
|
||||
instruction.table = value
|
||||
case "-D", "--delete":
|
||||
instruction.append = false
|
||||
instruction.chain = value
|
||||
case "-A", "--append":
|
||||
instruction.append = true
|
||||
instruction.chain = value
|
||||
case "-j", "--jump":
|
||||
instruction.target = value
|
||||
case "-p", "--protocol":
|
||||
instruction.protocol = value
|
||||
case "-m", "--match": // ignore match
|
||||
case "-i", "--in-interface":
|
||||
instruction.inputInterface = value
|
||||
case "-o", "--out-interface":
|
||||
instruction.outputInterface = value
|
||||
case "-s", "--source":
|
||||
instruction.source, err = parseIPPrefix(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing source IP CIDR: %w", err)
|
||||
}
|
||||
case "-d", "--destination":
|
||||
instruction.destination, err = parseIPPrefix(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing destination IP CIDR: %w", err)
|
||||
}
|
||||
case "--dport":
|
||||
const base, bitLength = 10, 16
|
||||
destinationPort, err := strconv.ParseUint(value, base, bitLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing destination port: %w", err)
|
||||
}
|
||||
instruction.destinationPort = uint16(destinationPort)
|
||||
case "--ctstate":
|
||||
instruction.ctstate = strings.Split(value, ",")
|
||||
case "--to-ports":
|
||||
portStrings := strings.Split(value, ",")
|
||||
instruction.toPorts = make([]uint16, len(portStrings))
|
||||
for i, portString := range portStrings {
|
||||
const base, bitLength = 10, 16
|
||||
port, err := strconv.ParseUint(portString, base, bitLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing port redirection: %w", err)
|
||||
}
|
||||
instruction.toPorts[i] = uint16(port)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
|
||||
slashIndex := strings.Index(value, "/")
|
||||
if slashIndex >= 0 {
|
||||
return netip.ParsePrefix(value)
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(value)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("parsing IP address: %w", err)
|
||||
}
|
||||
return netip.PrefixFrom(ip, ip.BitLen()), nil
|
||||
}
|
||||
136
internal/firewall/parse_test.go
Normal file
136
internal/firewall/parse_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseIptablesInstruction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
s string
|
||||
instruction iptablesInstruction
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"no_instruction": {
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "iptables command is malformed: empty instruction",
|
||||
},
|
||||
"uneven_fields": {
|
||||
s: "-A",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "iptables command is malformed: fields count 1 is not even: \"-A\"",
|
||||
},
|
||||
"unknown_key": {
|
||||
s: "-x something",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "parsing \"-x something\": iptables command is malformed: unknown key \"-x\"",
|
||||
},
|
||||
"one_pair": {
|
||||
s: "-A INPUT",
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
append: true,
|
||||
},
|
||||
},
|
||||
"instruction_A": {
|
||||
s: "-A INPUT -i tun0 -p tcp -m tcp -s 1.2.3.4/32 -d 5.6.7.8 --dport 10000 -j ACCEPT",
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
append: true,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
destination: netip.MustParsePrefix("5.6.7.8/32"),
|
||||
destinationPort: 10000,
|
||||
target: "ACCEPT",
|
||||
},
|
||||
},
|
||||
"nat_redirection": {
|
||||
s: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
instruction: iptablesInstruction{
|
||||
table: "nat",
|
||||
chain: "PREROUTING",
|
||||
append: false,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
destinationPort: 43716,
|
||||
target: "REDIRECT",
|
||||
toPorts: []uint16{5678},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule, err := parseIptablesInstruction(testCase.s)
|
||||
|
||||
assert.Equal(t, testCase.instruction, rule)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseIPPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
value string
|
||||
prefix netip.Prefix
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
errMessage: `parsing IP address: ParseAddr(""): unable to parse IP`,
|
||||
},
|
||||
"invalid": {
|
||||
value: "invalid",
|
||||
errMessage: `parsing IP address: ParseAddr("invalid"): unable to parse IP`,
|
||||
},
|
||||
"valid_ipv4_with_bits": {
|
||||
value: "10.0.0.0/16",
|
||||
prefix: netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 0, 0, 0}), 16),
|
||||
},
|
||||
"valid_ipv4_without_bits": {
|
||||
value: "10.0.0.4",
|
||||
prefix: netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 0, 0, 4}), 32),
|
||||
},
|
||||
"valid_ipv6_with_bits": {
|
||||
value: "2001:db8::/32",
|
||||
prefix: netip.PrefixFrom(
|
||||
netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8}),
|
||||
32),
|
||||
},
|
||||
"valid_ipv6_without_bits": {
|
||||
value: "2001:db8::",
|
||||
prefix: netip.PrefixFrom(
|
||||
netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8}),
|
||||
128),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
prefix, err := parseIPPrefix(testCase.value)
|
||||
|
||||
assert.Equal(t, testCase.prefix, prefix)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
// If the destination port is zero, the redirection for the source port is removed
|
||||
// and no new redirection is added.
|
||||
func (c *Config) RedirectPort(ctx context.Context, intf string, sourcePort,
|
||||
destinationPort uint16) (err error) {
|
||||
destinationPort uint16,
|
||||
) (err error) {
|
||||
c.stateMutex.Lock()
|
||||
defer c.stateMutex.Unlock()
|
||||
|
||||
@@ -90,7 +91,8 @@ func (p *portRedirections) remove(intf string, sourcePort uint16) {
|
||||
}
|
||||
|
||||
func (p *portRedirections) check(dryRun portRedirection) (alreadyExists bool,
|
||||
conflict *portRedirection) {
|
||||
conflict *portRedirection,
|
||||
) {
|
||||
slice := *p
|
||||
for _, redirection := range slice {
|
||||
interfaceMatch := redirection.interfaceName == "" ||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/golibs/command (interfaces: Runner)
|
||||
|
||||
// Package firewall is a generated GoMock package.
|
||||
package firewall
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
command "github.com/qdm12/golibs/command"
|
||||
)
|
||||
|
||||
// MockRunner is a mock of Runner interface.
|
||||
type MockRunner struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRunnerMockRecorder
|
||||
}
|
||||
|
||||
// MockRunnerMockRecorder is the mock recorder for MockRunner.
|
||||
type MockRunnerMockRecorder struct {
|
||||
mock *MockRunner
|
||||
}
|
||||
|
||||
// NewMockRunner creates a new mock instance.
|
||||
func NewMockRunner(ctrl *gomock.Controller) *MockRunner {
|
||||
mock := &MockRunner{ctrl: ctrl}
|
||||
mock.recorder = &MockRunnerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRunner) EXPECT() *MockRunnerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Run mocks base method.
|
||||
func (m *MockRunner) Run(arg0 command.ExecCmd) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Run", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run.
|
||||
func (mr *MockRunnerMockRecorder) Run(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockRunner)(nil).Run), arg0)
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/command"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,8 +17,9 @@ var (
|
||||
ErrIPTablesNotSupported = errors.New("no iptables supported found")
|
||||
)
|
||||
|
||||
func checkIptablesSupport(ctx context.Context, runner command.Runner,
|
||||
iptablesPathsToTry ...string) (iptablesPath string, err error) {
|
||||
func checkIptablesSupport(ctx context.Context, runner CmdRunner,
|
||||
iptablesPathsToTry ...string,
|
||||
) (iptablesPath string, err error) {
|
||||
iptablesPathToUnsupportedMessage := make(map[string]string, len(iptablesPathsToTry))
|
||||
for _, pathToTest := range iptablesPathsToTry {
|
||||
ok, unsupportedMessage, err := testIptablesPath(ctx, pathToTest, runner)
|
||||
@@ -62,8 +61,9 @@ func checkIptablesSupport(ctx context.Context, runner command.Runner,
|
||||
}
|
||||
|
||||
func testIptablesPath(ctx context.Context, path string,
|
||||
runner command.Runner) (ok bool, unsupportedMessage string,
|
||||
criticalErr error) {
|
||||
runner CmdRunner) (ok bool, unsupportedMessage string,
|
||||
criticalErr error,
|
||||
) {
|
||||
// Just listing iptables rules often work but we need
|
||||
// to modify them to ensure we can support the iptables
|
||||
// being tested.
|
||||
|
||||
@@ -6,13 +6,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=runner_mock_test.go -package $GOPACKAGE github.com/qdm12/golibs/command Runner
|
||||
|
||||
func newAppendTestRuleMatcher(path string) *cmdMatcher {
|
||||
return newCmdMatcher(path,
|
||||
"^-A$", "^OUTPUT$", "^-o$", "^[a-z0-9]{15}$",
|
||||
@@ -43,15 +40,15 @@ func Test_checkIptablesSupport(t *testing.T) {
|
||||
const inputPolicy = "ACCEPT"
|
||||
|
||||
testCases := map[string]struct {
|
||||
buildRunner func(ctrl *gomock.Controller) command.Runner
|
||||
buildRunner func(ctrl *gomock.Controller) CmdRunner
|
||||
iptablesPathsToTry []string
|
||||
iptablesPath string
|
||||
errSentinel error
|
||||
errMessage string
|
||||
}{
|
||||
"critical error when checking": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
|
||||
Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
|
||||
@@ -64,8 +61,8 @@ func Test_checkIptablesSupport(t *testing.T) {
|
||||
"output (exit code 4)",
|
||||
},
|
||||
"found valid path": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
|
||||
Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
|
||||
@@ -80,8 +77,8 @@ func Test_checkIptablesSupport(t *testing.T) {
|
||||
iptablesPath: "path1",
|
||||
},
|
||||
"all permission denied": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
|
||||
Return("Permission denied (you must be root) more context", errDummy)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
|
||||
@@ -95,8 +92,8 @@ func Test_checkIptablesSupport(t *testing.T) {
|
||||
"path2: context: Permission denied (you must be root) (exit code 4)",
|
||||
},
|
||||
"no valid path": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
|
||||
Return("output 1", errDummy)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
|
||||
@@ -113,15 +110,13 @@ func Test_checkIptablesSupport(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
runner := testCase.buildRunner(ctrl)
|
||||
|
||||
iptablesPath, err :=
|
||||
checkIptablesSupport(ctx, runner, testCase.iptablesPathsToTry...)
|
||||
iptablesPath, err := checkIptablesSupport(ctx, runner, testCase.iptablesPathsToTry...)
|
||||
|
||||
require.ErrorIs(t, err, testCase.errSentinel)
|
||||
if testCase.errSentinel != nil {
|
||||
@@ -141,15 +136,15 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
const inputPolicy = "ACCEPT"
|
||||
|
||||
testCases := map[string]struct {
|
||||
buildRunner func(ctrl *gomock.Controller) command.Runner
|
||||
buildRunner func(ctrl *gomock.Controller) CmdRunner
|
||||
ok bool
|
||||
unsupportedMessage string
|
||||
criticalErrWrapped error
|
||||
criticalErrMessage string
|
||||
}{
|
||||
"append test rule permission denied": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
|
||||
Return("Permission denied (you must be root)", errDummy)
|
||||
return runner
|
||||
@@ -157,8 +152,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
|
||||
},
|
||||
"append test rule unsupported": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
|
||||
Return("some output", errDummy)
|
||||
return runner
|
||||
@@ -166,8 +161,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "some output (exit code 4)",
|
||||
},
|
||||
"remove test rule error": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).
|
||||
Return("some output", errDummy)
|
||||
@@ -177,8 +172,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
criticalErrMessage: "failed cleaning up test rule: some output (exit code 4)",
|
||||
},
|
||||
"list input rules permission denied": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -188,8 +183,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
|
||||
},
|
||||
"list input rules unsupported": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -199,8 +194,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "some output (exit code 4)",
|
||||
},
|
||||
"list input rules no policy": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -211,8 +206,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
criticalErrMessage: "input policy not found: in INPUT rules: some\noutput",
|
||||
},
|
||||
"set policy permission denied": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -224,8 +219,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
|
||||
},
|
||||
"set policy unsupported": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -237,8 +232,8 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
unsupportedMessage: "some output (exit code 4)",
|
||||
},
|
||||
"success": {
|
||||
buildRunner: func(ctrl *gomock.Controller) command.Runner {
|
||||
runner := NewMockRunner(ctrl)
|
||||
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
|
||||
runner := NewMockCmdRunner(ctrl)
|
||||
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
|
||||
runner.EXPECT().Run(newListInputRulesMatcher(path)).
|
||||
@@ -252,15 +247,13 @@ func Test_testIptablesPath(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
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.unsupportedMessage, unsupportedMessage)
|
||||
@@ -290,7 +283,6 @@ func Test_isPermissionDenied(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -333,7 +325,6 @@ func Test_extractInputPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func (c *Config) SetVPNConnection(ctx context.Context,
|
||||
connection models.Connection, vpnIntf string) (err error) {
|
||||
connection models.Connection, vpnIntf string,
|
||||
) (err error) {
|
||||
c.stateMutex.Lock()
|
||||
defer c.stateMutex.Unlock()
|
||||
|
||||
|
||||
34
internal/format/duration.go
Normal file
34
internal/format/duration.go
Normal file
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
65
internal/format/duration_test.go
Normal file
65
internal/format/duration_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
"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 {
|
||||
httpClient *http.Client
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user