mirror of
https://github.com/qdm12/gluetun.git
synced 2026-06-28 14:57:32 +02:00
Compare commits
346 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35137cfba0 | |||
| 66b9f71ecf | |||
| 704a7fd7ef | |||
| f615e3c780 | |||
| f1a8303db7 | |||
| 628b0a22e2 | |||
| ea3d138bd6 | |||
| c3a6809447 | |||
| 792a5ff5f3 | |||
| 7eef1c89a7 | |||
| 8bc2fbd487 | |||
| a4eb625fbe | |||
| 17a7bf6d54 | |||
| b11de4f0c3 | |||
| e87a92efa0 | |||
| 44977f4d9e | |||
| c473579261 | |||
| d5eeec6fb3 | |||
| 7e7e8182ef | |||
| 64fd11d013 | |||
| 2006fae0e3 | |||
| 3b9c9b24bd | |||
| 11883aa830 | |||
| 1ae85aa5d0 | |||
| 763c5be119 | |||
| 5b88c76a14 | |||
| 086e3740f3 | |||
| 57cf276d31 | |||
| 405a6f699d | |||
| 72af17cc91 | |||
| 8a2e8bda0f | |||
| 5e6c11b045 | |||
| 85d2917e8e | |||
| 9a5995fa72 | |||
| 2438fc2c3a | |||
| 8aaf998fa1 | |||
| f0cbcbb60d | |||
| 4e5d4f7793 | |||
| 460ffb637a | |||
| c83d4b0926 | |||
| 00d1592899 | |||
| b5b0e01376 | |||
| b04529c380 | |||
| efea169495 | |||
| ba9fcb5b89 | |||
| 97ccadfd33 | |||
| e6fc792f4f | |||
| f4eeffe79a | |||
| 0394e31fe2 | |||
| e557971ae8 | |||
| a98b39a03f | |||
| 760fefd890 | |||
| 543d3fa95e | |||
| 93999062e4 | |||
| 853f4601a5 | |||
| 1d29f1f517 | |||
| d790e3385c | |||
| 069cde8a85 | |||
| d98afce793 | |||
| 57c53bc19e | |||
| c0af198155 | |||
| 3d53cea0f6 | |||
| f7a9ddc48b | |||
| 02a186c145 | |||
| 724cd3a15e | |||
| 199ad77ec9 | |||
| dd0edafbb1 | |||
| 9be2fc827b | |||
| b63702cf63 | |||
| ede2509132 | |||
| 100124e8b8 | |||
| 850a91b35f | |||
| 4a40f0fdee | |||
| b7735ecc00 | |||
| 457e5597bb | |||
| 2460b56c2b | |||
| 5b2f86f4e7 | |||
| 49317ecb8a | |||
| bd275aaea8 | |||
| 39bd9854f7 | |||
| c2c9504e94 | |||
| 48317a0d55 | |||
| 6c3f519c62 | |||
| b7cbea1ce6 | |||
| d8a3cc3dfa | |||
| b1da4c4b86 | |||
| 579bd8e416 | |||
| 7bf59ebfb4 | |||
| 4ac25b9dd1 | |||
| 4bcbd29fb9 | |||
| a8ee1d7a63 | |||
| c6c3a2bf1b | |||
| e7b25a0d5e | |||
| 11cd62f6b1 | |||
| ed26957a1a | |||
| 54b55c594f | |||
| ec24ffdfd8 | |||
| b9d49e0661 | |||
| 2bb4deccd5 | |||
| 0d0c0fb143 | |||
| 885e491bb7 | |||
| e75ae21dcd | |||
| 4b8dc8ded7 | |||
| 0eeee5c496 | |||
| d21953f62e | |||
| 034f8f6331 | |||
| 01487b5caf | |||
| 625a63e7c2 | |||
| 0c3e5d94d8 | |||
| d586793169 | |||
| c5eacac644 | |||
| 7fbf2cbee3 | |||
| 1dee183a70 | |||
| c66d8bed00 | |||
| 73b3e2c88a | |||
| ea87c0a2aa | |||
| 2192874de8 | |||
| 007c5159f4 | |||
| c6b211ef9b | |||
| 1c43a045d1 | |||
| 56b9e108be | |||
| 67b66bba9e | |||
| 8d86470905 | |||
| fb85ae79d1 | |||
| 783616f61d | |||
| bc79901f1e | |||
| 1c56189abc | |||
| 224618337c | |||
| 183d351b58 | |||
| 04d7cef294 | |||
| 5f903d1fbf | |||
| d43eb1658f | |||
| 36dfd5b631 | |||
| f81b8342d6 | |||
| cdec25da52 | |||
| 201d1041f4 | |||
| dc78b4ecce | |||
| d75b48d123 | |||
| e828ea1462 | |||
| be92aa2ac4 | |||
| 8f1fda7646 | |||
| 8eb990eb66 | |||
| 4698daea16 | |||
| b0a75673bd | |||
| 5f0c499808 | |||
| bdd69a1fb7 | |||
| 1af75bb30c | |||
| 9c1cd7e8b1 | |||
| facc6df3be | |||
| e292a4c9be | |||
| 9e4dd61c19 | |||
| fe3d4a94d4 | |||
| de38d759a4 | |||
| fba60af772 | |||
| 9b9b723887 | |||
| a10349e378 | |||
| 983330266a | |||
| 6eb511fb2a | |||
| 666f55767b | |||
| 0a0bb4cf71 | |||
| 2b0719225d | |||
| c97bd1bb7c | |||
| 10a7c75aa6 | |||
| 617f1b764f | |||
| 600f2ab643 | |||
| 7052d5490b | |||
| 6a5a836cb6 | |||
| a649b0adc1 | |||
| beaa8b5589 | |||
| e806fe02db | |||
| 92237658c3 | |||
| e627504fb5 | |||
| cc1c253bad | |||
| c27dac10fe | |||
| 7d1e2eb226 | |||
| 5b5aa5e014 | |||
| 9ee3ed754d | |||
| 0ca466fbd5 | |||
| 1c1d271967 | |||
| cc89b35b63 | |||
| fd6e5e4e90 | |||
| d702ed4122 | |||
| 2d00f3fe25 | |||
| 56db5a83c0 | |||
| f5206375c0 | |||
| c25c9f6f0e | |||
| 08a7aae5f1 | |||
| 57d8eb03c5 | |||
| 2b55161fbb | |||
| c4f2a224d4 | |||
| 8bb0cc324b | |||
| 2afa988174 | |||
| a35c994bc8 | |||
| 0fad44fb68 | |||
| 4f9dcff3f4 | |||
| 1abc90970d | |||
| a445ba072c | |||
| 9e5624d32b | |||
| 815fcdb711 | |||
| 0bb9f62755 | |||
| 93567a7804 | |||
| 0afbb71634 | |||
| 9f39d47150 | |||
| f9490656eb | |||
| 482421dda3 | |||
| 03f1fea123 | |||
| 31284542a2 | |||
| 5ff5fc4a5e | |||
| 5b93464fef | |||
| debf3474e7 | |||
| 2853ca9033 | |||
| 74d059dd77 | |||
| 9963e18a8a | |||
| 41cd8fb30d | |||
| 9ed6cd978d | |||
| c4b9d459ed | |||
| 6e99ca573e | |||
| 2cf4d6b469 | |||
| a17776673b | |||
| fcdba0a3cc | |||
| 4712d0cf79 | |||
| 113c113615 | |||
| 6023eb1878 | |||
| a1ece20617 | |||
| 0bc67b73a8 | |||
| c7ab5bd34c | |||
| 843bf08aa1 | |||
| 5b25cc95a9 | |||
| 0fddbc54a2 | |||
| 11fcfb7d19 | |||
| 3cd7d7edcb | |||
| 30609b6fe9 | |||
| 8a0921748b | |||
| 3fac02a82a | |||
| f11f142bee | |||
| 596faef8f2 | |||
| 3d1b6bc861 | |||
| 46ad576233 | |||
| 46beaac34b | |||
| 3025476e8b | |||
| cd6f9493a4 | |||
| 9984ad22d7 | |||
| 3565ba67c4 | |||
| ffb0bec4da | |||
| 4d2b8787e0 | |||
| d4831ad4a6 | |||
| 9e1b53a732 | |||
| d0113849d6 | |||
| 7b25fdfee8 | |||
| 5ed6e82922 | |||
| 7dbd14df27 | |||
| 96d8b53338 | |||
| 2bd19640d9 | |||
| 1047508bd7 | |||
| eb49306b80 | |||
| 43da9ddbb3 | |||
| 7fbc5c3c07 | |||
| e03f545e07 | |||
| 942f1f2c0f | |||
| baf566d7a5 | |||
| 6712adfe6b | |||
| 2e2e5f9df5 | |||
| 35e9b2365d | |||
| b0b769d2c1 | |||
| d3c7d3c7bc | |||
| 65f49ea012 | |||
| 5687555921 | |||
| 0fb75036a0 | |||
| 2b513dd43d | |||
| 687d9b4736 | |||
| c70c2ef932 | |||
| af3ada109b | |||
| 9d40564734 | |||
| 3734815ada | |||
| b9cc5c1fdc | |||
| c646ca5766 | |||
| 1394be5143 | |||
| 93442526f8 | |||
| d85402050b | |||
| b1c62cb525 | |||
| fae64a297a | |||
| 6e2682a9ce | |||
| 555049f09c | |||
| 712f7c3d35 | |||
| 7a51c211cd | |||
| c48189c1c4 | |||
| 9803fa1cfd | |||
| cf756f561a | |||
| a4021fedc3 | |||
| 31a36a9250 | |||
| 36fe349b70 | |||
| 3ef1cfd97c | |||
| 669feb45f1 | |||
| 85890520ab | |||
| 340016521e | |||
| ef523df42c | |||
| 5306e3bab1 | |||
| 72a49afd2b | |||
| 9b8edbb81e | |||
| a1554feb3f | |||
| 490410bf09 | |||
| 8c113f5268 | |||
| 075cbd5a0f | |||
| d82df2b431 | |||
| a09f8214d9 | |||
| 396e9c003e | |||
| b0c4a28be6 | |||
| 85325e4a31 | |||
| 9933dd3ec5 | |||
| 13532c8b4b | |||
| 3926797295 | |||
| febd3f784f | |||
| 61b053f0e1 | |||
| 8dae352ccc | |||
| e890c50da6 | |||
| ddd9f4d021 | |||
| 7e58b4baee | |||
| a21fbb9a4f | |||
| 3b7d27c919 | |||
| 68ddbfc0fe | |||
| a2047cb800 | |||
| fdd499146c | |||
| 37900341cf | |||
| 36bb368cad | |||
| f9bdb219d0 | |||
| 0374c14e42 | |||
| a035a151bd | |||
| e69966381d | |||
| 94dfb2b1f2 | |||
| 92011205be | |||
| c9707646bd | |||
| c50705736b | |||
| ec284c17f4 | |||
| ad6c52dc4c | |||
| 5f182febae | |||
| 86d82c1098 | |||
| 842b9004da | |||
| 6ac7ca4f0f | |||
| ddfcbe1bee | |||
| 88fd9388e4 | |||
| 69aafa53c9 | |||
| 3473fe9c15 | |||
| c655500045 | |||
| 96a8015af6 | |||
| ddd3876f92 | |||
| f1f34722ee |
@@ -1,2 +1,3 @@
|
||||
FROM qmcgaw/godevcontainer:v0.20-alpine
|
||||
RUN apk add wireguard-tools htop openssl
|
||||
FROM ghcr.io/qdm12/godevcontainer:v0.21-alpine
|
||||
RUN apk add wireguard-tools htop openssl tcpdump iptables
|
||||
RUN apk add nodejs npm && npm install markdownlint-cli2 --global && apk del npm
|
||||
|
||||
@@ -19,16 +19,16 @@ It works on Linux, Windows (WSL2) and OSX.
|
||||
mkdir -p ~/.ssh
|
||||
```
|
||||
|
||||
1. **For Docker on OSX**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||
1. **For OSX hosts**: ensure the project directory and your home directory `~` are accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
|
||||
1. Select `Dev-Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
For any customization to take effect, you should "rebuild and reopen":
|
||||
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P)
|
||||
2. Select `Dev Containers: Rebuild Container`
|
||||
2. Select `Dev-Containers: Rebuild Container`
|
||||
|
||||
Changes you can make are notably:
|
||||
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"ui.diagnostic.analyses": {
|
||||
"ST1000": false
|
||||
},
|
||||
"formatting.gofumpt": true,
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
|
||||
@@ -67,7 +67,6 @@ body:
|
||||
- VPNSecure.me
|
||||
- VPNUnlimited
|
||||
- VyprVPN
|
||||
- WeVPN
|
||||
- Windscribe
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
||||
|
||||
---
|
||||
|
||||
One of the following is required:
|
||||
Important notes:
|
||||
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
|
||||
## For Wireguard
|
||||
|
||||
Wireguard can be natively supported ONLY if:
|
||||
|
||||
- the `PrivateKey` field value is the same across all servers for one user account
|
||||
- the `Address` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `PublicKey` field value is:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||
- the same across all servers for one user account
|
||||
- the `Endpoint` field value:
|
||||
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||
- can be determined using a pattern, for example using country codes in hostnames
|
||||
|
||||
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||
|
||||
If **all** of these conditions are met, please provide an answer for each of them.
|
||||
|
||||
## For OpenVPN
|
||||
|
||||
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||
|
||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||
|
||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||
you can only use a custom Openvpn configuration file.
|
||||
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
||||
|
||||
+4
-2
@@ -23,6 +23,8 @@
|
||||
color: "959a9c"
|
||||
- name: "Closed: ☠️ cannot be done"
|
||||
color: "959a9c"
|
||||
- name: "Closed: 🤖🍺 drunk AI"
|
||||
color: "959a9c"
|
||||
|
||||
- name: "Priority: 🚨 Urgent"
|
||||
color: "03adfc"
|
||||
@@ -86,8 +88,6 @@
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Vyprvpn"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ WeVPN"
|
||||
color: "cfe8d4"
|
||||
- name: "☁️ Windscribe"
|
||||
color: "cfe8d4"
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Firewall ⛓️"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: MTU discovery 🔦"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: Routing 🛤️"
|
||||
color: "ffc7ea"
|
||||
- name: "Category: IPv6 🛰️"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Description
|
||||
|
||||
<!-- Please describe the reason for the changes being proposed. -->
|
||||
|
||||
# Issue
|
||||
|
||||
<!-- Please link to the issue(s) this change relates to. -->
|
||||
|
||||
# Assertions
|
||||
|
||||
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
|
||||
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
||||
+55
-17
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -45,6 +45,7 @@ jobs:
|
||||
level: error
|
||||
exclude: |
|
||||
./internal/storage/servers.json
|
||||
./.golangci.yml
|
||||
*.md
|
||||
|
||||
- name: Linting
|
||||
@@ -59,13 +60,46 @@ jobs:
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Verify dev cross platform compatibility
|
||||
run: docker build --target xcompile .
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
|
||||
verify-private:
|
||||
if: |
|
||||
github.repository == 'qdm12/gluetun' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
needs: [verify]
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- run: docker build -t qmcgaw/gluetun .
|
||||
|
||||
- name: Setup Go for CI utility
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ci/go.mod
|
||||
|
||||
- name: Build utility
|
||||
run: go build -C ./ci -o runner ./cmd/main.go
|
||||
|
||||
- name: Run Gluetun container with Mullvad configuration
|
||||
run: echo -e "${{ secrets.MULLVAD_WIREGUARD_PRIVATE_KEY }}\n${{ secrets.MULLVAD_WIREGUARD_ADDRESS }}" | ./ci/runner mullvad
|
||||
|
||||
- name: Run Gluetun container with ProtonVPN configuration
|
||||
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner protonvpn
|
||||
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -73,15 +107,15 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
- uses: github/codeql-action/init@v3
|
||||
go-version-file: go.mod
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
- uses: github/codeql-action/autobuild@v3
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
- uses: github/codeql-action/autobuild@v4
|
||||
- uses: github/codeql-action/analyze@v4
|
||||
|
||||
publish:
|
||||
if: |
|
||||
@@ -91,20 +125,20 @@ jobs:
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
needs: [verify, codeql]
|
||||
needs: [verify, verify-private, codeql]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
flavor: |
|
||||
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
@@ -119,15 +153,15 @@ jobs:
|
||||
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: qdm12
|
||||
@@ -135,10 +169,14 @@ jobs:
|
||||
|
||||
- name: Short commit
|
||||
id: shortcommit
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
run: |
|
||||
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
|
||||
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
SHORT_SHA=
|
||||
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -9,13 +9,17 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Closed issues are **NOT** monitored, so commenting here is likely to be not seen.
|
||||
If you think this is *still unresolved* and have **more information** to bring, please create another issue.
|
||||
Closed issues are **NOT** monitored, so commenting here will likely NOT be seen.
|
||||
If you think this is *still unresolved* and have **more information** to bring, please either
|
||||
re-open this issue or create another issue.
|
||||
|
||||
❤️😠 temporarily help the Gluetun community and fight the AI slop scam website `gluetun[dot]com` by setting `BORINGPOLL_GLUETUNCOM=on` on the latest image.
|
||||
See [the option in the wiki for more information](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/others.md)
|
||||
|
||||
This is an automated comment setup because @qdm12 is the sole maintainer of this project
|
||||
which became too popular to monitor issues closed.
|
||||
which became too popular to monitor closed issues.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"retryOn429": false,
|
||||
"fallbackRetryDelay": "30s",
|
||||
"aliveStatusCodes": [
|
||||
200
|
||||
200,
|
||||
429
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crazy-max/ghaction-github-labeler@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: crazy-max/ghaction-github-labeler@v6
|
||||
with:
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
@@ -18,12 +18,12 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v22
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint.json
|
||||
config: .markdownlint-cli2.jsonc
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
use-quiet-mode: yes
|
||||
config-file: .github/workflows/configs/mlc-config.json
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v4
|
||||
- uses: peter-evans/dockerhub-description@v5
|
||||
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||
with:
|
||||
username: qmcgaw
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Update servers list
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
provider:
|
||||
description: "VPN Provider to update"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- airvpn
|
||||
- cyberghost
|
||||
- expressvpn
|
||||
- fastestvpn
|
||||
- giganews
|
||||
- hidemyass
|
||||
- ipvanish
|
||||
- ivpn
|
||||
- mullvad
|
||||
- nordvpn
|
||||
- perfect privacy
|
||||
- privado
|
||||
- private internet access
|
||||
- privatevpn
|
||||
- protonvpn
|
||||
- purevpn
|
||||
- slickvpn
|
||||
- surfshark
|
||||
- torguard
|
||||
- vpnsecure
|
||||
- vpn unlimited
|
||||
- vyprvpn
|
||||
- windscribe
|
||||
schedule:
|
||||
- cron: "11 3 1 */2 *" # Run at 03:11 on the 1st of every 2nd month
|
||||
jobs:
|
||||
update-servers-list:
|
||||
if: github.repository == 'qdm12/gluetun'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Update servers list
|
||||
run: |
|
||||
SELECTED_PROVIDER="${{ github.event.inputs.provider || 'all' }}"
|
||||
|
||||
if [ "$SELECTED_PROVIDER" = "all" ]; then
|
||||
FLAGS="-all"
|
||||
else
|
||||
FLAGS="-providers $SELECTED_PROVIDER"
|
||||
fi
|
||||
|
||||
go run ./cmd/gluetun/main.go update $FLAGS \
|
||||
-maintainer \
|
||||
-proton-email "${{ secrets.PROTON_EMAIL }}" \
|
||||
-proton-password "${{ secrets.PROTON_PASSWORD }}"
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if git diff --exit-code internal/storage/servers.json >/dev/null; then
|
||||
echo "Error: internal/storage/servers.json was not modified."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check no other file changes
|
||||
run: |
|
||||
if ! git diff --exit-code --quiet ':!internal/storage/servers.json'; then
|
||||
echo "Error: Unexpected changes detected in files other than servers.json"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
id: createpr
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
branch-suffix: timestamp
|
||||
branch: bot/update-servers-list
|
||||
base: master
|
||||
delete-branch: true
|
||||
title: "feat(providers/${{ github.event.inputs.provider || 'all' }}): servers data update"
|
||||
body: |
|
||||
This PR was automatically generated by the [Update servers list](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow run.
|
||||
|
||||
# - name: Merge Pull Request
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# run: |
|
||||
# gh pr merge ${{ steps.createpr.outputs.pull-request-number }} --auto -m -d
|
||||
@@ -1 +1,2 @@
|
||||
scratch.txt
|
||||
.DS_Store
|
||||
|
||||
+72
-26
@@ -1,27 +1,78 @@
|
||||
linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
version: "2"
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- err113
|
||||
- containedctx
|
||||
- maintidx
|
||||
- path: "internal\\/server\\/.+\\.go"
|
||||
linters:
|
||||
- dupl
|
||||
- text: "returns interface \\(github\\.com\\/vishvananda\\/netlink\\.Link\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
- path: "internal\\/openvpn\\/pkcs8\\/descbc\\.go"
|
||||
text: "newCipherDESCBCBlock returns interface \\(github\\.com\\/youmark\\/pkcs8\\.Cipher\\)"
|
||||
linters:
|
||||
- ireturn
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
linters:
|
||||
settings:
|
||||
misspell:
|
||||
locale: US
|
||||
goconst:
|
||||
ignore-string-values:
|
||||
# commonly used settings strings
|
||||
- "^disabled$"
|
||||
# Firewall and routing strings
|
||||
- "^(ACCEPT|DROP)$"
|
||||
- "^--append$"
|
||||
- "^--delete$"
|
||||
- "^all$"
|
||||
- "^(tcp|udp)$"
|
||||
# Server route strings
|
||||
- "^/status$"
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- err113
|
||||
- maintidx
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
path: internal\/server\/.+\.go
|
||||
- linters:
|
||||
- ireturn
|
||||
text: returns interface \(golang\.org\/x\/sys\/unix\.Sockaddr\)
|
||||
- linters:
|
||||
- ireturn
|
||||
path: internal\/openvpn\/pkcs8\/descbc\.go
|
||||
text: newCipherDESCBCBlock returns interface \(github\.com\/youmark\/pkcs8\.Cipher\)
|
||||
- linters:
|
||||
- revive
|
||||
path: internal\/provider\/(common|utils)\/.+\.go
|
||||
text: "var-naming: avoid (bad|meaningless) package names"
|
||||
- linters:
|
||||
- lll
|
||||
source: "^// https://.+$"
|
||||
- linters:
|
||||
- mnd
|
||||
source: "^ cleanups\\.Add.+$"
|
||||
path: internal\/(wireguard|amneziawg)\/run\.go
|
||||
- linters:
|
||||
- err113
|
||||
- mnd
|
||||
path: ci\/.+\.go
|
||||
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
enable:
|
||||
# - cyclop
|
||||
# - errorlint
|
||||
@@ -29,7 +80,6 @@ linters:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- containedctx
|
||||
- copyloopvar
|
||||
- decorder
|
||||
@@ -43,7 +93,6 @@ linters:
|
||||
- exhaustive
|
||||
- fatcontext
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
@@ -52,9 +101,7 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
@@ -87,7 +134,6 @@ linters:
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tagalign
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
},
|
||||
"ignores": [
|
||||
".github/pull_request_template.md"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"MD013": false
|
||||
}
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Update a VPN provider servers data",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "cmd/gluetun/main.go",
|
||||
"args": [
|
||||
"update",
|
||||
"${input:updateMode}",
|
||||
"-providers",
|
||||
"${input:provider}"
|
||||
],
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "provider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a provider (or comma separated list of providers)",
|
||||
},
|
||||
{
|
||||
"id": "updateMode",
|
||||
"type": "pickString",
|
||||
"description": "Update mode to use",
|
||||
"options": [
|
||||
"-maintainer",
|
||||
"-enduser"
|
||||
],
|
||||
"default": "-maintainer"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Update a VPN provider servers data",
|
||||
"type": "shell",
|
||||
"command": "go",
|
||||
"args": [
|
||||
"run",
|
||||
"./cmd/gluetun/main.go",
|
||||
"update",
|
||||
"${input:updateMode}",
|
||||
"-providers",
|
||||
"${input:provider}"
|
||||
],
|
||||
},
|
||||
{
|
||||
"label": "Add a Gluetun Github Git remote",
|
||||
"type": "shell",
|
||||
"command": "git",
|
||||
"args": [
|
||||
"remote",
|
||||
"add",
|
||||
"${input:githubRemoteUsername}",
|
||||
"git@github.com:${input:githubRemoteUsername}/gluetun.git"
|
||||
],
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "provider",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a provider (or comma separated list of providers)",
|
||||
},
|
||||
{
|
||||
"id": "updateMode",
|
||||
"type": "pickString",
|
||||
"description": "Update mode to use",
|
||||
"options": [
|
||||
"-maintainer",
|
||||
"-enduser"
|
||||
],
|
||||
"default": "-maintainer"
|
||||
},
|
||||
{
|
||||
"id": "githubRemoteUsername",
|
||||
"type": "promptString",
|
||||
"description": "Please enter a Github username",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# AGENTS
|
||||
|
||||
Guidance for coding agents working in this repository.
|
||||
|
||||
## Scope and priorities
|
||||
|
||||
- Keep changes minimal and targeted. Feel free to do light refactors that are relevant to the modifications.
|
||||
- Breaking changes:
|
||||
- Do not introduce breaking usage behavior (cli flags, environment variables, etc.) unless explicitly agreed.
|
||||
- Do not introduce breaking changes for the Go API in the `pkg/` directory.
|
||||
- If a compatibility break seems beneficial, stop and ask for confirmation before implementing it.
|
||||
- Update or add tests when behavior changes.
|
||||
|
||||
## Go coding conventions
|
||||
|
||||
### General guidelines
|
||||
|
||||
- Use explicit, descriptive variable names by default.
|
||||
- Notable bad examples: `req`, `resp`, `cfg`, `v`
|
||||
- Allowed short-name exceptions:
|
||||
- indexes such as `i`, `j`
|
||||
- `ctx` for `context.Context`
|
||||
- `t` for `*testing.T` and `b` for `*testing.B`
|
||||
- `ctrl` for `*gomock.Controller`
|
||||
- `err` for `error`, `errs` for `[]error`
|
||||
- `wg` for `*sync.WaitGroup`
|
||||
- Avoid using global variables except for:
|
||||
- exported sentinel errors that are used outside the package boundaries
|
||||
- regular expressions defined with `regexp.MustCompile`
|
||||
- variables set by the build pipeline, such as `Version` and `BuildDate`
|
||||
- Constants
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Each one should be defined right above where it's used, instead of having multiple defined at the same place in a `const ()` block
|
||||
- If one is only used in a single production code function, define it right above it so it's more local for readability.
|
||||
- Do not define constants when constants exist in other packages, for example `http.StatusBadRequest` or `log.LevelDebug`.
|
||||
- Structs
|
||||
- Prefer defining them inline in a function if it's only used in that function, rather than at the package level.
|
||||
- Do not use the short if form, prefer the longer one
|
||||
- Follow modern Go, according to the Go version defined in go.mod. Prefer modern constructs when equivalent:
|
||||
- Example: use `for i := range 5` rather than `for i := 0; i < 5; i++`.
|
||||
- Example: use `new("string")` rather than helper wrappers such as `stringPtr("string")`.
|
||||
- Example: no need to pin variables in for loops when using them in goroutines or subtests.
|
||||
- Use `New(...) *Item` constructor per package. Each package should ideally only have one constructor, although this is not a strict rule. The constructor should return a pointer to the struct, and not an interface.
|
||||
- Always prefer using context-aware functions, for example:
|
||||
- `exec.CommandContext` rather than `exec.Command`
|
||||
- `http.NewRequestWithContext` rather than `http.NewRequest`
|
||||
- Never export a symbol unless absolutely necessary.
|
||||
- Always use the most restrictive builtin types. For example prefer `uint` over `int` if it's only zero or positive. Prefer `uint16` is the max value is 65535.
|
||||
- Prefer using builtin types whenever possible AND do not define single field structs unless necessary
|
||||
- Prefer splitting a code line only when it triggers the `lll` linter, do not split a command or arguments list for each element
|
||||
- Use `netip` types instead of `net` types whenever possible
|
||||
- Use constants instead of variables whenever possible, especially function-local inline constants.
|
||||
- Do not use `time.Sleep`, prefer using a `time.Timer` with a `select` statement also listening on a context cancelation
|
||||
- `panic`:
|
||||
- should only be used when a programming error is encountered and you should NOT return errors for programming errors (such as passing nil objects)
|
||||
- Its counterpart `recover` should not really be used, except for testing a panic in test code (or use `assert.PanicsWithValue`).
|
||||
|
||||
### Directory structure and file naming
|
||||
|
||||
- Executable main packages with a single `main()` function must be in the `cmd` directory.
|
||||
Prefer having top level logic and have a longer `main()` function rather than having an `internal/app` package.
|
||||
- Code lives by default in subpackages within the `internal` directory
|
||||
- Code needing to be imported by external Go modules must be in subpackages within the `pkg` directory
|
||||
- Example code especially using the `pkg` directory must be in `main` packages within the `examples` directory, each with a single `main.go` function.
|
||||
- If AND only if the repository is a Go library and not a Go application, you may have Go files at the root of the project to simplify import paths. Most of the code should still be in subpackages in the `internal` directory.
|
||||
- Interfaces should be defined in `interfaces.go` files for each package. If there are unexported interfaces which need to be mocked, which is rare, they should be defined in `interfaces_local.go` files.
|
||||
- Mock files are
|
||||
- `mocks_generate_test.go` which only contains `//go:generate` directives for generating mocks, and no actual code
|
||||
- `mocks_test.go` which contains the generated mocks from exported interfaces and no other code, and is ignored in coverage reports
|
||||
- `mocks_local_test.go` (rare) which contains the generated mocks from unexported interfaces and no other code, and is ignored in coverage reports
|
||||
- NEVER generate an exported mock in a non test file, prefer re-generating files across packages.
|
||||
- Package naming
|
||||
- Your package name should be the same as the directory containing it, **except for the `main` package**
|
||||
- Use single words for package names
|
||||
- Do not use generic names for package names such as `utils` or `helpers`
|
||||
- Package nesting
|
||||
- Try to avoid nesting packages by default
|
||||
- You can nest packages if you have different implementations for the same interface (e.g. a store interface)
|
||||
- You can nest packages if you start having a lot of Go files (more than 10) and it really does make sense to make subpackages
|
||||
|
||||
### Linting
|
||||
|
||||
The linter is `golangci-lint` with the configuration defined in `.golangci.yml`.
|
||||
|
||||
To exclude code from linting, prefer using, when absolutely necessary, command comments `//nolint:<linter>`.
|
||||
This allows the `nolintlint` linter to detect and report unnecessary `//nolint` comments later.
|
||||
You can notably use `//nolint:lll` and, for good valid reasons, `//nolint:gosec`. Sometimes `//nolint:mnd` when it just doesn't make sense to extract a constant such as `n = n << 4`
|
||||
Always prefer placing `//nolint` comments on the same code line where the error comes from, and not above a code block.
|
||||
|
||||
### Mocking
|
||||
|
||||
Mocking works with the `go.uber.org/mock` library, and the `mockgen` tool.
|
||||
|
||||
- Mocks from exported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Mocks from unexported interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_local_test.go` files. The source file for unexported interfaces is `interfaces_local.go`. The go generate command is similar to:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_local_test.go -package $GOPACKAGE -source interfaces_local.go
|
||||
```
|
||||
|
||||
- Mocks from external interfaces are generated using go generate commands in `mocks_generate_test.go` files, and stored in `mocks_<package-name>_test.go` files, using:
|
||||
|
||||
```go
|
||||
//go:generate mockgen -destination=mocks_<package-name>_test.go -package $GOPACKAGE module-name InterfaceA,InterfaceB
|
||||
```
|
||||
|
||||
- Generated mocks usage in tests:
|
||||
- Define mocks in the subtest, not in the parent test. You can also have a function returning the mocks as a field of the test case struct, which takes in the subtest `*testing.T` as argument, and call it in the subtest to get the mocks.
|
||||
- **Never** use `gomock.Any()` as argument. Always use concrete, precise arguments. You might need to define a custom GoMock matcher for your argument in some very niche and corner cases.
|
||||
- **Never** use `.AnyTimes()` on mocks. Always define the number of times a certain mock call should be called, with `.Times(3)` for example.
|
||||
- **Always** set the `.Return(...)` on the mock if the function returns something.
|
||||
- Avoid using **mock helpers** functions, prefer a bit of repetition than tight coupling and dependency
|
||||
|
||||
### main.go
|
||||
|
||||
- Make the program OS signal aware, so it attempts a graceful shutdown when interruped. Force quit the program on a second interrupt signal.
|
||||
|
||||
### Formatting
|
||||
|
||||
The Go formatter used is gofumpt.
|
||||
|
||||
### Errors
|
||||
|
||||
- Always prefer wrapping errors with some context with `fmt.Errorf("doing this: %w", err)`
|
||||
- In rare cases, you can just use `return err` notably:
|
||||
- If the function is called **recursively**, since we don't wrap the wrapping multiple times for each recursion
|
||||
- If the current function only statement is the call to another function, for example:
|
||||
|
||||
```go
|
||||
func (s *Struct) Fetch() error {
|
||||
return fetch() // do not wrap the error
|
||||
}
|
||||
```
|
||||
|
||||
- When wrapping errors, use verbs ending in "ing" and no "failed to" or "cannot" to avoid redundancy. For example, use `fmt.Errorf("resolving host: %w", err)` rather than `fmt.Errorf("failed to resolve host: %w", err)`.
|
||||
- When wrapping an error, the context should NEVER contain variables injected as arguments in the function returning an error, to avoid repeating the same variable in multiple error messages.
|
||||
- Testing errors:
|
||||
- If the error does not wrap a sentinel error, use `assert.ErrorContains` to check for error messages, rather than `assert.EqualError`, to avoid having to update tests for minor changes in error messages. And use `assert.NoError` to check for no error.
|
||||
- If the error wraps a sentinel error, use `assert.ErrorIs` to check both for the sentinel error or an expected nil error. You can also check the error message with `assert.ErrorContains`
|
||||
|
||||
### User program settings
|
||||
|
||||
- For configuration structs, each field Go zero value (i.e. `0` for `int`, `nil` for `*string`) should be an INVALID value in the user sense. This is used to detect when a field is not set, in order to default it, merge it or override it. For example if `""` is not a valid value, the field should be of type `string`. Conversely, if `""` is a valid value, the field should be of type `*string` to distinguish between "not set" and "set to empty string". Notably, boolean fields are ALWAYS of type `*bool` for this reason, since both `true` and `false` are valid values.
|
||||
- Configuration reading and handling relies on the Go library github.com/qdm12/gosettings please use it whenever appropriate.
|
||||
- Do not wrap errors coming from `reader.Reader` methods, since they already contain the necessary context.
|
||||
- All keys passed to `reader.Reader` methods must be in environment variable format, i.e. uppercase with underscores. These get converted to lowercase and dashes for flags notably.
|
||||
- For each settings structs, define the following methods, which are usually unexported, but can be exported especially for the top level Settings struct, in this order:
|
||||
- `func (s *Settings) setDefaults()` whichs sets defaults (using `gosettings.Default*` functions) on unset fields
|
||||
- If the settings need to be patched at runtime, which is rarely the case, define `func (s *Settings) overrideWith(other Settings)` which overrides the settings with another settings struct, only for fields that are set in the other struct (using `gosettings.OverrideWith` functions).
|
||||
- `func (s Settings) validate() error` which validates the settings, and returns an error if anything is invalid
|
||||
- `func (s *Settings) read(r *reader.Reader) error` which reads the settings from a gosettings/reader.Reader (which can be from multiple sources, such as environment variables, cli flags, config files etc.)
|
||||
- `func (s Settings) String() string` which uses `toLinesNode().String()` to return a string representation of the settings
|
||||
- `func (s Settings) toLinesNode() *gotree.Node` which a github.com/qdm12/gotree `*Node` representing the settings
|
||||
|
||||
### Testing
|
||||
|
||||
- Use the github.com/stretchr/testify library for assertions
|
||||
- Most tests should be table tests with parallel subtests
|
||||
- Prefer map-based table tests of the form `map[string]struct{ ... }`, with the key as the test name.
|
||||
Use underscores in test names, not spaces, to keep `go test` output searchable.
|
||||
- Use `testCases` for the table variable name, and `testCase` for each iterated case value.
|
||||
- Run all tests in parallel:
|
||||
- call `t.Parallel()` in the top-level test
|
||||
- call `t.Parallel()` in each subtest
|
||||
|
||||
### Libraries to use
|
||||
|
||||
- Logging: `github.com/qdm12/log`
|
||||
- Splash information at program start: `github.com/qdm12/gosplash`
|
||||
- Long running services (i.e. health server, http prod server, backup loop etc.): `github.com/qdm12/goservices`
|
||||
- String tree structures: `github.com/qdm12/gotree`
|
||||
|
||||
### Extra rules
|
||||
|
||||
- Do not use `http.DefaultClient`, use a custom `*http.Client` with a fixed timeout and share with dependency injections.
|
||||
- Do not check for injected dependencies being `nil`, prefer to just panic on a nil pointer. By default it's fine to panic if a developer injects a dependency `nil`. `nil` does not mean use a default.
|
||||
|
||||
## Validation checklist
|
||||
|
||||
Run the following before finishing changes:
|
||||
|
||||
1. Go building `go build ./...`
|
||||
1. Go linting `golangci-lint run`
|
||||
1. Go unit tests `go test ./...`
|
||||
1. If a module is added or modified, run `go mod tidy`
|
||||
1. If an interface or mock command is modified, run `go generate -run mockgen ./...`
|
||||
|
||||
If a Markdown file is modified and `markdownlint-cli2` is available, run `markdownlint-cli2 "**/*.md"`
|
||||
|
||||
If a command is unavailable in the current environment, report it clearly and provide the exact command needed once available.
|
||||
+97
-31
@@ -1,19 +1,19 @@
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GO_ALPINE_VERSION=3.20
|
||||
ARG GO_VERSION=1.23
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.61.0
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG GO_ALPINE_VERSION=3.23
|
||||
ARG GO_VERSION=1.25
|
||||
ARG XCPUTRANSLATE_VERSION=v0.9.0
|
||||
ARG GOLANGCI_LINT_VERSION=v2.4.0
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM --platform=${BUILDPLATFORM} ghcr.io/qdm12/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${GO_ALPINE_VERSION} AS base
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
|
||||
RUN apk --update add git g++ findutils
|
||||
RUN apk --update add git g++ findutils iptables
|
||||
ENV CGO_ENABLED=0
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
COPY --from=mockgen /bin /go/bin/mockgen
|
||||
@@ -32,7 +32,7 @@ ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=a
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
RUN golangci-lint run
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS mocks
|
||||
RUN git init && \
|
||||
@@ -46,6 +46,10 @@ RUN git init && \
|
||||
git diff --exit-code && \
|
||||
rm -rf .git/
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS xcompile
|
||||
RUN GOOS=darwin go build -o /dev/null ./...
|
||||
RUN GOOS=windows go build -o /dev/null ./...
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG VERSION=unknown
|
||||
@@ -106,8 +110,66 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||
WIREGUARD_ADDRESSES= \
|
||||
WIREGUARD_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||
WIREGUARD_MTU=1400 \
|
||||
WIREGUARD_MTU= \
|
||||
WIREGUARD_IMPLEMENTATION=auto \
|
||||
# Amnezia
|
||||
AMNEZIAWG_ENDPOINT_IP= \
|
||||
AMNEZIAWG_ENDPOINT_PORT= \
|
||||
AMNEZIAWG_CONF_SECRETFILE=/run/secrets/wg0.conf \
|
||||
AMNEZIAWG_PRIVATE_KEY= \
|
||||
AMNEZIAWG_PRIVATE_KEY_SECRETFILE=/run/secrets/wireguard_private_key \
|
||||
AMNEZIAWG_PRESHARED_KEY= \
|
||||
AMNEZIAWG_PRESHARED_KEY_SECRETFILE=/run/secrets/wireguard_preshared_key \
|
||||
AMNEZIAWG_PUBLIC_KEY= \
|
||||
AMNEZIAWG_ALLOWED_IPS= \
|
||||
AMNEZIAWG_PERSISTENT_KEEPALIVE_INTERVAL=0 \
|
||||
AMNEZIAWG_ADDRESSES= \
|
||||
AMNEZIAWG_ADDRESSES_SECRETFILE=/run/secrets/wireguard_addresses \
|
||||
AMNEZIAWG_MTU= \
|
||||
AMNEZIAWG_JC=0 \
|
||||
AMNEZIAWG_JMIN=0 \
|
||||
AMNEZIAWG_JMAX=0 \
|
||||
AMNEZIAWG_S1=0 \
|
||||
AMNEZIAWG_S2=0 \
|
||||
AMNEZIAWG_S3=0 \
|
||||
AMNEZIAWG_S4=0 \
|
||||
AMNEZIAWG_H1= \
|
||||
AMNEZIAWG_H2= \
|
||||
AMNEZIAWG_H3= \
|
||||
AMNEZIAWG_H4= \
|
||||
AMNEZIAWG_I1= \
|
||||
AMNEZIAWG_I2= \
|
||||
AMNEZIAWG_I3= \
|
||||
AMNEZIAWG_I4= \
|
||||
AMNEZIAWG_I5= \
|
||||
# Wireguard AmneziaWG userspace obfuscation (requires WIREGUARD_IMPLEMENTATION=amneziawg)
|
||||
AMNEZIAWG_JC=0 \
|
||||
AMNEZIAWG_JMIN=0 \
|
||||
AMNEZIAWG_JMAX=0 \
|
||||
AMNEZIAWG_S1=0 \
|
||||
AMNEZIAWG_S2=0 \
|
||||
AMNEZIAWG_S3=0 \
|
||||
AMNEZIAWG_S4=0 \
|
||||
AMNEZIAWG_H1= \
|
||||
AMNEZIAWG_H2= \
|
||||
AMNEZIAWG_H3= \
|
||||
AMNEZIAWG_H4= \
|
||||
AMNEZIAWG_I1= \
|
||||
AMNEZIAWG_I2= \
|
||||
AMNEZIAWG_I3= \
|
||||
AMNEZIAWG_I4= \
|
||||
AMNEZIAWG_I5= \
|
||||
# VPN server port forwarding
|
||||
VPN_PORT_FORWARDING=off \
|
||||
VPN_PORT_FORWARDING_PROVIDER= \
|
||||
VPN_PORT_FORWARDING_UP_COMMAND= \
|
||||
VPN_PORT_FORWARDING_DOWN_COMMAND= \
|
||||
VPN_PORT_FORWARDING_LISTENING_PORTS=0 \
|
||||
VPN_PORT_FORWARDING_PORTS_COUNT=1 \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
# PMTUD
|
||||
PMTUD_ICMP_ADDRESSES=1.1.1.1,8.8.8.8 \
|
||||
PMTUD_TCP_ADDRESSES=1.1.1.1:443,8.8.8.8:443,1.1.1.1:53,8.8.8.8:53,[2606:4700:4700::1111]:53,[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:443,[2001:4860:4860::8888]:443 \
|
||||
# VPN server filtering
|
||||
SERVER_REGIONS= \
|
||||
SERVER_COUNTRIES= \
|
||||
@@ -119,10 +181,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
OWNED_ONLY=no \
|
||||
# # Private Internet Access only:
|
||||
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
||||
VPN_PORT_FORWARDING=off \
|
||||
VPN_PORT_FORWARDING_LISTENING_PORT=0 \
|
||||
VPN_PORT_FORWARDING_PROVIDER= \
|
||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||
VPN_PORT_FORWARDING_USERNAME= \
|
||||
VPN_PORT_FORWARDING_PASSWORD= \
|
||||
# # Cyberghost only:
|
||||
@@ -156,28 +214,32 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
FIREWALL_VPN_INPUT_PORTS= \
|
||||
FIREWALL_INPUT_PORTS= \
|
||||
FIREWALL_OUTBOUND_SUBNETS= \
|
||||
FIREWALL_DEBUG=off \
|
||||
FIREWALL_IPTABLES_LOG_LEVEL=info \
|
||||
# IPv6
|
||||
IPV6_CHECK_ADDRESSES=[2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53 \
|
||||
# Logging
|
||||
LOG_LEVEL=info \
|
||||
# Health
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||
HEALTH_SUCCESS_WAIT_DURATION=5s \
|
||||
HEALTH_VPN_DURATION_INITIAL=6s \
|
||||
HEALTH_VPN_DURATION_ADDITION=5s \
|
||||
# DNS over TLS
|
||||
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_CACHING=on \
|
||||
DOT_IPV6=off \
|
||||
HEALTH_TARGET_ADDRESSES=cloudflare.com:443,github.com:443 \
|
||||
HEALTH_ICMP_TARGET_IPS=1.1.1.1,8.8.8.8 \
|
||||
HEALTH_SMALL_CHECK_TYPE=icmp \
|
||||
HEALTH_RESTART_VPN=on \
|
||||
# DNS
|
||||
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||
# Note: DNS_UPSTREAM_RESOLVERS defaults to cloudflare in code if DNS_UPSTREAM_PLAIN_ADDRESSES is empty
|
||||
DNS_UPSTREAM_RESOLVERS= \
|
||||
DNS_BLOCK_IPS= \
|
||||
DNS_BLOCK_IP_PREFIXES= \
|
||||
DNS_CACHING=on \
|
||||
DNS_UPSTREAM_IPV6=off \
|
||||
BLOCK_MALICIOUS=on \
|
||||
BLOCK_SURVEILLANCE=off \
|
||||
BLOCK_ADS=off \
|
||||
UNBLOCK= \
|
||||
DNS_UNBLOCK_HOSTNAMES= \
|
||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||
DNS_UPDATE_PERIOD=24h \
|
||||
DNS_ADDRESS=127.0.0.1 \
|
||||
DNS_KEEP_NAMESERVER=off \
|
||||
DNS_UPSTREAM_PLAIN_ADDRESSES= \
|
||||
# HTTP proxy
|
||||
HTTPPROXY= \
|
||||
HTTPPROXY_LOG=off \
|
||||
@@ -198,10 +260,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH=/gluetun/auth/config.toml \
|
||||
HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE="{}" \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
UPDATER_MIN_RATIO=0.8 \
|
||||
UPDATER_VPN_SERVICE_PROVIDERS= \
|
||||
UPDATER_PROTONVPN_EMAIL= \
|
||||
UPDATER_PROTONVPN_PASSWORD= \
|
||||
# Public IP
|
||||
PUBLICIP_FILE="/tmp/gluetun/ip" \
|
||||
PUBLICIP_ENABLED=on \
|
||||
@@ -216,9 +281,10 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
PPROF_HTTP_SERVER_ADDRESS=":6060" \
|
||||
# Extras
|
||||
VERSION_INFORMATION=on \
|
||||
BORINGPOLL_GLUETUNCOM=off \
|
||||
TZ= \
|
||||
PUID= \
|
||||
PGID=
|
||||
PUID=1000 \
|
||||
PGID=1000
|
||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Gluetun VPN client
|
||||
|
||||
Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
|
||||
|
||||
💁 You can optionally set `BORINGPOLL_GLUETUNCOM=on` to... [poll](./internal/boringpoll/boringpoll.go) that **scammy AI slop** website every few minutes so it costs them too much to keep it up. My gentle email reminders to take it down are being grossly ignored 🤷 This would make me very happy and serve this community.
|
||||
|
||||
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
|
||||

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

|
||||

|
||||

|
||||
@@ -56,14 +59,15 @@ 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**, **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
|
||||
- Based on Alpine 3.23 for a small Docker image of 43.1MB
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **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**
|
||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited**, **VyprVPN** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||
- Supports AmneziaWG only with the custom provider for now
|
||||
- DNS over TLS baked in with service provider(s) of your choice
|
||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
||||
- Choose the vpn network protocol, `udp` or `tcp`
|
||||
@@ -88,7 +92,7 @@ Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||
Here's a docker-compose.yml for the laziest:
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
---
|
||||
services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun
|
||||
@@ -124,6 +128,10 @@ services:
|
||||
|
||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||
|
||||
## Fun graphs
|
||||
|
||||
[](https://www.star-history.com/#qdm12/gluetun&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/qdm12/gluetun/ci/internal"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := log.New()
|
||||
if len(os.Args) < 2 {
|
||||
logger.Error("Usage: " + os.Args[0] + " <command>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "mullvad":
|
||||
err = internal.MullvadTest(ctx, logger)
|
||||
case "protonvpn":
|
||||
err = internal.ProtonVPNTest(ctx, logger)
|
||||
default:
|
||||
err = fmt.Errorf("unknown command: %s", os.Args[1])
|
||||
}
|
||||
stop()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info("test completed successfully")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module github.com/qdm12/gluetun/ci
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/qdm12/log v0.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
@@ -0,0 +1,27 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func MullvadTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"Wireguard private key",
|
||||
"Wireguard address",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=mullvad",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=USA",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
||||
"WIREGUARD_ADDRESSES=" + secrets[1],
|
||||
}
|
||||
return simpleTest(ctx, env, logger)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ProtonVPNTest(ctx context.Context, logger Logger) error {
|
||||
expectedSecrets := []string{
|
||||
"Wireguard private key",
|
||||
}
|
||||
secrets, err := readSecrets(ctx, expectedSecrets, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading secrets: %w", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"VPN_SERVICE_PROVIDER=protonvpn",
|
||||
"VPN_TYPE=wireguard",
|
||||
"LOG_LEVEL=debug",
|
||||
"SERVER_COUNTRIES=United States",
|
||||
"WIREGUARD_PRIVATE_KEY=" + secrets[0],
|
||||
}
|
||||
return simpleTest(ctx, env, logger)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Info(msg string)
|
||||
Infof(format string, args ...any)
|
||||
}
|
||||
|
||||
func readSecrets(ctx context.Context, expectedSecrets []string,
|
||||
logger Logger,
|
||||
) (lines []string, err error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
lines = make([]string, 0, len(expectedSecrets))
|
||||
|
||||
for i := range expectedSecrets {
|
||||
logger.Infof("🤫 reading %s from Stdin...", expectedSecrets[i])
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
lines = append(lines, strings.TrimSpace(scanner.Text()))
|
||||
logger.Infof("🤫 %s secret read successfully", expectedSecrets[i])
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("reading secrets from stdin: %w", err)
|
||||
}
|
||||
|
||||
if len(lines) < len(expectedSecrets) {
|
||||
return nil, fmt.Errorf("expected %d secrets via Stdin, but only received %d",
|
||||
len(expectedSecrets), len(lines))
|
||||
}
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
return nil, fmt.Errorf("secret on line %d/%d was empty", i+1, len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
func simpleTest(ctx context.Context, env []string, logger Logger) error {
|
||||
const timeout = 60 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Docker client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
config := &container.Config{
|
||||
Image: "qmcgaw/gluetun",
|
||||
StopTimeout: ptrTo(3),
|
||||
Env: env,
|
||||
}
|
||||
hostConfig := &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
||||
}
|
||||
networkConfig := (*network.NetworkingConfig)(nil)
|
||||
platform := (*v1.Platform)(nil)
|
||||
const containerName = "" // auto-generated name
|
||||
|
||||
response, err := client.ContainerCreate(ctx, config, hostConfig, networkConfig, platform, containerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating container: %w", err)
|
||||
}
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Println("Warning during container creation:", warning)
|
||||
}
|
||||
containerID := response.ID
|
||||
defer stopContainer(client, containerID)
|
||||
|
||||
beforeStartTime := time.Now()
|
||||
|
||||
err = client.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting container: %w", err)
|
||||
}
|
||||
|
||||
return waitForLogLine(ctx, client, containerID, beforeStartTime, logger)
|
||||
}
|
||||
|
||||
func stopContainer(client *client.Client, containerID string) {
|
||||
const stopTimeout = 5 * time.Second // must be higher than 3s, see above [container.Config]'s StopTimeout field
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||
defer stopCancel()
|
||||
|
||||
err := client.ContainerStop(stopCtx, containerID, container.StopOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("failed to stop container:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var successRegexp = regexp.MustCompile(`^.+Public IP address is .+$`)
|
||||
|
||||
func waitForLogLine(ctx context.Context, client *client.Client, containerID string,
|
||||
beforeStartTime time.Time, logger Logger,
|
||||
) error {
|
||||
logOptions := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
Since: beforeStartTime.Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
reader, err := client.ContainerLogs(ctx, containerID, logOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting container logs: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var linesSeen []string
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for ctx.Err() == nil {
|
||||
if scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) > 8 { // remove Docker log prefix
|
||||
line = line[8:]
|
||||
}
|
||||
linesSeen = append(linesSeen, line)
|
||||
if successRegexp.MatchString(line) {
|
||||
fmt.Println("✅ Success line logged")
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := scanner.Err()
|
||||
if err != nil && err != io.EOF {
|
||||
logSeenLines(logger, linesSeen)
|
||||
return fmt.Errorf("reading log stream: %w", err)
|
||||
}
|
||||
|
||||
// The scanner is either done or cannot read because of EOF
|
||||
logger.Info("the log scanner stopped")
|
||||
logSeenLines(logger, linesSeen)
|
||||
|
||||
// Check if the container is still running
|
||||
inspect, err := client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspecting container: %w", err)
|
||||
}
|
||||
if !inspect.State.Running {
|
||||
return fmt.Errorf("container stopped unexpectedly while waiting for log line. Exit code: %d", inspect.State.ExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func logSeenLines(logger Logger, lines []string) {
|
||||
fmt.Println("Logs seen so far:")
|
||||
for _, line := range lines {
|
||||
fmt.Println(" " + line)
|
||||
}
|
||||
}
|
||||
+120
-53
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -15,13 +16,17 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/alpine"
|
||||
"github.com/qdm12/gluetun/internal/boringpoll"
|
||||
"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"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
copenvpn "github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/dns"
|
||||
"github.com/qdm12/gluetun/internal/firewall"
|
||||
"github.com/qdm12/gluetun/internal/healthcheck"
|
||||
@@ -98,11 +103,13 @@ func main() {
|
||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||
}()
|
||||
|
||||
// Wait for OS signal or run error
|
||||
var err error
|
||||
select {
|
||||
case signal := <-signalCh:
|
||||
case receivedSignal := <-signalCh:
|
||||
signal.Stop(signalCh)
|
||||
fmt.Println("")
|
||||
logger.Warn("Caught OS signal " + signal.String() + ", shutting down")
|
||||
logger.Warn("Caught OS signal " + receivedSignal.String() + ", shutting down")
|
||||
cancel()
|
||||
case err = <-errorCh:
|
||||
close(errorCh)
|
||||
@@ -113,15 +120,14 @@ func main() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Shutdown timed sequence, and force exit on second OS signal
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case shutdownErr := <-errorCh:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timer.Stop()
|
||||
if shutdownErr != nil {
|
||||
logger.Warnf("Shutdown not completed gracefully: %s", shutdownErr)
|
||||
logger.Warnf("Shutdown failed: %s", shutdownErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -133,9 +139,6 @@ func main() {
|
||||
case <-timer.C:
|
||||
logger.Warn("Shutdown timed out")
|
||||
os.Exit(1)
|
||||
case signal := <-signalCh:
|
||||
logger.Warn("Caught OS signal " + signal.String() + ", forcing shut down")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +169,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
}
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z")
|
||||
defer fmt.Println(gluetunLogo)
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2026-04-30T00:00:00Z")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -177,7 +182,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
Created: buildInfo.Created,
|
||||
Announcement: "All control server routes will become private by default after the v3.41.0 release",
|
||||
Announcement: "Set BORINGPOLL_GLUETUNCOM=on to help combat AI slop and shutdown that scam website",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
@@ -205,9 +210,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
netLinker.PatchLoggerLevel(logLevel)
|
||||
|
||||
routingLogger := logger.New(log.SetComponent("routing"))
|
||||
if *allSettings.Firewall.Debug { // To remove in v4
|
||||
routingLogger.Patch(log.SetLevel(log.LevelDebug))
|
||||
}
|
||||
routingConf := routing.New(netLinker, routingLogger)
|
||||
|
||||
defaultRoutes, err := routingConf.DefaultRoutes()
|
||||
@@ -220,11 +222,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return err
|
||||
}
|
||||
|
||||
iptablesLogLevel, _ := log.ParseLevel(allSettings.Firewall.Iptables.LogLevel)
|
||||
iptablesLogger := logger.New(log.SetComponent("iptables"), log.SetLevel(iptablesLogLevel))
|
||||
|
||||
firewallLogger := logger.New(log.SetComponent("firewall"))
|
||||
if *allSettings.Firewall.Debug { // To remove in v4
|
||||
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
|
||||
}
|
||||
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
|
||||
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, iptablesLogger, cmder,
|
||||
defaultRoutes, localNetworks)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,6 +237,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = netLinker.FlushConntrack()
|
||||
if err != nil {
|
||||
logger.Warnf("flushing conntrack failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO run this in a loop or in openvpn to reload from file without restarting
|
||||
@@ -244,10 +250,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return err
|
||||
}
|
||||
|
||||
ipv6Supported, err := netLinker.IsIPv6Supported()
|
||||
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx,
|
||||
allSettings.IPv6.CheckAddresses, firewallConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
|
||||
ipv6SupportLevel == netlink.IPv6Internet
|
||||
|
||||
err = allSettings.Validate(storage, ipv6Supported, logger)
|
||||
if err != nil {
|
||||
@@ -262,19 +271,22 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||
|
||||
const clientTimeout = 15 * time.Second
|
||||
const clientTimeout = 35 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
// Create configurators
|
||||
alpineConf := alpine.New()
|
||||
ovpnConf := openvpn.New(
|
||||
logger.New(log.SetComponent("openvpn configurator")),
|
||||
cmder, puid, pgid)
|
||||
ovpnVersion := ovpnConf.Version26
|
||||
if allSettings.VPN.OpenVPN.Version == copenvpn.Openvpn25 {
|
||||
ovpnVersion = ovpnConf.Version25
|
||||
}
|
||||
|
||||
err = printVersions(ctx, logger, []printVersionElement{
|
||||
{name: "Alpine", getVersion: alpineConf.Version},
|
||||
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
|
||||
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
|
||||
{name: "IPtables", getVersion: firewallConf.Version},
|
||||
{name: "OpenVPN", getVersion: ovpnVersion},
|
||||
{name: "Firewall", getVersion: firewallConf.Version},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -382,7 +394,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||
routingConf, httpClient, firewallConf, portForwardLogger, cmder, puid, pgid)
|
||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||
@@ -390,7 +402,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||
dnsLogger)
|
||||
dnsLogger, localNetworksToPrefixes(localNetworks))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DNS loop: %w", err)
|
||||
}
|
||||
@@ -416,19 +428,38 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return fmt.Errorf("starting public ip loop: %w", err)
|
||||
}
|
||||
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
healthChecker := healthcheck.NewChecker(healthLogger)
|
||||
|
||||
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
|
||||
// main DNS local server to make sure no request is blocked by filters.
|
||||
dohDialer, err := doh.New(doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating updater DoH dialer: %w", err)
|
||||
}
|
||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
parallelResolver := resolver.NewParallelResolver(dohDialer)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||
openvpnFileExtractor, allSettings.Updater)
|
||||
|
||||
boringPollLogger := logger.New(log.SetComponent("boring poll"))
|
||||
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
|
||||
|
||||
vpnLogger := logger.New(log.SetComponent("vpn"))
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
|
||||
providers, storage, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper,
|
||||
cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
|
||||
buildInfo, *allSettings.Version.Enabled)
|
||||
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts,
|
||||
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer,
|
||||
ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper,
|
||||
dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled)
|
||||
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
|
||||
"vpn", goroutine.OptionTimeout(time.Second))
|
||||
go vpnLooper.Run(vpnCtx, vpnDone)
|
||||
@@ -461,15 +492,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone)
|
||||
otherGroupHandler.Add(shadowsocksHandler)
|
||||
|
||||
controlServerAddress := *allSettings.ControlServer.Address
|
||||
controlServerLogging := *allSettings.ControlServer.Log
|
||||
httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
|
||||
httpServer, err := server.New(httpServerCtx, allSettings.ControlServer,
|
||||
logger.New(log.SetComponent("http server")),
|
||||
allSettings.ControlServer.AuthFilePath,
|
||||
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
|
||||
storage, ipv6Supported)
|
||||
storage, ipv6SupportLevel.IsSupported())
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up control server: %w", err)
|
||||
}
|
||||
@@ -478,12 +506,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
<-httpServerReady
|
||||
controlGroupHandler.Add(httpServerHandler)
|
||||
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck"))
|
||||
healthcheckServer := healthcheck.NewServer(allSettings.Health, healthLogger, vpnLooper)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"HTTP health server", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
orderHandler := goshutdown.NewOrderHandler("gluetun",
|
||||
order.OptionTimeout(totalShutdownTimeout),
|
||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
@@ -545,31 +567,42 @@ func printVersions(ctx context.Context, logger infoer,
|
||||
return nil
|
||||
}
|
||||
|
||||
func localNetworksToPrefixes(localNetworks []routing.LocalNetwork) (prefixes []netip.Prefix) {
|
||||
prefixes = make([]netip.Prefix, len(localNetworks))
|
||||
for i, localNetwork := range localNetworks {
|
||||
prefixes[i] = localNetwork.IPNet
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
type netLinker interface {
|
||||
Addresser
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
IsIPv6Supported() (ok bool, err error)
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
FlushConntrack() error
|
||||
PatchLoggerLevel(level log.Level)
|
||||
}
|
||||
|
||||
type Addresser interface {
|
||||
AddrList(link netlink.Link, family int) (
|
||||
addresses []netlink.Addr, err error)
|
||||
AddrReplace(link netlink.Link, addr netlink.Addr) error
|
||||
AddrList(linkIndex uint32, family uint8) (
|
||||
addresses []netip.Prefix, err error)
|
||||
AddrReplace(linkIndex uint32, addr netip.Prefix) error
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(family int) (routes []netlink.Route, err error)
|
||||
RouteList(family uint8) (routes []netlink.Route, err error)
|
||||
RouteAdd(route netlink.Route) error
|
||||
RouteDel(route netlink.Route) error
|
||||
RouteReplace(route netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleList(family int) (rules []netlink.Rule, err error)
|
||||
RuleList(family uint8) (rules []netlink.Rule, err error)
|
||||
RuleAdd(rule netlink.Rule) error
|
||||
RuleDel(rule netlink.Rule) error
|
||||
}
|
||||
@@ -577,11 +610,12 @@ type Ruler interface {
|
||||
type Linker interface {
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkByIndex(index int) (link netlink.Link, err error)
|
||||
LinkAdd(link netlink.Link) (linkIndex int, err error)
|
||||
LinkDel(link netlink.Link) (err error)
|
||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||
LinkSetDown(link netlink.Link) (err error)
|
||||
LinkByIndex(index uint32) (link netlink.Link, err error)
|
||||
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
|
||||
LinkDel(linkIndex uint32) (err error)
|
||||
LinkSetUp(linkIndex uint32) (err error)
|
||||
LinkSetDown(linkIndex uint32) (err error)
|
||||
LinkSetMTU(linkIndex, mtu uint32) error
|
||||
}
|
||||
|
||||
type clier interface {
|
||||
@@ -602,4 +636,37 @@ type RunStarter interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, err error)
|
||||
RunAndLog(ctx context.Context, commandString string,
|
||||
logger command.Logger) (err error)
|
||||
}
|
||||
|
||||
const gluetunLogo = ` @@@
|
||||
@@@@
|
||||
@@@@@@
|
||||
@@@@.@@ @@@@@@@@@@
|
||||
@@@@.@@@ @@@@@@@@==@@@@
|
||||
@@@.@..@@ @@@@@@@=@..==@@@@
|
||||
@@@@ @@@.@@.@@ @@@@@@===@@@@.=@@@
|
||||
@...-@@ @@@@.@@.@@@ @@@ @@@@@@=======@@@=@@@@
|
||||
@@@@@@@@ @@@.-%@.+@@@@@@@@ @@@@@%============@@@@
|
||||
@@@.--@..@@@@.-@@@@@@@==============@@@@
|
||||
@@@@ @@@-@--@@.@@.---@@@@@==============#@@@@@
|
||||
@@@ @@@.@@-@@.@@--@@@@@===============@@@@@@
|
||||
@@@@.@--@@@@@@@@@@================@@@@@@@
|
||||
@@@..--@@*@@@@@@================@@@@+*@@
|
||||
@@@.---@@.@@@@=================@@@@--@@
|
||||
@@@-.---@@@@@@================@@@@*--@@@
|
||||
@@@.:-#@@@@@@===============*@@@@.---@@
|
||||
@@@.-------.@@@============@@@@@@.--@@@
|
||||
@@@..--------:@@@=========@@@@@@@@.--@@@
|
||||
@@@.-@@@@@@@@@@@========@@@@@ @@@.--@@
|
||||
@@.@@@@===============@@@@@ @@@@@@---@@@@@@
|
||||
@@@@@@@==============@@@@@@@@@@@@*@---@@@@@@@@
|
||||
@@@@@@=============@@@@@ @@@...------------.*@@@
|
||||
@@@@%===========@@@@@@ @@@..------@@@@.-----.-@@@
|
||||
@@@@@@.=======@@@@@@ @@@.-------@@@@@@-.------=@@
|
||||
@@@@@@@@@===@@@@@@ @@.------@@@@ @@@@.-----@@@
|
||||
@@@==@@@=@@@@@@@ @@@.-@@@@@@@ @@@@@@@--@@
|
||||
@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@
|
||||
@@@@@@@@ @@@@ @@@@
|
||||
`
|
||||
|
||||
@@ -1,59 +1,65 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.23
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.18
|
||||
github.com/ProtonMail/go-srp v0.0.7
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16
|
||||
github.com/breml/rootcerts v0.3.4
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/jsimonetti/rtnetlink v1.4.2
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
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/mdlayher/genetlink v1.3.2
|
||||
github.com/mdlayher/netlink v1.9.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
|
||||
github.com/qdm12/gotree v0.3.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.6.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ti-mo/netfilter v0.5.3
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
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.org/x/net v0.51.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/text v0.35.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
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/google/go-cmp v0.7.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/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/qdm12/goservices v0.1.0 // indirect
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/vishvananda/netns v0.0.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.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/tools v0.42.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
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
|
||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16 h1:XY6HOq/xtqH8ZXMncRWkjFs85EKdN10NLNnw23kTpE0=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16/go.mod h1:nRkPpIzjCxMW8pZKXTRkpqAQVlmFJdVOGkeQSC7wbms=
|
||||
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/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
|
||||
github.com/breml/rootcerts v0.3.4/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/davecgh/go-spew v1.1.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=
|
||||
@@ -11,14 +29,14 @@ 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/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.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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -32,18 +50,20 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
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/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
|
||||
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
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/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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
@@ -54,16 +74,16 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
|
||||
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/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||
github.com/qdm12/goshutdown v0.3.0/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/gosplash v0.2.1-0.20260305164749-b713de4fee6c h1:l8qz53IqEXRGK0X62gWwipG077Fz5eNM7qe4mUbAr/Q=
|
||||
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c/go.mod h1:vgRg8Skq9+RNp1THecwMI7SGsnIwO/NPMfYenNTgpAc=
|
||||
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=
|
||||
@@ -81,59 +101,81 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
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 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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=
|
||||
github.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20201119102817-f84b799fce68/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
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=
|
||||
@@ -148,13 +190,13 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-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/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
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=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package amneziawg
|
||||
|
||||
type Amneziawg struct {
|
||||
logger Logger
|
||||
settings Settings
|
||||
netlink NetLinker
|
||||
}
|
||||
|
||||
func New(settings Settings, netlink NetLinker,
|
||||
logger Logger,
|
||||
) (a *Amneziawg, err error) {
|
||||
settings.SetDefaults()
|
||||
if err := settings.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Amneziawg{
|
||||
logger: logger,
|
||||
settings: settings,
|
||||
netlink: netlink,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const validKeyString = "oMNSf/zJ0pt1ciy+qIRk8Rlyfs9accwuRLnKd85Yl1Q="
|
||||
logger := NewMockLogger(nil)
|
||||
netLinker := NewMockNetLinker(nil)
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
amneziawg *Amneziawg
|
||||
err error
|
||||
}{
|
||||
"bad_settings": {
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
PrivateKey: "",
|
||||
},
|
||||
},
|
||||
err: wireguard.ErrPrivateKeyMissing,
|
||||
},
|
||||
"minimal valid settings": {
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
PrivateKey: validKeyString,
|
||||
PublicKey: validKeyString,
|
||||
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 0),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
|
||||
},
|
||||
FirewallMark: 100,
|
||||
},
|
||||
},
|
||||
amneziawg: &Amneziawg{
|
||||
logger: logger,
|
||||
netlink: netLinker,
|
||||
settings: Settings{
|
||||
Wireguard: wireguard.Settings{
|
||||
InterfaceName: "wg0",
|
||||
PrivateKey: validKeyString,
|
||||
PublicKey: validKeyString,
|
||||
Endpoint: netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 51820),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
|
||||
},
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
},
|
||||
FirewallMark: 100,
|
||||
MTU: device.DefaultMTU,
|
||||
IPv6: ptrTo(false),
|
||||
Implementation: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wireguard, err := New(testCase.settings, netLinker, logger)
|
||||
|
||||
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.amneziawg, wireguard)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package amneziawg
|
||||
|
||||
func ptrTo[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package amneziawg
|
||||
|
||||
//go:generate mockgen -destination=log_mock_test.go -package amneziawg . Logger
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Debugf(format string, args ...interface{})
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: Logger)
|
||||
|
||||
// Package amneziawg is a generated GoMock package.
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debug mocks base method.
|
||||
func (m *MockLogger) Debug(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Debug", arg0)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug.
|
||||
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||
}
|
||||
|
||||
// Debugf mocks base method.
|
||||
func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Debugf", varargs...)
|
||||
}
|
||||
|
||||
// Debugf indicates an expected call of Debugf.
|
||||
func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
|
||||
// Errorf mocks base method.
|
||||
func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Errorf", varargs...)
|
||||
}
|
||||
|
||||
// Errorf indicates an expected call of Errorf.
|
||||
func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
|
||||
}
|
||||
|
||||
// Info mocks base method.
|
||||
func (m *MockLogger) Info(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Info", arg0)
|
||||
}
|
||||
|
||||
// Info indicates an expected call of Info.
|
||||
func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=netlinker_mock_test.go -package amneziawg . NetLinker
|
||||
|
||||
type NetLinker interface {
|
||||
AddrReplace(linkIndex uint32, addr netip.Prefix) error
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(family uint8) (routes []netlink.Route, err error)
|
||||
RouteAdd(route netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleAdd(rule netlink.Rule) error
|
||||
RuleDel(rule netlink.Rule) error
|
||||
}
|
||||
|
||||
type Linker interface {
|
||||
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkSetUp(linkIndex uint32) error
|
||||
LinkSetDown(linkIndex uint32) error
|
||||
LinkDel(linkIndex uint32) error
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/amneziawg (interfaces: NetLinker)
|
||||
|
||||
// Package amneziawg is a generated GoMock package.
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
netip "net/netip"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
netlink "github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
|
||||
// MockNetLinker is a mock of NetLinker interface.
|
||||
type MockNetLinker struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNetLinkerMockRecorder
|
||||
}
|
||||
|
||||
// MockNetLinkerMockRecorder is the mock recorder for MockNetLinker.
|
||||
type MockNetLinkerMockRecorder struct {
|
||||
mock *MockNetLinker
|
||||
}
|
||||
|
||||
// NewMockNetLinker creates a new mock instance.
|
||||
func NewMockNetLinker(ctrl *gomock.Controller) *MockNetLinker {
|
||||
mock := &MockNetLinker{ctrl: ctrl}
|
||||
mock.recorder = &MockNetLinkerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockNetLinker) EXPECT() *MockNetLinkerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddrReplace mocks base method.
|
||||
func (m *MockNetLinker) AddrReplace(arg0 uint32, arg1 netip.Prefix) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddrReplace", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddrReplace indicates an expected call of AddrReplace.
|
||||
func (mr *MockNetLinkerMockRecorder) AddrReplace(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrReplace", reflect.TypeOf((*MockNetLinker)(nil).AddrReplace), arg0, arg1)
|
||||
}
|
||||
|
||||
// IsWireguardSupported mocks base method.
|
||||
func (m *MockNetLinker) IsWireguardSupported() (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsWireguardSupported")
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsWireguardSupported indicates an expected call of IsWireguardSupported.
|
||||
func (mr *MockNetLinkerMockRecorder) IsWireguardSupported() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWireguardSupported", reflect.TypeOf((*MockNetLinker)(nil).IsWireguardSupported))
|
||||
}
|
||||
|
||||
// LinkAdd mocks base method.
|
||||
func (m *MockNetLinker) LinkAdd(arg0 netlink.Link) (uint32, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkAdd", arg0)
|
||||
ret0, _ := ret[0].(uint32)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkAdd indicates an expected call of LinkAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkAdd", reflect.TypeOf((*MockNetLinker)(nil).LinkAdd), arg0)
|
||||
}
|
||||
|
||||
// LinkByName mocks base method.
|
||||
func (m *MockNetLinker) LinkByName(arg0 string) (netlink.Link, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkByName", arg0)
|
||||
ret0, _ := ret[0].(netlink.Link)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkByName indicates an expected call of LinkByName.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkByName(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkByName", reflect.TypeOf((*MockNetLinker)(nil).LinkByName), arg0)
|
||||
}
|
||||
|
||||
// LinkDel mocks base method.
|
||||
func (m *MockNetLinker) LinkDel(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkDel", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkDel indicates an expected call of LinkDel.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkDel(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkDel", reflect.TypeOf((*MockNetLinker)(nil).LinkDel), arg0)
|
||||
}
|
||||
|
||||
// LinkList mocks base method.
|
||||
func (m *MockNetLinker) LinkList() ([]netlink.Link, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkList")
|
||||
ret0, _ := ret[0].([]netlink.Link)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LinkList indicates an expected call of LinkList.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkList() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkList", reflect.TypeOf((*MockNetLinker)(nil).LinkList))
|
||||
}
|
||||
|
||||
// LinkSetDown mocks base method.
|
||||
func (m *MockNetLinker) LinkSetDown(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkSetDown", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkSetDown indicates an expected call of LinkSetDown.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkSetDown(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetDown", reflect.TypeOf((*MockNetLinker)(nil).LinkSetDown), arg0)
|
||||
}
|
||||
|
||||
// LinkSetUp mocks base method.
|
||||
func (m *MockNetLinker) LinkSetUp(arg0 uint32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LinkSetUp", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LinkSetUp indicates an expected call of LinkSetUp.
|
||||
func (mr *MockNetLinkerMockRecorder) LinkSetUp(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetUp", reflect.TypeOf((*MockNetLinker)(nil).LinkSetUp), arg0)
|
||||
}
|
||||
|
||||
// RouteAdd mocks base method.
|
||||
func (m *MockNetLinker) RouteAdd(arg0 netlink.Route) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RouteAdd", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RouteAdd indicates an expected call of RouteAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) RouteAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteAdd", reflect.TypeOf((*MockNetLinker)(nil).RouteAdd), arg0)
|
||||
}
|
||||
|
||||
// RouteList mocks base method.
|
||||
func (m *MockNetLinker) RouteList(arg0 byte) ([]netlink.Route, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RouteList", arg0)
|
||||
ret0, _ := ret[0].([]netlink.Route)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RouteList indicates an expected call of RouteList.
|
||||
func (mr *MockNetLinkerMockRecorder) RouteList(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteList", reflect.TypeOf((*MockNetLinker)(nil).RouteList), arg0)
|
||||
}
|
||||
|
||||
// RuleAdd mocks base method.
|
||||
func (m *MockNetLinker) RuleAdd(arg0 netlink.Rule) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RuleAdd", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RuleAdd indicates an expected call of RuleAdd.
|
||||
func (mr *MockNetLinkerMockRecorder) RuleAdd(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleAdd", reflect.TypeOf((*MockNetLinker)(nil).RuleAdd), arg0)
|
||||
}
|
||||
|
||||
// RuleDel mocks base method.
|
||||
func (m *MockNetLinker) RuleDel(arg0 netlink.Rule) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RuleDel", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RuleDel indicates an expected call of RuleDel.
|
||||
func (mr *MockNetLinkerMockRecorder) RuleDel(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RuleDel", reflect.TypeOf((*MockNetLinker)(nil).RuleDel), arg0)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
amneziaconn "github.com/amnezia-vpn/amneziawg-go/conn"
|
||||
amneziadevice "github.com/amnezia-vpn/amneziawg-go/device"
|
||||
amneziatun "github.com/amnezia-vpn/amneziawg-go/tun"
|
||||
"github.com/qdm12/gluetun/internal/cleanup"
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
)
|
||||
|
||||
var (
|
||||
errTunNameMismatch = errors.New("TUN device name is mismatching")
|
||||
errDeviceWaited = errors.New("device waited for")
|
||||
)
|
||||
|
||||
// Run runs the amneziawg interface and waits until the context is done, then it cleans up the
|
||||
// interface and returns any error that occurred during setup or waiting. It sends an error to
|
||||
// waitError if any error occurs during setup or waiting, otherwise it sends nil when the context
|
||||
// is done. It sends a signal to ready when the setup is complete and the interface is ready to use.
|
||||
// See https://github.com/amnezia-vpn/amneziawg-go/blob/master/main.go
|
||||
func (a *Amneziawg) Run(ctx context.Context, waitError chan<- error, ready chan<- struct{}) {
|
||||
setup := func(ctx context.Context, cleanups *cleanup.Cleanups) (
|
||||
linkIndex uint32, waitAndCleanup func() error, err error,
|
||||
) {
|
||||
return setupUserspace(ctx, a.settings.Wireguard.InterfaceName,
|
||||
a.netlink, a.settings.Wireguard.MTU, cleanups, a.logger, a.settings)
|
||||
}
|
||||
|
||||
wireguard.Run(ctx, waitError, ready, setup, a.settings.Wireguard, a.netlink, a.logger)
|
||||
}
|
||||
|
||||
func setupUserspace(ctx context.Context,
|
||||
interfaceName string, netLinker NetLinker, mtu uint32,
|
||||
cleanups *cleanup.Cleanups, logger Logger,
|
||||
settings Settings,
|
||||
) (
|
||||
linkIndex uint32, waitAndCleanup func() error, err error,
|
||||
) {
|
||||
tun, err := amneziatun.CreateTUN(interfaceName, int(mtu))
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("creating TUN device: %w", err)
|
||||
}
|
||||
|
||||
cleanups.Add("closing TUN device", 7, tun.Close)
|
||||
|
||||
tunName, err := tun.Name()
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("getting created TUN device name: %w", err)
|
||||
} else if tunName != interfaceName {
|
||||
return 0, nil, fmt.Errorf("%w: expected %q and got %q",
|
||||
errTunNameMismatch, interfaceName, tunName)
|
||||
}
|
||||
|
||||
link, err := netLinker.LinkByName(interfaceName)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("finding link %s: %w", interfaceName, err)
|
||||
}
|
||||
cleanups.Add("deleting link", 5, func() error {
|
||||
return netLinker.LinkDel(link.Index)
|
||||
})
|
||||
|
||||
bind := amneziaconn.NewDefaultBind()
|
||||
cleanups.Add("closing bind", 7, bind.Close)
|
||||
|
||||
deviceLogger := amneziadevice.Logger{
|
||||
Verbosef: logger.Debugf,
|
||||
Errorf: logger.Errorf,
|
||||
}
|
||||
device := amneziadevice.NewDevice(tun, bind, &deviceLogger)
|
||||
|
||||
cleanups.Add("closing Wireguard device", 6, func() error {
|
||||
device.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
uapiFile, err := wireguard.UAPIOpen(interfaceName)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("opening UAPI socket: %w", err)
|
||||
}
|
||||
cleanups.Add("closing UAPI file", 3, uapiFile.Close)
|
||||
|
||||
uapiListener, err := wireguard.UAPIListen(interfaceName, uapiFile)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listening on UAPI socket: %w", err)
|
||||
}
|
||||
cleanups.Add("closing UAPI listener", 2, uapiListener.Close)
|
||||
|
||||
uapiConfig := settings.uapiConfig()
|
||||
err = device.IpcSet(uapiConfig)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("setting amneziawg uapi config: %w", err)
|
||||
}
|
||||
|
||||
// acceptAndHandle exits when uapiListener is closed
|
||||
uapiAcceptErrorCh := make(chan error)
|
||||
go acceptAndHandle(uapiListener, device, uapiAcceptErrorCh)
|
||||
waitAndCleanup = func() error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case err = <-uapiAcceptErrorCh:
|
||||
close(uapiAcceptErrorCh)
|
||||
case <-device.Wait():
|
||||
err = errDeviceWaited
|
||||
}
|
||||
|
||||
cleanups.Cleanup(logger)
|
||||
|
||||
<-uapiAcceptErrorCh // wait for acceptAndHandle to exit
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return link.Index, waitAndCleanup, nil
|
||||
}
|
||||
|
||||
func acceptAndHandle(uapi net.Listener, device *amneziadevice.Device,
|
||||
uapiAcceptErrorCh chan<- error,
|
||||
) {
|
||||
for { // stopped by uapiFile.Close()
|
||||
conn, err := uapi.Accept()
|
||||
if err != nil {
|
||||
uapiAcceptErrorCh <- err
|
||||
return
|
||||
}
|
||||
go device.IpcHandle(conn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package amneziawg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/wireguard"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Wireguard wireguard.Settings
|
||||
JunkPacketCount uint16
|
||||
JunkPacketMin uint16
|
||||
JunkPacketMax uint16
|
||||
PaddingS1 uint16
|
||||
PaddingS2 uint16
|
||||
PaddingS3 uint16
|
||||
PaddingS4 uint16
|
||||
HeaderH1 string
|
||||
HeaderH2 string
|
||||
HeaderH3 string
|
||||
HeaderH4 string
|
||||
InitPacketI1 string
|
||||
InitPacketI2 string
|
||||
InitPacketI3 string
|
||||
InitPacketI4 string
|
||||
InitPacketI5 string
|
||||
}
|
||||
|
||||
func (s Settings) uapiConfig() string {
|
||||
uintFields := map[string]uint16{
|
||||
"jc": s.JunkPacketCount,
|
||||
"jmin": s.JunkPacketMin,
|
||||
"jmax": s.JunkPacketMax,
|
||||
"s1": s.PaddingS1,
|
||||
"s2": s.PaddingS2,
|
||||
"s3": s.PaddingS3,
|
||||
"s4": s.PaddingS4,
|
||||
}
|
||||
stringFields := map[string]string{
|
||||
"h1": s.HeaderH1,
|
||||
"h2": s.HeaderH2,
|
||||
"h3": s.HeaderH3,
|
||||
"h4": s.HeaderH4,
|
||||
"i1": s.InitPacketI1,
|
||||
"i2": s.InitPacketI2,
|
||||
"i3": s.InitPacketI3,
|
||||
"i4": s.InitPacketI4,
|
||||
"i5": s.InitPacketI5,
|
||||
}
|
||||
lines := make([]string, 0, len(uintFields)+len(stringFields))
|
||||
|
||||
for key, val := range uintFields {
|
||||
lines = append(lines, fmt.Sprintf("%s=%d", key, val))
|
||||
}
|
||||
|
||||
for key, val := range stringFields {
|
||||
lines = append(lines, key+"="+val)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.Wireguard.SetDefaults()
|
||||
}
|
||||
|
||||
func (s *Settings) Check() error {
|
||||
return s.Wireguard.Check()
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package boringpoll
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
type BoringPoll struct {
|
||||
// Injected dependencies
|
||||
client *http.Client
|
||||
logger Logger
|
||||
|
||||
// Internal state
|
||||
urlToData map[string]*urlData
|
||||
|
||||
// Internal signals and channels
|
||||
cancel context.CancelFunc
|
||||
done *sync.WaitGroup
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type urlData struct{}
|
||||
|
||||
func New(client *http.Client, logger Logger, settings settings.BoringPoll) *BoringPoll {
|
||||
urlToData := make(map[string]*urlData)
|
||||
if *settings.GluetunCom {
|
||||
urlToData["https://gluetun.com/wp-json"] = &urlData{}
|
||||
}
|
||||
return &BoringPoll{
|
||||
client: client,
|
||||
logger: logger,
|
||||
urlToData: urlToData,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BoringPoll) Start() (runError <-chan error, err error) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if len(b.urlToData) == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
const minPeriod = time.Minute
|
||||
const maxPeriod = 5 * time.Minute
|
||||
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
|
||||
|
||||
var ready, done sync.WaitGroup
|
||||
b.done = &done
|
||||
ready.Add(len(b.urlToData))
|
||||
done.Add(len(b.urlToData))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
b.cancel = cancel
|
||||
for url := range b.urlToData {
|
||||
go func(url string) {
|
||||
defer done.Done()
|
||||
|
||||
b.logger.Infof("running against %s periodically between %s and %s "+
|
||||
"and will log every %s downloaded",
|
||||
url, minPeriod, maxPeriod, byteCountSI(logEveryBytes))
|
||||
totalDownloaded := uint64(0)
|
||||
lastDownloaded := uint64(0)
|
||||
consecutiveFails := 0
|
||||
const maxConsecutiveErrs = 3
|
||||
const coolDownTimeout = time.Hour
|
||||
timer := time.NewTimer(time.Hour)
|
||||
var err error
|
||||
|
||||
ready.Done()
|
||||
for {
|
||||
timeout := minPeriod + time.Duration(rand.Int63n(int64(maxPeriod-minPeriod))) //nolint:gosec
|
||||
if consecutiveFails >= maxConsecutiveErrs {
|
||||
b.logger.Debugf("pausing poll to %s for %s due to %d consecutive errors, last error: %s",
|
||||
url, coolDownTimeout, consecutiveFails, err)
|
||||
timeout = coolDownTimeout
|
||||
}
|
||||
timer.Reset(timeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
totalDownloaded += lastDownloaded
|
||||
if totalDownloaded > 0 {
|
||||
b.logger.Infof("stopping poll to %s, downloaded %s!", url, byteCountSI(totalDownloaded))
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
var n int64
|
||||
n, err = fetchURL(ctx, b.client, url)
|
||||
if err != nil {
|
||||
consecutiveFails++
|
||||
continue
|
||||
}
|
||||
consecutiveFails = 0
|
||||
totalDownloaded += uint64(n) //nolint:gosec
|
||||
lastDownloaded += uint64(n) //nolint:gosec
|
||||
if lastDownloaded >= logEveryBytes {
|
||||
b.logger.Infof("thanks for helping! You have downloaded %s from %s so far!",
|
||||
byteCountSI(totalDownloaded), url)
|
||||
lastDownloaded = 0
|
||||
}
|
||||
}
|
||||
}(url)
|
||||
}
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func fetchURL(ctx context.Context, client *http.Client, url string) (downloaded int64, err error) {
|
||||
const requestTimeout = 10 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return 0, err
|
||||
}
|
||||
request.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
request.Header.Set("Pragma", "no-cache")
|
||||
request.Header.Set("Expires", "0")
|
||||
request.Header.Set("User-Agent", getRandomUserAgent())
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
downloaded, err = io.Copy(io.Discard, response.Body)
|
||||
_ = response.Body.Close()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return downloaded, nil
|
||||
}
|
||||
|
||||
func getRandomUserAgent() string {
|
||||
//nolint:lll
|
||||
userAgents := [...]string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/121.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPad; CPU OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Android 14; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0",
|
||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
|
||||
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
||||
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
||||
}
|
||||
return userAgents[rand.Intn(len(userAgents))] //nolint:gosec
|
||||
}
|
||||
|
||||
func (b *BoringPoll) Stop() error {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
if b.cancel == nil {
|
||||
return nil
|
||||
}
|
||||
b.cancel()
|
||||
b.done.Wait()
|
||||
b.cancel = nil
|
||||
b.done = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func byteCountSI(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%dB", b)
|
||||
}
|
||||
|
||||
div, exp := uint64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package boringpoll
|
||||
|
||||
type Logger interface {
|
||||
Infof(format string, args ...any)
|
||||
Debugf(format string, args ...any)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cleanup
|
||||
|
||||
import "sort"
|
||||
|
||||
type Cleanups []cleanup
|
||||
|
||||
type cleanup struct {
|
||||
operation string
|
||||
orderIndex uint
|
||||
cleanup func() error
|
||||
done bool
|
||||
}
|
||||
|
||||
// Add adds a cleanup function to the list of cleanups, with a description of the
|
||||
// operation being cleaned up, and an order index that determines the order in which
|
||||
// the cleanup functions are run. The lower the order index, the earlier the cleanup
|
||||
// function is run.
|
||||
func (c *Cleanups) Add(operation string, orderIndex uint,
|
||||
cleanupFunc func() error,
|
||||
) {
|
||||
closer := cleanup{
|
||||
operation: operation,
|
||||
orderIndex: orderIndex,
|
||||
cleanup: cleanupFunc,
|
||||
}
|
||||
*c = append(*c, closer)
|
||||
}
|
||||
|
||||
// Cleanup runs the cleanup functions in the order of their orderIndex,
|
||||
// and logs any error that occurs during cleanup.
|
||||
// It can also be re-called in case a cleanup fails, and already cleaned up
|
||||
// functions will not be re-run.
|
||||
func (c *Cleanups) Cleanup(logger Logger) {
|
||||
closers := *c
|
||||
|
||||
sort.Slice(closers, func(i, j int) bool {
|
||||
return closers[i].orderIndex < closers[j].orderIndex
|
||||
})
|
||||
|
||||
for i, closer := range closers {
|
||||
if closer.done {
|
||||
continue
|
||||
}
|
||||
closers[i].done = true
|
||||
logger.Debug(closer.operation + "...")
|
||||
err := closer.cleanup()
|
||||
if err != nil {
|
||||
logger.Error("failed " + closer.operation + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Cleanups(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
var ACloseCalled, BCloseCalled, CCloseCalled bool
|
||||
var (
|
||||
AErr error
|
||||
BErr = errors.New("B failed")
|
||||
CErr = errors.New("C failed")
|
||||
)
|
||||
|
||||
var cleanups Cleanups
|
||||
cleanups.Add("cleaning up A", 5, func() error {
|
||||
ACloseCalled = true
|
||||
return AErr
|
||||
})
|
||||
|
||||
cleanups.Add("cleaning up B", 3, func() error {
|
||||
BCloseCalled = true
|
||||
return BErr
|
||||
})
|
||||
|
||||
cleanups.Add("cleaning up C", 2, func() error {
|
||||
CCloseCalled = true
|
||||
return CErr
|
||||
})
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
prevCall := logger.EXPECT().Debug("cleaning up C...")
|
||||
prevCall = logger.EXPECT().Error("failed cleaning up C: C failed").After(prevCall)
|
||||
prevCall = logger.EXPECT().Debug("cleaning up B...").After(prevCall)
|
||||
prevCall = logger.EXPECT().Error("failed cleaning up B: B failed").After(prevCall)
|
||||
logger.EXPECT().Debug("cleaning up A...").After(prevCall)
|
||||
|
||||
cleanups.Cleanup(logger)
|
||||
|
||||
cleanups.Cleanup(logger) // run twice should not close already closed
|
||||
|
||||
for _, cleanup := range cleanups {
|
||||
assert.True(t, cleanup.done)
|
||||
}
|
||||
|
||||
assert.True(t, ACloseCalled)
|
||||
assert.True(t, BCloseCalled)
|
||||
assert.True(t, CCloseCalled)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cleanup
|
||||
|
||||
type Logger interface {
|
||||
Debug(string)
|
||||
Error(string)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package cleanup
|
||||
|
||||
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Logger
|
||||
@@ -0,0 +1,58 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/gluetun/internal/cleanup (interfaces: Logger)
|
||||
|
||||
// Package cleanup is a generated GoMock package.
|
||||
package cleanup
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLogger is a mock of Logger interface.
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger.
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance.
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debug mocks base method.
|
||||
func (m *MockLogger) Debug(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Debug", arg0)
|
||||
}
|
||||
|
||||
// Debug indicates an expected call of Debug.
|
||||
func (mr *MockLoggerMockRecorder) Debug(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), arg0)
|
||||
}
|
||||
|
||||
// Error mocks base method.
|
||||
func (m *MockLogger) Error(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Error", arg0)
|
||||
}
|
||||
|
||||
// Error indicates an expected call of Error.
|
||||
func (mr *MockLoggerMockRecorder) Error(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), arg0)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type noopFirewall struct{}
|
||||
|
||||
func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr,
|
||||
_ uint16, _ bool,
|
||||
) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -7,3 +7,4 @@ func newNoopLogger() *noopLogger {
|
||||
}
|
||||
|
||||
func (l *noopLogger) Info(string) {}
|
||||
func (l *noopLogger) Warn(string) {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
"github.com/qdm12/gluetun/internal/openvpn/extract"
|
||||
"github.com/qdm12/gluetun/internal/provider"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
@@ -40,7 +41,9 @@ type IPFetcher interface {
|
||||
}
|
||||
|
||||
type IPv6Checker interface {
|
||||
IsIPv6Supported() (supported bool, err error)
|
||||
FindIPv6SupportLevel(ctx context.Context,
|
||||
checkAddresses []netip.AddrPort, firewall netlink.Firewall,
|
||||
) (level netlink.IPv6SupportLevel, err error)
|
||||
}
|
||||
|
||||
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
@@ -56,13 +59,16 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allSettings.SetDefaults()
|
||||
|
||||
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
|
||||
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(),
|
||||
allSettings.IPv6.CheckAddresses, &noopFirewall{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking for IPv6 support: %w", err)
|
||||
}
|
||||
|
||||
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
|
||||
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -75,16 +81,16 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, warner, client,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
|
||||
providerConf := providers.Get(allSettings.VPN.Provider.Name)
|
||||
connection, err := providerConf.GetConnection(
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
|
||||
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := providerConf.OpenVPNConfig(connection,
|
||||
allSettings.VPN.OpenVPN, ipv6Supported)
|
||||
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
|
||||
|
||||
fmt.Println(strings.Join(lines, "\n"))
|
||||
return nil
|
||||
|
||||
+43
-5
@@ -6,9 +6,12 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -24,6 +27,8 @@ import (
|
||||
var (
|
||||
ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified")
|
||||
ErrNoProviderSpecified = errors.New("no provider was specified")
|
||||
ErrUsernameMissing = errors.New("username is required for this provider")
|
||||
ErrPasswordMissing = errors.New("password is required for this provider")
|
||||
)
|
||||
|
||||
type UpdaterLogger interface {
|
||||
@@ -35,22 +40,30 @@ type UpdaterLogger interface {
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var csvProviders, ipToken string
|
||||
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
||||
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
const defaultMinRatio = 0.8
|
||||
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
||||
"Minimum ratio of servers to find for the update to succeed")
|
||||
flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers")
|
||||
flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for")
|
||||
flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use")
|
||||
flagSet.StringVar(&protonUsername, "proton-username", "",
|
||||
"(Retro-compatibility) Username to use to authenticate with Proton. Use -proton-email instead.") // v4 remove this
|
||||
flagSet.StringVar(&protonEmail, "proton-email", "", "Email to use to authenticate with Proton")
|
||||
flagSet.StringVar(&protonPassword, "proton-password", "", "Password to use to authenticate with Proton")
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dnsServer != "" {
|
||||
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
}
|
||||
|
||||
if !endUserMode && !maintainerMode {
|
||||
return fmt.Errorf("%w", ErrModeUnspecified)
|
||||
}
|
||||
@@ -64,6 +77,16 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
options.Providers = strings.Split(csvProviders, ",")
|
||||
}
|
||||
|
||||
if slices.Contains(options.Providers, providers.Protonvpn) {
|
||||
if protonEmail == "" && protonUsername != "" {
|
||||
protonEmail = protonUsername + "@protonmail.com"
|
||||
logger.Warn("use -proton-email instead of -proton-username in the future. " +
|
||||
"This assumes the email is " + protonEmail + " and may not work.")
|
||||
}
|
||||
options.ProtonEmail = &protonEmail
|
||||
options.ProtonPassword = &protonPassword
|
||||
}
|
||||
|
||||
options.SetDefaults(options.Providers[0])
|
||||
|
||||
err := options.Validate()
|
||||
@@ -71,15 +94,30 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return fmt.Errorf("options validation failed: %w", err)
|
||||
}
|
||||
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
serversDataPath := constants.ServersData
|
||||
if maintainerMode {
|
||||
serversDataPath = ""
|
||||
}
|
||||
storage, err := storage.New(logger, serversDataPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
|
||||
dohSettings := doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{
|
||||
dnsprovider.Cloudflare(),
|
||||
dnsprovider.Google(),
|
||||
},
|
||||
}
|
||||
dnsDialer, err := doh.New(dohSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
||||
}
|
||||
|
||||
const clientTimeout = 10 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||
parallelResolver := resolver.NewParallelResolver(dnsDialer)
|
||||
nameTokenPairs := []api.NameToken{
|
||||
{Name: string(api.IPInfo), Token: ipToken},
|
||||
{Name: string(api.IP2Location)},
|
||||
@@ -94,7 +132,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
openvpnFileExtractor := extract.New()
|
||||
|
||||
providers := provider.NewProviders(storage, time.Now, logger, httpClient,
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
|
||||
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
|
||||
|
||||
updater := updater.New(httpClient, storage, providers, logger)
|
||||
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package healthcheck
|
||||
package command
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
errCommandEmpty = errors.New("command is empty")
|
||||
errSingleQuoteUnterminated = errors.New("unterminated single-quoted string")
|
||||
errDoubleQuoteUnterminated = errors.New("unterminated double-quoted string")
|
||||
errEscapeUnterminated = errors.New("unterminated backslash-escape")
|
||||
)
|
||||
|
||||
// split splits a command string into a slice of arguments.
|
||||
// This is especially important for commands such as:
|
||||
// /bin/sh -c "echo hello"
|
||||
// which should be split into: ["/bin/sh", "-c", "echo hello"]
|
||||
// It supports backslash-escapes, single-quotes and double-quotes.
|
||||
// It does not support:
|
||||
// - the $" quoting style.
|
||||
// - expansion (brace, shell or pathname).
|
||||
func split(command string) (words []string, err error) {
|
||||
if command == "" {
|
||||
return nil, fmt.Errorf("%w", errCommandEmpty)
|
||||
}
|
||||
|
||||
const bufferSize = 1024
|
||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||
|
||||
startIndex := 0
|
||||
|
||||
for startIndex < len(command) {
|
||||
// skip any split characters at the start
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex:])
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character):
|
||||
startIndex += runeSize
|
||||
case character == '\\':
|
||||
// Look ahead to eventually skip an escaped newline
|
||||
if command[startIndex+runeSize:] == "" {
|
||||
return nil, fmt.Errorf("%w: %q", errEscapeUnterminated, command)
|
||||
}
|
||||
character, runeSize := utf8.DecodeRuneInString(command[startIndex+runeSize:])
|
||||
if character == '\n' {
|
||||
startIndex += runeSize + runeSize // backslash and newline
|
||||
}
|
||||
default:
|
||||
var word string
|
||||
buffer.Reset()
|
||||
word, startIndex, err = splitWord(command, startIndex, buffer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("splitting word in %q: %w", command, err)
|
||||
}
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// WARNING: buffer must be cleared before calling this function.
|
||||
func splitWord(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
character, runeLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += runeLength
|
||||
if character == '"' ||
|
||||
character == '\'' ||
|
||||
character == '\\' ||
|
||||
character == ' ' ||
|
||||
character == '\n' ||
|
||||
character == '\t' {
|
||||
buffer.WriteString(input[startIndex : cursor-runeLength])
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.ContainsRune(" \n\t", character): // spacing character
|
||||
return buffer.String(), cursor, nil
|
||||
case character == '"':
|
||||
return handleDoubleQuoted(input, cursor, buffer)
|
||||
case character == '\'':
|
||||
return handleSingleQuoted(input, cursor, buffer)
|
||||
case character == '\\':
|
||||
return handleEscaped(input, cursor, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(input[startIndex:])
|
||||
return buffer.String(), len(input), nil
|
||||
}
|
||||
|
||||
func handleDoubleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
cursor := startIndex
|
||||
for cursor < len(input) {
|
||||
nextCharacter, nextRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += nextRuneLength
|
||||
switch nextCharacter {
|
||||
case '"': // end of the double quoted string
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength])
|
||||
return splitWord(input, cursor, buffer)
|
||||
case '\\': // escaped character
|
||||
escapedCharacter, escapedRuneLength := utf8.DecodeRuneInString(input[cursor:])
|
||||
cursor += escapedRuneLength
|
||||
if !strings.ContainsRune("$`\"\n\\", escapedCharacter) {
|
||||
break
|
||||
}
|
||||
buffer.WriteString(input[startIndex : cursor-nextRuneLength-escapedRuneLength])
|
||||
if escapedCharacter != '\n' {
|
||||
// skip backslash entirely for the newline character
|
||||
buffer.WriteRune(escapedCharacter)
|
||||
}
|
||||
startIndex = cursor
|
||||
}
|
||||
}
|
||||
return "", 0, fmt.Errorf("%w", errDoubleQuoteUnterminated)
|
||||
}
|
||||
|
||||
func handleSingleQuoted(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
closingQuoteIndex := strings.IndexRune(input[startIndex:], '\'')
|
||||
if closingQuoteIndex == -1 {
|
||||
return "", 0, fmt.Errorf("%w", errSingleQuoteUnterminated)
|
||||
}
|
||||
buffer.WriteString(input[startIndex : startIndex+closingQuoteIndex])
|
||||
const singleQuoteRuneLength = 1
|
||||
startIndex += closingQuoteIndex + singleQuoteRuneLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
|
||||
func handleEscaped(input string, startIndex int, buffer *bytes.Buffer) (
|
||||
word string, newStartIndex int, err error,
|
||||
) {
|
||||
if input[startIndex:] == "" {
|
||||
return "", 0, fmt.Errorf("%w", errEscapeUnterminated)
|
||||
}
|
||||
character, runeLength := utf8.DecodeRuneInString(input[startIndex:])
|
||||
if character != '\n' { // backslash-escaped newline is ignored
|
||||
buffer.WriteString(input[startIndex : startIndex+runeLength])
|
||||
}
|
||||
startIndex += runeLength
|
||||
return splitWord(input, startIndex, buffer)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_split(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
command string
|
||||
words []string
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
command: "",
|
||||
errWrapped: errCommandEmpty,
|
||||
errMessage: "command is empty",
|
||||
},
|
||||
"concrete_sh_command": {
|
||||
command: `/bin/sh -c "echo 123"`,
|
||||
words: []string{"/bin/sh", "-c", "echo 123"},
|
||||
},
|
||||
"single_word": {
|
||||
command: "word1",
|
||||
words: []string{"word1"},
|
||||
},
|
||||
"two_words_single_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_multiple_space": {
|
||||
command: "word1 word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"two_words_no_expansion": {
|
||||
command: "word1* word2?",
|
||||
words: []string{"word1*", "word2?"},
|
||||
},
|
||||
"escaped_single quote": {
|
||||
command: "ain\\'t good",
|
||||
words: []string{"ain't", "good"},
|
||||
},
|
||||
"escaped_single_quote_all_single_quoted": {
|
||||
command: "'ain'\\''t good'",
|
||||
words: []string{"ain't good"},
|
||||
},
|
||||
"empty_single_quoted": {
|
||||
command: "word1 '' word2",
|
||||
words: []string{"word1", "", "word2"},
|
||||
},
|
||||
"escaped_newline": {
|
||||
command: "word1\\\nword2",
|
||||
words: []string{"word1word2"},
|
||||
},
|
||||
"quoted_newline": {
|
||||
command: "text \"with\na\" quoted newline",
|
||||
words: []string{"text", "with\na", "quoted", "newline"},
|
||||
},
|
||||
"quoted_escaped_newline": {
|
||||
command: "\"word1\\d\\\\\\\" word2\\\nword3 word4\"",
|
||||
words: []string{"word1\\d\\\" word2word3 word4"},
|
||||
},
|
||||
"escaped_separated_newline": {
|
||||
command: "word1 \\\n word2",
|
||||
words: []string{"word1", "word2"},
|
||||
},
|
||||
"double_quotes_no_spacing": {
|
||||
command: "word1\"word2\"word3",
|
||||
words: []string{"word1word2word3"},
|
||||
},
|
||||
"unterminated_single_quote": {
|
||||
command: "'abc'\\''def",
|
||||
errWrapped: errSingleQuoteUnterminated,
|
||||
errMessage: `splitting word in "'abc'\\''def": unterminated single-quoted string`,
|
||||
},
|
||||
"unterminated_double_quote": {
|
||||
command: "\"abc'def",
|
||||
errWrapped: errDoubleQuoteUnterminated,
|
||||
errMessage: `splitting word in "\"abc'def": unterminated double-quoted string`,
|
||||
},
|
||||
"unterminated_escape": {
|
||||
command: "abc\\",
|
||||
errWrapped: errEscapeUnterminated,
|
||||
errMessage: `splitting word in "abc\\": unterminated backslash-escape`,
|
||||
},
|
||||
"unterminated_escape_only": {
|
||||
command: " \\",
|
||||
errWrapped: errEscapeUnterminated,
|
||||
errMessage: `unterminated backslash-escape: " \\"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
words, err := split(testCase.command)
|
||||
|
||||
assert.Equal(t, testCase.words, words)
|
||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
||||
if testCase.errWrapped != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (c *Cmder) RunAndLog(ctx context.Context, command string, logger Logger) (err error) {
|
||||
args, err := split(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing command: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec G204
|
||||
stdout, stderr, waitError, err := c.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
logger.Info(line)
|
||||
case line = <-stderr:
|
||||
logger.Error(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type AmneziaWg struct {
|
||||
// Wireguard contains the configuration for Wireguard, given
|
||||
// AmneziaWg is based on Wireguard
|
||||
Wireguard Wireguard `json:"wireguard"`
|
||||
JunkPacketCount *uint16 `json:"junk_packet_count"`
|
||||
JunkPacketMin *uint16 `json:"junk_packet_min"`
|
||||
JunkPacketMax *uint16 `json:"junk_packet_max"`
|
||||
PaddingS1 *uint16 `json:"padding_s1"`
|
||||
PaddingS2 *uint16 `json:"padding_s2"`
|
||||
PaddingS3 *uint16 `json:"padding_s3"`
|
||||
PaddingS4 *uint16 `json:"padding_s4"`
|
||||
HeaderH1 *string `json:"header_h1"`
|
||||
HeaderH2 *string `json:"header_h2"`
|
||||
HeaderH3 *string `json:"header_h3"`
|
||||
HeaderH4 *string `json:"header_h4"`
|
||||
InitPacketI1 *string `json:"init_packet_i1"`
|
||||
InitPacketI2 *string `json:"init_packet_i2"`
|
||||
InitPacketI3 *string `json:"init_packet_i3"`
|
||||
InitPacketI4 *string `json:"init_packet_i4"`
|
||||
InitPacketI5 *string `json:"init_packet_i5"`
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) read(r *reader.Reader) (err error) {
|
||||
const amneziawg = true
|
||||
err = a.Wireguard.read(r, amneziawg)
|
||||
if err != nil {
|
||||
return err // do not wrap this error
|
||||
}
|
||||
|
||||
uint16Fields := map[string]**uint16{
|
||||
"AMNEZIAWG_JC": &a.JunkPacketCount,
|
||||
"AMNEZIAWG_JMIN": &a.JunkPacketMin,
|
||||
"AMNEZIAWG_JMAX": &a.JunkPacketMax,
|
||||
"AMNEZIAWG_S1": &a.PaddingS1,
|
||||
"AMNEZIAWG_S2": &a.PaddingS2,
|
||||
"AMNEZIAWG_S3": &a.PaddingS3,
|
||||
"AMNEZIAWG_S4": &a.PaddingS4,
|
||||
}
|
||||
for key, dst := range uint16Fields {
|
||||
*dst, err = r.Uint16Ptr(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stringFields := map[string]**string{
|
||||
"AMNEZIAWG_H1": &a.HeaderH1,
|
||||
"AMNEZIAWG_H2": &a.HeaderH2,
|
||||
"AMNEZIAWG_H3": &a.HeaderH3,
|
||||
"AMNEZIAWG_H4": &a.HeaderH4,
|
||||
"AMNEZIAWG_I1": &a.InitPacketI1,
|
||||
"AMNEZIAWG_I2": &a.InitPacketI2,
|
||||
"AMNEZIAWG_I3": &a.InitPacketI3,
|
||||
"AMNEZIAWG_I4": &a.InitPacketI4,
|
||||
"AMNEZIAWG_I5": &a.InitPacketI5,
|
||||
}
|
||||
opt := reader.ForceLowercase(false)
|
||||
for key, dst := range stringFields {
|
||||
*dst = r.Get(key, opt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AmneziaWg) copy() (copied AmneziaWg) {
|
||||
return AmneziaWg{
|
||||
Wireguard: a.Wireguard.copy(),
|
||||
JunkPacketCount: gosettings.CopyPointer(a.JunkPacketCount),
|
||||
JunkPacketMin: gosettings.CopyPointer(a.JunkPacketMin),
|
||||
JunkPacketMax: gosettings.CopyPointer(a.JunkPacketMax),
|
||||
PaddingS1: gosettings.CopyPointer(a.PaddingS1),
|
||||
PaddingS2: gosettings.CopyPointer(a.PaddingS2),
|
||||
PaddingS3: gosettings.CopyPointer(a.PaddingS3),
|
||||
PaddingS4: gosettings.CopyPointer(a.PaddingS4),
|
||||
HeaderH1: gosettings.CopyPointer(a.HeaderH1),
|
||||
HeaderH2: gosettings.CopyPointer(a.HeaderH2),
|
||||
HeaderH3: gosettings.CopyPointer(a.HeaderH3),
|
||||
HeaderH4: gosettings.CopyPointer(a.HeaderH4),
|
||||
InitPacketI1: gosettings.CopyPointer(a.InitPacketI1),
|
||||
InitPacketI2: gosettings.CopyPointer(a.InitPacketI2),
|
||||
InitPacketI3: gosettings.CopyPointer(a.InitPacketI3),
|
||||
InitPacketI4: gosettings.CopyPointer(a.InitPacketI4),
|
||||
InitPacketI5: gosettings.CopyPointer(a.InitPacketI5),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) overrideWith(other AmneziaWg) {
|
||||
a.Wireguard.overrideWith(other.Wireguard)
|
||||
a.JunkPacketCount = gosettings.OverrideWithPointer(a.JunkPacketCount, other.JunkPacketCount)
|
||||
a.JunkPacketMin = gosettings.OverrideWithPointer(a.JunkPacketMin, other.JunkPacketMin)
|
||||
a.JunkPacketMax = gosettings.OverrideWithPointer(a.JunkPacketMax, other.JunkPacketMax)
|
||||
a.PaddingS1 = gosettings.OverrideWithPointer(a.PaddingS1, other.PaddingS1)
|
||||
a.PaddingS2 = gosettings.OverrideWithPointer(a.PaddingS2, other.PaddingS2)
|
||||
a.PaddingS3 = gosettings.OverrideWithPointer(a.PaddingS3, other.PaddingS3)
|
||||
a.PaddingS4 = gosettings.OverrideWithPointer(a.PaddingS4, other.PaddingS4)
|
||||
a.HeaderH1 = gosettings.OverrideWithPointer(a.HeaderH1, other.HeaderH1)
|
||||
a.HeaderH2 = gosettings.OverrideWithPointer(a.HeaderH2, other.HeaderH2)
|
||||
a.HeaderH3 = gosettings.OverrideWithPointer(a.HeaderH3, other.HeaderH3)
|
||||
a.HeaderH4 = gosettings.OverrideWithPointer(a.HeaderH4, other.HeaderH4)
|
||||
a.InitPacketI1 = gosettings.OverrideWithPointer(a.InitPacketI1, other.InitPacketI1)
|
||||
a.InitPacketI2 = gosettings.OverrideWithPointer(a.InitPacketI2, other.InitPacketI2)
|
||||
a.InitPacketI3 = gosettings.OverrideWithPointer(a.InitPacketI3, other.InitPacketI3)
|
||||
a.InitPacketI4 = gosettings.OverrideWithPointer(a.InitPacketI4, other.InitPacketI4)
|
||||
a.InitPacketI5 = gosettings.OverrideWithPointer(a.InitPacketI5, other.InitPacketI5)
|
||||
}
|
||||
|
||||
func (a *AmneziaWg) setDefaults(vpnProvider string) {
|
||||
a.Wireguard.setDefaults(vpnProvider)
|
||||
a.Wireguard.Implementation = "userspace" // unused except in logs
|
||||
a.JunkPacketCount = gosettings.DefaultPointer(a.JunkPacketCount, 0)
|
||||
a.JunkPacketMin = gosettings.DefaultPointer(a.JunkPacketMin, 0)
|
||||
a.JunkPacketMax = gosettings.DefaultPointer(a.JunkPacketMax, 0)
|
||||
a.PaddingS1 = gosettings.DefaultPointer(a.PaddingS1, 0)
|
||||
a.PaddingS2 = gosettings.DefaultPointer(a.PaddingS2, 0)
|
||||
a.PaddingS3 = gosettings.DefaultPointer(a.PaddingS3, 0)
|
||||
a.PaddingS4 = gosettings.DefaultPointer(a.PaddingS4, 0)
|
||||
a.HeaderH1 = gosettings.DefaultPointer(a.HeaderH1, "")
|
||||
a.HeaderH2 = gosettings.DefaultPointer(a.HeaderH2, "")
|
||||
a.HeaderH3 = gosettings.DefaultPointer(a.HeaderH3, "")
|
||||
a.HeaderH4 = gosettings.DefaultPointer(a.HeaderH4, "")
|
||||
a.InitPacketI1 = gosettings.DefaultPointer(a.InitPacketI1, "")
|
||||
a.InitPacketI2 = gosettings.DefaultPointer(a.InitPacketI2, "")
|
||||
a.InitPacketI3 = gosettings.DefaultPointer(a.InitPacketI3, "")
|
||||
a.InitPacketI4 = gosettings.DefaultPointer(a.InitPacketI4, "")
|
||||
a.InitPacketI5 = gosettings.DefaultPointer(a.InitPacketI5, "")
|
||||
}
|
||||
|
||||
func (a AmneziaWg) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("AmneziaWG settings:")
|
||||
node.AppendNode(a.Wireguard.toLinesNode())
|
||||
|
||||
uintFields := []struct {
|
||||
key string
|
||||
val *uint16
|
||||
}{
|
||||
{"JC", a.JunkPacketCount},
|
||||
{"JMIN", a.JunkPacketMin},
|
||||
{"JMAX", a.JunkPacketMax},
|
||||
{"S1", a.PaddingS1},
|
||||
{"S2", a.PaddingS2},
|
||||
{"S3", a.PaddingS3},
|
||||
{"S4", a.PaddingS4},
|
||||
}
|
||||
for _, f := range uintFields {
|
||||
node.Appendf("%s: %d", f.key, *f.val)
|
||||
}
|
||||
|
||||
stringFields := []struct {
|
||||
key string
|
||||
val *string
|
||||
}{
|
||||
{"H1", a.HeaderH1},
|
||||
{"H2", a.HeaderH2},
|
||||
{"H3", a.HeaderH3},
|
||||
{"H4", a.HeaderH4},
|
||||
{"I1", a.InitPacketI1},
|
||||
{"I2", a.InitPacketI2},
|
||||
{"I3", a.InitPacketI3},
|
||||
{"I4", a.InitPacketI4},
|
||||
{"I5", a.InitPacketI5},
|
||||
}
|
||||
for _, f := range stringFields {
|
||||
node.Appendf("%s: %s", f.key, *f.val)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAmenziawgImplementationNotValid = errors.New("AmneziaWG implementation is not valid")
|
||||
ErrJunkPacketBounds = errors.New("junk packet minimum must be lower than or equal to maximum")
|
||||
ErrJunkPacketMinMaxNotSet = errors.New("junk packet min and max must be set when junk packet count is set")
|
||||
ErrJunkPacketCountNotSet = errors.New("junk packet count must be set when junk packet min or max is set")
|
||||
ErrHeaderRangeMalformed = errors.New("header range is malformed")
|
||||
)
|
||||
|
||||
func (a AmneziaWg) validate(vpnProvider string, ipv6Supported bool) error {
|
||||
const amneziaWG = true
|
||||
err := a.Wireguard.validate(vpnProvider, ipv6Supported, amneziaWG)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard settings: %w", err)
|
||||
}
|
||||
|
||||
if *a.JunkPacketCount == 0 {
|
||||
if *a.JunkPacketMin != 0 || *a.JunkPacketMax != 0 {
|
||||
return fmt.Errorf("%w: jc=%d and jmin=%d and jmax=%d",
|
||||
ErrJunkPacketCountNotSet, a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
}
|
||||
} else {
|
||||
if *a.JunkPacketMin == 0 || *a.JunkPacketMax == 0 {
|
||||
return fmt.Errorf("%w: jc=%d and jmin=%d and jmax=%d",
|
||||
ErrJunkPacketMinMaxNotSet, a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
} else if *a.JunkPacketMin > *a.JunkPacketMax {
|
||||
return fmt.Errorf("%w: jmin=%d and jmax=%d",
|
||||
ErrJunkPacketBounds, *a.JunkPacketMin, *a.JunkPacketMax)
|
||||
}
|
||||
}
|
||||
|
||||
nameToHeaderRange := map[string]string{
|
||||
"h1": *a.HeaderH1,
|
||||
"h2": *a.HeaderH2,
|
||||
"h3": *a.HeaderH3,
|
||||
"h4": *a.HeaderH4,
|
||||
}
|
||||
for name, headerRange := range nameToHeaderRange {
|
||||
if headerRange == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(headerRange, "-")
|
||||
switch len(fields) {
|
||||
case 1:
|
||||
_, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s value %s is not a number",
|
||||
ErrHeaderRangeMalformed, name, headerRange)
|
||||
}
|
||||
case 2: //nolint:mnd
|
||||
for _, field := range fields {
|
||||
_, err := strconv.Atoi(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s value %s is not a valid range",
|
||||
ErrHeaderRangeMalformed, name, headerRange)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%w: %s value %s must be in the form n or n-m",
|
||||
ErrHeaderRangeMalformed, name, headerRange)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type BoringPoll struct {
|
||||
GluetunCom *bool
|
||||
}
|
||||
|
||||
func (b BoringPoll) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BoringPoll) Copy() BoringPoll {
|
||||
return BoringPoll{
|
||||
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BoringPoll) overrideWith(other BoringPoll) {
|
||||
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
|
||||
}
|
||||
|
||||
func (b *BoringPoll) setDefaults() {
|
||||
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
|
||||
}
|
||||
|
||||
func (b BoringPoll) String() string {
|
||||
return b.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (b BoringPoll) toLinesNode() *gotree.Node {
|
||||
if !*b.GluetunCom {
|
||||
return nil
|
||||
}
|
||||
|
||||
node := gotree.New("Boring-poll settings:")
|
||||
node.Append("gluetun.com: on")
|
||||
return node
|
||||
}
|
||||
|
||||
func (b *BoringPoll) read(r *reader.Reader) (err error) {
|
||||
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,9 +9,15 @@ import (
|
||||
|
||||
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.",
|
||||
"DOT_VERBOSITY": "DOT_VERBOSITY is obsolete, use LOG_LEVEL instead.",
|
||||
"DOT_VERBOSITY_DETAILS": "DOT_VERBOSITY_DETAILS is obsolete because it was specific to Unbound.",
|
||||
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
|
||||
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
|
||||
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
|
||||
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
|
||||
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
|
||||
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
|
||||
}
|
||||
sortedKeys := maps.Keys(keyToMessage)
|
||||
slices.Sort(sortedKeys)
|
||||
|
||||
@@ -1,42 +1,96 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
const (
|
||||
DNSUpstreamTypeDot = "dot"
|
||||
DNSUpstreamTypeDoh = "doh"
|
||||
DNSUpstreamTypePlain = "plain"
|
||||
)
|
||||
|
||||
// DNS contains settings to configure DNS.
|
||||
type DNS struct {
|
||||
// ServerAddress is the DNS server to use inside
|
||||
// the Go program and for the system.
|
||||
// It defaults to '127.0.0.1' to be used with the
|
||||
// DoT server. It cannot be the zero value in the internal
|
||||
// state.
|
||||
ServerAddress netip.Addr
|
||||
// KeepNameserver is true if the existing DNS server
|
||||
// found in /etc/resolv.conf should be used
|
||||
// Note setting this to true will likely DNS traffic
|
||||
// outside the VPN tunnel since it would go through
|
||||
// the local DNS server of your Docker/Kubernetes
|
||||
// configuration, which is likely not going through the tunnel.
|
||||
// This will also disable the DNS over TLS server and the
|
||||
// `ServerAddress` field will be ignored.
|
||||
// It defaults to false and cannot be nil in the
|
||||
// internal state.
|
||||
KeepNameserver *bool
|
||||
// DOT contains settings to configure the DoT
|
||||
// server.
|
||||
DoT DoT
|
||||
// UpstreamType can be [DNSUpstreamTypeDot], [DNSUpstreamTypeDoh]
|
||||
// or [DNSUpstreamTypePlain]. It defaults to [DNSUpstreamTypeDot].
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS providers.
|
||||
// It defaults to ["cloudflare"] and is ignored if the UpstreamType is
|
||||
// [DNSUpstreamTypePlain] and the UpstreamPlainAddresses field is set.
|
||||
Providers []string `json:"providers"`
|
||||
// Caching is true if the server should cache
|
||||
// DNS responses.
|
||||
Caching *bool `json:"caching"`
|
||||
// IPv6 is true if the server should connect over IPv6.
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
|
||||
// addresses to use by the built-in DNS server forwarder.
|
||||
// Note, if the upstream type is [dnsUpstreamTypePlain] and this field is set,
|
||||
// the Providers field is ignored.
|
||||
UpstreamPlainAddresses []netip.AddrPort
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
|
||||
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
|
||||
)
|
||||
|
||||
func (d DNS) validate() (err error) {
|
||||
err = d.DoT.validate()
|
||||
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
|
||||
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||
}
|
||||
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
selectedHasPlainIPv4, selectedHasPlainIPv6 := false, false
|
||||
for _, addrPort := range d.UpstreamPlainAddresses {
|
||||
if !selectedHasPlainIPv4 && addrPort.Addr().Is4() {
|
||||
selectedHasPlainIPv4 = true
|
||||
}
|
||||
if !selectedHasPlainIPv6 && addrPort.Addr().Is6() {
|
||||
selectedHasPlainIPv6 = true
|
||||
}
|
||||
if selectedHasPlainIPv4 && selectedHasPlainIPv6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case *d.IPv6 && !selectedHasPlainIPv6:
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
|
||||
case !*d.IPv6 && !selectedHasPlainIPv4:
|
||||
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
|
||||
}
|
||||
}
|
||||
// Note: all DNS built in providers have both IPv4 and IPv6 addresses for all modes
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating DoT settings: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -44,9 +98,13 @@ func (d DNS) validate() (err error) {
|
||||
|
||||
func (d *DNS) Copy() (copied DNS) {
|
||||
return DNS{
|
||||
ServerAddress: d.ServerAddress,
|
||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||
DoT: d.DoT.copy(),
|
||||
UpstreamType: d.UpstreamType,
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
UpstreamPlainAddresses: gosettings.CopySlice(d.UpstreamPlainAddresses),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,16 +112,30 @@ func (d *DNS) Copy() (copied DNS) {
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DNS) overrideWith(other DNS) {
|
||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.overrideWith(other.DoT)
|
||||
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
d.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
|
||||
}
|
||||
|
||||
func (d *DNS) setDefaults() {
|
||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||
d.DoT.setDefaults()
|
||||
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, defaultDNSProviders())
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
func defaultDNSProviders() []string {
|
||||
return []string{
|
||||
provider.Cloudflare().Name,
|
||||
}
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
@@ -72,30 +144,102 @@ func (d DNS) String() string {
|
||||
|
||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS settings:")
|
||||
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
|
||||
if *d.KeepNameserver {
|
||||
return node
|
||||
|
||||
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||
|
||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||
if len(d.UpstreamPlainAddresses) > 0 {
|
||||
if d.UpstreamType == DNSUpstreamTypePlain {
|
||||
for _, addr := range d.UpstreamPlainAddresses {
|
||||
upstreamResolvers.Append(addr.String())
|
||||
}
|
||||
} else {
|
||||
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, provider := range d.Providers {
|
||||
upstreamResolvers.Append(provider)
|
||||
}
|
||||
}
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
|
||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||
|
||||
update := "disabled"
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||
|
||||
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
|
||||
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||
|
||||
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.DoT.read(r)
|
||||
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.readUpstreamPlainAddresses(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
|
||||
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_TYPE=plain
|
||||
// for these to be used. This is an added safety measure to reduce misunderstandings, and
|
||||
// reduce odd settings overrides.
|
||||
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retro-compatibility - remove in v4
|
||||
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
|
||||
// upstream plain addresses, assuming port 53, and force the upstream type to plain
|
||||
// to maintain retro-compatibility behavior.
|
||||
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
|
||||
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
|
||||
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !serverAddress.IsValid() {
|
||||
return nil
|
||||
}
|
||||
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
|
||||
if isLocalhost {
|
||||
return nil
|
||||
}
|
||||
const defaultPlainPort = 53
|
||||
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
|
||||
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
|
||||
d.UpstreamType = DNSUpstreamTypePlain
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_defaultDNSProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
names := defaultDNSProviders()
|
||||
|
||||
found := false
|
||||
providers := provider.NewProviders()
|
||||
for _, name := range names {
|
||||
provider, err := providers.Get(name)
|
||||
require.NoError(t, err)
|
||||
if len(provider.Plain.IPv4) > 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
|
||||
}
|
||||
@@ -22,6 +22,11 @@ type DNSBlacklist struct {
|
||||
AddBlockedHosts []string
|
||||
AddBlockedIPs []netip.Addr
|
||||
AddBlockedIPPrefixes []netip.Prefix
|
||||
// RebindingProtectionExemptHostnames is a list of hostnames
|
||||
// exempt from DNS rebinding protection. It can contain parent
|
||||
// domains which are of the form "*.example.com". Note the wildcard
|
||||
// can only be used at the start of the hostname.
|
||||
RebindingProtectionExemptHostnames []string
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) setDefaults() {
|
||||
@@ -33,8 +38,9 @@ func (b *DNSBlacklist) setDefaults() {
|
||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||
|
||||
var (
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt host is not valid")
|
||||
)
|
||||
|
||||
func (b DNSBlacklist) validate() (err error) {
|
||||
@@ -50,18 +56,28 @@ func (b DNSBlacklist) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
if len(host) > 2 && host[:2] == "*." {
|
||||
host = host[2:]
|
||||
}
|
||||
if !hostRegex.MatchString(host) {
|
||||
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||
return DNSBlacklist{
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: gosettings.CopySlice(b.AddBlockedIPPrefixes),
|
||||
RebindingProtectionExemptHostnames: gosettings.CopySlice(b.RebindingProtectionExemptHostnames),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +89,8 @@ func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||
b.AddBlockedIPPrefixes = gosettings.OverrideWithSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
b.RebindingProtectionExemptHostnames = gosettings.OverrideWithSlice(b.RebindingProtectionExemptHostnames,
|
||||
other.RebindingProtectionExemptHostnames)
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
||||
@@ -129,6 +147,13 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.RebindingProtectionExemptHostnames) > 0 {
|
||||
exemptHostsNode := node.Append("Rebinding protection exempt hostnames:")
|
||||
for _, host := range b.RebindingProtectionExemptHostnames {
|
||||
exemptHostsNode.Append(host)
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -149,23 +174,47 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
||||
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||
|
||||
b.RebindingProtectionExemptHostnames = r.CSV("DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
||||
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
||||
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO v4 remove this block below
|
||||
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ips = append(ips, privateIPs...)
|
||||
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||
|
||||
return ips, ipPrefixes, nil
|
||||
}
|
||||
|
||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||
|
||||
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||
if len(privateAddresses) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// DoT contains settings to configure the DoT server.
|
||||
type DoT struct {
|
||||
// Enabled is true if the DoT server should be running
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
Enabled *bool
|
||||
// UpdatePeriod is the period to update DNS block lists.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Providers is a list of DNS 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")
|
||||
|
||||
func (d DoT) validate() (err error) {
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
providers := provider.NewProviders()
|
||||
for _, providerName := range d.Providers {
|
||||
_, err := providers.Get(providerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoT) copy() (copied DoT) {
|
||||
return DoT{
|
||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||
Providers: gosettings.CopySlice(d.Providers),
|
||||
Caching: gosettings.CopyPointer(d.Caching),
|
||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.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)
|
||||
}
|
||||
|
||||
func (d *DoT) setDefaults() {
|
||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||
provider.Cloudflare().Name,
|
||||
})
|
||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS over TLS settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
||||
if !*d.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
update := "disabled" //nolint:goconst
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (d *DoT) read(reader *reader.Reader) (err error) {
|
||||
d.Enabled, err = reader.BoolPtr("DOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = d.Blacklist.read(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -36,6 +36,8 @@ var (
|
||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
|
||||
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
|
||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||
|
||||
@@ -15,7 +15,7 @@ type Firewall struct {
|
||||
InputPorts []uint16
|
||||
OutboundSubnets []netip.Prefix
|
||||
Enabled *bool
|
||||
Debug *bool
|
||||
Iptables Iptables
|
||||
}
|
||||
|
||||
func (f Firewall) validate() (err error) {
|
||||
@@ -33,6 +33,11 @@ func (f Firewall) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = f.Iptables.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("iptables settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -51,7 +56,7 @@ func (f *Firewall) copy() (copied Firewall) {
|
||||
InputPorts: gosettings.CopySlice(f.InputPorts),
|
||||
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
|
||||
Enabled: gosettings.CopyPointer(f.Enabled),
|
||||
Debug: gosettings.CopyPointer(f.Debug),
|
||||
Iptables: f.Iptables.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +68,12 @@ func (f *Firewall) overrideWith(other Firewall) {
|
||||
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
|
||||
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
|
||||
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
|
||||
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
|
||||
f.Iptables.overrideWith(other.Iptables)
|
||||
}
|
||||
|
||||
func (f *Firewall) setDefaults() {
|
||||
func (f *Firewall) setDefaults(globalLogLevel string) {
|
||||
f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
|
||||
f.Debug = gosettings.DefaultPointer(f.Debug, false)
|
||||
f.Iptables.setDefaults(globalLogLevel)
|
||||
}
|
||||
|
||||
func (f Firewall) String() string {
|
||||
@@ -83,9 +88,7 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||
return node
|
||||
}
|
||||
|
||||
if *f.Debug {
|
||||
node.Appendf("Debug mode: on")
|
||||
}
|
||||
node.AppendNode(f.Iptables.toLinesNode())
|
||||
|
||||
if len(f.VPNInputPorts) > 0 {
|
||||
vpnInputPortsNode := node.Appendf("VPN input ports:")
|
||||
@@ -133,9 +136,9 @@ func (f *Firewall) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
|
||||
err = f.Iptables.read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reading iptables settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -15,7 +16,10 @@ func Test_Firewall_validate(t *testing.T) {
|
||||
errWrapped error
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {},
|
||||
"empty": {
|
||||
errWrapped: log.ErrLevelNotRecognized,
|
||||
errMessage: "iptables settings: log level: level is not recognized: ",
|
||||
},
|
||||
"zero_vpn_input_port": {
|
||||
firewall: Firewall{
|
||||
VPNInputPorts: []uint16{0},
|
||||
@@ -41,6 +45,7 @@ func Test_Firewall_validate(t *testing.T) {
|
||||
},
|
||||
"public_outbound_subnet": {
|
||||
firewall: Firewall{
|
||||
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
|
||||
OutboundSubnets: []netip.Prefix{
|
||||
netip.MustParsePrefix("1.2.3.4/32"),
|
||||
},
|
||||
@@ -48,6 +53,7 @@ func Test_Firewall_validate(t *testing.T) {
|
||||
},
|
||||
"valid_settings": {
|
||||
firewall: Firewall{
|
||||
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
|
||||
VPNInputPorts: []uint16{100, 101},
|
||||
InputPorts: []uint16{200, 201},
|
||||
OutboundSubnets: []netip.Prefix{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
@@ -17,34 +18,51 @@ type Health struct {
|
||||
// for the health check server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ServerAddress string
|
||||
// ReadHeaderTimeout is the HTTP server header read timeout
|
||||
// duration of the HTTP server. It defaults to 100 milliseconds.
|
||||
ReadHeaderTimeout time.Duration
|
||||
// ReadTimeout is the HTTP read timeout duration of the
|
||||
// HTTP server. It defaults to 500 milliseconds.
|
||||
ReadTimeout time.Duration
|
||||
// TargetAddress is the address (host or host:port)
|
||||
// to TCP dial to periodically for the health check.
|
||||
// It cannot be the empty string in the internal state.
|
||||
TargetAddress string
|
||||
// SuccessWait is the duration to wait to re-run the
|
||||
// healthcheck after a successful healthcheck.
|
||||
// It defaults to 5 seconds and cannot be zero in
|
||||
// the internal state.
|
||||
SuccessWait time.Duration
|
||||
// VPN has health settings specific to the VPN loop.
|
||||
VPN HealthyWait
|
||||
// TargetAddresses are the addresses (host or host:port)
|
||||
// to TCP TLS dial to periodically for the health check.
|
||||
// Addresses after the first one are used as fallbacks for retries.
|
||||
// It cannot be empty in the internal state.
|
||||
TargetAddresses []string
|
||||
// ICMPTargetIPs are the IP addresses to use for ICMP echo requests
|
||||
// in the health checker. The slice can be set to a single
|
||||
// unspecified address (0.0.0.0) such that the VPN server IP is used,
|
||||
// although this can be less reliable. It defaults to [1.1.1.1,8.8.8.8],
|
||||
// and cannot be left empty in the internal state.
|
||||
ICMPTargetIPs []netip.Addr
|
||||
// SmallCheckType is the type of small health check to perform.
|
||||
// It can be "icmp" or "dns", and defaults to "icmp".
|
||||
// Note it changes automatically to dns if icmp is not supported.
|
||||
SmallCheckType string
|
||||
// RestartVPN indicates whether to restart the VPN connection
|
||||
// when the healthcheck fails.
|
||||
RestartVPN *bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
|
||||
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
|
||||
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
|
||||
)
|
||||
|
||||
func (h Health) Validate() (err error) {
|
||||
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
|
||||
if err != nil {
|
||||
return fmt.Errorf("server listening address is not valid: %w", err)
|
||||
}
|
||||
|
||||
err = h.VPN.validate()
|
||||
for _, ip := range h.ICMPTargetIPs {
|
||||
switch {
|
||||
case !ip.IsValid():
|
||||
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
|
||||
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
|
||||
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
|
||||
ErrICMPTargetIPsNotCompatible)
|
||||
}
|
||||
}
|
||||
|
||||
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
|
||||
if err != nil {
|
||||
return fmt.Errorf("health VPN settings: %w", err)
|
||||
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -52,12 +70,11 @@ func (h Health) Validate() (err error) {
|
||||
|
||||
func (h *Health) copy() (copied Health) {
|
||||
return Health{
|
||||
ServerAddress: h.ServerAddress,
|
||||
ReadHeaderTimeout: h.ReadHeaderTimeout,
|
||||
ReadTimeout: h.ReadTimeout,
|
||||
TargetAddress: h.TargetAddress,
|
||||
SuccessWait: h.SuccessWait,
|
||||
VPN: h.VPN.copy(),
|
||||
ServerAddress: h.ServerAddress,
|
||||
TargetAddresses: h.TargetAddresses,
|
||||
ICMPTargetIPs: gosettings.CopySlice(h.ICMPTargetIPs),
|
||||
SmallCheckType: h.SmallCheckType,
|
||||
RestartVPN: gosettings.CopyPointer(h.RestartVPN),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,23 +83,21 @@ func (h *Health) copy() (copied Health) {
|
||||
// settings.
|
||||
func (h *Health) OverrideWith(other Health) {
|
||||
h.ServerAddress = gosettings.OverrideWithComparable(h.ServerAddress, other.ServerAddress)
|
||||
h.ReadHeaderTimeout = gosettings.OverrideWithComparable(h.ReadHeaderTimeout, other.ReadHeaderTimeout)
|
||||
h.ReadTimeout = gosettings.OverrideWithComparable(h.ReadTimeout, other.ReadTimeout)
|
||||
h.TargetAddress = gosettings.OverrideWithComparable(h.TargetAddress, other.TargetAddress)
|
||||
h.SuccessWait = gosettings.OverrideWithComparable(h.SuccessWait, other.SuccessWait)
|
||||
h.VPN.overrideWith(other.VPN)
|
||||
h.TargetAddresses = gosettings.OverrideWithSlice(h.TargetAddresses, other.TargetAddresses)
|
||||
h.ICMPTargetIPs = gosettings.OverrideWithSlice(h.ICMPTargetIPs, other.ICMPTargetIPs)
|
||||
h.SmallCheckType = gosettings.OverrideWithComparable(h.SmallCheckType, other.SmallCheckType)
|
||||
h.RestartVPN = gosettings.OverrideWithPointer(h.RestartVPN, other.RestartVPN)
|
||||
}
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = gosettings.DefaultComparable(h.ServerAddress, "127.0.0.1:9999")
|
||||
const defaultReadHeaderTimeout = 100 * time.Millisecond
|
||||
h.ReadHeaderTimeout = gosettings.DefaultComparable(h.ReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
const defaultReadTimeout = 500 * time.Millisecond
|
||||
h.ReadTimeout = gosettings.DefaultComparable(h.ReadTimeout, defaultReadTimeout)
|
||||
h.TargetAddress = gosettings.DefaultComparable(h.TargetAddress, "cloudflare.com:443")
|
||||
const defaultSuccessWait = 5 * time.Second
|
||||
h.SuccessWait = gosettings.DefaultComparable(h.SuccessWait, defaultSuccessWait)
|
||||
h.VPN.setDefaults()
|
||||
h.TargetAddresses = gosettings.DefaultSlice(h.TargetAddresses, []string{"cloudflare.com:443", "github.com:443"})
|
||||
h.ICMPTargetIPs = gosettings.DefaultSlice(h.ICMPTargetIPs, []netip.Addr{
|
||||
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
||||
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
||||
})
|
||||
h.SmallCheckType = gosettings.DefaultComparable(h.SmallCheckType, "icmp")
|
||||
h.RestartVPN = gosettings.DefaultPointer(h.RestartVPN, true)
|
||||
}
|
||||
|
||||
func (h Health) String() string {
|
||||
@@ -92,28 +107,40 @@ func (h Health) String() string {
|
||||
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Health settings:")
|
||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||
node.Appendf("Target address: %s", h.TargetAddress)
|
||||
node.Appendf("Duration to wait after success: %s", h.SuccessWait)
|
||||
node.Appendf("Read header timeout: %s", h.ReadHeaderTimeout)
|
||||
node.Appendf("Read timeout: %s", h.ReadTimeout)
|
||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||
targetAddrs := node.Appendf("Target addresses:")
|
||||
for _, targetAddr := range h.TargetAddresses {
|
||||
targetAddrs.Append(targetAddr)
|
||||
}
|
||||
switch h.SmallCheckType {
|
||||
case "icmp":
|
||||
icmpNode := node.Appendf("Small health check type: ICMP echo request")
|
||||
if len(h.ICMPTargetIPs) == 1 && h.ICMPTargetIPs[0].IsUnspecified() {
|
||||
icmpNode.Appendf("ICMP target IP: VPN server IP address")
|
||||
} else {
|
||||
icmpIPs := icmpNode.Appendf("ICMP target IPs:")
|
||||
for _, ip := range h.ICMPTargetIPs {
|
||||
icmpIPs.Append(ip.String())
|
||||
}
|
||||
}
|
||||
case "dns":
|
||||
node.Appendf("Small health check type: Plain DNS lookup over UDP")
|
||||
}
|
||||
node.Appendf("Restart VPN on healthcheck failure: %s", gosettings.BoolToYesNo(h.RestartVPN))
|
||||
return node
|
||||
}
|
||||
|
||||
func (h *Health) Read(r *reader.Reader) (err error) {
|
||||
h.ServerAddress = r.String("HEALTH_SERVER_ADDRESS")
|
||||
h.TargetAddress = r.String("HEALTH_TARGET_ADDRESS",
|
||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING"))
|
||||
|
||||
h.SuccessWait, err = r.Duration("HEALTH_SUCCESS_WAIT_DURATION")
|
||||
h.TargetAddresses = r.CSV("HEALTH_TARGET_ADDRESSES",
|
||||
reader.RetroKeys("HEALTH_ADDRESS_TO_PING", "HEALTH_TARGET_ADDRESS"))
|
||||
h.ICMPTargetIPs, err = r.CSVNetipAddresses("HEALTH_ICMP_TARGET_IPS", reader.RetroKeys("HEALTH_ICMP_TARGET_IP"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.VPN.read(r)
|
||||
h.SmallCheckType = r.String("HEALTH_SMALL_CHECK_TYPE")
|
||||
h.RestartVPN, err = r.BoolPtr("HEALTH_RESTART_VPN")
|
||||
if err != nil {
|
||||
return fmt.Errorf("VPN health settings: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type HealthyWait struct {
|
||||
// Initial is the initial duration to wait for the program
|
||||
// to be healthy before taking action.
|
||||
// It cannot be nil in the internal state.
|
||||
Initial *time.Duration
|
||||
// Addition is the duration to add to the Initial duration
|
||||
// after Initial has expired to wait longer for the program
|
||||
// to be healthy.
|
||||
// It cannot be nil in the internal state.
|
||||
Addition *time.Duration
|
||||
}
|
||||
|
||||
func (h HealthyWait) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||
return HealthyWait{
|
||||
Initial: gosettings.CopyPointer(h.Initial),
|
||||
Addition: gosettings.CopyPointer(h.Addition),
|
||||
}
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
||||
h.Initial = gosettings.OverrideWithPointer(h.Initial, other.Initial)
|
||||
h.Addition = gosettings.OverrideWithPointer(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
func (h *HealthyWait) setDefaults() {
|
||||
const initialDurationDefault = 6 * time.Second
|
||||
const additionDurationDefault = 5 * time.Second
|
||||
h.Initial = gosettings.DefaultPointer(h.Initial, initialDurationDefault)
|
||||
h.Addition = gosettings.DefaultPointer(h.Addition, additionDurationDefault)
|
||||
}
|
||||
|
||||
func (h HealthyWait) String() string {
|
||||
return h.toLinesNode("Health").String()
|
||||
}
|
||||
|
||||
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
||||
node = gotree.New(kind + " wait durations:")
|
||||
node.Appendf("Initial duration: %s", *h.Initial)
|
||||
node.Appendf("Additional duration: %s", *h.Addition)
|
||||
return node
|
||||
}
|
||||
|
||||
func (h *HealthyWait) read(r *reader.Reader) (err error) {
|
||||
h.Initial, err = r.DurationPtr(
|
||||
"HEALTH_VPN_DURATION_INITIAL",
|
||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Addition, err = r.DurationPtr(
|
||||
"HEALTH_VPN_DURATION_ADDITION",
|
||||
reader.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
// Iptables contains settings to customize iptables.
|
||||
type Iptables struct {
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
func (i Iptables) validate() (err error) {
|
||||
_, err = log.ParseLevel(i.LogLevel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("log level: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Iptables) copy() (copied Iptables) {
|
||||
return Iptables{
|
||||
LogLevel: i.LogLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Iptables) overrideWith(other Iptables) {
|
||||
i.LogLevel = gosettings.OverrideWithComparable(i.LogLevel, other.LogLevel)
|
||||
}
|
||||
|
||||
func (i *Iptables) setDefaults(globalLogLevel string) {
|
||||
defaultLevel := globalLogLevel
|
||||
if defaultLevel == log.LevelDebug.String() {
|
||||
// Given iptables debug logger is quite verbose, we only turn it to debug level
|
||||
// if it is explicitly asked to be at debug level; even if the global logger is
|
||||
// at the debug level, we keep iptables at info level by default.
|
||||
defaultLevel = log.LevelInfo.String()
|
||||
}
|
||||
i.LogLevel = gosettings.DefaultComparable(i.LogLevel, defaultLevel)
|
||||
}
|
||||
|
||||
func (i Iptables) String() string {
|
||||
return i.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (i Iptables) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Iptables settings:")
|
||||
node.Appendf("Log level: %s", i.LogLevel)
|
||||
return node
|
||||
}
|
||||
|
||||
func (i *Iptables) read(r *reader.Reader) (err error) {
|
||||
debugMode, err := r.BoolPtr("FIREWALL_DEBUG", reader.IsRetro("FIREWALL_IPTABLES_LOG_LEVEL"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugMode != nil && *debugMode {
|
||||
i.LogLevel = log.LevelDebug.String()
|
||||
}
|
||||
i.LogLevel = r.String("FIREWALL_IPTABLES_LOG_LEVEL")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// IPv6 contains settings regarding IPv6 configuration.
|
||||
type IPv6 struct {
|
||||
// CheckAddresses are the TCP ip:port addresses to dial to check if
|
||||
// IPv6 is supported, in case a default IPv6 route is found.
|
||||
// It defaults to google and cloudflare IPv6 anycast addresses
|
||||
// [2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53
|
||||
CheckAddresses []netip.AddrPort
|
||||
}
|
||||
|
||||
func (i IPv6) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IPv6) copy() (copied IPv6) {
|
||||
return IPv6{
|
||||
CheckAddresses: gosettings.CopySlice(i.CheckAddresses),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IPv6) overrideWith(other IPv6) {
|
||||
i.CheckAddresses = gosettings.OverrideWithSlice(i.CheckAddresses, other.CheckAddresses)
|
||||
}
|
||||
|
||||
func (i *IPv6) setDefaults() {
|
||||
defaultCheckAddresses := []netip.AddrPort{
|
||||
netip.MustParseAddrPort("[2001:4860:4860::8888]:53"),
|
||||
netip.MustParseAddrPort("[2606:4700:4700::1111]:53"),
|
||||
}
|
||||
i.CheckAddresses = gosettings.DefaultSlice(i.CheckAddresses, defaultCheckAddresses)
|
||||
}
|
||||
|
||||
func (i IPv6) String() string {
|
||||
return i.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (i IPv6) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("IPv6 settings:")
|
||||
addrsNode := node.Appendf("Check addresses:")
|
||||
for _, addr := range i.CheckAddresses {
|
||||
addrsNode.Append(addr.String())
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (i *IPv6) read(r *reader.Reader) (err error) {
|
||||
i.CheckAddresses, err = r.CSVNetipAddrPorts("IPV6_CHECK_ADDRESSES")
|
||||
return err
|
||||
}
|
||||
@@ -209,8 +209,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
|
||||
case
|
||||
providers.Airvpn,
|
||||
providers.Cyberghost,
|
||||
providers.VPNUnlimited,
|
||||
providers.Wevpn:
|
||||
providers.VPNUnlimited:
|
||||
if clientKey == "" {
|
||||
return fmt.Errorf("%w", ErrMissingValue)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
@@ -24,6 +25,12 @@ type OpenVPNSelection struct {
|
||||
// and can be udp or tcp. It cannot be the empty string
|
||||
// in the internal state.
|
||||
Protocol string `json:"protocol"`
|
||||
// EndpointIP is the server endpoint IP address.
|
||||
// If set, it overrides any IP address from the picked
|
||||
// built-in server connection. To indicate it should
|
||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||
// It can never be the zero value in the internal state.
|
||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||
// CustomPort is the OpenVPN server endpoint port.
|
||||
// It can be set to 0 to indicate no custom port should
|
||||
// be used. It cannot be nil in the internal state.
|
||||
@@ -53,7 +60,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Giganews,
|
||||
providers.Ipvanish,
|
||||
providers.Perfectprivacy,
|
||||
providers.Privado,
|
||||
providers.Vyprvpn,
|
||||
) {
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
@@ -68,8 +74,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Privatevpn, providers.Torguard:
|
||||
// no custom port allowed
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Privado, providers.Purevpn,
|
||||
providers.Giganews, providers.Ipvanish,
|
||||
providers.Nordvpn, providers.Purevpn,
|
||||
providers.Surfshark, providers.VPNSecure,
|
||||
providers.VPNUnlimited, providers.Vyprvpn:
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
@@ -92,18 +98,18 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Perfectprivacy:
|
||||
allowedTCP = []uint16{44, 443, 4433}
|
||||
allowedUDP = []uint16{44, 443, 4433}
|
||||
case providers.Privado:
|
||||
allowedTCP = []uint16{443, 1194, 8080, 8443}
|
||||
allowedUDP = []uint16{443, 1194, 8080, 8443}
|
||||
case providers.PrivateInternetAccess:
|
||||
allowedTCP = []uint16{80, 110, 443}
|
||||
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
|
||||
case providers.Protonvpn:
|
||||
allowedTCP = []uint16{443, 5995, 8443}
|
||||
allowedUDP = []uint16{80, 443, 1194, 4569, 5060}
|
||||
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
|
||||
case providers.SlickVPN:
|
||||
allowedTCP = []uint16{443, 8080, 8888}
|
||||
allowedUDP = []uint16{443, 8080, 8888}
|
||||
case providers.Wevpn:
|
||||
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||
allowedUDP = []uint16{80, 1194, 1198}
|
||||
case providers.Windscribe:
|
||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||
@@ -126,7 +132,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
// Validate EncPreset
|
||||
if vpnProvider == providers.PrivateInternetAccess {
|
||||
validEncryptionPresets := []string{
|
||||
presets.None,
|
||||
presets.Normal,
|
||||
presets.Strong,
|
||||
}
|
||||
@@ -142,6 +147,7 @@ func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
||||
return OpenVPNSelection{
|
||||
ConfFile: gosettings.CopyPointer(o.ConfFile),
|
||||
Protocol: o.Protocol,
|
||||
EndpointIP: o.EndpointIP,
|
||||
CustomPort: gosettings.CopyPointer(o.CustomPort),
|
||||
PIAEncPreset: gosettings.CopyPointer(o.PIAEncPreset),
|
||||
}
|
||||
@@ -151,12 +157,14 @@ func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
||||
o.ConfFile = gosettings.OverrideWithPointer(o.ConfFile, other.ConfFile)
|
||||
o.Protocol = gosettings.OverrideWithComparable(o.Protocol, other.Protocol)
|
||||
o.CustomPort = gosettings.OverrideWithPointer(o.CustomPort, other.CustomPort)
|
||||
o.EndpointIP = gosettings.OverrideWithValidator(o.EndpointIP, other.EndpointIP)
|
||||
o.PIAEncPreset = gosettings.OverrideWithPointer(o.PIAEncPreset, other.PIAEncPreset)
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||
o.ConfFile = gosettings.DefaultPointer(o.ConfFile, "")
|
||||
o.Protocol = gosettings.DefaultComparable(o.Protocol, constants.UDP)
|
||||
o.EndpointIP = gosettings.DefaultValidator(o.EndpointIP, netip.IPv4Unspecified())
|
||||
o.CustomPort = gosettings.DefaultPointer(o.CustomPort, 0)
|
||||
|
||||
var defaultEncPreset string
|
||||
@@ -174,6 +182,10 @@ func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OpenVPN server selection settings:")
|
||||
node.Appendf("Protocol: %s", strings.ToUpper(o.Protocol))
|
||||
|
||||
if !o.EndpointIP.IsUnspecified() {
|
||||
node.Appendf("Endpoint IP address: %s", o.EndpointIP)
|
||||
}
|
||||
|
||||
if *o.CustomPort != 0 {
|
||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||
}
|
||||
@@ -194,6 +206,12 @@ func (o *OpenVPNSelection) read(r *reader.Reader) (err error) {
|
||||
|
||||
o.Protocol = r.String("OPENVPN_PROTOCOL", reader.RetroKeys("PROTOCOL"))
|
||||
|
||||
o.EndpointIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.CustomPort, err = r.Uint16Ptr("OPENVPN_ENDPOINT_PORT",
|
||||
reader.RetroKeys("PORT", "OPENVPN_PORT", "VPN_ENDPOINT_PORT"))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// PMTUD contains settings to configure Path MTU Discovery.
|
||||
type PMTUD struct {
|
||||
// ICMPAddresses is the redundancy list of addresses to use
|
||||
// for ICMP path MTU discovery. Each address MUST handle ICMP
|
||||
// packets for PMTUD to work.
|
||||
// It cannot be nil in the internal state.
|
||||
ICMPAddresses []netip.Addr `json:"icmp_addresses"`
|
||||
// TCPAddresses is the redundancy list of addresses to use
|
||||
// for TCP path MTU discovery. Each address MUST have a listening
|
||||
// TCP server on the port specified.
|
||||
// It cannot be nil in the internal state.
|
||||
TCPAddresses []netip.AddrPort `json:"tcp_addresses"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPMTUDICMPAddressNotValid = errors.New("PMTUD ICMP address is not valid")
|
||||
ErrPMTUDTCPAddressNotValid = errors.New("PMTUD TCP address is not valid")
|
||||
)
|
||||
|
||||
// Validate validates PMTUD settings.
|
||||
func (p PMTUD) validate() (err error) {
|
||||
for i, addr := range p.ICMPAddresses {
|
||||
if !addr.IsValid() {
|
||||
return fmt.Errorf("%w: at index %d", ErrPMTUDICMPAddressNotValid, i)
|
||||
}
|
||||
}
|
||||
for i, addr := range p.TCPAddresses {
|
||||
if !addr.IsValid() {
|
||||
return fmt.Errorf("%w: at index %d", ErrPMTUDTCPAddressNotValid, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PMTUD) copy() (copied PMTUD) {
|
||||
return PMTUD{
|
||||
ICMPAddresses: gosettings.CopySlice(p.ICMPAddresses),
|
||||
TCPAddresses: gosettings.CopySlice(p.TCPAddresses),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PMTUD) overrideWith(other PMTUD) {
|
||||
p.ICMPAddresses = gosettings.OverrideWithSlice(p.ICMPAddresses, other.ICMPAddresses)
|
||||
p.TCPAddresses = gosettings.OverrideWithSlice(p.TCPAddresses, other.TCPAddresses)
|
||||
}
|
||||
|
||||
func (p *PMTUD) setDefaults() {
|
||||
defaultICMPAddresses := []netip.Addr{
|
||||
netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
||||
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
||||
}
|
||||
p.ICMPAddresses = gosettings.DefaultSlice(p.ICMPAddresses, defaultICMPAddresses)
|
||||
|
||||
const dnsPort, tlsPort = 53, 443
|
||||
defaultTCPAddresses := []netip.AddrPort{
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), dnsPort),
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 8, 8, 8}), dnsPort),
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), tlsPort),
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{8, 8, 8, 8}), tlsPort),
|
||||
netip.AddrPortFrom(netip.MustParseAddr("2606:4700:4700::1111"), dnsPort),
|
||||
netip.AddrPortFrom(netip.MustParseAddr("2001:4860:4860::8888"), dnsPort),
|
||||
netip.AddrPortFrom(netip.MustParseAddr("2606:4700:4700::1111"), tlsPort),
|
||||
netip.AddrPortFrom(netip.MustParseAddr("2001:4860:4860::8888"), tlsPort),
|
||||
}
|
||||
p.TCPAddresses = gosettings.DefaultSlice(p.TCPAddresses, defaultTCPAddresses)
|
||||
}
|
||||
|
||||
func (p PMTUD) String() string {
|
||||
return p.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (p PMTUD) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Path MTU discovery:")
|
||||
|
||||
icmpAddrNode := node.Append("ICMP addresses:")
|
||||
for _, addr := range p.ICMPAddresses {
|
||||
icmpAddrNode.Append(addr.String())
|
||||
}
|
||||
|
||||
tcpAddrNode := node.Append("TCP addresses:")
|
||||
for _, addr := range p.TCPAddresses {
|
||||
tcpAddrNode.Append(addr.String())
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *PMTUD) read(r *reader.Reader) (err error) {
|
||||
p.ICMPAddresses, err = r.CSVNetipAddresses("PMTUD_ICMP_ADDRESSES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.TCPAddresses, err = r.CSVNetipAddrPorts("PMTUD_TCP_ADDRESSES")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -29,16 +31,36 @@ type PortForwarding struct {
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
Filepath *string `json:"status_file_path"`
|
||||
// ListeningPort is the port traffic would be redirected to from the
|
||||
// forwarded port. The redirection is disabled if it is set to 0, which
|
||||
// is its default as well.
|
||||
ListeningPort *uint16 `json:"listening_port"`
|
||||
// UpCommand is the command to use when the port forwarding is up.
|
||||
// It can be the empty string to indicate not to run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
UpCommand *string `json:"up_command"`
|
||||
// DownCommand is the command to use after the port forwarding goes down.
|
||||
// It can be the empty string to indicate to NOT run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
DownCommand *string `json:"down_command"`
|
||||
// ListeningPorts are the ports traffic would be redirected to from the
|
||||
// forwarded ports. The redirection is disabled if it is the slice [0],
|
||||
// which is its default as well. If set and not [0], its length must match
|
||||
// the PortsCount value, such that each forwarded port is redirected to
|
||||
// the corresponding listening port.
|
||||
ListeningPorts []uint16 `json:"listening_port"`
|
||||
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
|
||||
// and be between 1 and 5. For other providers, it must be set to 1 if port
|
||||
// forwarding is enabled.
|
||||
PortsCount uint16 `json:"ports_count"`
|
||||
// Username is only used for Private Internet Access port forwarding.
|
||||
Username string `json:"username"`
|
||||
// Password is only used for Private Internet Access port forwarding.
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPortsCountTooHigh = errors.New("ports count too high")
|
||||
ErrListeningPortsLen = errors.New("listening ports length must be equal to ports count")
|
||||
ErrListeningPortZero = errors.New("listening port cannot be 0")
|
||||
)
|
||||
|
||||
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
if !*p.Enabled {
|
||||
return nil
|
||||
@@ -67,13 +89,36 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if providerSelected == providers.PrivateInternetAccess {
|
||||
switch providerSelected {
|
||||
case providers.PrivateInternetAccess:
|
||||
const maxPortsCount = 1
|
||||
switch {
|
||||
case p.PortsCount > maxPortsCount:
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
case p.Username == "":
|
||||
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
|
||||
case p.Password == "":
|
||||
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
|
||||
}
|
||||
case providers.Protonvpn:
|
||||
const maxPortsCount = 4
|
||||
if p.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
}
|
||||
default:
|
||||
const maxPortsCount = 1
|
||||
if p.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("%w: %d > %d", ErrPortsCountTooHigh, p.PortsCount, maxPortsCount)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
||||
switch {
|
||||
case len(p.ListeningPorts) != int(p.PortsCount):
|
||||
return fmt.Errorf("%w: %d != %d", ErrListeningPortsLen, len(p.ListeningPorts), p.PortsCount)
|
||||
case slices.Contains(p.ListeningPorts, 0):
|
||||
return fmt.Errorf("%w: in %v", ErrListeningPortZero, p.ListeningPorts)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -81,12 +126,14 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
|
||||
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||
return PortForwarding{
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
UpCommand: gosettings.CopyPointer(p.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(p.DownCommand),
|
||||
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
|
||||
Username: p.Username,
|
||||
Password: p.Password,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +141,9 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
|
||||
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
|
||||
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
|
||||
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts)
|
||||
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
|
||||
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
|
||||
}
|
||||
@@ -103,7 +152,10 @@ func (p *PortForwarding) setDefaults() {
|
||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
|
||||
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
|
||||
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
|
||||
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
|
||||
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
|
||||
}
|
||||
|
||||
func (p PortForwarding) String() string {
|
||||
@@ -117,11 +169,14 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
node = gotree.New("Automatic port forwarding settings:")
|
||||
|
||||
listeningPort := "disabled"
|
||||
if *p.ListeningPort != 0 {
|
||||
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
||||
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
|
||||
|
||||
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
|
||||
redirNode := node.Appendf("Redirection for listening ports:")
|
||||
for i, port := range p.ListeningPorts {
|
||||
redirNode.Appendf("Port #%d -> %d", i+1, port)
|
||||
}
|
||||
}
|
||||
node.Appendf("Redirection listening port: %s", listeningPort)
|
||||
|
||||
if *p.Provider == "" {
|
||||
node.Appendf("Use port forwarding code for current provider")
|
||||
@@ -135,6 +190,13 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
node.Appendf("Forwarded port file path: %s", filepath)
|
||||
|
||||
if *p.UpCommand != "" {
|
||||
node.Appendf("Forwarded port up command: %s", *p.UpCommand)
|
||||
}
|
||||
if *p.DownCommand != "" {
|
||||
node.Appendf("Forwarded port down command: %s", *p.DownCommand)
|
||||
}
|
||||
|
||||
if p.Username != "" {
|
||||
credentialsNode := node.Appendf("Credentials:")
|
||||
credentialsNode.Appendf("Username: %s", p.Username)
|
||||
@@ -163,7 +225,19 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
|
||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||
))
|
||||
|
||||
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||
p.UpCommand = r.Get("VPN_PORT_FORWARDING_UP_COMMAND",
|
||||
reader.ForceLowercase(false))
|
||||
|
||||
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
|
||||
reader.ForceLowercase(false))
|
||||
|
||||
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
|
||||
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -28,10 +30,18 @@ type Provider struct {
|
||||
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
|
||||
// Validate Name
|
||||
var validNames []string
|
||||
if vpnType == vpn.OpenVPN {
|
||||
switch vpnType {
|
||||
case vpn.AmneziaWg:
|
||||
validNames = []string{providers.Custom}
|
||||
case vpn.OpenVPN:
|
||||
validNames = providers.AllWithCustom()
|
||||
validNames = append(validNames, "pia") // Retro-compatibility
|
||||
} else { // Wireguard
|
||||
// Remove Mullvad since it no longer supports OpenVPN as of January 15th, 2026
|
||||
mullvadIndex := slices.Index(validNames, providers.Mullvad)
|
||||
validNames[mullvadIndex], validNames[len(validNames)-1] = validNames[len(validNames)-1], validNames[mullvadIndex]
|
||||
validNames = validNames[:len(validNames)-1]
|
||||
sort.Strings(validNames)
|
||||
case vpn.Wireguard:
|
||||
validNames = []string{
|
||||
providers.Airvpn,
|
||||
providers.Custom,
|
||||
@@ -45,7 +55,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
||||
}
|
||||
}
|
||||
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
|
||||
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||
return fmt.Errorf("%w for %s: %w", ErrVPNProviderNameNotValid, vpnType, err)
|
||||
}
|
||||
|
||||
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -24,6 +27,9 @@ type ControlServer struct {
|
||||
// It cannot be empty in the internal state and defaults to
|
||||
// /gluetun/auth/config.toml.
|
||||
AuthFilePath string
|
||||
// AuthDefaultRole is a JSON encoded object defining the default role
|
||||
// that applies to all routes without a previously user-defined role assigned to.
|
||||
AuthDefaultRole string
|
||||
}
|
||||
|
||||
func (c ControlServer) validate() (err error) {
|
||||
@@ -44,14 +50,30 @@ func (c ControlServer) validate() (err error) {
|
||||
ErrControlServerPrivilegedPort, port, uid)
|
||||
}
|
||||
|
||||
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
|
||||
jsonDecoder.DisallowUnknownFields()
|
||||
var role auth.Role
|
||||
err = jsonDecoder.Decode(&role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid JSON: %w", err)
|
||||
}
|
||||
|
||||
if role.Auth != "" {
|
||||
err = role.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("default authentication role is not valid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlServer) copy() (copied ControlServer) {
|
||||
return ControlServer{
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
Address: gosettings.CopyPointer(c.Address),
|
||||
Log: gosettings.CopyPointer(c.Log),
|
||||
AuthFilePath: c.AuthFilePath,
|
||||
AuthDefaultRole: c.AuthDefaultRole,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +84,21 @@ 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)
|
||||
c.AuthDefaultRole = gosettings.OverrideWithComparable(c.AuthDefaultRole, other.AuthDefaultRole)
|
||||
}
|
||||
|
||||
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")
|
||||
c.AuthDefaultRole = gosettings.DefaultComparable(c.AuthDefaultRole, "{}")
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
role.Name = "default"
|
||||
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
|
||||
c.AuthDefaultRole = string(roleBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (c ControlServer) String() string {
|
||||
@@ -79,6 +110,11 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||
node.Appendf("Listening address: %s", *c.Address)
|
||||
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
|
||||
node.Appendf("Authentication file path: %s", c.AuthFilePath)
|
||||
if c.AuthDefaultRole != "{}" {
|
||||
var role auth.Role
|
||||
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
|
||||
node.AppendNode(role.ToLinesNode())
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -91,6 +127,7 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
|
||||
c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")
|
||||
|
||||
c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
|
||||
c.AuthDefaultRole = r.String("HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE", reader.ForceLowercase(false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package settings
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
@@ -17,17 +16,11 @@ import (
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type ServerSelection struct { //nolint:maligned
|
||||
type ServerSelection struct {
|
||||
// VPN is the VPN type which can be 'openvpn'
|
||||
// or 'wireguard'. It cannot be the empty string
|
||||
// in the internal state.
|
||||
VPN string `json:"vpn"`
|
||||
// TargetIP is the server endpoint IP address to use.
|
||||
// It will override any IP address from the picked
|
||||
// built-in server. It cannot be the empty value in the internal
|
||||
// state, and can be set to the unspecified address to indicate
|
||||
// there is not target IP address to use.
|
||||
TargetIP netip.Addr `json:"target_ip"`
|
||||
// Countries is the list of countries to filter VPN servers with.
|
||||
Countries []string `json:"countries"`
|
||||
// Categories is the list of categories to filter VPN servers with.
|
||||
@@ -94,7 +87,7 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
filterChoicesGetter FilterChoicesGetter, warner Warner,
|
||||
) (err error) {
|
||||
switch ss.VPN {
|
||||
case vpn.OpenVPN, vpn.Wireguard:
|
||||
case vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard:
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||
}
|
||||
@@ -299,7 +292,6 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
return ServerSelection{
|
||||
VPN: ss.VPN,
|
||||
TargetIP: ss.TargetIP,
|
||||
Countries: gosettings.CopySlice(ss.Countries),
|
||||
Categories: gosettings.CopySlice(ss.Categories),
|
||||
Regions: gosettings.CopySlice(ss.Regions),
|
||||
@@ -323,7 +315,6 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
|
||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
ss.VPN = gosettings.OverrideWithComparable(ss.VPN, other.VPN)
|
||||
ss.TargetIP = gosettings.OverrideWithValidator(ss.TargetIP, other.TargetIP)
|
||||
ss.Countries = gosettings.OverrideWithSlice(ss.Countries, other.Countries)
|
||||
ss.Categories = gosettings.OverrideWithSlice(ss.Categories, other.Categories)
|
||||
ss.Regions = gosettings.OverrideWithSlice(ss.Regions, other.Regions)
|
||||
@@ -346,7 +337,6 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
|
||||
func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled bool) {
|
||||
ss.VPN = gosettings.DefaultComparable(ss.VPN, vpn.OpenVPN)
|
||||
ss.TargetIP = gosettings.DefaultValidator(ss.TargetIP, netip.IPv4Unspecified())
|
||||
ss.OwnedOnly = gosettings.DefaultPointer(ss.OwnedOnly, false)
|
||||
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||
@@ -354,11 +344,8 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
||||
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
|
||||
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
|
||||
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||
defaultPortForwardOnly := false
|
||||
if portForwardingEnabled && helpers.IsOneOf(vpnProvider,
|
||||
providers.PrivateInternetAccess, providers.Protonvpn) {
|
||||
defaultPortForwardOnly = true
|
||||
}
|
||||
defaultPortForwardOnly := portForwardingEnabled &&
|
||||
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||
ss.OpenVPN.setDefaults(vpnProvider)
|
||||
ss.Wireguard.setDefaults()
|
||||
@@ -371,9 +358,6 @@ func (ss ServerSelection) String() string {
|
||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Server selection settings:")
|
||||
node.Appendf("VPN type: %s", ss.VPN)
|
||||
if !ss.TargetIP.IsUnspecified() {
|
||||
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||
}
|
||||
|
||||
if len(ss.Countries) > 0 {
|
||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||
@@ -464,12 +448,6 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
||||
) (err error) {
|
||||
ss.VPN = vpnType
|
||||
|
||||
ss.TargetIP, err = r.NetipAddr("OPENVPN_ENDPOINT_IP",
|
||||
reader.RetroKeys("OPENVPN_TARGET_IP", "VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
countriesRetroKeys := []string{"COUNTRY"}
|
||||
if vpnProvider == providers.Cyberghost {
|
||||
countriesRetroKeys = append(countriesRetroKeys, "REGION")
|
||||
@@ -540,7 +518,8 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
||||
return err
|
||||
}
|
||||
|
||||
err = ss.Wireguard.read(r)
|
||||
amneziawg := ss.VPN == vpn.AmneziaWg
|
||||
err = ss.Wireguard.read(r, amneziawg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -27,7 +26,9 @@ type Settings struct {
|
||||
Updater Updater
|
||||
Version Version
|
||||
VPN VPN
|
||||
IPv6 IPv6
|
||||
Pprof pprof.Settings
|
||||
BoringPoll BoringPoll
|
||||
}
|
||||
|
||||
type FilterChoicesGetter interface {
|
||||
@@ -53,10 +54,12 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
|
||||
"system": s.System.validate,
|
||||
"updater": s.Updater.Validate,
|
||||
"version": s.Version.validate,
|
||||
"ipv6": s.IPv6.validate,
|
||||
// Pprof validation done in pprof constructor
|
||||
"VPN": func() error {
|
||||
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
},
|
||||
"boring poll": s.BoringPoll.validate,
|
||||
}
|
||||
|
||||
for name, validation := range nameToValidation {
|
||||
@@ -85,6 +88,8 @@ func (s *Settings) copy() (copied Settings) {
|
||||
Version: s.Version.copy(),
|
||||
VPN: s.VPN.Copy(),
|
||||
Pprof: s.Pprof.Copy(),
|
||||
BoringPoll: s.BoringPoll.Copy(),
|
||||
IPv6: s.IPv6.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +111,8 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.OverrideWith(other.VPN)
|
||||
patchedSettings.Pprof.OverrideWith(other.Pprof)
|
||||
patchedSettings.BoringPoll.overrideWith(other.BoringPoll)
|
||||
patchedSettings.IPv6.overrideWith(other.IPv6)
|
||||
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -117,10 +124,12 @@ func (s *Settings) OverrideWith(other Settings,
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.ControlServer.setDefaults()
|
||||
s.DNS.setDefaults()
|
||||
s.Firewall.setDefaults()
|
||||
s.Log.setDefaults()
|
||||
s.Firewall.setDefaults(s.Log.Level)
|
||||
s.Health.SetDefaults()
|
||||
s.HTTPProxy.setDefaults()
|
||||
s.Log.setDefaults()
|
||||
s.IPv6.setDefaults()
|
||||
s.PublicIP.setDefaults()
|
||||
s.Shadowsocks.setDefaults()
|
||||
s.Storage.setDefaults()
|
||||
@@ -129,6 +138,7 @@ func (s *Settings) SetDefaults() {
|
||||
s.VPN.setDefaults()
|
||||
s.Updater.SetDefaults(s.VPN.Provider.Name)
|
||||
s.Pprof.SetDefaults()
|
||||
s.BoringPoll.setDefaults()
|
||||
}
|
||||
|
||||
func (s Settings) String() string {
|
||||
@@ -142,6 +152,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node.AppendNode(s.DNS.toLinesNode())
|
||||
node.AppendNode(s.Firewall.toLinesNode())
|
||||
node.AppendNode(s.Log.toLinesNode())
|
||||
node.AppendNode(s.IPv6.toLinesNode())
|
||||
node.AppendNode(s.Health.toLinesNode())
|
||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||
@@ -152,6 +163,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node.AppendNode(s.Updater.toLinesNode())
|
||||
node.AppendNode(s.Version.toLinesNode())
|
||||
node.AppendNode(s.Pprof.ToLinesNode())
|
||||
node.AppendNode(s.BoringPoll.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -174,13 +186,11 @@ func (s Settings) Warnings() (warnings []string) {
|
||||
"by creating an issue, attaching the new certificate and we will update Gluetun.")
|
||||
}
|
||||
|
||||
// TODO remove in v4
|
||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||
" so the DNS over TLS (DoT) server will not be used."+
|
||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||
" corresponding to the first DoT provider chosen is used.")
|
||||
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
|
||||
if upstreamAddress.Addr().IsPrivate() {
|
||||
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
|
||||
"DNS traffic might leak out of the VPN tunnel to that address.")
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
@@ -208,7 +218,9 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
|
||||
"updater": s.Updater.read,
|
||||
"version": s.Version.read,
|
||||
"VPN": s.VPN.read,
|
||||
"IPv6": s.IPv6.read,
|
||||
"profiling": s.Pprof.Read,
|
||||
"boring poll": s.BoringPoll.read,
|
||||
}
|
||||
|
||||
for name, read := range readFunctions {
|
||||
|
||||
@@ -29,41 +29,58 @@ func Test_Settings_String(t *testing.T) {
|
||||
| | └── OpenVPN server selection settings:
|
||||
| | ├── Protocol: UDP
|
||||
| | └── Private Internet Access encryption preset: strong
|
||||
| └── OpenVPN settings:
|
||||
| ├── OpenVPN version: 2.6
|
||||
| ├── User: [not set]
|
||||
| ├── Password: [not set]
|
||||
| ├── Private Internet Access encryption preset: strong
|
||||
| ├── Network interface: tun0
|
||||
| ├── Run OpenVPN as: root
|
||||
| └── Verbosity level: 1
|
||||
| ├── OpenVPN settings:
|
||||
| | ├── OpenVPN version: 2.6
|
||||
| | ├── User: [not set]
|
||||
| | ├── Password: [not set]
|
||||
| | ├── Private Internet Access encryption preset: strong
|
||||
| | ├── Network interface: tun0
|
||||
| | ├── Run OpenVPN as: root
|
||||
| | └── Verbosity level: 1
|
||||
| └── Path MTU discovery:
|
||||
| ├── ICMP addresses:
|
||||
| | ├── 1.1.1.1
|
||||
| | └── 8.8.8.8
|
||||
| └── TCP addresses:
|
||||
| ├── 1.1.1.1:53
|
||||
| ├── 8.8.8.8:53
|
||||
| ├── 1.1.1.1:443
|
||||
| ├── 8.8.8.8:443
|
||||
| ├── [2606:4700:4700::1111]:53
|
||||
| ├── [2001:4860:4860::8888]:53
|
||||
| ├── [2606:4700:4700::1111]:443
|
||||
| └── [2001:4860:4860::8888]:443
|
||||
├── DNS settings:
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
| ├── Upstream resolver type: dot
|
||||
| ├── Upstream resolvers:
|
||||
| | └── Cloudflare
|
||||
| ├── Caching: yes
|
||||
| ├── IPv6: no
|
||||
| ├── Update period: every 24h0m0s
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
├── Firewall settings:
|
||||
| └── Enabled: yes
|
||||
| ├── Enabled: yes
|
||||
| └── Iptables settings:
|
||||
| └── Log level: INFO
|
||||
├── Log settings:
|
||||
| └── Log level: INFO
|
||||
├── IPv6 settings:
|
||||
| └── Check addresses:
|
||||
| ├── [2001:4860:4860::8888]:53
|
||||
| └── [2606:4700:4700::1111]:53
|
||||
├── Health settings:
|
||||
| ├── Server listening address: 127.0.0.1:9999
|
||||
| ├── Target address: cloudflare.com:443
|
||||
| ├── Duration to wait after success: 5s
|
||||
| ├── Read header timeout: 100ms
|
||||
| ├── Read timeout: 500ms
|
||||
| └── VPN wait durations:
|
||||
| ├── Initial duration: 6s
|
||||
| └── Additional duration: 5s
|
||||
| ├── Target addresses:
|
||||
| | ├── cloudflare.com:443
|
||||
| | └── github.com:443
|
||||
| ├── Small health check type: ICMP echo request
|
||||
| | └── ICMP target IPs:
|
||||
| | ├── 1.1.1.1
|
||||
| | └── 8.8.8.8
|
||||
| └── Restart VPN on healthcheck failure: yes
|
||||
├── Shadowsocks server settings:
|
||||
| └── Enabled: no
|
||||
├── HTTP proxy settings:
|
||||
|
||||
@@ -15,7 +15,7 @@ type Shadowsocks struct {
|
||||
// It defaults to false, and cannot be nil in the internal state.
|
||||
Enabled *bool
|
||||
// Settings are settings for the TCP+UDP server.
|
||||
tcpudp.Settings
|
||||
Settings tcpudp.Settings
|
||||
}
|
||||
|
||||
func (s Shadowsocks) validate() (err error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -20,10 +21,6 @@ type Updater struct {
|
||||
// updater. It cannot be nil in the internal state.
|
||||
// TODO change to value and add Enabled field.
|
||||
Period *time.Duration
|
||||
// DNSAddress is the DNS server address to use
|
||||
// to resolve VPN server hostnames to IP addresses.
|
||||
// It cannot be the empty string in the internal state.
|
||||
DNSAddress string
|
||||
// MinRatio is the minimum ratio of servers to
|
||||
// find per provider, compared to the total current
|
||||
// number of servers. It defaults to 0.8.
|
||||
@@ -31,6 +28,10 @@ type Updater struct {
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
Providers []string
|
||||
// ProtonEmail is the email to authenticate with the Proton API.
|
||||
ProtonEmail *string
|
||||
// ProtonPassword is the password to authenticate with the Proton API.
|
||||
ProtonPassword *string
|
||||
}
|
||||
|
||||
func (u Updater) Validate() (err error) {
|
||||
@@ -51,6 +52,18 @@ func (u Updater) Validate() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
if provider == providers.Protonvpn {
|
||||
authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != ""
|
||||
if authenticatedAPI {
|
||||
switch {
|
||||
case *u.ProtonEmail == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
|
||||
case *u.ProtonPassword == "":
|
||||
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -58,10 +71,11 @@ func (u Updater) Validate() (err error) {
|
||||
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +84,14 @@ func (u *Updater) copy() (copied Updater) {
|
||||
// settings.
|
||||
func (u *Updater) overrideWith(other Updater) {
|
||||
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
|
||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||
u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword)
|
||||
}
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
u.Period = gosettings.DefaultPointer(u.Period, 0)
|
||||
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
|
||||
|
||||
if u.MinRatio == 0 {
|
||||
const defaultMinRatio = 0.8
|
||||
@@ -87,6 +101,10 @@ func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
if len(u.Providers) == 0 && vpnProvider != providers.Custom {
|
||||
u.Providers = []string{vpnProvider}
|
||||
}
|
||||
|
||||
// Set these to empty strings to avoid nil pointer panics
|
||||
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
|
||||
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
|
||||
}
|
||||
|
||||
func (u Updater) String() string {
|
||||
@@ -100,9 +118,12 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
node = gotree.New("Server data updater settings:")
|
||||
node.Appendf("Update period: %s", *u.Period)
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||
node.Appendf("Proton API email: %s", *u.ProtonEmail)
|
||||
node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -113,11 +134,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
u.DNSAddress, err = readUpdaterDNSAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,14 +141,15 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
|
||||
|
||||
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
|
||||
if u.ProtonEmail == nil {
|
||||
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
|
||||
if protonUsername != "" {
|
||||
protonEmail := protonUsername + "@protonmail.com"
|
||||
u.ProtonEmail = &protonEmail
|
||||
}
|
||||
}
|
||||
u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUpdaterDNSAddress() (address string, err error) {
|
||||
// TODO this is currently using Cloudflare in
|
||||
// plaintext to not be blocked by DNS over TLS by default.
|
||||
// If a plaintext address is set in the DNS settings, this one will be used.
|
||||
// use custom future encrypted DNS written in Go without blocking
|
||||
// as it's too much trouble to start another parallel unbound instance for now.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -16,14 +16,26 @@ type VPN struct {
|
||||
// empty string in the internal state.
|
||||
Type string `json:"type"`
|
||||
Provider Provider `json:"provider"`
|
||||
AmneziaWg AmneziaWg `json:"amneziawg"`
|
||||
OpenVPN OpenVPN `json:"openvpn"`
|
||||
Wireguard Wireguard `json:"wireguard"`
|
||||
PMTUD PMTUD `json:"pmtud"`
|
||||
// UpCommand is the command to use when the VPN connection is up.
|
||||
// It can be the empty string to indicate not to run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
UpCommand *string `json:"up_command"`
|
||||
// DownCommand is the command to use after the VPN connection goes down.
|
||||
// It can be the empty string to indicate to NOT run a command.
|
||||
// It cannot be nil in the internal state.
|
||||
DownCommand *string `json:"down_command"`
|
||||
}
|
||||
|
||||
// Validate validates VPN settings, using the filter choices getter (aka servers data storage),
|
||||
// and if IPv6 is supported or not.
|
||||
// TODO v4 remove pointer for receiver (because of Surfshark).
|
||||
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
|
||||
// Validate Type
|
||||
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
|
||||
validVPNTypes := []string{vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard}
|
||||
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
|
||||
}
|
||||
@@ -33,42 +45,66 @@ func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bo
|
||||
return fmt.Errorf("provider settings: %w", err)
|
||||
}
|
||||
|
||||
if v.Type == vpn.OpenVPN {
|
||||
switch v.Type {
|
||||
case vpn.AmneziaWg:
|
||||
err = v.AmneziaWg.validate(v.Provider.Name, ipv6Supported)
|
||||
if err != nil {
|
||||
return fmt.Errorf("AmneziaWG settings: %w", err)
|
||||
}
|
||||
case vpn.OpenVPN:
|
||||
err := v.OpenVPN.validate(v.Provider.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenVPN settings: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported)
|
||||
case vpn.Wireguard:
|
||||
const amneziawg = false
|
||||
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported, amneziawg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Wireguard settings: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = v.PMTUD.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("PMTUD settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VPN) Copy() (copied VPN) {
|
||||
return VPN{
|
||||
Type: v.Type,
|
||||
Provider: v.Provider.copy(),
|
||||
OpenVPN: v.OpenVPN.copy(),
|
||||
Wireguard: v.Wireguard.copy(),
|
||||
Type: v.Type,
|
||||
Provider: v.Provider.copy(),
|
||||
AmneziaWg: v.AmneziaWg.copy(),
|
||||
OpenVPN: v.OpenVPN.copy(),
|
||||
Wireguard: v.Wireguard.copy(),
|
||||
PMTUD: v.PMTUD.copy(),
|
||||
UpCommand: gosettings.CopyPointer(v.UpCommand),
|
||||
DownCommand: gosettings.CopyPointer(v.DownCommand),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VPN) OverrideWith(other VPN) {
|
||||
v.Type = gosettings.OverrideWithComparable(v.Type, other.Type)
|
||||
v.Provider.overrideWith(other.Provider)
|
||||
v.AmneziaWg.overrideWith(other.AmneziaWg)
|
||||
v.OpenVPN.overrideWith(other.OpenVPN)
|
||||
v.Wireguard.overrideWith(other.Wireguard)
|
||||
v.PMTUD.overrideWith(other.PMTUD)
|
||||
v.UpCommand = gosettings.OverrideWithPointer(v.UpCommand, other.UpCommand)
|
||||
v.DownCommand = gosettings.OverrideWithPointer(v.DownCommand, other.DownCommand)
|
||||
}
|
||||
|
||||
func (v *VPN) setDefaults() {
|
||||
v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN)
|
||||
v.Provider.setDefaults()
|
||||
v.AmneziaWg.setDefaults(v.Provider.Name)
|
||||
v.OpenVPN.setDefaults(v.Provider.Name)
|
||||
v.Wireguard.setDefaults(v.Provider.Name)
|
||||
v.PMTUD.setDefaults()
|
||||
v.UpCommand = gosettings.DefaultPointer(v.UpCommand, "")
|
||||
v.DownCommand = gosettings.DefaultPointer(v.DownCommand, "")
|
||||
}
|
||||
|
||||
func (v VPN) String() string {
|
||||
@@ -80,11 +116,22 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
node.AppendNode(v.Provider.toLinesNode())
|
||||
|
||||
if v.Type == vpn.OpenVPN {
|
||||
switch v.Type {
|
||||
case vpn.AmneziaWg:
|
||||
node.AppendNode(v.AmneziaWg.toLinesNode())
|
||||
case vpn.OpenVPN:
|
||||
node.AppendNode(v.OpenVPN.toLinesNode())
|
||||
} else {
|
||||
case vpn.Wireguard:
|
||||
node.AppendNode(v.Wireguard.toLinesNode())
|
||||
}
|
||||
node.AppendNode(v.PMTUD.toLinesNode())
|
||||
|
||||
if *v.UpCommand != "" {
|
||||
node.Appendf("Up command: %s", *v.UpCommand)
|
||||
}
|
||||
if *v.DownCommand != "" {
|
||||
node.Appendf("Down command: %s", *v.DownCommand)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
@@ -97,15 +144,30 @@ func (v *VPN) read(r *reader.Reader) (err error) {
|
||||
return fmt.Errorf("VPN provider: %w", err)
|
||||
}
|
||||
|
||||
err = v.AmneziaWg.read(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("AmneziaWG: %w", err)
|
||||
}
|
||||
|
||||
err = v.OpenVPN.read(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenVPN: %w", err)
|
||||
}
|
||||
|
||||
err = v.Wireguard.read(r)
|
||||
const amneziawg = false
|
||||
err = v.Wireguard.read(r, amneziawg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard: %w", err)
|
||||
}
|
||||
|
||||
err = v.PMTUD.read(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PMTUD: %w", err)
|
||||
}
|
||||
|
||||
v.UpCommand = r.Get("VPN_UP_COMMAND", reader.ForceLowercase(false))
|
||||
|
||||
v.DownCommand = r.Get("VPN_DOWN_COMMAND", reader.ForceLowercase(false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
@@ -38,11 +37,9 @@ type Wireguard struct {
|
||||
Interface string `json:"interface"`
|
||||
PersistentKeepaliveInterval *time.Duration `json:"persistent_keep_alive_interval"`
|
||||
// Maximum Transmission Unit (MTU) of the Wireguard interface.
|
||||
// It cannot be zero in the internal state, and defaults to
|
||||
// 1400. Note it is not the wireguard-go MTU default of 1420
|
||||
// because this impacts bandwidth a lot on some VPN providers,
|
||||
// see https://github.com/qdm12/gluetun/issues/1650.
|
||||
MTU uint16 `json:"mtu"`
|
||||
// It cannot be nil in the internal state, and defaults to
|
||||
// 0 indicating to use PMTUD.
|
||||
MTU *uint32 `json:"mtu"`
|
||||
// Implementation is the Wireguard implementation to use.
|
||||
// It can be "auto", "userspace" or "kernelspace".
|
||||
// It defaults to "auto" and cannot be the empty string
|
||||
@@ -53,23 +50,8 @@ type Wireguard struct {
|
||||
var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
|
||||
// Validate validates Wireguard settings.
|
||||
// It should only be ran if the VPN type chosen is Wireguard.
|
||||
func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error) {
|
||||
if !helpers.IsOneOf(vpnProvider,
|
||||
providers.Airvpn,
|
||||
providers.Custom,
|
||||
providers.Fastestvpn,
|
||||
providers.Ivpn,
|
||||
providers.Mullvad,
|
||||
providers.Nordvpn,
|
||||
providers.Protonvpn,
|
||||
providers.Surfshark,
|
||||
providers.Windscribe,
|
||||
) {
|
||||
// do not validate for VPN provider not supporting Wireguard
|
||||
return nil
|
||||
}
|
||||
|
||||
// It should only be ran if the VPN type chosen is Wireguard or AmneziaWg.
|
||||
func (w Wireguard) validate(vpnProvider string, ipv6Supported, amneziawg bool) (err error) {
|
||||
// Validate PrivateKey
|
||||
if *w.PrivateKey == "" {
|
||||
return fmt.Errorf("%w", ErrWireguardPrivateKeyNotSet)
|
||||
@@ -138,9 +120,11 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
||||
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
|
||||
}
|
||||
|
||||
validImplementations := []string{"auto", "userspace", "kernelspace"}
|
||||
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err)
|
||||
if !amneziawg { // amneziawg should have its own Implementation field and ignore this one
|
||||
validImplementations := []string{"auto", "userspace", "kernelspace"}
|
||||
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -191,8 +175,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
||||
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
||||
w.PersistentKeepaliveInterval = gosettings.DefaultPointer(w.PersistentKeepaliveInterval, 0)
|
||||
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
|
||||
const defaultMTU = 1400
|
||||
w.MTU = gosettings.DefaultComparable(w.MTU, defaultMTU)
|
||||
w.MTU = gosettings.DefaultPointer(w.MTU, 0)
|
||||
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
|
||||
}
|
||||
|
||||
@@ -228,7 +211,11 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
|
||||
interfaceNode := node.Appendf("Network interface: %s", w.Interface)
|
||||
interfaceNode.Appendf("MTU: %d", w.MTU)
|
||||
if *w.MTU == 0 {
|
||||
interfaceNode.Append("MTU: use path MTU discovery")
|
||||
} else {
|
||||
interfaceNode.Appendf("MTU: %d", *w.MTU)
|
||||
}
|
||||
|
||||
if w.Implementation != "auto" {
|
||||
node.Appendf("Implementation: %s", w.Implementation)
|
||||
@@ -237,14 +224,21 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
return node
|
||||
}
|
||||
|
||||
func (w *Wireguard) read(r *reader.Reader) (err error) {
|
||||
w.PrivateKey = r.Get("WIREGUARD_PRIVATE_KEY", reader.ForceLowercase(false))
|
||||
w.PreSharedKey = r.Get("WIREGUARD_PRESHARED_KEY", reader.ForceLowercase(false))
|
||||
func (w *Wireguard) read(r *reader.Reader, amneziaWG bool) (err error) {
|
||||
prefix := "WIREGUARD"
|
||||
if amneziaWG {
|
||||
prefix = "AMNEZIAWG"
|
||||
}
|
||||
w.PrivateKey = r.Get(prefix+"_PRIVATE_KEY", reader.ForceLowercase(false))
|
||||
w.PreSharedKey = r.Get(prefix+"_PRESHARED_KEY", reader.ForceLowercase(false))
|
||||
w.Interface = r.String("VPN_INTERFACE",
|
||||
reader.RetroKeys("WIREGUARD_INTERFACE"), reader.ForceLowercase(false))
|
||||
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
|
||||
reader.RetroKeys(prefix+"_INTERFACE"), reader.ForceLowercase(false))
|
||||
|
||||
addressStrings := r.CSV("WIREGUARD_ADDRESSES", reader.RetroKeys("WIREGUARD_ADDRESS"))
|
||||
if !amneziaWG {
|
||||
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
|
||||
}
|
||||
|
||||
addressStrings := r.CSV(prefix+"_ADDRESSES", reader.RetroKeys(prefix+"_ADDRESS"))
|
||||
// WARNING: do not initialize w.Addresses to an empty slice
|
||||
// or the defaults for nordvpn will not work.
|
||||
for _, addressString := range addressStrings {
|
||||
@@ -259,21 +253,19 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
|
||||
w.Addresses = append(w.Addresses, address)
|
||||
}
|
||||
|
||||
w.AllowedIPs, err = r.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
|
||||
w.AllowedIPs, err = r.CSVNetipPrefixes(prefix + "_ALLOWED_IPS")
|
||||
if err != nil {
|
||||
return err // already wrapped
|
||||
}
|
||||
|
||||
w.PersistentKeepaliveInterval, err = r.DurationPtr("WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL")
|
||||
w.PersistentKeepaliveInterval, err = r.DurationPtr(prefix + "_PERSISTENT_KEEPALIVE_INTERVAL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mtuPtr, err := r.Uint16Ptr("WIREGUARD_MTU")
|
||||
w.MTU, err = r.Uint32Ptr(prefix + "_MTU")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if mtuPtr != nil {
|
||||
w.MTU = *mtuPtr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
|
||||
type WireguardSelection struct {
|
||||
// EndpointIP is the server endpoint IP address.
|
||||
// It is only used with VPN providers generating Wireguard
|
||||
// configurations specific to each server and user.
|
||||
// To indicate it should not be used, it should be set
|
||||
// to netip.IPv4Unspecified(). It can never be the zero value
|
||||
// in the internal state.
|
||||
// It is notably required with the custom provider.
|
||||
// Otherwise it overrides any IP address from the picked
|
||||
// built-in server connection. To indicate it should
|
||||
// not be used, it should be set to [netip.IPv4Unspecified].
|
||||
// It can never be the zero value in the internal state.
|
||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||
// EndpointPort is a the server port to use for the VPN server.
|
||||
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||
@@ -152,17 +152,22 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
||||
return node
|
||||
}
|
||||
|
||||
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
|
||||
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||
func (w *WireguardSelection) read(r *reader.Reader, amneziaWG bool) (err error) {
|
||||
prefix := "WIREGUARD"
|
||||
if amneziaWG {
|
||||
prefix = "AMNEZIAWG"
|
||||
}
|
||||
w.EndpointIP, err = r.NetipAddr(prefix+"_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w - note this MUST be an IP address, "+
|
||||
"see https://github.com/qdm12/gluetun/issues/788", err)
|
||||
}
|
||||
|
||||
w.EndpointPort, err = r.Uint16Ptr(prefix+"_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.PublicKey = r.String("WIREGUARD_PUBLIC_KEY", reader.ForceLowercase(false))
|
||||
w.PublicKey = r.String(prefix+"_PUBLIC_KEY", reader.ForceLowercase(false))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func (s *Source) lazyLoadAmneziawgConf() AmneziawgConfig {
|
||||
if s.cached.amneziawgLoaded {
|
||||
return s.cached.amneziawgConf
|
||||
}
|
||||
|
||||
s.cached.amneziawgLoaded = true
|
||||
var err error
|
||||
s.cached.amneziawgConf, err = ParseAmneziawgConf(filepath.Join(s.rootDirectory, "amneziawg", "awg0.conf"))
|
||||
if err != nil {
|
||||
s.warner.Warnf("skipping Amneziawg config: %s", err)
|
||||
}
|
||||
return s.cached.amneziawgConf
|
||||
}
|
||||
|
||||
type AmneziawgConfig struct {
|
||||
Wireguard WireguardConfig
|
||||
Jc *string
|
||||
Jmin *string
|
||||
Jmax *string
|
||||
S1 *string
|
||||
S2 *string
|
||||
S3 *string
|
||||
S4 *string
|
||||
H1 *string
|
||||
H2 *string
|
||||
H3 *string
|
||||
H4 *string
|
||||
I1 *string
|
||||
I2 *string
|
||||
I3 *string
|
||||
I4 *string
|
||||
I5 *string
|
||||
}
|
||||
|
||||
func ParseAmneziawgConf(path string) (config AmneziawgConfig, err error) {
|
||||
iniFile, err := ini.InsensitiveLoad(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return AmneziawgConfig{}, nil
|
||||
}
|
||||
return AmneziawgConfig{}, fmt.Errorf("loading ini from reader: %w", err)
|
||||
}
|
||||
|
||||
config.Wireguard, err = ParseWireguardConf(path)
|
||||
if err != nil {
|
||||
return AmneziawgConfig{}, err
|
||||
}
|
||||
|
||||
interfaceSection, err := iniFile.GetSection("Interface")
|
||||
if err != nil {
|
||||
// can never happen
|
||||
return AmneziawgConfig{}, fmt.Errorf("getting interface section: %w", err)
|
||||
}
|
||||
|
||||
config.Jc = getINIKeyFromSection(interfaceSection, "Jc")
|
||||
config.Jmin = getINIKeyFromSection(interfaceSection, "Jmin")
|
||||
config.Jmax = getINIKeyFromSection(interfaceSection, "Jmax")
|
||||
config.S1 = getINIKeyFromSection(interfaceSection, "S1")
|
||||
config.S2 = getINIKeyFromSection(interfaceSection, "S2")
|
||||
config.S3 = getINIKeyFromSection(interfaceSection, "S3")
|
||||
config.S4 = getINIKeyFromSection(interfaceSection, "S4")
|
||||
config.H1 = getINIKeyFromSection(interfaceSection, "H1")
|
||||
config.H2 = getINIKeyFromSection(interfaceSection, "H2")
|
||||
config.H3 = getINIKeyFromSection(interfaceSection, "H3")
|
||||
config.H4 = getINIKeyFromSection(interfaceSection, "H4")
|
||||
config.I1 = getINIKeyFromSection(interfaceSection, "I1")
|
||||
config.I2 = getINIKeyFromSection(interfaceSection, "I2")
|
||||
config.I3 = getINIKeyFromSection(interfaceSection, "I3")
|
||||
config.I4 = getINIKeyFromSection(interfaceSection, "I4")
|
||||
config.I5 = getINIKeyFromSection(interfaceSection, "I5")
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Source_ParseAmneziawgConf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("no_file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
noFile := filepath.Join(t.TempDir(), "doesnotexist")
|
||||
wireguard, err := ParseAmneziawgConf(noFile)
|
||||
assert.Equal(t, AmneziawgConfig{}, wireguard)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
fileContent string
|
||||
amneziawg AmneziawgConfig
|
||||
errMessage string
|
||||
}{
|
||||
"ini_load_error": {
|
||||
fileContent: "invalid",
|
||||
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
|
||||
},
|
||||
"empty_file": {
|
||||
errMessage: `getting interface section: section "interface" does not exist`,
|
||||
},
|
||||
"success": {
|
||||
fileContent: `
|
||||
[Interface]
|
||||
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
|
||||
Address = 10.38.22.35/32
|
||||
DNS = 193.138.218.74
|
||||
Jc = 4
|
||||
H1 = 721391205
|
||||
I1 = <b 0x1234>
|
||||
|
||||
[Peer]
|
||||
PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||
`,
|
||||
amneziawg: AmneziawgConfig{
|
||||
Wireguard: WireguardConfig{
|
||||
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
|
||||
PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="),
|
||||
Addresses: ptrTo("10.38.22.35/32"),
|
||||
},
|
||||
Jc: ptrTo("4"),
|
||||
H1: ptrTo("721391205"),
|
||||
I1: ptrTo("<b 0x1234>"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configFile := filepath.Join(t.TempDir(), "awg.conf")
|
||||
const permission = fs.FileMode(0o600)
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), permission)
|
||||
require.NoError(t, err)
|
||||
|
||||
wireguard, err := ParseAmneziawgConf(configFile)
|
||||
|
||||
assert.Equal(t, testCase.amneziawg, wireguard)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ type Source struct {
|
||||
cached struct {
|
||||
wireguardLoaded bool
|
||||
wireguardConf WireguardConfig
|
||||
amneziawgLoaded bool
|
||||
amneziawgConf AmneziawgConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +73,11 @@ func (s *Source) Get(key string) (value string, isSet bool) {
|
||||
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
|
||||
}
|
||||
|
||||
value, isSet, matched := s.getAmneziawgKey(key)
|
||||
if matched {
|
||||
return value, isSet
|
||||
}
|
||||
|
||||
value, isSet, err := ReadFromFile(path)
|
||||
if err != nil {
|
||||
s.warner.Warnf("skipping %s: reading file: %s", path, err)
|
||||
@@ -78,6 +85,58 @@ func (s *Source) Get(key string) (value string, isSet bool) {
|
||||
return value, isSet
|
||||
}
|
||||
|
||||
func (s *Source) getAmneziawgKey(key string) (value string, isSet, matched bool) {
|
||||
switch key {
|
||||
case "amnezia_private_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PrivateKey)
|
||||
case "amnezia_preshared_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PreSharedKey)
|
||||
case "amnezia_addresses":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.Addresses)
|
||||
case "amnezia_public_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PublicKey)
|
||||
case "amnezia_endpoint_ip":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointIP)
|
||||
case "amnezia_endpoint_port":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointPort)
|
||||
case "amnezia_jc":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jc)
|
||||
case "amnezia_jmin":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmin)
|
||||
case "amnezia_jmax":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmax)
|
||||
case "amnezia_s1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S1)
|
||||
case "amnezia_s2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S2)
|
||||
case "amnezia_s3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S3)
|
||||
case "amnezia_s4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S4)
|
||||
case "amnezia_h1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H1)
|
||||
case "amnezia_h2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H2)
|
||||
case "amnezia_h3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H3)
|
||||
case "amnezia_h4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H4)
|
||||
case "amnezia_i1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I1)
|
||||
case "amnezia_i2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I2)
|
||||
case "amnezia_i3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I3)
|
||||
case "amnezia_i4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I4)
|
||||
case "amnezia_i5":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I5)
|
||||
default:
|
||||
return "", false, false
|
||||
}
|
||||
return value, isSet, true
|
||||
}
|
||||
|
||||
func (s *Source) KeyTransform(key string) string {
|
||||
switch key {
|
||||
// TODO v4 remove these irregular cases
|
||||
|
||||
@@ -3,6 +3,7 @@ package files
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -82,12 +83,15 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||
if endpoint != nil {
|
||||
parts := strings.Split(*endpoint, ":")
|
||||
endpointIP = &parts[0]
|
||||
const partsWithPort = 2
|
||||
if len(parts) >= partsWithPort {
|
||||
endpointPort = new(string)
|
||||
*endpointPort = strings.Join(parts[1:], ":")
|
||||
host, port, err := net.SplitHostPort(*endpoint)
|
||||
if err == nil {
|
||||
endpointIP = &host
|
||||
// IPv6 hosts contain colons; port is managed by the provider for those
|
||||
if !strings.Contains(host, ":") {
|
||||
endpointPort = &port
|
||||
}
|
||||
} else {
|
||||
endpointIP = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,11 @@ Endpoint = 1.2.3.4:51820`,
|
||||
endpointIP: ptrTo("1.2.3.4"),
|
||||
endpointPort: ptrTo("51820"),
|
||||
},
|
||||
"ipv6_endpoint": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = [2a02:bbbb:aaaa:8075::10]:51820`,
|
||||
endpointIP: ptrTo("2a02:bbbb:aaaa:8075::10"),
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/sources/files"
|
||||
)
|
||||
|
||||
func (s *Source) lazyLoadAmneziawgConf() files.AmneziawgConfig {
|
||||
if s.cached.amneziawgLoaded {
|
||||
return s.cached.amneziawgConf
|
||||
}
|
||||
|
||||
path := os.Getenv("AMNEZIAWG_CONF_SECRETFILE")
|
||||
if path == "" {
|
||||
path = filepath.Join(s.rootDirectory, "amneziawg", "awg0.conf")
|
||||
}
|
||||
|
||||
s.cached.amneziawgLoaded = true
|
||||
var err error
|
||||
s.cached.amneziawgConf, err = files.ParseAmneziawgConf(path)
|
||||
if err != nil {
|
||||
s.warner.Warnf("skipping Amneziawg config: %s", err)
|
||||
}
|
||||
return s.cached.amneziawgConf
|
||||
}
|
||||
@@ -15,6 +15,8 @@ type Source struct {
|
||||
cached struct {
|
||||
wireguardLoaded bool
|
||||
wireguardConf files.WireguardConfig
|
||||
amneziawgLoaded bool
|
||||
amneziawgConf files.AmneziawgConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +85,11 @@ func (s *Source) Get(key string) (value string, isSet bool) {
|
||||
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
|
||||
}
|
||||
|
||||
value, isSet, matched := s.getAmneziaWg(key)
|
||||
if matched {
|
||||
return value, isSet
|
||||
}
|
||||
|
||||
value, isSet, err := files.ReadFromFile(path)
|
||||
if err != nil {
|
||||
s.warner.Warnf("skipping %s: reading file: %s", path, err)
|
||||
@@ -104,3 +111,55 @@ func (s *Source) KeyTransform(key string) string {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) getAmneziaWg(key string) (value string, isSet, matched bool) {
|
||||
switch key {
|
||||
case "amneziawg_private_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PrivateKey)
|
||||
case "amneziawg_preshared_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PreSharedKey)
|
||||
case "amneziawg_addresses":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.Addresses)
|
||||
case "amneziawg_public_key":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.PublicKey)
|
||||
case "amneziawg_endpoint_ip":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointIP)
|
||||
case "amneziawg_endpoint_port":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Wireguard.EndpointPort)
|
||||
case "amneziawg_jc":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jc)
|
||||
case "amneziawg_jmin":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmin)
|
||||
case "amneziawg_jmax":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().Jmax)
|
||||
case "amneziawg_s1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S1)
|
||||
case "amneziawg_s2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S2)
|
||||
case "amneziawg_s3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S3)
|
||||
case "amneziawg_s4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().S4)
|
||||
case "amneziawg_h1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H1)
|
||||
case "amneziawg_h2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H2)
|
||||
case "amneziawg_h3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H3)
|
||||
case "amneziawg_h4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().H4)
|
||||
case "amneziawg_i1":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I1)
|
||||
case "amneziawg_i2":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I2)
|
||||
case "amneziawg_i3":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I3)
|
||||
case "amneziawg_i4":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I4)
|
||||
case "amneziawg_i5":
|
||||
value, isSet = strPtrToStringIsSet(s.lazyLoadAmneziawgConf().I5)
|
||||
default:
|
||||
return "", false, false
|
||||
}
|
||||
return value, isSet, true
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ const (
|
||||
VPNSecure = "vpnsecure"
|
||||
VPNUnlimited = "vpn unlimited"
|
||||
Vyprvpn = "vyprvpn"
|
||||
Wevpn = "wevpn"
|
||||
Windscribe = "windscribe"
|
||||
)
|
||||
|
||||
@@ -56,7 +55,6 @@ func All() []string {
|
||||
VPNSecure,
|
||||
VPNUnlimited,
|
||||
Vyprvpn,
|
||||
Wevpn,
|
||||
Windscribe,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vpn
|
||||
|
||||
const (
|
||||
AmneziaWg = "amneziawg"
|
||||
OpenVPN = "openvpn"
|
||||
Wireguard = "wireguard"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func leakCheck(ctx context.Context, client *http.Client) (report string, err error) {
|
||||
const sessionLength = 40
|
||||
session := generateRandomString(sessionLength)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
type result struct {
|
||||
dnsToCount map[string]uint
|
||||
err error
|
||||
}
|
||||
resultsCh := make(chan result)
|
||||
|
||||
const requestsCount = 5
|
||||
for range requestsCount {
|
||||
go func() {
|
||||
dnsToCount, err := triggerDNSQuery(ctx, client, session)
|
||||
resultsCh <- result{dnsToCount: dnsToCount, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
dnsToCount := make(map[string]uint)
|
||||
for range requestsCount {
|
||||
result := <-resultsCh
|
||||
if result.err != nil {
|
||||
if err == nil {
|
||||
cancel()
|
||||
err = fmt.Errorf("request failed: %w", result.err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for dns, count := range result.dnsToCount {
|
||||
dnsToCount[dns] += count
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return formatPercentages(dnsToCount), nil
|
||||
}
|
||||
|
||||
func generateRandomString(length uint) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.IntN(len(charset))] //nolint:gosec
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var errIPLeakSessionMismatch = errors.New("ipleak.net session mismatch")
|
||||
|
||||
func triggerDNSQuery(ctx context.Context, client *http.Client, session string) (
|
||||
dnsToCount map[string]uint, err error,
|
||||
) {
|
||||
const randomLength = 12
|
||||
randomPart := generateRandomString(randomLength)
|
||||
url := fmt.Sprintf("https://%s-%s.ipleak.net/dnsdetection/", session, randomPart)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("performing request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
type ipLeakData struct {
|
||||
Session string `json:"session"`
|
||||
IP map[string]uint `json:"ip"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data ipLeakData
|
||||
err = decoder.Decode(&data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding response: %w", err)
|
||||
} else if data.Session != session {
|
||||
return nil, fmt.Errorf("%w: expected %s, got %s", errIPLeakSessionMismatch, session, data.Session)
|
||||
}
|
||||
|
||||
return data.IP, nil
|
||||
}
|
||||
|
||||
func formatPercentages(data map[string]uint) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var total uint
|
||||
keys := make([]string, 0, len(data))
|
||||
for k, v := range data {
|
||||
total += v
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if data[keys[i]] == data[keys[j]] {
|
||||
return keys[i] < keys[j] // Tie-breaker: alphabetical
|
||||
}
|
||||
return data[keys[i]] > data[keys[j]]
|
||||
})
|
||||
|
||||
results := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
var pct float64
|
||||
if total > 0 {
|
||||
pct = math.Ceil((float64(data[key]) / float64(total)) * 100) //nolint:mnd
|
||||
}
|
||||
results[i] = fmt.Sprintf("%s (%.0f%%)", key, pct)
|
||||
}
|
||||
|
||||
return strings.Join(results, ", ")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_leakCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const timeout = 10 * time.Second
|
||||
ctx, cancel := context.WithTimeout(t.Context(), timeout)
|
||||
t.Cleanup(cancel)
|
||||
client := http.DefaultClient
|
||||
report, err := leakCheck(ctx, client)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, report)
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package dns
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Infof(format string, args ...any)
|
||||
Warn(s string)
|
||||
Warnf(format string, args ...any)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
+36
-18
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||
@@ -16,28 +17,30 @@ import (
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
server *server.Server
|
||||
filter *mapfilter.Filter
|
||||
localResolvers []netip.Addr
|
||||
localSubnets []netip.Prefix
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
running chan<- models.LoopStatus
|
||||
stop <-chan struct{}
|
||||
stopped chan<- struct{}
|
||||
updateTicker <-chan struct{}
|
||||
backoffTime time.Duration
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
}
|
||||
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
|
||||
func NewLoop(settings settings.DNS,
|
||||
client *http.Client, logger Logger,
|
||||
client *http.Client, logger Logger, localSubnets []netip.Prefix,
|
||||
) (loop *Loop, err error) {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
@@ -48,7 +51,9 @@ func NewLoop(settings settings.DNS,
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
state := state.New(statusManager, settings, updateTicker)
|
||||
|
||||
filter, err := mapfilter.New(mapfilter.Settings{})
|
||||
filter, err := mapfilter.New(mapfilter.Settings{
|
||||
Logger: buildFilterLogger(logger),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating map filter: %w", err)
|
||||
}
|
||||
@@ -58,6 +63,7 @@ func NewLoop(settings settings.DNS,
|
||||
state: state,
|
||||
server: nil,
|
||||
filter: filter,
|
||||
localSubnets: localSubnets,
|
||||
resolvConf: "/etc/resolv.conf",
|
||||
client: client,
|
||||
logger: logger,
|
||||
@@ -100,3 +106,15 @@ func (l *Loop) signalOrSetStatus(status models.LoopStatus) {
|
||||
l.statusManager.SetStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
type filterLogger struct {
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (l *filterLogger) Log(msg string) {
|
||||
l.logger.Debug(msg)
|
||||
}
|
||||
|
||||
func buildFilterLogger(logger Logger) *filterLogger {
|
||||
return &filterLogger{logger: logger}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"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), 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 {
|
||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||
} else {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
+27
-31
@@ -2,21 +2,20 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
if *l.GetSettings().KeepNameserver {
|
||||
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
|
||||
"this will likely leak DNS traffic outside the VPN " +
|
||||
"and go through your container network DNS outside the VPN tunnel!")
|
||||
} else {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
var err error
|
||||
l.localResolvers, err = nameserver.GetPrivateDNSServers()
|
||||
if err != nil {
|
||||
l.logger.Error("getting private DNS servers: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -26,43 +25,44 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
// Upper scope variables for the DNS over TLS server only
|
||||
// Upper scope variables for the DNS forwarder server only
|
||||
// Their values are to be used if DOT=off
|
||||
var runError <-chan error
|
||||
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
var settings settings.DNS
|
||||
for {
|
||||
settings = l.GetSettings()
|
||||
var err error
|
||||
runError, err = l.setupServer(ctx)
|
||||
runError, err = l.setupServer(ctx, settings)
|
||||
if err == nil {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Info("ready")
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
break
|
||||
}
|
||||
|
||||
l.signalOrSetStatus(constants.Crashed)
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, errUpdateBlockLists) {
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.logger.Infof("ready and using DNS server with %s upstream resolvers", settings.UpstreamType)
|
||||
|
||||
err = l.updateFiles(ctx, settings)
|
||||
if err != nil {
|
||||
l.logger.Warn("downloading block lists failed, skipping: " + err.Error())
|
||||
}
|
||||
l.signalOrSetStatus(constants.Running)
|
||||
|
||||
l.userTrigger = false
|
||||
|
||||
report, err := leakCheck(ctx, l.client)
|
||||
if err != nil {
|
||||
l.logger.Warnf("running leak check: %s", err)
|
||||
} else {
|
||||
l.logger.Infof("leak check report: %s", report)
|
||||
}
|
||||
|
||||
exitLoop := l.runWait(ctx, runError)
|
||||
if exitLoop {
|
||||
return
|
||||
@@ -80,8 +80,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
l.stopped <- struct{}{}
|
||||
case <-l.start:
|
||||
@@ -90,8 +88,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
return false
|
||||
case err := <-runError: // unexpected error
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.logAndWait(ctx, err)
|
||||
return false
|
||||
}
|
||||
@@ -101,6 +97,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
func (l *Loop) stopServer() {
|
||||
stopErr := l.server.Stop()
|
||||
if stopErr != nil {
|
||||
l.logger.Error("stopping DoT server: " + stopErr.Error())
|
||||
l.logger.Error("stopping server: " + stopErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user