Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9807ff90f7 | ||
|
|
cd95746624 | ||
|
|
f40955d747 | ||
|
|
68dd982606 | ||
|
|
66d1cf7478 | ||
|
|
c689a4a746 | ||
|
|
30dafff034 | ||
|
|
e3b5ce688e | ||
|
|
586775d5f2 | ||
|
|
c559de9aed | ||
|
|
b6ec1a6ee6 | ||
|
|
c2e3116d71 | ||
|
|
191556cfe0 | ||
|
|
c63885d1f1 | ||
|
|
1b6164cb91 | ||
|
|
a75d6bed55 | ||
|
|
9a454fa971 | ||
|
|
9d59668dca | ||
|
|
30d5fd68ef | ||
|
|
9843b19d2b | ||
|
|
329fee5e68 | ||
|
|
ef5f521ce0 | ||
|
|
c882df1a8f | ||
|
|
c826707d42 | ||
|
|
8a17cd87c3 | ||
|
|
f8da1e79bc | ||
|
|
cfc29d6a6b | ||
|
|
5467652b8b | ||
|
|
daa63c276d | ||
|
|
ab96acdc5b | ||
|
|
6e108706a1 | ||
|
|
4a6c229504 | ||
|
|
ed3a72790a | ||
|
|
4bf5777f23 | ||
|
|
f0f9bdb883 | ||
|
|
4984d90b5a | ||
|
|
b5e648d13a | ||
|
|
f71a1b083b | ||
|
|
75fd869625 | ||
|
|
657b4b787f | ||
|
|
32d6453918 | ||
|
|
c326b616b4 | ||
|
|
d5376629df | ||
|
|
3e825d7a08 | ||
|
|
059b12883f | ||
|
|
74aa509644 | ||
|
|
4105f74ce1 | ||
|
|
8318be3159 | ||
|
|
de196490db | ||
|
|
ab7d1ccf3d | ||
|
|
ed49a7a7c0 | ||
|
|
135832d985 | ||
|
|
1adbd9f692 | ||
|
|
26e1c92841 | ||
|
|
3c5b3514fb | ||
|
|
f884293f6e | ||
|
|
c67bd1aa2a | ||
|
|
77ace9377d | ||
|
|
6e676209ff | ||
|
|
80917d58b2 | ||
|
|
fa49f13f19 | ||
|
|
1fcabd152f | ||
|
|
385879c297 | ||
|
|
e0515cb458 | ||
|
|
1c43a1d55b | ||
|
|
6c639fcf7f | ||
|
|
ec1f252528 | ||
|
|
ee413f59a2 | ||
|
|
d4df87286e | ||
|
|
a194906bdd | ||
|
|
9b00763a69 | ||
|
|
4d627bb7b1 | ||
|
|
dc8fc5f81f | ||
|
|
b787e12e25 | ||
|
|
f96448947f | ||
|
|
e64e5af4c3 | ||
|
|
aa6dc786a4 | ||
|
|
84300db7c1 | ||
|
|
2ac0f35060 | ||
|
|
1a865f56d5 | ||
|
|
0406de399d | ||
|
|
71201411f4 | ||
|
|
c435bbb32c | ||
|
|
4cbfea41f2 | ||
|
|
f9c9ad34f7 | ||
|
|
4ea474b896 | ||
|
|
6aa4a93665 | ||
|
|
ea25a0ff89 | ||
|
|
659da67ed5 | ||
|
|
ffc6d2e593 | ||
|
|
03ce08e23d | ||
|
|
3449e7a0e1 | ||
|
|
c0062fb807 | ||
|
|
1ac031e78c | ||
|
|
e556871e8b | ||
|
|
082a38b769 | ||
|
|
39ae57f49d | ||
|
|
9024912e17 | ||
|
|
eecfb3952f | ||
|
|
0ebfe534d3 | ||
|
|
c5cc240a6c | ||
|
|
1a5a0148ea | ||
|
|
abe2aceb18 | ||
|
|
fa541b8fc2 | ||
|
|
a681d38dfb | ||
|
|
a7b96e3f4d | ||
|
|
04ef92edab | ||
|
|
919b55c3aa | ||
|
|
9c0f187a12 | ||
|
|
075a1e2a80 | ||
|
|
f31a846cda | ||
|
|
9bef46db77 | ||
|
|
d83217f7ac | ||
|
|
1cd2fec796 | ||
|
|
235f24ee5b | ||
|
|
2e34c6009e | ||
|
|
c0eb2f2315 | ||
|
|
8ad16cdc12 | ||
|
|
fae6544431 | ||
|
|
f8a41b2133 | ||
|
|
ff9b56d6d8 | ||
|
|
99d5a591b9 | ||
|
|
fbe252a9b6 | ||
|
|
76a92b90e3 | ||
|
|
2873b06275 | ||
|
|
9cdd6294d2 |
@@ -47,7 +47,7 @@ You can customize **settings** and **extensions** in the [devcontainer.json](dev
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
|
||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh).
|
||||
|
||||
### Publish a port
|
||||
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
|
||||
|
||||
## Resources
|
||||
|
||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
|
||||
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/bug.yml
vendored
12
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -7,13 +7,18 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
|
||||
- you do not fill out **the title of the issue** ☝️
|
||||
- you do not provide the **Gluetun version** as requested below
|
||||
- you provide **less than 10 lines of logs** as requested below
|
||||
- type: dropdown
|
||||
id: urgent
|
||||
attributes:
|
||||
label: Is this urgent?
|
||||
description: |
|
||||
Is this a critical bug, or do you need this fixed urgently?
|
||||
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help.
|
||||
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
@@ -75,6 +80,7 @@ body:
|
||||
- Portainer
|
||||
- Kubernetes
|
||||
- Podman
|
||||
- Unraid
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
@@ -84,7 +90,7 @@ body:
|
||||
label: What is the version of Gluetun
|
||||
description: |
|
||||
Copy paste the version line at the top of your logs.
|
||||
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -97,7 +103,7 @@ body:
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Share your logs
|
||||
label: Share your logs (at least 10 lines)
|
||||
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
||||
render: plain text
|
||||
validations:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,7 @@
|
||||
contact_links:
|
||||
- name: Report a Wiki issue
|
||||
url: https://github.com/qdm12/gluetun-wiki/issues/new
|
||||
about: Please create an issue on the gluetun-wiki repository.
|
||||
- name: Configuration help?
|
||||
url: https://github.com/qdm12/gluetun/discussions/new
|
||||
about: Please create a Github discussion.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/provider.md
vendored
2
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -14,4 +14,4 @@ One of the following is required:
|
||||
|
||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||
you can only use a custom Openvpn configuration file.
|
||||
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.
|
||||
[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.
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/wiki issue.yml
vendored
18
.github/ISSUE_TEMPLATE/wiki issue.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Wiki issue
|
||||
description: Report a Wiki issue
|
||||
title: "Wiki issue: "
|
||||
labels: ["📄 Wiki issue"]
|
||||
body:
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: "URL to the Wiki page"
|
||||
placeholder: "https://github.com/qdm12/gluetun/wiki/OpenVPN-options"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "What's the issue?"
|
||||
validations:
|
||||
required: true
|
||||
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -45,6 +45,7 @@ jobs:
|
||||
level: error
|
||||
exclude: |
|
||||
./internal/storage/servers.json
|
||||
*.md
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
@@ -72,12 +73,12 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: github/codeql-action/init@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
- uses: github/codeql-action/autobuild@v2
|
||||
- uses: github/codeql-action/analyze@v2
|
||||
- uses: github/codeql-action/autobuild@v3
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
|
||||
publish:
|
||||
if: |
|
||||
@@ -94,13 +95,13 @@ jobs:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
@@ -115,15 +116,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@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: qdm12
|
||||
@@ -134,7 +135,7 @@ jobs:
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v4.1.1
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/ppc64le
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
25
.github/workflows/dockerhub-description.yml
vendored
25
.github/workflows/dockerhub-description.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Docker Hub description
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- README.md
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
jobs:
|
||||
docker-hub-description:
|
||||
if: github.repository == 'qdm12/gluetun'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v3
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
repository: qmcgaw/gluetun
|
||||
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
||||
readme-filepath: README.md
|
||||
4
.github/workflows/labels.yml
vendored
4
.github/workflows/labels.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: crazy-max/ghaction-github-labeler@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crazy-max/ghaction-github-labeler@v5
|
||||
with:
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
21
.github/workflows/markdown-skip.yml
vendored
Normal file
21
.github/workflows/markdown-skip.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Markdown
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
|
||||
jobs:
|
||||
markdown:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: No trigger path triggered for required markdown workflow.
|
||||
run: exit 0
|
||||
46
.github/workflows/markdown.yml
vendored
Normal file
46
.github/workflows/markdown.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Markdown
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
|
||||
jobs:
|
||||
markdown:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v14
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint.json
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
locale: "US"
|
||||
level: error
|
||||
pattern: |
|
||||
*.md
|
||||
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: yes
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v3
|
||||
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
repository: qmcgaw/gluetun
|
||||
short-description: Lightweight Swiss-knife VPN client to connect to several VPN providers
|
||||
readme-filepath: README.md
|
||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"MD013": false
|
||||
}
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,8 +1,8 @@
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.20
|
||||
ARG GO_VERSION=1.21
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.53.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.54.1
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
@@ -90,12 +90,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
OPENVPN_FLAGS= \
|
||||
OPENVPN_CIPHERS= \
|
||||
OPENVPN_AUTH= \
|
||||
OPENVPN_PROCESS_USER= \
|
||||
OPENVPN_PROCESS_USER=root \
|
||||
OPENVPN_CUSTOM_CONFIG= \
|
||||
# Wireguard
|
||||
WIREGUARD_PRIVATE_KEY= \
|
||||
WIREGUARD_PRESHARED_KEY= \
|
||||
WIREGUARD_PUBLIC_KEY= \
|
||||
WIREGUARD_ALLOWED_IPS= \
|
||||
WIREGUARD_ADDRESSES= \
|
||||
WIREGUARD_MTU=1400 \
|
||||
WIREGUARD_IMPLEMENTATION=auto \
|
||||
@@ -110,6 +111,8 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
# # 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" \
|
||||
# # Cyberghost only:
|
||||
OPENVPN_CERT= \
|
||||
@@ -165,6 +168,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
HTTPPROXY= \
|
||||
HTTPPROXY_LOG=off \
|
||||
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
||||
HTTPPROXY_STEALTH=off \
|
||||
HTTPPROXY_USER= \
|
||||
HTTPPROXY_PASSWORD= \
|
||||
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||
@@ -177,6 +181,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
||||
# Control server
|
||||
HTTP_CONTROL_SERVER_LOG=on \
|
||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||
# Server data updater
|
||||
UPDATER_PERIOD=0 \
|
||||
|
||||
32
README.md
32
README.md
@@ -38,17 +38,16 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
- [Setup](#setup)
|
||||
- [Features](#features)
|
||||
- Problem?
|
||||
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
|
||||
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
|
||||
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
||||
- Suggestion?
|
||||
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
||||
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
||||
- Happy?
|
||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
||||
- **Want to add a VPN provider?** check [Development](https://github.com/qdm12/gluetun/wiki/Development) and [Add a provider](https://github.com/qdm12/gluetun/wiki/Add-a-provider)
|
||||
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
|
||||
- Video:
|
||||
|
||||
[](https://youtu.be/0F6I03LQcI4)
|
||||
@@ -61,9 +60,9 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||
- Supports OpenVPN for all providers listed
|
||||
- Supports Wireguard both kernelspace and userspace
|
||||
- For **Mullvad**, **Ivpn**, **Surfshark** and **Windscribe**
|
||||
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
|
||||
- For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Surfshark** and **Windscribe**
|
||||
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
|
||||
- DNS over TLS baked in with service provider(s) of your choice
|
||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
||||
@@ -71,10 +70,10 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
|
||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
|
||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
||||
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
|
||||
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding)
|
||||
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||
- Unbound subprogram drops root privileges once launched
|
||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||
@@ -83,9 +82,9 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
|
||||
|
||||
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
|
||||
|
||||
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
|
||||
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
|
||||
|
||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
|
||||
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new)
|
||||
|
||||
Here's a docker-compose.yml for the laziest:
|
||||
|
||||
@@ -95,7 +94,8 @@ services:
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun
|
||||
# container_name: gluetun
|
||||
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
|
||||
# line above must be uncommented to allow external containers to connect.
|
||||
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
@@ -107,7 +107,7 @@ services:
|
||||
volumes:
|
||||
- /yourpath:/gluetun
|
||||
environment:
|
||||
# See https://github.com/qdm12/gluetun/wiki
|
||||
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
||||
- VPN_SERVICE_PROVIDER=ivpn
|
||||
- VPN_TYPE=openvpn
|
||||
# OpenVPN:
|
||||
@@ -118,13 +118,13 @@ services:
|
||||
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||
# Timezone for accurate log times
|
||||
- TZ=
|
||||
# Server list updater. See https://github.com/qdm12/gluetun/wiki/Updating-Servers#periodic-update
|
||||
# Server list updater
|
||||
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||
- UPDATER_PERIOD=
|
||||
- UPDATER_VPN_SERVICE_PROVIDERS=
|
||||
```
|
||||
|
||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||
|
||||
## License
|
||||
|
||||
[](https://github.com/qdm12/gluetun/master/LICENSE)
|
||||
[](https://github.com/qdm12/gluetun/blob/master/LICENSE)
|
||||
|
||||
@@ -82,10 +82,10 @@ func main() {
|
||||
cli := cli.New()
|
||||
cmder := command.NewCmder()
|
||||
|
||||
envReader := env.New(logger)
|
||||
filesReader := files.New()
|
||||
secretsReader := secrets.New()
|
||||
muxReader := mux.New(envReader, filesReader, secretsReader)
|
||||
filesReader := files.New()
|
||||
envReader := env.New(logger)
|
||||
muxReader := mux.New(secretsReader, filesReader, envReader)
|
||||
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
@@ -159,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
}
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
BuildDate: buildInfo.Created,
|
||||
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
|
||||
Announcement: "Wiki moved to https://github.com/qdm12/gluetun-wiki",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
@@ -331,11 +331,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
|
||||
const tunDevice = "/dev/net/tun"
|
||||
if err := tun.Check(tunDevice); err != nil {
|
||||
err = tun.Check(tunDevice)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
|
||||
}
|
||||
logger.Info(err.Error() + "; creating it...")
|
||||
err = tun.Create(tunDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("creating tun device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +380,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||
httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
|
||||
"port forwarding", goroutine.OptionTimeout(time.Second))
|
||||
go portForwardLooper.Run(portForwardCtx, portForwardDone)
|
||||
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting port forwarding loop: %w", err)
|
||||
}
|
||||
|
||||
unboundLogger := logger.New(log.SetComponent("dns over tls"))
|
||||
unboundLogger := logger.New(log.SetComponent("dns"))
|
||||
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
||||
unboundLogger)
|
||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||
@@ -399,15 +404,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
publicIPLooper := publicip.NewLoop(ipFetcher,
|
||||
logger.New(log.SetComponent("ip getter")),
|
||||
allSettings.PublicIP, puid, pgid)
|
||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go publicIPLooper.Run(pubIPCtx, pubIPDone)
|
||||
otherGroupHandler.Add(pubIPHandler)
|
||||
|
||||
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
|
||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
||||
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
|
||||
tickersGroupHandler.Add(pubIPTickerHandler)
|
||||
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting public ip loop: %w", err)
|
||||
}
|
||||
|
||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||
|
||||
@@ -481,13 +481,31 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||
order.OptionOnFailure(defaultShutdownOnFailure))
|
||||
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||
vpnHandler, portForwardHandler, otherGroupHandler)
|
||||
vpnHandler, otherGroupHandler)
|
||||
|
||||
// Start VPN for the first time in a blocking call
|
||||
// until the VPN is launched
|
||||
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
||||
|
||||
<-ctx.Done()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stoppers := []interface {
|
||||
String() string
|
||||
Stop() error
|
||||
}{
|
||||
portForwardLooper, publicIPLooper,
|
||||
}
|
||||
for _, stopper := range stoppers {
|
||||
err := stopper.Stop()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
|
||||
}
|
||||
}
|
||||
case err := <-portForwardRunError:
|
||||
logger.Errorf("port forwarding loop crashed: %s", err)
|
||||
case err := <-publicIPRunError:
|
||||
logger.Errorf("public IP loop crashed: %s", err)
|
||||
}
|
||||
|
||||
return orderHandler.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
26
go.mod
26
go.mod
@@ -1,29 +1,33 @@
|
||||
module github.com/qdm12/gluetun
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.11
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/breml/rootcerts v0.2.17
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/klauspost/compress v1.17.4
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/qdm12/dns v1.11.0
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/qdm12/gosettings v0.3.0-rc13
|
||||
github.com/qdm12/gosettings v0.4.0-rc1
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.1.0
|
||||
github.com/qdm12/gotree v0.2.0
|
||||
github.com/qdm12/govalid v0.2.0-rc1
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/qdm12/ss-server v0.5.0-rc1
|
||||
github.com/qdm12/ss-server v0.5.0
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/text v0.10.0
|
||||
golang.org/x/net v0.19.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
)
|
||||
|
||||
@@ -32,7 +36,7 @@ require (
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/genetlink v1.2.0 // indirect
|
||||
github.com/mdlayher/netlink v1.6.2 // indirect
|
||||
github.com/mdlayher/socket v0.2.3 // indirect
|
||||
@@ -42,8 +46,8 @@ require (
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
|
||||
53
go.sum
53
go.sum
@@ -4,8 +4,8 @@ github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/g
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/breml/rootcerts v0.2.11 h1:njUAtoyZ6HUXPAPk63tGz0BEZk1/6gyfqK5fTzksHkM=
|
||||
github.com/breml/rootcerts v0.2.11/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I=
|
||||
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -14,8 +14,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
@@ -37,6 +37,7 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
@@ -49,6 +50,10 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -62,8 +67,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
|
||||
github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
|
||||
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
||||
@@ -75,6 +80,7 @@ github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaU
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
@@ -91,8 +97,8 @@ github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8
|
||||
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
||||
github.com/qdm12/gosettings v0.3.0-rc13 h1:fag+/hFPBUcNk3a5ifUbwNS2VgXFpxindkl8mQNk76U=
|
||||
github.com/qdm12/gosettings v0.3.0-rc13/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
|
||||
github.com/qdm12/gosettings v0.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38=
|
||||
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
|
||||
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.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
|
||||
@@ -103,8 +109,8 @@ github.com/qdm12/govalid v0.2.0-rc1 h1:4iYQvU4ibrASgzelsEgZX4JyKX3UTB/DcHObzQ7BX
|
||||
github.com/qdm12/govalid v0.2.0-rc1/go.mod h1:/uWzVWMuS71wmbsVnlUxpQiy6EAXqm8eQ2RbyA72roQ=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
github.com/qdm12/ss-server v0.5.0-rc1 h1:2rJEhDnUUc9AKtvyVu+CrnJwvdEjMaB1zFRQvTUlDPw=
|
||||
github.com/qdm12/ss-server v0.5.0-rc1/go.mod h1:IoFYGpVpxfIB/dMTr0PnSegdhV1gEfZLS9Tr1Qn8uRg=
|
||||
github.com/qdm12/ss-server v0.5.0 h1:ARAqJayohDM51BmJ/R5Yplkpo+Qxgp7xizBF1HWd7uQ=
|
||||
github.com/qdm12/ss-server v0.5.0/go.mod h1:eFd8PL/uy0ZvJ4KeSUzToruJctVQoYqXk+LRy9vcOiI=
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g=
|
||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
@@ -119,6 +125,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
@@ -136,8 +144,8 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 h1:QJ/xcIANMLApehfgPCHnfK1hZiaMmbaTVmPv7DAoTbo=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -146,8 +154,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
@@ -165,8 +173,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -195,8 +203,9 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -204,9 +213,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@@ -226,6 +236,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:m
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
@@ -236,6 +248,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
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-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
|
||||
@@ -33,15 +33,20 @@ func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
|
||||
func (c *CLI) FormatServers(args []string) error {
|
||||
var format, output string
|
||||
allProviders := providers.All()
|
||||
allProviderFlags := make([]string, len(allProviders))
|
||||
for i, provider := range allProviders {
|
||||
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
|
||||
}
|
||||
|
||||
providersToFormat := make(map[string]*bool, len(allProviders))
|
||||
for _, provider := range allProviders {
|
||||
for _, provider := range allProviderFlags {
|
||||
providersToFormat[provider] = new(bool)
|
||||
}
|
||||
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
|
||||
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
|
||||
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||
titleCaser := cases.Title(language.English)
|
||||
for _, provider := range allProviders {
|
||||
for _, provider := range allProviderFlags {
|
||||
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
||||
}
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
@@ -68,7 +73,13 @@ func (c *CLI) FormatServers(args []string) error {
|
||||
ErrMultipleProvidersToFormat, len(providers),
|
||||
strings.Join(providers, ", "))
|
||||
}
|
||||
providerToFormat := providers[0]
|
||||
|
||||
var providerToFormat string
|
||||
for _, providerToFormat = range allProviders {
|
||||
if strings.ReplaceAll(providerToFormat, " ", "-") == providers[0] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger := newNoopLogger()
|
||||
storage, err := storage.New(logger, constants.ServersData)
|
||||
|
||||
@@ -16,10 +16,14 @@ type DNS struct {
|
||||
// DoT server. It cannot be the zero value in the internal
|
||||
// state.
|
||||
ServerAddress netip.Addr
|
||||
// KeepNameserver is true if the Docker DNS server
|
||||
// found in /etc/resolv.conf should be kept.
|
||||
// Note settings this to true will go around the
|
||||
// DoT server blocking.
|
||||
// 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
|
||||
@@ -75,8 +79,11 @@ func (d DNS) String() string {
|
||||
|
||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS settings:")
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
|
||||
if *d.KeepNameserver {
|
||||
return node
|
||||
}
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
return node
|
||||
}
|
||||
|
||||
update := "disabled"
|
||||
update := "disabled" //nolint:goconst
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ var (
|
||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||
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")
|
||||
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
|
||||
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
||||
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
||||
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
||||
|
||||
@@ -45,7 +45,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Ipvanish,
|
||||
providers.Perfectprivacy,
|
||||
providers.Privado,
|
||||
providers.VPNUnlimited,
|
||||
providers.Vyprvpn,
|
||||
) {
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
@@ -56,7 +55,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
if *o.CustomPort != 0 {
|
||||
switch vpnProvider {
|
||||
// no restriction on port
|
||||
case providers.Cyberghost, providers.HideMyAss,
|
||||
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||
providers.Privatevpn, providers.Torguard:
|
||||
// no custom port allowed
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
@@ -99,6 +98,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Windscribe:
|
||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||
default:
|
||||
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
|
||||
}
|
||||
|
||||
allowedPorts := allowedUDP
|
||||
|
||||
@@ -15,21 +15,40 @@ type PortForwarding struct {
|
||||
// Enabled is true if port forwarding should be activated.
|
||||
// It cannot be nil for the internal state.
|
||||
Enabled *bool `json:"enabled"`
|
||||
// Provider is set to specify which custom port forwarding code
|
||||
// should be used. This is especially necessary for the custom
|
||||
// provider using Wireguard for a provider where Wireguard is not
|
||||
// natively supported but custom port forwading code is available.
|
||||
// It defaults to the empty string, meaning the current provider
|
||||
// should be the one used for port forwarding.
|
||||
// It cannot be nil for the internal state.
|
||||
Provider *string `json:"provider"`
|
||||
// Filepath is the port forwarding status file path
|
||||
// to use. It can be the empty string to indicate not
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
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"`
|
||||
}
|
||||
|
||||
func (p PortForwarding) validate(vpnProvider string) (err error) {
|
||||
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
if !*p.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate Enabled
|
||||
validProviders := []string{providers.PrivateInternetAccess}
|
||||
if err = validate.IsOneOf(vpnProvider, validProviders...); err != nil {
|
||||
// Validate current provider or custom provider specified
|
||||
providerSelected := vpnProvider
|
||||
if *p.Provider != "" {
|
||||
providerSelected = *p.Provider
|
||||
}
|
||||
validProviders := []string{
|
||||
providers.PrivateInternetAccess,
|
||||
providers.Protonvpn,
|
||||
}
|
||||
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
|
||||
}
|
||||
|
||||
@@ -44,26 +63,34 @@ func (p PortForwarding) validate(vpnProvider string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForwarding) copy() (copied PortForwarding) {
|
||||
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||
return PortForwarding{
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||
Provider: gosettings.CopyPointer(p.Provider),
|
||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PortForwarding) mergeWith(other PortForwarding) {
|
||||
p.Enabled = gosettings.MergeWithPointer(p.Enabled, other.Enabled)
|
||||
p.Provider = gosettings.MergeWithPointer(p.Provider, other.Provider)
|
||||
p.Filepath = gosettings.MergeWithPointer(p.Filepath, other.Filepath)
|
||||
p.ListeningPort = gosettings.MergeWithPointer(p.ListeningPort, other.ListeningPort)
|
||||
}
|
||||
|
||||
func (p *PortForwarding) overrideWith(other PortForwarding) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (p PortForwarding) String() string {
|
||||
@@ -76,7 +103,18 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
}
|
||||
|
||||
node = gotree.New("Automatic port forwarding settings:")
|
||||
node.Appendf("Enabled: yes")
|
||||
|
||||
listeningPort := "disabled"
|
||||
if *p.ListeningPort != 0 {
|
||||
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
|
||||
}
|
||||
node.Appendf("Redirection listening port: %s", listeningPort)
|
||||
|
||||
if *p.Provider == "" {
|
||||
node.Appendf("Use port forwarding code for current provider")
|
||||
} else {
|
||||
node.Appendf("Use code for provider: %s", *p.Provider)
|
||||
}
|
||||
|
||||
filepath := *p.Filepath
|
||||
if filepath == "" {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
||||
return fmt.Errorf("server selection: %w", err)
|
||||
}
|
||||
|
||||
err = p.PortForwarding.validate(*p.Name)
|
||||
err = p.PortForwarding.Validate(*p.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port forwarding: %w", err)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (p *Provider) copy() (copied Provider) {
|
||||
return Provider{
|
||||
Name: gosettings.CopyPointer(p.Name),
|
||||
ServerSelection: p.ServerSelection.copy(),
|
||||
PortForwarding: p.PortForwarding.copy(),
|
||||
PortForwarding: p.PortForwarding.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func (p *Provider) mergeWith(other Provider) {
|
||||
func (p *Provider) overrideWith(other Provider) {
|
||||
p.Name = gosettings.OverrideWithPointer(p.Name, other.Name)
|
||||
p.ServerSelection.overrideWith(other.ServerSelection)
|
||||
p.PortForwarding.overrideWith(other.PortForwarding)
|
||||
p.PortForwarding.OverrideWith(other.PortForwarding)
|
||||
}
|
||||
|
||||
func (p *Provider) setDefaults() {
|
||||
|
||||
@@ -23,6 +23,20 @@ type PublicIP struct {
|
||||
IPFilepath *string
|
||||
}
|
||||
|
||||
// UpdateWith deep copies the receiving settings, overrides the copy with
|
||||
// fields set in the partialUpdate argument, validates the new settings
|
||||
// and returns them if they are valid, or returns an error otherwise.
|
||||
// In all cases, the receiving settings are unmodified.
|
||||
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
|
||||
updatedSettings = p.copy()
|
||||
updatedSettings.overrideWith(partialUpdate)
|
||||
err = updatedSettings.validate()
|
||||
if err != nil {
|
||||
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
|
||||
}
|
||||
return updatedSettings, nil
|
||||
}
|
||||
|
||||
func (p PublicIP) validate() (err error) {
|
||||
const minPeriod = 5 * time.Second
|
||||
if *p.Period < minPeriod {
|
||||
|
||||
@@ -38,8 +38,8 @@ func Test_Settings_String(t *testing.T) {
|
||||
| ├── Run OpenVPN as: root
|
||||
| └── Verbosity level: 1
|
||||
├── DNS settings:
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
|
||||
@@ -25,6 +25,10 @@ type Wireguard struct {
|
||||
PreSharedKey *string `json:"pre_shared_key"`
|
||||
// Addresses are the Wireguard interface addresses.
|
||||
Addresses []netip.Prefix `json:"addresses"`
|
||||
// AllowedIPs are the Wireguard allowed IPs.
|
||||
// If left unset, they default to "0.0.0.0/0"
|
||||
// and, if IPv6 is supported, "::0".
|
||||
AllowedIPs []netip.Prefix `json:"allowed_ips"`
|
||||
// Interface is the name of the Wireguard interface
|
||||
// to create. It cannot be the empty string in the
|
||||
// internal state.
|
||||
@@ -66,7 +70,12 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
||||
}
|
||||
_, err = wgtypes.ParseKey(*w.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("private key is not valid: %w", err)
|
||||
err = fmt.Errorf("private key is not valid: %w", err)
|
||||
if vpnProvider == providers.Nordvpn &&
|
||||
err.Error() == "wgtypes: incorrect key size: 48" {
|
||||
err = fmt.Errorf("%w - you might be using your access token instead of the Wireguard private key", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if vpnProvider == providers.Airvpn {
|
||||
@@ -89,13 +98,26 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
||||
}
|
||||
for i, ipNet := range w.Addresses {
|
||||
if !ipNet.IsValid() {
|
||||
return fmt.Errorf("%w: for address at index %d: %s",
|
||||
ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
|
||||
return fmt.Errorf("%w: for address at index %d",
|
||||
ErrWireguardInterfaceAddressNotSet, i)
|
||||
}
|
||||
|
||||
if !ipv6Supported && ipNet.Addr().Is6() {
|
||||
return fmt.Errorf("%w: address %s",
|
||||
ErrWireguardInterfaceAddressIPv6, ipNet)
|
||||
ErrWireguardInterfaceAddressIPv6, ipNet.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Validate AllowedIPs
|
||||
// WARNING: do not check for IPv6 networks in the allowed IPs,
|
||||
// the wireguard code will take care to ignore it.
|
||||
if len(w.AllowedIPs) == 0 {
|
||||
return fmt.Errorf("%w", ErrWireguardAllowedIPsNotSet)
|
||||
}
|
||||
for i, allowedIP := range w.AllowedIPs {
|
||||
if !allowedIP.IsValid() {
|
||||
return fmt.Errorf("%w: for allowed ip %d of %d",
|
||||
ErrWireguardAllowedIPNotSet, i+1, len(w.AllowedIPs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +140,7 @@ func (w *Wireguard) copy() (copied Wireguard) {
|
||||
PrivateKey: gosettings.CopyPointer(w.PrivateKey),
|
||||
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey),
|
||||
Addresses: gosettings.CopySlice(w.Addresses),
|
||||
AllowedIPs: gosettings.CopySlice(w.AllowedIPs),
|
||||
Interface: w.Interface,
|
||||
MTU: w.MTU,
|
||||
Implementation: w.Implementation,
|
||||
@@ -128,6 +151,7 @@ func (w *Wireguard) mergeWith(other Wireguard) {
|
||||
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey)
|
||||
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey)
|
||||
w.Addresses = gosettings.MergeWithSlice(w.Addresses, other.Addresses)
|
||||
w.AllowedIPs = gosettings.MergeWithSlice(w.AllowedIPs, other.AllowedIPs)
|
||||
w.Interface = gosettings.MergeWithString(w.Interface, other.Interface)
|
||||
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
|
||||
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
|
||||
@@ -137,6 +161,7 @@ func (w *Wireguard) overrideWith(other Wireguard) {
|
||||
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey)
|
||||
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey)
|
||||
w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses)
|
||||
w.AllowedIPs = gosettings.OverrideWithSlice(w.AllowedIPs, other.AllowedIPs)
|
||||
w.Interface = gosettings.OverrideWithString(w.Interface, other.Interface)
|
||||
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU)
|
||||
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
|
||||
@@ -150,6 +175,11 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
||||
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
|
||||
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
|
||||
}
|
||||
defaultAllowedIPs := []netip.Prefix{
|
||||
netip.PrefixFrom(netip.IPv4Unspecified(), 0),
|
||||
netip.PrefixFrom(netip.IPv6Unspecified(), 0),
|
||||
}
|
||||
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
|
||||
w.Interface = gosettings.DefaultString(w.Interface, "wg0")
|
||||
const defaultMTU = 1400
|
||||
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU)
|
||||
@@ -178,6 +208,11 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
addressesNode.Appendf(address.String())
|
||||
}
|
||||
|
||||
allowedIPsNode := node.Appendf("Allowed IPs:")
|
||||
for _, allowedIP := range w.AllowedIPs {
|
||||
allowedIPsNode.Appendf(allowedIP.String())
|
||||
}
|
||||
|
||||
interfaceNode := node.Appendf("Network interface: %s", w.Interface)
|
||||
interfaceNode.Appendf("MTU: %d", w.MTU)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type WireguardSelection struct {
|
||||
// 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 netaddr.IPv4Unspecified(). It can never be the zero value
|
||||
// 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.
|
||||
|
||||
@@ -16,6 +16,8 @@ func (s *Source) readPortForward() (
|
||||
return portForwarding, err
|
||||
}
|
||||
|
||||
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER")
|
||||
|
||||
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
|
||||
env.ForceLowercase(false),
|
||||
env.RetroKeys(
|
||||
@@ -23,5 +25,10 @@ func (s *Source) readPortForward() (
|
||||
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
|
||||
))
|
||||
|
||||
portForwarding.ListeningPort, err = s.env.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
|
||||
if err != nil {
|
||||
return portForwarding, err
|
||||
}
|
||||
|
||||
return portForwarding, nil
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ func (s *Source) readServerSelection(vpnProvider, vpnType string) (
|
||||
return ss, err
|
||||
}
|
||||
|
||||
// VPNUnlimited only
|
||||
// Surfshark only
|
||||
ss.MultiHopOnly, err = s.env.BoolPtr("MULTIHOP_ONLY")
|
||||
if err != nil {
|
||||
return ss, err
|
||||
}
|
||||
|
||||
// VPNUnlimited only
|
||||
ss.MultiHopOnly, err = s.env.BoolPtr("STREAM_ONLY")
|
||||
ss.StreamOnly, err = s.env.BoolPtr("STREAM_ONLY")
|
||||
if err != nil {
|
||||
return ss, err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
|
||||
if err != nil {
|
||||
return wireguard, err // already wrapped
|
||||
}
|
||||
wireguard.AllowedIPs, err = s.env.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
|
||||
if err != nil {
|
||||
return wireguard, err // already wrapped
|
||||
}
|
||||
mtuPtr, err := s.env.Uint16Ptr("WIREGUARD_MTU")
|
||||
if err != nil {
|
||||
return wireguard, err
|
||||
|
||||
3
internal/configuration/sources/files/helpers_test.go
Normal file
3
internal/configuration/sources/files/helpers_test.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package files
|
||||
|
||||
func ptrTo[T any](x T) *T { return &x }
|
||||
16
internal/configuration/sources/files/provider.go
Normal file
16
internal/configuration/sources/files/provider.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
func (s *Source) readProvider() (provider settings.Provider, err error) {
|
||||
provider.ServerSelection, err = s.readServerSelection()
|
||||
if err != nil {
|
||||
return provider, fmt.Errorf("server selection: %w", err)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -4,10 +4,15 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
type Source struct{}
|
||||
type Source struct {
|
||||
wireguardConfigPath string
|
||||
}
|
||||
|
||||
func New() *Source {
|
||||
return &Source{}
|
||||
const wireguardConfigPath = "/gluetun/wireguard/wg0.conf"
|
||||
return &Source{
|
||||
wireguardConfigPath: wireguardConfigPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) String() string { return "files" }
|
||||
|
||||
16
internal/configuration/sources/files/serverselection.go
Normal file
16
internal/configuration/sources/files/serverselection.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
func (s *Source) readServerSelection() (selection settings.ServerSelection, err error) {
|
||||
selection.Wireguard, err = s.readWireguardSelection()
|
||||
if err != nil {
|
||||
return selection, fmt.Errorf("wireguard: %w", err)
|
||||
}
|
||||
|
||||
return selection, nil
|
||||
}
|
||||
@@ -7,10 +7,20 @@ import (
|
||||
)
|
||||
|
||||
func (s *Source) readVPN() (vpn settings.VPN, err error) {
|
||||
vpn.Provider, err = s.readProvider()
|
||||
if err != nil {
|
||||
return vpn, fmt.Errorf("provider: %w", err)
|
||||
}
|
||||
|
||||
vpn.OpenVPN, err = s.readOpenVPN()
|
||||
if err != nil {
|
||||
return vpn, fmt.Errorf("OpenVPN: %w", err)
|
||||
}
|
||||
|
||||
vpn.Wireguard, err = s.readWireguard()
|
||||
if err != nil {
|
||||
return vpn, fmt.Errorf("wireguard: %w", err)
|
||||
}
|
||||
|
||||
return vpn, nil
|
||||
}
|
||||
|
||||
120
internal/configuration/sources/files/wireguard.go
Normal file
120
internal/configuration/sources/files/wireguard.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
|
||||
regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
|
||||
)
|
||||
|
||||
func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
|
||||
fileStringPtr, err := ReadFromFile(s.wireguardConfigPath)
|
||||
if err != nil {
|
||||
return wireguard, fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
if fileStringPtr == nil {
|
||||
return wireguard, nil
|
||||
}
|
||||
|
||||
rawData := []byte(*fileStringPtr)
|
||||
iniFile, err := ini.Load(rawData)
|
||||
if err != nil {
|
||||
return wireguard, fmt.Errorf("loading ini from reader: %w", err)
|
||||
}
|
||||
|
||||
interfaceSection, err := iniFile.GetSection("Interface")
|
||||
if err == nil {
|
||||
err = parseWireguardInterfaceSection(interfaceSection, &wireguard)
|
||||
if err != nil {
|
||||
return wireguard, fmt.Errorf("parsing interface section: %w", err)
|
||||
}
|
||||
} else if !regexINISectionNotExist.MatchString(err.Error()) {
|
||||
// can never happen
|
||||
return wireguard, fmt.Errorf("getting interface section: %w", err)
|
||||
}
|
||||
|
||||
peerSection, err := iniFile.GetSection("Peer")
|
||||
if err == nil {
|
||||
wireguard.PreSharedKey, err = parseINIWireguardKey(peerSection, "PresharedKey")
|
||||
if err != nil {
|
||||
return wireguard, fmt.Errorf("parsing peer section: %w", err)
|
||||
}
|
||||
} else if !regexINISectionNotExist.MatchString(err.Error()) {
|
||||
// can never happen
|
||||
return wireguard, fmt.Errorf("getting peer section: %w", err)
|
||||
}
|
||||
|
||||
return wireguard, nil
|
||||
}
|
||||
|
||||
func parseWireguardInterfaceSection(interfaceSection *ini.Section,
|
||||
wireguard *settings.Wireguard) (err error) {
|
||||
wireguard.PrivateKey, err = parseINIWireguardKey(interfaceSection, "PrivateKey")
|
||||
if err != nil {
|
||||
return err // error is already wrapped correctly
|
||||
}
|
||||
|
||||
wireguard.Addresses, err = parseINIWireguardAddress(interfaceSection)
|
||||
if err != nil {
|
||||
return err // error is already wrapped correctly
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseINIWireguardKey(section *ini.Section, keyName string) (
|
||||
key *string, err error) {
|
||||
iniKey, err := section.GetKey(keyName)
|
||||
if err != nil {
|
||||
if regexINIKeyNotExist.MatchString(err.Error()) {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
// can never happen
|
||||
return nil, fmt.Errorf("getting %s key: %w", keyName, err)
|
||||
}
|
||||
|
||||
key = new(string)
|
||||
*key = iniKey.String()
|
||||
_, err = wgtypes.ParseKey(*key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %s: %w", keyName, *key, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func parseINIWireguardAddress(section *ini.Section) (
|
||||
addresses []netip.Prefix, err error) {
|
||||
addressKey, err := section.GetKey("Address")
|
||||
if err != nil {
|
||||
if regexINIKeyNotExist.MatchString(err.Error()) {
|
||||
return nil, nil
|
||||
}
|
||||
// can never happen
|
||||
return nil, fmt.Errorf("getting Address key: %w", err)
|
||||
}
|
||||
|
||||
addressStrings := strings.Split(addressKey.String(), ",")
|
||||
addresses = make([]netip.Prefix, len(addressStrings))
|
||||
for i, addressString := range addressStrings {
|
||||
addressString = strings.TrimSpace(addressString)
|
||||
if !strings.ContainsRune(addressString, '/') {
|
||||
addressString += "/32"
|
||||
}
|
||||
addresses[i], err = netip.ParsePrefix(addressString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return addresses, nil
|
||||
}
|
||||
263
internal/configuration/sources/files/wireguard_test.go
Normal file
263
internal/configuration/sources/files/wireguard_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func Test_Source_readWireguard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("fail reading from file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dirPath := t.TempDir()
|
||||
source := &Source{
|
||||
wireguardConfigPath: dirPath,
|
||||
}
|
||||
wireguard, err := source.readWireguard()
|
||||
assert.Equal(t, settings.Wireguard{}, wireguard)
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, `reading file: read .+: is a directory`, err.Error())
|
||||
})
|
||||
|
||||
t.Run("no file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
noFile := filepath.Join(t.TempDir(), "doesnotexist")
|
||||
source := &Source{
|
||||
wireguardConfigPath: noFile,
|
||||
}
|
||||
wireguard, err := source.readWireguard()
|
||||
assert.Equal(t, settings.Wireguard{}, wireguard)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
fileContent string
|
||||
wireguard settings.Wireguard
|
||||
errMessage string
|
||||
}{
|
||||
"ini load error": {
|
||||
fileContent: "invalid",
|
||||
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
|
||||
},
|
||||
"empty file": {},
|
||||
"interface section parsing error": {
|
||||
fileContent: `
|
||||
[Interface]
|
||||
PrivateKey = x
|
||||
`,
|
||||
errMessage: "parsing interface section: parsing PrivateKey: " +
|
||||
"x: wgtypes: failed to parse base64-encoded key: " +
|
||||
"illegal base64 data at input byte 0",
|
||||
},
|
||||
"success": {
|
||||
fileContent: `
|
||||
[Interface]
|
||||
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
|
||||
Address = 10.38.22.35/32
|
||||
DNS = 193.138.218.74
|
||||
|
||||
[Peer]
|
||||
PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||
`,
|
||||
wireguard: settings.Wireguard{
|
||||
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
|
||||
PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
source := &Source{
|
||||
wireguardConfigPath: configFile,
|
||||
}
|
||||
|
||||
wireguard, err := source.readWireguard()
|
||||
|
||||
assert.Equal(t, testCase.wireguard, wireguard)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseWireguardInterfaceSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
iniData string
|
||||
wireguard settings.Wireguard
|
||||
errMessage string
|
||||
}{
|
||||
"private key error": {
|
||||
iniData: `[Interface]
|
||||
PrivateKey = x`,
|
||||
errMessage: "parsing PrivateKey: x: " +
|
||||
"wgtypes: failed to parse base64-encoded key: " +
|
||||
"illegal base64 data at input byte 0",
|
||||
},
|
||||
"address error": {
|
||||
iniData: `[Interface]
|
||||
Address = x
|
||||
`,
|
||||
errMessage: "parsing address: netip.ParsePrefix(\"x/32\"): ParseAddr(\"x\"): unable to parse IP",
|
||||
},
|
||||
"success": {
|
||||
iniData: `
|
||||
[Interface]
|
||||
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
|
||||
Address = 10.38.22.35/32
|
||||
`,
|
||||
wireguard: settings.Wireguard{
|
||||
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
iniFile, err := ini.Load([]byte(testCase.iniData))
|
||||
require.NoError(t, err)
|
||||
iniSection, err := iniFile.GetSection("Interface")
|
||||
require.NoError(t, err)
|
||||
|
||||
var wireguard settings.Wireguard
|
||||
err = parseWireguardInterfaceSection(iniSection, &wireguard)
|
||||
|
||||
assert.Equal(t, testCase.wireguard, wireguard)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseINIWireguardKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
fileContent string
|
||||
keyName string
|
||||
key *string
|
||||
errMessage string
|
||||
}{
|
||||
"key does not exist": {
|
||||
fileContent: `[Interface]`,
|
||||
keyName: "PrivateKey",
|
||||
},
|
||||
"bad Wireguard key": {
|
||||
fileContent: `[Interface]
|
||||
PrivateKey = x`,
|
||||
keyName: "PrivateKey",
|
||||
errMessage: "parsing PrivateKey: x: " +
|
||||
"wgtypes: failed to parse base64-encoded key: " +
|
||||
"illegal base64 data at input byte 0",
|
||||
},
|
||||
"success": {
|
||||
fileContent: `[Interface]
|
||||
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`,
|
||||
keyName: "PrivateKey",
|
||||
key: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
iniFile, err := ini.Load([]byte(testCase.fileContent))
|
||||
require.NoError(t, err)
|
||||
iniSection, err := iniFile.GetSection("Interface")
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := parseINIWireguardKey(iniSection, testCase.keyName)
|
||||
|
||||
assert.Equal(t, testCase.key, key)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseINIWireguardAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
fileContent string
|
||||
addresses []netip.Prefix
|
||||
errMessage string
|
||||
}{
|
||||
"key does not exist": {
|
||||
fileContent: `[Interface]`,
|
||||
},
|
||||
"bad address": {
|
||||
fileContent: `[Interface]
|
||||
Address = x`,
|
||||
errMessage: "parsing address: netip.ParsePrefix(\"x/32\"): ParseAddr(\"x\"): unable to parse IP",
|
||||
},
|
||||
"success": {
|
||||
fileContent: `[Interface]
|
||||
Address = 1.2.3.4/32, 5.6.7.8/32`,
|
||||
addresses: []netip.Prefix{
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 32),
|
||||
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
iniFile, err := ini.Load([]byte(testCase.fileContent))
|
||||
require.NoError(t, err)
|
||||
iniSection, err := iniFile.GetSection("Interface")
|
||||
require.NoError(t, err)
|
||||
|
||||
addresses, err := parseINIWireguardAddress(iniSection)
|
||||
|
||||
assert.Equal(t, testCase.addresses, addresses)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
internal/configuration/sources/files/wireguardselection.go
Normal file
83
internal/configuration/sources/files/wireguardselection.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/govalid/port"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
|
||||
)
|
||||
|
||||
func (s *Source) readWireguardSelection() (selection settings.WireguardSelection, err error) {
|
||||
fileStringPtr, err := ReadFromFile(s.wireguardConfigPath)
|
||||
if err != nil {
|
||||
return selection, fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
if fileStringPtr == nil {
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
rawData := []byte(*fileStringPtr)
|
||||
iniFile, err := ini.Load(rawData)
|
||||
if err != nil {
|
||||
return selection, fmt.Errorf("loading ini from reader: %w", err)
|
||||
}
|
||||
|
||||
peerSection, err := iniFile.GetSection("Peer")
|
||||
if err == nil {
|
||||
err = parseWireguardPeerSection(peerSection, &selection)
|
||||
if err != nil {
|
||||
return selection, fmt.Errorf("parsing peer section: %w", err)
|
||||
}
|
||||
} else if !regexINISectionNotExist.MatchString(err.Error()) {
|
||||
// can never happen
|
||||
return selection, fmt.Errorf("getting peer section: %w", err)
|
||||
}
|
||||
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
func parseWireguardPeerSection(peerSection *ini.Section,
|
||||
selection *settings.WireguardSelection) (err error) {
|
||||
publicKeyPtr, err := parseINIWireguardKey(peerSection, "PublicKey")
|
||||
if err != nil {
|
||||
return err // error is already wrapped correctly
|
||||
} else if publicKeyPtr != nil {
|
||||
selection.PublicKey = *publicKeyPtr
|
||||
}
|
||||
|
||||
endpointKey, err := peerSection.GetKey("Endpoint")
|
||||
if err == nil {
|
||||
endpoint := endpointKey.String()
|
||||
host, portString, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("splitting endpoint: %w", err)
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrEndpointHostNotIP, err)
|
||||
}
|
||||
|
||||
endpointPort, err := port.Validate(portString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port from Endpoint key: %w", err)
|
||||
}
|
||||
|
||||
selection.EndpointIP = ip
|
||||
selection.EndpointPort = &endpointPort
|
||||
} else if !regexINIKeyNotExist.MatchString(err.Error()) {
|
||||
// can never happen
|
||||
return fmt.Errorf("getting endpoint key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
181
internal/configuration/sources/files/wireguardselection_test.go
Normal file
181
internal/configuration/sources/files/wireguardselection_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func uint16Ptr(n uint16) *uint16 { return &n }
|
||||
|
||||
func Test_Source_readWireguardSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("fail reading from file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dirPath := t.TempDir()
|
||||
source := &Source{
|
||||
wireguardConfigPath: dirPath,
|
||||
}
|
||||
wireguard, err := source.readWireguardSelection()
|
||||
assert.Equal(t, settings.WireguardSelection{}, wireguard)
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, `reading file: read .+: is a directory`, err.Error())
|
||||
})
|
||||
|
||||
t.Run("no file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
noFile := filepath.Join(t.TempDir(), "doesnotexist")
|
||||
source := &Source{
|
||||
wireguardConfigPath: noFile,
|
||||
}
|
||||
wireguard, err := source.readWireguardSelection()
|
||||
assert.Equal(t, settings.WireguardSelection{}, wireguard)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
fileContent string
|
||||
selection settings.WireguardSelection
|
||||
errMessage string
|
||||
}{
|
||||
"ini load error": {
|
||||
fileContent: "invalid",
|
||||
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
|
||||
},
|
||||
"empty file": {},
|
||||
"peer section parsing error": {
|
||||
fileContent: `
|
||||
[Peer]
|
||||
PublicKey = x
|
||||
`,
|
||||
errMessage: "parsing peer section: parsing PublicKey: " +
|
||||
"x: wgtypes: failed to parse base64-encoded key: " +
|
||||
"illegal base64 data at input byte 0",
|
||||
},
|
||||
"success": {
|
||||
fileContent: `
|
||||
[Peer]
|
||||
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
|
||||
Endpoint = 1.2.3.4:51820
|
||||
`,
|
||||
selection: settings.WireguardSelection{
|
||||
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
|
||||
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||
EndpointPort: uint16Ptr(51820),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configFile := filepath.Join(t.TempDir(), "wg.conf")
|
||||
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
source := &Source{
|
||||
wireguardConfigPath: configFile,
|
||||
}
|
||||
|
||||
wireguard, err := source.readWireguardSelection()
|
||||
|
||||
assert.Equal(t, testCase.selection, wireguard)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseWireguardPeerSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
iniData string
|
||||
selection settings.WireguardSelection
|
||||
errMessage string
|
||||
}{
|
||||
"public key error": {
|
||||
iniData: `[Peer]
|
||||
PublicKey = x`,
|
||||
errMessage: "parsing PublicKey: x: " +
|
||||
"wgtypes: failed to parse base64-encoded key: " +
|
||||
"illegal base64 data at input byte 0",
|
||||
},
|
||||
"public key set": {
|
||||
iniData: `[Peer]
|
||||
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`,
|
||||
selection: settings.WireguardSelection{
|
||||
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
|
||||
},
|
||||
},
|
||||
"missing port in endpoint": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = x`,
|
||||
errMessage: "splitting endpoint: address x: missing port in address",
|
||||
},
|
||||
"endpoint host is not IP": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = website.com:51820`,
|
||||
errMessage: "endpoint host is not an IP: ParseAddr(\"website.com\"): unexpected character (at \"website.com\")",
|
||||
},
|
||||
"endpoint port is not valid": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = 1.2.3.4:518299`,
|
||||
errMessage: "port from Endpoint key: port cannot be higher than 65535: 518299",
|
||||
},
|
||||
"valid endpoint": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = 1.2.3.4:51820`,
|
||||
selection: settings.WireguardSelection{
|
||||
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||
EndpointPort: uint16Ptr(51820),
|
||||
},
|
||||
},
|
||||
"all set": {
|
||||
iniData: `[Peer]
|
||||
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
|
||||
Endpoint = 1.2.3.4:51820`,
|
||||
selection: settings.WireguardSelection{
|
||||
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
|
||||
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||
EndpointPort: uint16Ptr(51820),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
iniFile, err := ini.Load([]byte(testCase.iniData))
|
||||
require.NoError(t, err)
|
||||
iniSection, err := iniFile.GetSection("Peer")
|
||||
require.NoError(t, err)
|
||||
|
||||
var selection settings.WireguardSelection
|
||||
err = parseWireguardPeerSection(iniSection, &selection)
|
||||
|
||||
assert.Equal(t, testCase.selection, selection)
|
||||
if testCase.errMessage != "" {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), *settings.KeepNameserver)
|
||||
const keepNameserver = false
|
||||
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
@@ -39,7 +40,8 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||
}
|
||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), *settings.KeepNameserver)
|
||||
const keepNameserver = false
|
||||
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ import (
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution?
|
||||
// TODO this one is kept if DNS_KEEP_NAMESERVER=on and should be replaced
|
||||
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)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-l.start:
|
||||
@@ -27,7 +32,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
unboundCancel := func() { waitError <- nil }
|
||||
closeStreams := func() {}
|
||||
|
||||
for *l.GetSettings().DoT.Enabled {
|
||||
settings := l.GetSettings()
|
||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||
var err error
|
||||
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
||||
if err == nil {
|
||||
@@ -50,7 +56,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
l.logAndWait(ctx, err)
|
||||
}
|
||||
|
||||
if !*l.GetSettings().DoT.Enabled {
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package firewall
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
)
|
||||
|
||||
func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
|
||||
@@ -49,6 +51,13 @@ func (c *Config) disable(ctx context.Context) (err error) {
|
||||
if err = c.setIPv6AllPolicies(ctx, "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("setting ipv6 policies: %w", err)
|
||||
}
|
||||
|
||||
const remove = true
|
||||
err = c.redirectPorts(ctx, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing port redirections: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,6 +131,11 @@ func (c *Config) enable(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.redirectPorts(ctx, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting ports: %w", err)
|
||||
}
|
||||
|
||||
if err := c.runUserPostRules(ctx, c.customRulesPath, remove); err != nil {
|
||||
return fmt.Errorf("running user defined post firewall rules: %w", err)
|
||||
}
|
||||
@@ -147,7 +161,16 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
|
||||
|
||||
func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
|
||||
for _, subnet := range c.outboundSubnets {
|
||||
subnetIsIPv6 := subnet.Addr().Is6()
|
||||
firewallUpdated := false
|
||||
for _, defaultRoute := range c.defaultRoutes {
|
||||
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||
if !ipFamilyMatch {
|
||||
continue
|
||||
}
|
||||
firewallUpdated = true
|
||||
|
||||
const remove = false
|
||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||
defaultRoute.AssignedIP, subnet, remove)
|
||||
@@ -155,6 +178,11 @@ func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !firewallUpdated {
|
||||
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
|
||||
"no default route matching its family", subnet))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -172,3 +200,14 @@ func (c *Config) allowInputPorts(ctx context.Context) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) redirectPorts(ctx context.Context, remove bool) (err error) {
|
||||
for _, portRedirection := range c.portRedirections {
|
||||
err = c.redirectPort(ctx, portRedirection.interfaceName, portRedirection.sourcePort,
|
||||
portRedirection.destinationPort, remove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type Config struct { //nolint:maligned
|
||||
vpnIntf string
|
||||
outboundSubnets []netip.Prefix
|
||||
allowedInputPorts map[uint16]map[string]struct{} // port to interfaces set mapping
|
||||
portRedirections portRedirections
|
||||
stateMutex sync.Mutex
|
||||
}
|
||||
|
||||
|
||||
@@ -142,9 +142,13 @@ func (c *Config) acceptEstablishedRelatedTraffic(ctx context.Context, remove boo
|
||||
|
||||
func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
||||
defaultInterface string, connection models.Connection, remove bool) error {
|
||||
protocol := connection.Protocol
|
||||
if protocol == "tcp-client" {
|
||||
protocol = "tcp"
|
||||
}
|
||||
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
|
||||
appendOrDelete(remove), connection.IP, defaultInterface, connection.Protocol,
|
||||
connection.Protocol, connection.Port)
|
||||
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
|
||||
protocol, connection.Port)
|
||||
if connection.IP.Is4() {
|
||||
return c.runIptablesInstruction(ctx, instruction)
|
||||
} else if c.ip6Tables == "" {
|
||||
@@ -198,6 +202,38 @@ func (c *Config) acceptInputToPort(ctx context.Context, intf string, port uint16
|
||||
})
|
||||
}
|
||||
|
||||
// Used for VPN server side port forwarding, with intf set to the VPN tunnel interface.
|
||||
func (c *Config) redirectPort(ctx context.Context, intf string,
|
||||
sourcePort, destinationPort uint16, remove bool) (err error) {
|
||||
interfaceFlag := "-i " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
}
|
||||
|
||||
err = c.runIptablesInstructions(ctx, []string{
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -p udp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting IPv4 source port %d to destination port %d on interface %s: %w",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
|
||||
err = c.runIP6tablesInstructions(ctx, []string{
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -p udp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) runUserPostRules(ctx context.Context, filepath string, remove bool) error {
|
||||
file, err := os.OpenFile(filepath, os.O_RDONLY, 0)
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/netlink"
|
||||
"github.com/qdm12/gluetun/internal/subnet"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,16 @@ func (c *Config) SetOutboundSubnets(ctx context.Context, subnets []netip.Prefix)
|
||||
func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Prefix) {
|
||||
const remove = true
|
||||
for _, subNet := range subnets {
|
||||
subnetIsIPv6 := subNet.Addr().Is6()
|
||||
firewallUpdated := false
|
||||
for _, defaultRoute := range c.defaultRoutes {
|
||||
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||
if !ipFamilyMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
firewallUpdated = true
|
||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||
defaultRoute.AssignedIP, subNet, remove)
|
||||
if err != nil {
|
||||
@@ -45,6 +55,12 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !firewallUpdated {
|
||||
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
|
||||
"no default route matching its family", subNet))
|
||||
continue
|
||||
}
|
||||
c.outboundSubnets = subnet.RemoveSubnetFromSubnets(c.outboundSubnets, subNet)
|
||||
}
|
||||
}
|
||||
@@ -52,13 +68,28 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
|
||||
func (c *Config) addOutboundSubnets(ctx context.Context, subnets []netip.Prefix) error {
|
||||
const remove = false
|
||||
for _, subnet := range subnets {
|
||||
subnetIsIPv6 := subnet.Addr().Is6()
|
||||
firewallUpdated := false
|
||||
for _, defaultRoute := range c.defaultRoutes {
|
||||
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||
if !ipFamilyMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
firewallUpdated = true
|
||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||
defaultRoute.AssignedIP, subnet, remove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !firewallUpdated {
|
||||
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
|
||||
"no default route matching its family", subnet))
|
||||
continue
|
||||
}
|
||||
c.outboundSubnets = append(c.outboundSubnets, subnet)
|
||||
}
|
||||
return nil
|
||||
|
||||
119
internal/firewall/redirect.go
Normal file
119
internal/firewall/redirect.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RedirectPort redirects a source port to a destination port on the interface
|
||||
// intf. If intf is empty, it is set to "*" which means all interfaces.
|
||||
// If a redirection for the source port given already exists, it is removed first.
|
||||
// If the destination port is zero, the redirection for the source port is removed
|
||||
// and no new redirection is added.
|
||||
func (c *Config) RedirectPort(ctx context.Context, intf string, sourcePort,
|
||||
destinationPort uint16) (err error) {
|
||||
c.stateMutex.Lock()
|
||||
defer c.stateMutex.Unlock()
|
||||
|
||||
if sourcePort == 0 {
|
||||
panic("source port cannot be 0")
|
||||
}
|
||||
|
||||
newRedirection := portRedirection{
|
||||
interfaceName: intf,
|
||||
sourcePort: sourcePort,
|
||||
destinationPort: destinationPort,
|
||||
}
|
||||
|
||||
if !c.enabled {
|
||||
c.logger.Info("firewall disabled, only updating redirected ports internal state")
|
||||
if destinationPort == 0 {
|
||||
c.portRedirections.remove(intf, sourcePort)
|
||||
return nil
|
||||
}
|
||||
exists, conflict := c.portRedirections.check(newRedirection)
|
||||
switch {
|
||||
case exists:
|
||||
return nil
|
||||
case conflict != nil:
|
||||
c.portRedirections.remove(conflict.interfaceName,
|
||||
conflict.sourcePort)
|
||||
}
|
||||
c.portRedirections.append(newRedirection)
|
||||
return nil
|
||||
}
|
||||
|
||||
exists, conflict := c.portRedirections.check(newRedirection)
|
||||
switch {
|
||||
case exists:
|
||||
return nil
|
||||
case conflict != nil:
|
||||
const remove = true
|
||||
err = c.redirectPort(ctx, conflict.interfaceName, conflict.sourcePort,
|
||||
conflict.destinationPort, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing conflicting redirection: %w", err)
|
||||
}
|
||||
c.portRedirections.remove(conflict.interfaceName,
|
||||
conflict.sourcePort)
|
||||
}
|
||||
|
||||
const remove = false
|
||||
err = c.redirectPort(ctx, intf, sourcePort, destinationPort, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting port: %w", err)
|
||||
}
|
||||
c.portRedirections.append(newRedirection)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type portRedirection struct {
|
||||
interfaceName string
|
||||
sourcePort uint16
|
||||
destinationPort uint16
|
||||
}
|
||||
|
||||
type portRedirections []portRedirection
|
||||
|
||||
func (p *portRedirections) remove(intf string, sourcePort uint16) {
|
||||
slice := *p
|
||||
for i, redirection := range slice {
|
||||
interfaceMatch := intf == "" || intf == redirection.interfaceName
|
||||
if redirection.sourcePort == sourcePort && interfaceMatch {
|
||||
// Remove redirection - note: order does not matter
|
||||
slice[i] = slice[len(slice)-1]
|
||||
slice = slice[:len(slice)-1]
|
||||
}
|
||||
}
|
||||
*p = slice
|
||||
}
|
||||
|
||||
func (p *portRedirections) check(dryRun portRedirection) (alreadyExists bool,
|
||||
conflict *portRedirection) {
|
||||
slice := *p
|
||||
for _, redirection := range slice {
|
||||
interfaceMatch := redirection.interfaceName == "" ||
|
||||
redirection.interfaceName == dryRun.interfaceName
|
||||
|
||||
if redirection.sourcePort == dryRun.sourcePort &&
|
||||
redirection.destinationPort == dryRun.destinationPort &&
|
||||
interfaceMatch {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if redirection.sourcePort == dryRun.sourcePort &&
|
||||
interfaceMatch {
|
||||
// Source port has a redirection already for the same interface or all interfaces
|
||||
return false, &redirection
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// append should be called after running `check` to avoid rule conflicts.
|
||||
func (p *portRedirections) append(newRedirection portRedirection) {
|
||||
slice := *p
|
||||
slice = append(slice, newRedirection)
|
||||
*p = slice
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type vpnHealth struct {
|
||||
func (s *Server) onUnhealthyVPN(ctx context.Context) {
|
||||
s.logger.Info("program has been unhealthy for " +
|
||||
s.vpn.healthyWait.String() + ": restarting VPN " +
|
||||
"(see https://github.com/qdm12/gluetun/wiki/Healthcheck)")
|
||||
"(see https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md)")
|
||||
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped)
|
||||
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
|
||||
s.vpn.healthyWait += *s.config.VPN.Addition
|
||||
|
||||
207
internal/mod/info.go
Normal file
207
internal/mod/info.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type state uint8
|
||||
|
||||
const (
|
||||
unloaded state = iota
|
||||
loading
|
||||
loaded
|
||||
builtin
|
||||
)
|
||||
|
||||
type moduleInfo struct {
|
||||
state state
|
||||
dependencyPaths []string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrModulesDirectoryNotFound = errors.New("modules directory not found")
|
||||
)
|
||||
|
||||
func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
|
||||
var utsName unix.Utsname
|
||||
err = unix.Uname(&utsName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting unix uname release: %w", err)
|
||||
}
|
||||
release := unix.ByteSliceToString(utsName.Release[:])
|
||||
release = strings.TrimSpace(release)
|
||||
|
||||
modulePaths := []string{
|
||||
filepath.Join("/lib/modules", release),
|
||||
filepath.Join("/usr/lib/modules", release),
|
||||
}
|
||||
|
||||
var modulesPath string
|
||||
var found bool
|
||||
for _, modulesPath = range modulePaths {
|
||||
info, err := os.Stat(modulesPath)
|
||||
if err == nil && info.IsDir() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: %s are not valid existing directories"+
|
||||
"; have you bind mounted the /lib/modules directory?",
|
||||
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
|
||||
}
|
||||
|
||||
dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
|
||||
dependencyFile, err := os.Open(dependencyFilepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening dependency file: %w", err)
|
||||
}
|
||||
|
||||
modulesInfo = make(map[string]moduleInfo)
|
||||
scanner := bufio.NewScanner(dependencyFile)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Split(line, ":")
|
||||
path := filepath.Join(modulesPath, strings.TrimSpace(parts[0]))
|
||||
dependenciesString := strings.TrimSpace(parts[1])
|
||||
|
||||
if dependenciesString == "" {
|
||||
modulesInfo[path] = moduleInfo{}
|
||||
continue
|
||||
}
|
||||
|
||||
dependencyNames := strings.Split(dependenciesString, " ")
|
||||
dependencies := make([]string, len(dependencyNames))
|
||||
for i := range dependencyNames {
|
||||
dependencies[i] = filepath.Join(modulesPath, dependencyNames[i])
|
||||
}
|
||||
modulesInfo[path] = moduleInfo{dependencyPaths: dependencies}
|
||||
}
|
||||
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
_ = dependencyFile.Close()
|
||||
return nil, fmt.Errorf("modules dependency file scanning: %w", err)
|
||||
}
|
||||
|
||||
err = dependencyFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("closing dependency file: %w", err)
|
||||
}
|
||||
|
||||
err = getBuiltinModules(modulesPath, modulesInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting builtin modules: %w", err)
|
||||
}
|
||||
|
||||
err = getLoadedModules(modulesInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting loaded modules: %w", err)
|
||||
}
|
||||
|
||||
return modulesInfo, nil
|
||||
}
|
||||
|
||||
func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error {
|
||||
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("opening builtin modules file: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
txt := scanner.Text()
|
||||
path := filepath.Join(modulesDirPath, strings.TrimSpace(txt))
|
||||
info := modulesInfo[path]
|
||||
info.state = builtin
|
||||
modulesInfo[path] = info
|
||||
}
|
||||
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("scanning builtin modules file: %w", err)
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing builtin modules file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedModules(modulesInfo map[string]moduleInfo) (err error) {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
// File cannot be opened, so assume no module is loaded
|
||||
return nil //nolint:nilerr
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
parts := strings.Split(scanner.Text(), " ")
|
||||
name := parts[0]
|
||||
path, err := findModulePath(name, modulesInfo)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("finding module path: %w", err)
|
||||
}
|
||||
info := modulesInfo[path]
|
||||
info.state = loaded
|
||||
modulesInfo[path] = info
|
||||
}
|
||||
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return fmt.Errorf("scanning modules: %w", err)
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing process modules file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrModulePathNotFound = errors.New("module path not found")
|
||||
)
|
||||
|
||||
func findModulePath(moduleName string, modulesInfo map[string]moduleInfo) (modulePath string, err error) {
|
||||
// Kernel module names can have underscores or hyphens in their names,
|
||||
// but only one or the other in one particular name.
|
||||
nameHyphensOnly := strings.ReplaceAll(moduleName, "_", "-")
|
||||
nameUnderscoresOnly := strings.ReplaceAll(moduleName, "-", "_")
|
||||
|
||||
validModuleExtensions := []string{".ko", ".ko.gz", ".ko.xz", ".ko.zst"}
|
||||
const nameVariants = 2
|
||||
validFilenames := make(map[string]struct{}, nameVariants*len(validModuleExtensions))
|
||||
for _, ext := range validModuleExtensions {
|
||||
validFilenames[nameHyphensOnly+ext] = struct{}{}
|
||||
validFilenames[nameUnderscoresOnly+ext] = struct{}{}
|
||||
}
|
||||
|
||||
for modulePath := range modulesInfo {
|
||||
moduleFileName := path.Base(modulePath)
|
||||
_, valid := validFilenames[moduleFileName]
|
||||
if valid {
|
||||
return modulePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: for %q", ErrModulePathNotFound, moduleName)
|
||||
}
|
||||
115
internal/mod/load.go
Normal file
115
internal/mod/load.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/klauspost/pgzip"
|
||||
"github.com/ulikunitz/xz"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrModuleInfoNotFound = errors.New("module info not found")
|
||||
ErrCircularDependency = errors.New("circular dependency")
|
||||
)
|
||||
|
||||
func initDependencies(path string, modulesInfo map[string]moduleInfo) (err error) {
|
||||
info, ok := modulesInfo[path]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", ErrModuleInfoNotFound, path)
|
||||
}
|
||||
|
||||
switch info.state {
|
||||
case unloaded:
|
||||
case loaded, builtin:
|
||||
return nil
|
||||
case loading:
|
||||
return fmt.Errorf("%w: %s is already in the loading state",
|
||||
ErrCircularDependency, path)
|
||||
}
|
||||
|
||||
info.state = loading
|
||||
modulesInfo[path] = info
|
||||
|
||||
for _, dependencyPath := range info.dependencyPaths {
|
||||
err = initDependencies(dependencyPath, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init dependencies for %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initModule(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading module: %w", err)
|
||||
}
|
||||
info.state = loaded
|
||||
modulesInfo[path] = info
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initModule(path string) (err error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening module file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
var reader io.Reader
|
||||
switch filepath.Ext(file.Name()) {
|
||||
case ".xz":
|
||||
reader, err = xz.NewReader(file)
|
||||
case ".gz":
|
||||
reader, err = pgzip.NewReader(file)
|
||||
case ".zst":
|
||||
reader, err = zstd.NewReader(file)
|
||||
default:
|
||||
const moduleParams = ""
|
||||
const flags = 0
|
||||
err = unix.FinitModule(int(file.Fd()), moduleParams, flags)
|
||||
switch {
|
||||
case err == nil, err == unix.EEXIST: //nolint:goerr113
|
||||
return nil
|
||||
case err != unix.ENOSYS: //nolint:goerr113
|
||||
if strings.HasSuffix(err.Error(), "operation not permitted") {
|
||||
err = fmt.Errorf("%w; did you set the SYS_MODULE capability to your container?", err)
|
||||
}
|
||||
return fmt.Errorf("finit module %s: %w", path, err)
|
||||
case flags != 0:
|
||||
return err // unix.ENOSYS error
|
||||
default: // Fall back to init_module(2).
|
||||
reader = file
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading from %s: %w", path, err)
|
||||
}
|
||||
|
||||
image, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading module image from %s: %w", path, err)
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing module file %s: %w", path, err)
|
||||
}
|
||||
|
||||
const params = ""
|
||||
err = unix.InitModule(image, params)
|
||||
switch err {
|
||||
case nil, unix.EEXIST:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("init module read from %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
37
internal/mod/probe.go
Normal file
37
internal/mod/probe.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Probe loads the given kernel module and its dependencies.
|
||||
func Probe(moduleName string) error {
|
||||
modulesInfo, err := getModulesInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting modules information: %w", err)
|
||||
}
|
||||
|
||||
modulePath, err := findModulePath(moduleName, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding module path: %w", err)
|
||||
}
|
||||
|
||||
info := modulesInfo[modulePath]
|
||||
if info.state == builtin || info.state == loaded {
|
||||
return nil
|
||||
}
|
||||
|
||||
info.state = loading
|
||||
for _, dependencyModulePath := range info.dependencyPaths {
|
||||
err = initDependencies(dependencyModulePath, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init dependencies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initModule(modulePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init module: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -120,13 +120,13 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
|
||||
case providers.Mullvad:
|
||||
return []string{countryHeader, cityHeader, ispHeader, ownedHeader, hostnameHeader, vpnHeader}
|
||||
case providers.Nordvpn:
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader}
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, vpnHeader}
|
||||
case providers.Perfectprivacy:
|
||||
return []string{cityHeader, tcpHeader, udpHeader}
|
||||
case providers.Privado:
|
||||
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader}
|
||||
case providers.PrivateInternetAccess:
|
||||
return []string{regionHeader, hostnameHeader, tcpHeader, udpHeader, portForwardHeader}
|
||||
return []string{regionHeader, hostnameHeader, nameHeader, tcpHeader, udpHeader, portForwardHeader}
|
||||
case providers.Privatevpn:
|
||||
return []string{countryHeader, cityHeader, hostnameHeader}
|
||||
case providers.Protonvpn:
|
||||
@@ -136,7 +136,8 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
|
||||
case providers.SlickVPN:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader}
|
||||
case providers.Surfshark:
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader, multiHopHeader, tcpHeader, udpHeader}
|
||||
return []string{regionHeader, countryHeader, cityHeader, hostnameHeader,
|
||||
vpnHeader, multiHopHeader, tcpHeader, udpHeader}
|
||||
case providers.Torguard:
|
||||
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
|
||||
case providers.VPNSecure:
|
||||
|
||||
94
internal/natpmp/checks.go
Normal file
94
internal/natpmp/checks.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRequestSizeTooSmall = errors.New("message size is too small")
|
||||
)
|
||||
|
||||
func checkRequest(request []byte) (err error) {
|
||||
const minMessageSize = 2 // version number + operation code
|
||||
if len(request) < minMessageSize {
|
||||
return fmt.Errorf("%w: need at least %d bytes and got %d byte(s)",
|
||||
ErrRequestSizeTooSmall, minMessageSize, len(request))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrResponseSizeTooSmall = errors.New("response size is too small")
|
||||
ErrResponseSizeUnexpected = errors.New("response size is unexpected")
|
||||
ErrProtocolVersionUnknown = errors.New("protocol version is unknown")
|
||||
ErrOperationCodeUnexpected = errors.New("operation code is unexpected")
|
||||
)
|
||||
|
||||
func checkResponse(response []byte, expectedOperationCode byte,
|
||||
expectedResponseSize uint) (err error) {
|
||||
const minResponseSize = 4
|
||||
if len(response) < minResponseSize {
|
||||
return fmt.Errorf("%w: need at least %d bytes and got %d byte(s)",
|
||||
ErrResponseSizeTooSmall, minResponseSize, len(response))
|
||||
}
|
||||
|
||||
if len(response) != int(expectedResponseSize) {
|
||||
return fmt.Errorf("%w: expected %d bytes and got %d byte(s)",
|
||||
ErrResponseSizeUnexpected, expectedResponseSize, len(response))
|
||||
}
|
||||
|
||||
protocolVersion := response[0]
|
||||
if protocolVersion != 0 {
|
||||
return fmt.Errorf("%w: %d", ErrProtocolVersionUnknown, protocolVersion)
|
||||
}
|
||||
|
||||
operationCode := response[1]
|
||||
if operationCode != expectedOperationCode {
|
||||
return fmt.Errorf("%w: expected 0x%x and got 0x%x",
|
||||
ErrOperationCodeUnexpected, expectedOperationCode, operationCode)
|
||||
}
|
||||
|
||||
resultCode := binary.BigEndian.Uint16(response[2:4])
|
||||
err = checkResultCode(resultCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("result code: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrVersionNotSupported = errors.New("version is not supported")
|
||||
ErrNotAuthorized = errors.New("not authorized")
|
||||
ErrNetworkFailure = errors.New("network failure")
|
||||
ErrOutOfResources = errors.New("out of resources")
|
||||
ErrOperationCodeNotSupported = errors.New("operation code is not supported")
|
||||
ErrResultCodeUnknown = errors.New("result code is unknown")
|
||||
)
|
||||
|
||||
// checkResultCode checks the result code and returns an error
|
||||
// if the result code is not a success (0).
|
||||
// See https://www.ietf.org/rfc/rfc6886.html#section-3.5
|
||||
//
|
||||
//nolint:gomnd
|
||||
func checkResultCode(resultCode uint16) (err error) {
|
||||
switch resultCode {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return fmt.Errorf("%w", ErrVersionNotSupported)
|
||||
case 2:
|
||||
return fmt.Errorf("%w", ErrNotAuthorized)
|
||||
case 3:
|
||||
return fmt.Errorf("%w", ErrNetworkFailure)
|
||||
case 4:
|
||||
return fmt.Errorf("%w", ErrOutOfResources)
|
||||
case 5:
|
||||
return fmt.Errorf("%w", ErrOperationCodeNotSupported)
|
||||
default:
|
||||
return fmt.Errorf("%w: %d", ErrResultCodeUnknown, resultCode)
|
||||
}
|
||||
}
|
||||
161
internal/natpmp/checks_test.go
Normal file
161
internal/natpmp/checks_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_checkRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
request []byte
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"too_short": {
|
||||
request: []byte{1},
|
||||
err: ErrRequestSizeTooSmall,
|
||||
errMessage: "message size is too small: need at least 2 bytes and got 1 byte(s)",
|
||||
},
|
||||
"success": {
|
||||
request: []byte{0, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := checkRequest(testCase.request)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
if testCase.err != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
response []byte
|
||||
expectedOperationCode byte
|
||||
expectedResponseSize uint
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"too_short": {
|
||||
response: []byte{1},
|
||||
err: ErrResponseSizeTooSmall,
|
||||
errMessage: "response size is too small: need at least 4 bytes and got 1 byte(s)",
|
||||
},
|
||||
"size_mismatch": {
|
||||
response: []byte{0, 0, 0, 0},
|
||||
expectedResponseSize: 5,
|
||||
err: ErrResponseSizeUnexpected,
|
||||
errMessage: "response size is unexpected: expected 5 bytes and got 4 byte(s)",
|
||||
},
|
||||
"protocol_unknown": {
|
||||
response: []byte{1, 0, 0, 0},
|
||||
expectedResponseSize: 4,
|
||||
err: ErrProtocolVersionUnknown,
|
||||
errMessage: "protocol version is unknown: 1",
|
||||
},
|
||||
"operation_code_unexpected": {
|
||||
response: []byte{0, 2, 0, 0},
|
||||
expectedOperationCode: 1,
|
||||
expectedResponseSize: 4,
|
||||
err: ErrOperationCodeUnexpected,
|
||||
errMessage: "operation code is unexpected: expected 0x1 and got 0x2",
|
||||
},
|
||||
"result_code_failure": {
|
||||
response: []byte{0, 1, 0, 1},
|
||||
expectedOperationCode: 1,
|
||||
expectedResponseSize: 4,
|
||||
err: ErrVersionNotSupported,
|
||||
errMessage: "result code: version is not supported",
|
||||
},
|
||||
"success": {
|
||||
response: []byte{0, 1, 0, 0},
|
||||
expectedOperationCode: 1,
|
||||
expectedResponseSize: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := checkResponse(testCase.response,
|
||||
testCase.expectedOperationCode,
|
||||
testCase.expectedResponseSize)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
if testCase.err != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkResultCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
resultCode uint16
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"success": {},
|
||||
"version_unsupported": {
|
||||
resultCode: 1,
|
||||
err: ErrVersionNotSupported,
|
||||
errMessage: "version is not supported",
|
||||
},
|
||||
"not_authorized": {
|
||||
resultCode: 2,
|
||||
err: ErrNotAuthorized,
|
||||
errMessage: "not authorized",
|
||||
},
|
||||
"network_failure": {
|
||||
resultCode: 3,
|
||||
err: ErrNetworkFailure,
|
||||
errMessage: "network failure",
|
||||
},
|
||||
"out_of_resources": {
|
||||
resultCode: 4,
|
||||
err: ErrOutOfResources,
|
||||
errMessage: "out of resources",
|
||||
},
|
||||
"unsupported_operation_code": {
|
||||
resultCode: 5,
|
||||
err: ErrOperationCodeNotSupported,
|
||||
errMessage: "operation code is not supported",
|
||||
},
|
||||
"unknown": {
|
||||
resultCode: 6,
|
||||
err: ErrResultCodeUnknown,
|
||||
errMessage: "result code is unknown: 6",
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := checkResultCode(testCase.resultCode)
|
||||
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
if testCase.err != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
28
internal/natpmp/externaladdress.go
Normal file
28
internal/natpmp/externaladdress.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExternalAddress fetches the duration since the start of epoch and the external
|
||||
// IPv4 address of the gateway.
|
||||
// See https://www.ietf.org/rfc/rfc6886.html#section-3.2
|
||||
func (c *Client) ExternalAddress(ctx context.Context, gateway netip.Addr) (
|
||||
durationSinceStartOfEpoch time.Duration,
|
||||
externalIPv4Address netip.Addr, err error) {
|
||||
request := []byte{0, 0} // version 0, operationCode 0
|
||||
const responseSize = 12
|
||||
response, err := c.rpc(ctx, gateway, request, responseSize)
|
||||
if err != nil {
|
||||
return 0, externalIPv4Address, fmt.Errorf("executing remote procedure call: %w", err)
|
||||
}
|
||||
|
||||
secondsSinceStartOfEpoch := binary.BigEndian.Uint32(response[4:8])
|
||||
durationSinceStartOfEpoch = time.Duration(secondsSinceStartOfEpoch) * time.Second
|
||||
externalIPv4Address = netip.AddrFrom4([4]byte{response[8], response[9], response[10], response[11]})
|
||||
return durationSinceStartOfEpoch, externalIPv4Address, nil
|
||||
}
|
||||
71
internal/natpmp/externaladdress_test.go
Normal file
71
internal/natpmp/externaladdress_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Client_ExternalAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
gateway netip.Addr
|
||||
initialConnDuration time.Duration
|
||||
exchanges []udpExchange
|
||||
durationSinceStartOfEpoch time.Duration
|
||||
externalIPv4Address netip.Addr
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"failure": {
|
||||
ctx: canceledCtx,
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
initialConnDuration: initialConnectionDuration,
|
||||
err: context.Canceled,
|
||||
errMessage: "executing remote procedure call: reading from udp connection: context canceled",
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
initialConnDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0, 0},
|
||||
response: []byte{0x0, 0x80, 0x0, 0x0, 0x0, 0x13, 0xf2, 0x4f, 0x49, 0x8c, 0x36, 0x9a},
|
||||
}},
|
||||
durationSinceStartOfEpoch: time.Duration(0x13f24f) * time.Second,
|
||||
externalIPv4Address: netip.AddrFrom4([4]byte{0x49, 0x8c, 0x36, 0x9a}),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
remoteAddress := launchUDPServer(t, testCase.exchanges)
|
||||
|
||||
client := Client{
|
||||
serverPort: uint16(remoteAddress.Port),
|
||||
initialConnectionDuration: testCase.initialConnDuration,
|
||||
maxRetries: 1,
|
||||
}
|
||||
|
||||
durationSinceStartOfEpoch, externalIPv4Address, err :=
|
||||
client.ExternalAddress(testCase.ctx, testCase.gateway)
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
if testCase.err != nil {
|
||||
assert.EqualError(t, err, testCase.errMessage)
|
||||
}
|
||||
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
||||
assert.Equal(t, testCase.externalIPv4Address, externalIPv4Address)
|
||||
})
|
||||
}
|
||||
}
|
||||
103
internal/natpmp/helpers_test.go
Normal file
103
internal/natpmp/helpers_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// enough for slow machines for local UDP server.
|
||||
const initialConnectionDuration = 3 * time.Second
|
||||
|
||||
type udpExchange struct {
|
||||
request []byte
|
||||
response []byte
|
||||
close bool // to trigger a client error
|
||||
}
|
||||
|
||||
// launchUDPServer launches an UDP server which will expect
|
||||
// the requests precised in each of the given exchanges,
|
||||
// and respond the given corresponding response.
|
||||
// The server shuts down gracefully at the end of the test.
|
||||
// The remote address (127.0.0.1:port) is returned, where
|
||||
// port is dynamically assigned by the OS so calling tests
|
||||
// can run in parallel.
|
||||
func launchUDPServer(t *testing.T, exchanges []udpExchange) (
|
||||
remoteAddress *net.UDPAddr) {
|
||||
t.Helper()
|
||||
|
||||
conn, err := net.ListenUDP("udp", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
listeningAddress, ok := conn.LocalAddr().(*net.UDPAddr)
|
||||
require.True(t, ok, "listening address is not UDP")
|
||||
remoteAddress = &net.UDPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Port: listeningAddress.Port,
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
err := conn.Close()
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
<-done
|
||||
})
|
||||
|
||||
var maxBufferSize int
|
||||
for _, exchange := range exchanges {
|
||||
if len(exchange.request) > maxBufferSize {
|
||||
maxBufferSize = len(exchange.request)
|
||||
}
|
||||
}
|
||||
|
||||
buffer := make([]byte, maxBufferSize)
|
||||
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
close(ready)
|
||||
for _, exchange := range exchanges {
|
||||
n, clientAddress, err := conn.ReadFromUDP(buffer)
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
t.Error("at least one exchange is missing")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(exchange.request), n,
|
||||
"request message size is unexpected")
|
||||
if n > 0 {
|
||||
assert.Equal(t, exchange.request, buffer[:n],
|
||||
"request message is unexpected")
|
||||
}
|
||||
|
||||
if exchange.close {
|
||||
err = conn.Close()
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
// connection might be already closed by client production code
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.WriteToUDP(exchange.response, clientAddress)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := conn.Close()
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
// The connection closing can be raced by the test
|
||||
// cleanup function defined above.
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
<-ready
|
||||
|
||||
return remoteAddress
|
||||
}
|
||||
26
internal/natpmp/natpmp.go
Normal file
26
internal/natpmp/natpmp.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is a NAT-PMP protocol client.
|
||||
type Client struct {
|
||||
serverPort uint16
|
||||
initialConnectionDuration time.Duration
|
||||
maxRetries uint
|
||||
}
|
||||
|
||||
// New creates a new NAT-PMP client.
|
||||
func New() (client *Client) {
|
||||
const natpmpPort = 5351
|
||||
|
||||
// Parameters described in https://www.ietf.org/rfc/rfc6886.html#section-3.1
|
||||
const initialConnectionDuration = 250 * time.Millisecond
|
||||
const maxTries = 9 // 64 seconds
|
||||
return &Client{
|
||||
serverPort: natpmpPort,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
maxRetries: maxTries,
|
||||
}
|
||||
}
|
||||
20
internal/natpmp/natpmp_test.go
Normal file
20
internal/natpmp/natpmp_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedClient := &Client{
|
||||
serverPort: 5351,
|
||||
initialConnectionDuration: 250 * time.Millisecond,
|
||||
maxRetries: 9,
|
||||
}
|
||||
client := New()
|
||||
assert.Equal(t, expectedClient, client)
|
||||
}
|
||||
60
internal/natpmp/portmapping.go
Normal file
60
internal/natpmp/portmapping.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNetworkProtocolUnknown = errors.New("network protocol is unknown")
|
||||
ErrLifetimeTooLong = errors.New("lifetime is too long")
|
||||
)
|
||||
|
||||
// Add or delete a port mapping. To delete a mapping, set both the
|
||||
// requestedExternalPort and lifetime to 0.
|
||||
// See https://www.ietf.org/rfc/rfc6886.html#section-3.3
|
||||
func (c *Client) AddPortMapping(ctx context.Context, gateway netip.Addr,
|
||||
protocol string, internalPort, requestedExternalPort uint16,
|
||||
lifetime time.Duration) (durationSinceStartOfEpoch time.Duration,
|
||||
assignedInternalPort, assignedExternalPort uint16, assignedLifetime time.Duration,
|
||||
err error) {
|
||||
lifetimeSecondsFloat := lifetime.Seconds()
|
||||
const maxLifetimeSeconds = uint64(^uint32(0))
|
||||
if uint64(lifetimeSecondsFloat) > maxLifetimeSeconds {
|
||||
return 0, 0, 0, 0, fmt.Errorf("%w: %d seconds must at most %d seconds",
|
||||
ErrLifetimeTooLong, uint64(lifetimeSecondsFloat), maxLifetimeSeconds)
|
||||
}
|
||||
const messageSize = 12
|
||||
message := make([]byte, messageSize)
|
||||
message[0] = 0 // Version 0
|
||||
switch protocol {
|
||||
case "udp":
|
||||
message[1] = 1 // operationCode 1
|
||||
case "tcp":
|
||||
message[1] = 2 // operationCode 2
|
||||
default:
|
||||
return 0, 0, 0, 0, fmt.Errorf("%w: %s", ErrNetworkProtocolUnknown, protocol)
|
||||
}
|
||||
// [2:3] are reserved.
|
||||
binary.BigEndian.PutUint16(message[4:6], internalPort)
|
||||
binary.BigEndian.PutUint16(message[6:8], requestedExternalPort)
|
||||
binary.BigEndian.PutUint32(message[8:12], uint32(lifetimeSecondsFloat))
|
||||
|
||||
const responseSize = 16
|
||||
response, err := c.rpc(ctx, gateway, message, responseSize)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("executing remote procedure call: %w", err)
|
||||
}
|
||||
|
||||
secondsSinceStartOfEpoch := binary.BigEndian.Uint32(response[4:8])
|
||||
durationSinceStartOfEpoch = time.Duration(secondsSinceStartOfEpoch) * time.Second
|
||||
assignedInternalPort = binary.BigEndian.Uint16(response[8:10])
|
||||
assignedExternalPort = binary.BigEndian.Uint16(response[10:12])
|
||||
lifetimeInSeconds := binary.BigEndian.Uint32(response[12:16])
|
||||
assignedLifetime = time.Duration(lifetimeInSeconds) * time.Second
|
||||
return durationSinceStartOfEpoch, assignedInternalPort, assignedExternalPort, assignedLifetime, nil
|
||||
}
|
||||
149
internal/natpmp/portmapping_test.go
Normal file
149
internal/natpmp/portmapping_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Client_AddPortMapping(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
gateway netip.Addr
|
||||
protocol string
|
||||
internalPort uint16
|
||||
requestedExternalPort uint16
|
||||
lifetime time.Duration
|
||||
initialConnectionDuration time.Duration
|
||||
exchanges []udpExchange
|
||||
durationSinceStartOfEpoch time.Duration
|
||||
assignedInternalPort uint16
|
||||
assignedExternalPort uint16
|
||||
assignedLifetime time.Duration
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"lifetime_too_long": {
|
||||
lifetime: time.Duration(uint64(^uint32(0))+1) * time.Second,
|
||||
err: ErrLifetimeTooLong,
|
||||
errMessage: "lifetime is too long: 4294967296 seconds must at most 4294967295 seconds",
|
||||
},
|
||||
"protocol_unknown": {
|
||||
lifetime: time.Second,
|
||||
protocol: "xyz",
|
||||
err: ErrNetworkProtocolUnknown,
|
||||
errMessage: "network protocol is unknown: xyz",
|
||||
},
|
||||
"rpc_error": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
protocol: "udp",
|
||||
internalPort: 123,
|
||||
requestedExternalPort: 456,
|
||||
lifetime: 1200 * time.Second,
|
||||
initialConnectionDuration: time.Millisecond,
|
||||
exchanges: []udpExchange{{close: true}},
|
||||
err: ErrConnectionTimeout,
|
||||
errMessage: "executing remote procedure call: connection timeout: after 1ms",
|
||||
},
|
||||
"add_udp": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
protocol: "udp",
|
||||
internalPort: 123,
|
||||
requestedExternalPort: 456,
|
||||
lifetime: 1200 * time.Second,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x1, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x0, 0x81, 0x0, 0x0, 0x0, 0x13, 0xfe, 0xff, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
}},
|
||||
durationSinceStartOfEpoch: 0x13feff * time.Second,
|
||||
assignedInternalPort: 0x7b,
|
||||
assignedExternalPort: 0x1c8,
|
||||
assignedLifetime: 0x4b0 * time.Second,
|
||||
},
|
||||
"add_tcp": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
protocol: "tcp",
|
||||
internalPort: 123,
|
||||
requestedExternalPort: 456,
|
||||
lifetime: 1200 * time.Second,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x14, 0x3, 0x21, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
}},
|
||||
durationSinceStartOfEpoch: 0x140321 * time.Second,
|
||||
assignedInternalPort: 0x7b,
|
||||
assignedExternalPort: 0x1c8,
|
||||
assignedLifetime: 0x4b0 * time.Second,
|
||||
},
|
||||
"remove_udp": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
protocol: "udp",
|
||||
internalPort: 123,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x1, 0x0, 0x0, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
response: []byte{0x0, 0x81, 0x0, 0x0, 0x0, 0x14, 0x3, 0xd5, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
durationSinceStartOfEpoch: 0x1403d5 * time.Second,
|
||||
assignedInternalPort: 0x7b,
|
||||
},
|
||||
"remove_tcp": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
protocol: "tcp",
|
||||
internalPort: 123,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
response: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
durationSinceStartOfEpoch: 0x140496 * time.Second,
|
||||
assignedInternalPort: 0x7b,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
remoteAddress := launchUDPServer(t, testCase.exchanges)
|
||||
|
||||
client := Client{
|
||||
serverPort: uint16(remoteAddress.Port),
|
||||
initialConnectionDuration: testCase.initialConnectionDuration,
|
||||
maxRetries: 1,
|
||||
}
|
||||
|
||||
durationSinceStartOfEpoch, assignedInternalPort,
|
||||
assignedExternalPort, assignedLifetime, err :=
|
||||
client.AddPortMapping(testCase.ctx, testCase.gateway,
|
||||
testCase.protocol, testCase.internalPort,
|
||||
testCase.requestedExternalPort, testCase.lifetime)
|
||||
|
||||
assert.Equal(t, testCase.durationSinceStartOfEpoch, durationSinceStartOfEpoch)
|
||||
assert.Equal(t, testCase.assignedInternalPort, assignedInternalPort)
|
||||
assert.Equal(t, testCase.assignedExternalPort, assignedExternalPort)
|
||||
assert.Equal(t, testCase.assignedLifetime, assignedLifetime)
|
||||
if testCase.errMessage != "" {
|
||||
if testCase.err != nil {
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
}
|
||||
assert.Regexp(t, "^"+testCase.errMessage+"$", err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
123
internal/natpmp/rpc.go
Normal file
123
internal/natpmp/rpc.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGatewayIPUnspecified = errors.New("gateway IP is unspecified")
|
||||
ErrConnectionTimeout = errors.New("connection timeout")
|
||||
)
|
||||
|
||||
func (c *Client) rpc(ctx context.Context, gateway netip.Addr,
|
||||
request []byte, responseSize uint) (
|
||||
response []byte, err error) {
|
||||
if gateway.IsUnspecified() || !gateway.IsValid() {
|
||||
return nil, fmt.Errorf("%w", ErrGatewayIPUnspecified)
|
||||
}
|
||||
|
||||
err = checkRequest(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking request: %w", err)
|
||||
}
|
||||
|
||||
gatewayAddress := &net.UDPAddr{
|
||||
IP: gateway.AsSlice(),
|
||||
Port: int(c.serverPort),
|
||||
}
|
||||
|
||||
connection, err := net.DialUDP("udp", nil, gatewayAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing udp: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
endGoroutineDone := make(chan struct{})
|
||||
defer func() {
|
||||
cancel()
|
||||
<-endGoroutineDone
|
||||
}()
|
||||
go func() {
|
||||
defer close(endGoroutineDone)
|
||||
// Context is canceled either by the parent context or
|
||||
// when this function returns.
|
||||
<-ctx.Done()
|
||||
closeErr := connection.Close()
|
||||
if closeErr == nil {
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
err = fmt.Errorf("closing connection: %w", closeErr)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("%w; closing connection: %w", err, closeErr)
|
||||
}()
|
||||
|
||||
const maxResponseSize = 16
|
||||
response = make([]byte, maxResponseSize)
|
||||
|
||||
// Connection duration doubles on every network error
|
||||
// Note it does not double if the source IP mismatches the gateway IP.
|
||||
connectionDuration := c.initialConnectionDuration
|
||||
|
||||
var totalRetryDuration time.Duration
|
||||
|
||||
var retryCount uint
|
||||
for retryCount = 0; retryCount < c.maxRetries; retryCount++ {
|
||||
deadline := time.Now().Add(connectionDuration)
|
||||
err = connection.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting connection deadline: %w", err)
|
||||
}
|
||||
|
||||
_, err = connection.Write(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing to connection: %w", err)
|
||||
}
|
||||
|
||||
bytesRead, receivedRemoteAddress, err := connection.ReadFromUDP(response)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("reading from udp connection: %w", ctx.Err())
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
totalRetryDuration += connectionDuration
|
||||
connectionDuration *= 2
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("reading from udp connection: %w", err)
|
||||
}
|
||||
|
||||
if !receivedRemoteAddress.IP.Equal(gatewayAddress.IP) {
|
||||
// Upon receiving a response packet, the client MUST check the source IP
|
||||
// address, and silently discard the packet if the address is not the
|
||||
// address of the gateway to which the request was sent.
|
||||
continue
|
||||
}
|
||||
|
||||
response = response[:bytesRead]
|
||||
break
|
||||
}
|
||||
|
||||
if retryCount == c.maxRetries {
|
||||
return nil, fmt.Errorf("%w: after %s",
|
||||
ErrConnectionTimeout, totalRetryDuration)
|
||||
}
|
||||
|
||||
// Opcodes between 0 and 127 are client requests. Opcodes from 128 to
|
||||
// 255 are corresponding server responses.
|
||||
const operationCodeMask = 128
|
||||
expectedOperationCode := request[1] | operationCodeMask
|
||||
err = checkResponse(response, expectedOperationCode, responseSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking response: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
166
internal/natpmp/rpc_test.go
Normal file
166
internal/natpmp/rpc_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package natpmp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Client_rpc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
ctx context.Context
|
||||
gateway netip.Addr
|
||||
request []byte
|
||||
responseSize uint
|
||||
initialConnectionDuration time.Duration
|
||||
exchanges []udpExchange
|
||||
expectedResponse []byte
|
||||
err error
|
||||
errMessage string
|
||||
}{
|
||||
"gateway_ip_unspecified": {
|
||||
gateway: netip.IPv6Unspecified(),
|
||||
request: []byte{0, 0},
|
||||
err: ErrGatewayIPUnspecified,
|
||||
errMessage: "gateway IP is unspecified",
|
||||
},
|
||||
"request_too_small": {
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0},
|
||||
initialConnectionDuration: time.Nanosecond, // doesn't matter
|
||||
err: ErrRequestSizeTooSmall,
|
||||
errMessage: `checking request: message size is too small: ` +
|
||||
`need at least 2 bytes and got 1 byte\(s\)`,
|
||||
},
|
||||
"write_error": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0, 0},
|
||||
errMessage: `writing to connection: write udp ` +
|
||||
`127.0.0.1:[1-9][0-9]{0,4}->127.0.0.1:[1-9][0-9]{0,4}: ` +
|
||||
`i/o timeout`,
|
||||
},
|
||||
"call_error": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0, 1},
|
||||
initialConnectionDuration: time.Millisecond,
|
||||
exchanges: []udpExchange{
|
||||
{request: []byte{0, 1}, close: true},
|
||||
},
|
||||
err: ErrConnectionTimeout,
|
||||
errMessage: "connection timeout: after 1ms",
|
||||
},
|
||||
"response_too_small": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0, 0},
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0, 0},
|
||||
response: []byte{1},
|
||||
}},
|
||||
err: ErrResponseSizeTooSmall,
|
||||
errMessage: `checking response: response size is too small: ` +
|
||||
`need at least 4 bytes and got 1 byte\(s\)`,
|
||||
},
|
||||
"unexpected_response_size": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
responseSize: 5,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0, 1, 2, 3}, // size 4
|
||||
}},
|
||||
err: ErrResponseSizeUnexpected,
|
||||
errMessage: `checking response: response size is unexpected: ` +
|
||||
`expected 5 bytes and got 4 byte\(s\)`,
|
||||
},
|
||||
"unknown_protocol_version": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
responseSize: 16,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x1, 0x82, 0x0, 0x0, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
err: ErrProtocolVersionUnknown,
|
||||
errMessage: "checking response: protocol version is unknown: 1",
|
||||
},
|
||||
"unexpected_operation_code": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
responseSize: 16,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x0, 0x88, 0x0, 0x0, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
err: ErrOperationCodeUnexpected,
|
||||
errMessage: "checking response: operation code is unexpected: expected 0x82 and got 0x88",
|
||||
},
|
||||
"failure_result_code": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
responseSize: 16,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x0, 0x82, 0x0, 0x11, 0x0, 0x14, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
err: ErrResultCodeUnknown,
|
||||
errMessage: "checking response: result code: result code is unknown: 17",
|
||||
},
|
||||
"success": {
|
||||
ctx: context.Background(),
|
||||
gateway: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
responseSize: 16,
|
||||
initialConnectionDuration: initialConnectionDuration,
|
||||
exchanges: []udpExchange{{
|
||||
request: []byte{0x0, 0x2, 0x0, 0x0, 0x0, 0x7b, 0x1, 0xc8, 0x0, 0x0, 0x4, 0xb0},
|
||||
response: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x0, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
}},
|
||||
expectedResponse: []byte{0x0, 0x82, 0x0, 0x0, 0x0, 0x0, 0x4, 0x96, 0x0, 0x7b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
remoteAddress := launchUDPServer(t, testCase.exchanges)
|
||||
|
||||
client := Client{
|
||||
serverPort: uint16(remoteAddress.Port),
|
||||
initialConnectionDuration: testCase.initialConnectionDuration,
|
||||
maxRetries: 1,
|
||||
}
|
||||
|
||||
response, err := client.rpc(testCase.ctx, testCase.gateway,
|
||||
testCase.request, testCase.responseSize)
|
||||
|
||||
if testCase.errMessage != "" {
|
||||
if testCase.err != nil {
|
||||
assert.ErrorIs(t, err, testCase.err)
|
||||
}
|
||||
assert.Regexp(t, "^"+testCase.errMessage+"$", err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, testCase.expectedResponse, response)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,16 +14,21 @@ func (n *NetLink) IsIPv6Supported() (supported bool, err error) {
|
||||
// as IPv6 routes at container start, see:
|
||||
// https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949
|
||||
for _, route := range routes {
|
||||
link, err := n.LinkByIndex(route.LinkIndex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("finding link corresponding to route: %w", err)
|
||||
}
|
||||
|
||||
sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6()
|
||||
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
|
||||
if sourceIsIPv6 || destinationIsIPv6 {
|
||||
link, err := n.LinkByIndex(route.LinkIndex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("finding IPv6 supported link: %w", err)
|
||||
}
|
||||
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
|
||||
return true, nil
|
||||
switch {
|
||||
case !sourceIsIPv6 && !destinationIsIPv6,
|
||||
destinationIsIPv6 && route.Dst.Addr().IsLoopback():
|
||||
continue
|
||||
}
|
||||
|
||||
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
n.debugLogger.Debugf("IPv6 is not supported after searching %d routes",
|
||||
|
||||
@@ -5,10 +5,42 @@ package netlink
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/mod"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
||||
// Check for Wireguard family without loading the wireguard module.
|
||||
// Some kernels have the wireguard module built-in, and don't have a
|
||||
// modules directory, such as WSL2 kernels.
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking for wireguard family: %w", err)
|
||||
}
|
||||
if ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Try loading the wireguard module, since some systems do not load
|
||||
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
||||
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
||||
err = mod.Probe("wireguard")
|
||||
if err != nil {
|
||||
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
||||
return false, nil
|
||||
}
|
||||
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
||||
|
||||
// Re-check if the Wireguard family is now available, after loading
|
||||
// the wireguard kernel module.
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking for wireguard family: %w", err)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func hasWireguardFamily() (ok bool, err error) {
|
||||
families, err := netlink.GenlFamilyList()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("listing gen 1 families: %w", err)
|
||||
|
||||
@@ -18,6 +18,11 @@ var (
|
||||
func extractDataFromLines(lines []string) (
|
||||
connection models.Connection, err error) {
|
||||
for i, line := range lines {
|
||||
hashSymbolIndex := strings.Index(line, "#")
|
||||
if hashSymbolIndex >= 0 {
|
||||
line = line[:hashSymbolIndex]
|
||||
}
|
||||
|
||||
ip, port, protocol, err := extractDataFromLine(line)
|
||||
if err != nil {
|
||||
return connection, fmt.Errorf("on line %d: %w", i+1, err)
|
||||
@@ -40,7 +45,7 @@ func extractDataFromLines(lines []string) (
|
||||
|
||||
if connection.Port == 0 {
|
||||
connection.Port = 1194
|
||||
if connection.Protocol == constants.TCP {
|
||||
if strings.HasPrefix(connection.Protocol, "tcp") {
|
||||
connection.Port = 443
|
||||
}
|
||||
}
|
||||
@@ -64,6 +69,13 @@ func extractDataFromLine(line string) (
|
||||
return ip, 0, "", fmt.Errorf("extracting from remote line: %w", err)
|
||||
}
|
||||
return ip, port, protocol, nil
|
||||
|
||||
case strings.HasPrefix(line, "port "):
|
||||
port, err = extractPort(line)
|
||||
if err != nil {
|
||||
return ip, 0, "", fmt.Errorf("extracting from port line: %w", err)
|
||||
}
|
||||
return ip, port, "", nil
|
||||
}
|
||||
|
||||
return ip, 0, "", nil
|
||||
@@ -81,7 +93,7 @@ func extractProto(line string) (protocol string, err error) {
|
||||
}
|
||||
|
||||
switch fields[1] {
|
||||
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
|
||||
case "tcp", "tcp4", "tcp6", "tcp-client", "udp", "udp4", "udp6":
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[1])
|
||||
}
|
||||
@@ -133,3 +145,25 @@ func extractRemote(line string) (ip netip.Addr, port uint16,
|
||||
|
||||
return ip, port, protocol, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errPostLineFieldsCount = errors.New("post line has not 2 fields as expected")
|
||||
)
|
||||
|
||||
func extractPort(line string) (port uint16, err error) {
|
||||
fields := strings.Fields(line)
|
||||
const expectedFieldsCount = 2
|
||||
if len(fields) != expectedFieldsCount {
|
||||
return 0, fmt.Errorf("%w: %s", errPostLineFieldsCount, line)
|
||||
}
|
||||
|
||||
portInt, err := strconv.Atoi(fields[1])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %s", errPortNotValid, line)
|
||||
} else if portInt < 1 || portInt > 65535 {
|
||||
return 0, fmt.Errorf("%w: %d must be between 1 and 65535", errPortNotValid, portInt)
|
||||
}
|
||||
port = uint16(portInt)
|
||||
|
||||
return port, nil
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@ func Test_extractDataFromLine(t *testing.T) {
|
||||
line: "proto tcp",
|
||||
protocol: constants.TCP,
|
||||
},
|
||||
"tcp-client": {
|
||||
line: "proto tcp-client",
|
||||
protocol: "tcp-client",
|
||||
},
|
||||
"extract remote error": {
|
||||
line: "remote bad",
|
||||
isErr: errHostNotIP,
|
||||
@@ -114,6 +118,14 @@ func Test_extractDataFromLine(t *testing.T) {
|
||||
port: 1194,
|
||||
protocol: constants.UDP,
|
||||
},
|
||||
"extract_port_fail": {
|
||||
line: "port a",
|
||||
isErr: errPortNotValid,
|
||||
},
|
||||
"extract_port_success": {
|
||||
line: "port 1194",
|
||||
port: 1194,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
|
||||
@@ -52,7 +52,7 @@ Your credentials might be wrong 🤨
|
||||
That error usually happens because either:
|
||||
|
||||
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
|
||||
Update your server information using https://github.com/qdm12/gluetun/wiki/Updating-Servers
|
||||
Check out https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||
|
||||
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as SERVER_REGIONS
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ func Test_processLogLine(t *testing.T) {
|
||||
That error usually happens because either:
|
||||
|
||||
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
|
||||
Update your server information using https://github.com/qdm12/gluetun/wiki/Updating-Servers
|
||||
Check out https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
|
||||
|
||||
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as SERVER_REGIONS
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package portforward
|
||||
|
||||
import "context"
|
||||
|
||||
// firewallBlockPort obtains the state port thread safely and blocks
|
||||
// it in the firewall if it is not the zero value (0).
|
||||
func (l *Loop) firewallBlockPort(ctx context.Context) {
|
||||
port := l.state.GetPortForwarded()
|
||||
if port == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err := l.portAllower.RemoveAllowedPort(ctx, port)
|
||||
if err != nil {
|
||||
l.logger.Error("cannot block previous port in firewall: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// firewallAllowPort obtains the state port thread safely and allows
|
||||
// it in the firewall if it is not the zero value (0).
|
||||
func (l *Loop) firewallAllowPort(ctx context.Context) {
|
||||
port := l.state.GetPortForwarded()
|
||||
if port == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
startData := l.state.GetStartData()
|
||||
err := l.portAllower.SetAllowedPort(ctx, port, startData.Interface)
|
||||
if err != nil {
|
||||
l.logger.Error("cannot allow port: " + err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (l *Loop) removePortForwardedFile() {
|
||||
filepath := *l.state.GetSettings().Filepath
|
||||
l.logger.Info("removing port file " + filepath)
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) writePortForwardedFile(port uint16) {
|
||||
filepath := *l.state.GetSettings().Filepath
|
||||
l.logger.Info("writing port file " + filepath)
|
||||
if err := writePortForwardedToFile(filepath, port, l.puid, l.pgid); err != nil {
|
||||
l.logger.Error("writing port forwarded to file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func writePortForwardedToFile(filepath string, port uint16, uid, gid int) (err error) {
|
||||
const perms = os.FileMode(0644)
|
||||
err = os.WriteFile(filepath, []byte(fmt.Sprint(port)), perms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Chown(filepath, uid, gid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chowning file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package portforward
|
||||
|
||||
func (l *Loop) GetPortForwarded() (port uint16) {
|
||||
return l.state.GetPortForwarded()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
l.logger.Info("retrying in " + l.backoffTime.String())
|
||||
timer := time.NewTimer(l.backoffTime)
|
||||
l.backoffTime *= 2
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,29 @@ package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Start(ctx context.Context) (runError <-chan error, err error)
|
||||
Stop() (err error)
|
||||
GetPortForwarded() (port uint16)
|
||||
}
|
||||
|
||||
type Routing interface {
|
||||
VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error)
|
||||
}
|
||||
|
||||
type PortAllower interface {
|
||||
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
||||
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
||||
RedirectPort(ctx context.Context, intf string, sourcePort,
|
||||
destinationPort uint16) (err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package portforward
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
@@ -1,64 +1,162 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/loopstate"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/portforward/state"
|
||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
statusManager *loopstate.State
|
||||
state *state.State
|
||||
// Fixed parameters
|
||||
puid int
|
||||
pgid int
|
||||
// Objects
|
||||
// State
|
||||
settings Settings
|
||||
settingsMutex sync.RWMutex
|
||||
service Service
|
||||
// Fixed injected objets
|
||||
routing Routing
|
||||
client *http.Client
|
||||
portAllower PortAllower
|
||||
logger Logger
|
||||
// Fixed parameters
|
||||
uid, gid int
|
||||
// Internal channels and locks
|
||||
start chan struct{}
|
||||
running chan models.LoopStatus
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
startMu sync.Mutex
|
||||
backoffTime time.Duration
|
||||
userTrigger bool
|
||||
// runCtx is used to detect when the loop has exited
|
||||
// when performing an update
|
||||
runCtx context.Context //nolint:containedctx
|
||||
runCancel context.CancelFunc
|
||||
runDone <-chan struct{}
|
||||
updateTrigger chan<- Settings
|
||||
updatedResult <-chan error
|
||||
}
|
||||
|
||||
const defaultBackoffTime = 5 * time.Second
|
||||
|
||||
func NewLoop(settings settings.PortForwarding,
|
||||
func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||
client *http.Client, portAllower PortAllower,
|
||||
logger Logger, puid, pgid int) *Loop {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
stop := make(chan struct{})
|
||||
stopped := make(chan struct{})
|
||||
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
state := state.New(statusManager, settings)
|
||||
|
||||
logger Logger, uid, gid int) *Loop {
|
||||
return &Loop{
|
||||
statusManager: statusManager,
|
||||
state: state,
|
||||
puid: puid,
|
||||
pgid: pgid,
|
||||
// Objects
|
||||
settings: Settings{
|
||||
VPNIsUp: ptrTo(false),
|
||||
Service: service.Settings{
|
||||
Enabled: settings.Enabled,
|
||||
Filepath: *settings.Filepath,
|
||||
ListeningPort: *settings.ListeningPort,
|
||||
},
|
||||
},
|
||||
routing: routing,
|
||||
client: client,
|
||||
portAllower: portAllower,
|
||||
logger: logger,
|
||||
start: start,
|
||||
running: running,
|
||||
stop: stop,
|
||||
stopped: stopped,
|
||||
userTrigger: true,
|
||||
backoffTime: defaultBackoffTime,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) String() string {
|
||||
return "port forwarding loop"
|
||||
}
|
||||
|
||||
func (l *Loop) Start(_ context.Context) (runError <-chan error, _ error) {
|
||||
l.runCtx, l.runCancel = context.WithCancel(context.Background())
|
||||
runDone := make(chan struct{})
|
||||
l.runDone = runDone
|
||||
|
||||
updateTrigger := make(chan Settings)
|
||||
l.updateTrigger = updateTrigger
|
||||
updateResult := make(chan error)
|
||||
l.updatedResult = updateResult
|
||||
runErrorCh := make(chan error)
|
||||
|
||||
go l.run(l.runCtx, runDone, runErrorCh, updateTrigger, updateResult)
|
||||
|
||||
return runErrorCh, nil
|
||||
}
|
||||
|
||||
func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
runErrorCh chan<- error, updateTrigger <-chan Settings,
|
||||
updateResult chan<- error) {
|
||||
defer close(runDone)
|
||||
|
||||
var serviceRunError <-chan error
|
||||
for {
|
||||
updateReceived := false
|
||||
select {
|
||||
case <-runCtx.Done():
|
||||
// Stop call takes care of stopping the service
|
||||
return
|
||||
case partialUpdate := <-updateTrigger:
|
||||
updatedSettings, err := l.settings.updateWith(partialUpdate, *l.settings.VPNIsUp)
|
||||
if err != nil {
|
||||
updateResult <- err
|
||||
continue
|
||||
}
|
||||
updateReceived = true
|
||||
l.settingsMutex.Lock()
|
||||
l.settings = updatedSettings
|
||||
l.settingsMutex.Unlock()
|
||||
case err := <-serviceRunError:
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
firstRun := serviceRunError == nil
|
||||
if !firstRun {
|
||||
err := l.service.Stop()
|
||||
if err != nil {
|
||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
serviceSettings := l.settings.Service.Copy()
|
||||
// Only enable port forward if the VPN tunnel is up
|
||||
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
|
||||
|
||||
l.service = service.New(serviceSettings, l.routing, l.client,
|
||||
l.portAllower, l.logger, l.uid, l.gid)
|
||||
|
||||
var err error
|
||||
serviceRunError, err = l.service.Start(runCtx)
|
||||
if updateReceived {
|
||||
// Signal to the Update call that the service has started
|
||||
// and if it failed to start.
|
||||
updateResult <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) UpdateWith(partialUpdate Settings) (err error) {
|
||||
select {
|
||||
case l.updateTrigger <- partialUpdate:
|
||||
select {
|
||||
case err = <-l.updatedResult:
|
||||
return err
|
||||
case <-l.runCtx.Done():
|
||||
return l.runCtx.Err()
|
||||
}
|
||||
case <-l.runCtx.Done():
|
||||
// loop has been stopped, no update can be done
|
||||
return l.runCtx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loop) Stop() (err error) {
|
||||
l.runCancel()
|
||||
<-l.runDone
|
||||
|
||||
if l.service != nil {
|
||||
return l.service.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loop) GetPortForwarded() (port uint16) {
|
||||
if l.service == nil {
|
||||
return 0
|
||||
}
|
||||
return l.service.GetPortForwarded()
|
||||
}
|
||||
|
||||
func ptrTo[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
select {
|
||||
case <-l.start: // l.state.SetStartData called beforehand
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
pfCtx, pfCancel := context.WithCancel(ctx)
|
||||
|
||||
portCh := make(chan uint16)
|
||||
errorCh := make(chan error)
|
||||
|
||||
startData := l.state.GetStartData()
|
||||
|
||||
go func(ctx context.Context, startData StartData) {
|
||||
port, err := startData.PortForwarder.PortForward(ctx, l.client, l.logger,
|
||||
startData.Gateway, startData.ServerName)
|
||||
if err != nil {
|
||||
errorCh <- err
|
||||
return
|
||||
}
|
||||
portCh <- port
|
||||
|
||||
// Infinite loop
|
||||
err = startData.PortForwarder.KeepPortForward(ctx,
|
||||
startData.Gateway, startData.ServerName)
|
||||
errorCh <- err
|
||||
}(pfCtx, startData)
|
||||
|
||||
if l.userTrigger {
|
||||
l.userTrigger = false
|
||||
l.running <- constants.Running
|
||||
} else { // crash
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.statusManager.SetStatus(constants.Running)
|
||||
}
|
||||
|
||||
stayHere := true
|
||||
stopped := false
|
||||
for stayHere {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
pfCancel()
|
||||
if stopped {
|
||||
return
|
||||
}
|
||||
<-errorCh
|
||||
close(errorCh)
|
||||
close(portCh)
|
||||
l.removePortForwardedFile()
|
||||
l.firewallBlockPort(ctx)
|
||||
l.state.SetPortForwarded(0)
|
||||
return
|
||||
case <-l.start:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("starting")
|
||||
pfCancel()
|
||||
stayHere = false
|
||||
case <-l.stop:
|
||||
l.userTrigger = true
|
||||
l.logger.Info("stopping")
|
||||
pfCancel()
|
||||
<-errorCh
|
||||
l.removePortForwardedFile()
|
||||
l.firewallBlockPort(ctx)
|
||||
l.state.SetPortForwarded(0)
|
||||
l.stopped <- struct{}{}
|
||||
stopped = true
|
||||
case port := <-portCh:
|
||||
l.logger.Info("port forwarded is " + strconv.Itoa(int(port)))
|
||||
l.firewallBlockPort(ctx)
|
||||
l.state.SetPortForwarded(port)
|
||||
l.firewallAllowPort(ctx)
|
||||
l.writePortForwardedFile(port)
|
||||
case err := <-errorCh:
|
||||
pfCancel()
|
||||
close(errorCh)
|
||||
close(portCh)
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
l.logAndWait(ctx, err)
|
||||
stayHere = false
|
||||
}
|
||||
}
|
||||
pfCancel() // for linting
|
||||
}
|
||||
}
|
||||
23
internal/portforward/service/fs.go
Normal file
23
internal/portforward/service/fs.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (s *Service) writePortForwardedFile(port uint16) (err error) {
|
||||
filepath := s.settings.Filepath
|
||||
s.logger.Info("writing port file " + filepath)
|
||||
const perms = os.FileMode(0644)
|
||||
err = os.WriteFile(filepath, []byte(fmt.Sprint(port)), perms)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Chown(filepath, s.puid, s.pgid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chowning file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
33
internal/portforward/service/interfaces.go
Normal file
33
internal/portforward/service/interfaces.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type PortAllower interface {
|
||||
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
||||
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
||||
RedirectPort(ctx context.Context, intf string, sourcePort,
|
||||
destinationPort uint16) (err error)
|
||||
}
|
||||
|
||||
type Routing interface {
|
||||
VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
type PortForwarder interface {
|
||||
Name() string
|
||||
PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
port uint16, err error)
|
||||
KeepPortForward(ctx context.Context, objects utils.PortForwardObjects) (err error)
|
||||
}
|
||||
47
internal/portforward/service/service.go
Normal file
47
internal/portforward/service/service.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
// State
|
||||
portMutex sync.RWMutex
|
||||
port uint16
|
||||
// Fixed parameters
|
||||
settings Settings
|
||||
puid int
|
||||
pgid int
|
||||
// Fixed injected objets
|
||||
routing Routing
|
||||
client *http.Client
|
||||
portAllower PortAllower
|
||||
logger Logger
|
||||
// Internal channels and locks
|
||||
startStopMutex sync.Mutex
|
||||
keepPortCancel context.CancelFunc
|
||||
keepPortDoneCh <-chan struct{}
|
||||
}
|
||||
|
||||
func New(settings Settings, routing Routing, client *http.Client,
|
||||
portAllower PortAllower, logger Logger, puid, pgid int) *Service {
|
||||
return &Service{
|
||||
// Fixed parameters
|
||||
settings: settings,
|
||||
puid: puid,
|
||||
pgid: pgid,
|
||||
// Fixed injected objets
|
||||
routing: routing,
|
||||
client: client,
|
||||
portAllower: portAllower,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetPortForwarded() (port uint16) {
|
||||
s.portMutex.RLock()
|
||||
defer s.portMutex.RUnlock()
|
||||
return s.port
|
||||
}
|
||||
68
internal/portforward/service/settings.go
Normal file
68
internal/portforward/service/settings.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gosettings"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Enabled *bool
|
||||
PortForwarder PortForwarder
|
||||
Filepath string
|
||||
Interface string // needed for PIA and ProtonVPN, tun0 for example
|
||||
ServerName string // needed for PIA
|
||||
ListeningPort uint16
|
||||
}
|
||||
|
||||
func (s Settings) Copy() (copied Settings) {
|
||||
copied.Enabled = gosettings.CopyPointer(s.Enabled)
|
||||
copied.PortForwarder = s.PortForwarder
|
||||
copied.Filepath = s.Filepath
|
||||
copied.Interface = s.Interface
|
||||
copied.ServerName = s.ServerName
|
||||
copied.ListeningPort = s.ListeningPort
|
||||
return copied
|
||||
}
|
||||
|
||||
func (s *Settings) OverrideWith(update Settings) {
|
||||
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, update.Enabled)
|
||||
s.PortForwarder = gosettings.OverrideWithInterface(s.PortForwarder, update.PortForwarder)
|
||||
s.Filepath = gosettings.OverrideWithString(s.Filepath, update.Filepath)
|
||||
s.Interface = gosettings.OverrideWithString(s.Interface, update.Interface)
|
||||
s.ServerName = gosettings.OverrideWithString(s.ServerName, update.ServerName)
|
||||
s.ListeningPort = gosettings.OverrideWithNumber(s.ListeningPort, update.ListeningPort)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPortForwarderNotSet = errors.New("port forwarder not set")
|
||||
ErrServerNameNotSet = errors.New("server name not set")
|
||||
ErrFilepathNotSet = errors.New("file path not set")
|
||||
ErrInterfaceNotSet = errors.New("interface not set")
|
||||
)
|
||||
|
||||
func (s *Settings) Validate(forStartup bool) (err error) {
|
||||
// Minimal validation
|
||||
if s.Filepath == "" {
|
||||
return fmt.Errorf("%w", ErrFilepathNotSet)
|
||||
}
|
||||
|
||||
if !forStartup {
|
||||
// No additional validation needed if the service
|
||||
// is not to be started with the given settings.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Startup validation requires additional fields set.
|
||||
switch {
|
||||
case s.PortForwarder == nil:
|
||||
return fmt.Errorf("%w", ErrPortForwarderNotSet)
|
||||
case s.Interface == "":
|
||||
return fmt.Errorf("%w", ErrInterfaceNotSet)
|
||||
case s.PortForwarder.Name() == providers.PrivateInternetAccess && s.ServerName == "":
|
||||
return fmt.Errorf("%w", ErrServerNameNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
81
internal/portforward/service/start.go
Normal file
81
internal/portforward/service/start.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
func (s *Service) Start(ctx context.Context) (runError <-chan error, err error) {
|
||||
s.startStopMutex.Lock()
|
||||
defer s.startStopMutex.Unlock()
|
||||
|
||||
if !*s.settings.Enabled {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
s.logger.Info("starting")
|
||||
|
||||
gateway, err := s.routing.VPNLocalGatewayIP(s.settings.Interface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting VPN local gateway IP: %w", err)
|
||||
}
|
||||
|
||||
obj := utils.PortForwardObjects{
|
||||
Logger: s.logger,
|
||||
Gateway: gateway,
|
||||
Client: s.client,
|
||||
ServerName: s.settings.ServerName,
|
||||
}
|
||||
port, err := s.settings.PortForwarder.PortForward(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("port forwarding for the first time: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("port forwarded is " + fmt.Sprint(int(port)))
|
||||
|
||||
err = s.portAllower.SetAllowedPort(ctx, port, s.settings.Interface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("allowing port in firewall: %w", err)
|
||||
}
|
||||
|
||||
if s.settings.ListeningPort != 0 {
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, port, s.settings.ListeningPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redirecting port in firewall: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.writePortForwardedFile(port)
|
||||
if err != nil {
|
||||
_ = s.cleanup()
|
||||
return nil, fmt.Errorf("writing port file: %w", err)
|
||||
}
|
||||
|
||||
s.portMutex.Lock()
|
||||
s.port = port
|
||||
s.portMutex.Unlock()
|
||||
|
||||
keepPortCtx, keepPortCancel := context.WithCancel(context.Background())
|
||||
s.keepPortCancel = keepPortCancel
|
||||
runErrorCh := make(chan error)
|
||||
keepPortDoneCh := make(chan struct{})
|
||||
s.keepPortDoneCh = keepPortDoneCh
|
||||
|
||||
go func(ctx context.Context, portForwarder PortForwarder,
|
||||
obj utils.PortForwardObjects, runError chan<- error, doneCh chan<- struct{}) {
|
||||
defer close(doneCh)
|
||||
err = portForwarder.KeepPortForward(ctx, obj)
|
||||
crashed := ctx.Err() == nil
|
||||
if !crashed { // stopped by Stop call
|
||||
return
|
||||
}
|
||||
s.startStopMutex.Lock()
|
||||
defer s.startStopMutex.Unlock()
|
||||
_ = s.cleanup()
|
||||
runError <- err
|
||||
}(keepPortCtx, s.settings.PortForwarder, obj, runErrorCh, keepPortDoneCh)
|
||||
|
||||
return runErrorCh, nil
|
||||
}
|
||||
57
internal/portforward/service/stop.go
Normal file
57
internal/portforward/service/stop.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (s *Service) Stop() (err error) {
|
||||
s.startStopMutex.Lock()
|
||||
defer s.startStopMutex.Unlock()
|
||||
|
||||
s.portMutex.RLock()
|
||||
serviceNotRunning := s.port == 0
|
||||
s.portMutex.RUnlock()
|
||||
if serviceNotRunning {
|
||||
// TODO replace with goservices.ErrAlreadyStopped
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("stopping")
|
||||
|
||||
s.keepPortCancel()
|
||||
<-s.keepPortDoneCh
|
||||
|
||||
return s.cleanup()
|
||||
}
|
||||
|
||||
func (s *Service) cleanup() (err error) {
|
||||
s.portMutex.Lock()
|
||||
defer s.portMutex.Unlock()
|
||||
|
||||
err = s.portAllower.RemoveAllowedPort(context.Background(), s.port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("blocking previous port in firewall: %w", err)
|
||||
}
|
||||
|
||||
if s.settings.ListeningPort != 0 {
|
||||
ctx := context.Background()
|
||||
const listeningPort = 0 // 0 to clear the redirection
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, s.port, listeningPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing previous port redirection in firewall: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.port = 0
|
||||
|
||||
filepath := s.settings.Filepath
|
||||
s.logger.Info("removing port file " + filepath)
|
||||
err = os.Remove(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing port file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,16 +1,44 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||
"github.com/qdm12/gosettings"
|
||||
)
|
||||
|
||||
func (l *Loop) GetSettings() (settings settings.PortForwarding) {
|
||||
return l.state.GetSettings()
|
||||
type Settings struct {
|
||||
// VPNIsUp can be optionally set to signal the loop
|
||||
// the VPN is up (true) or down (false). If left to nil,
|
||||
// it is assumed the VPN is in the same previous state.
|
||||
VPNIsUp *bool
|
||||
Service service.Settings
|
||||
}
|
||||
|
||||
func (l *Loop) SetSettings(ctx context.Context, settings settings.PortForwarding) (
|
||||
outcome string) {
|
||||
return l.state.SetSettings(ctx, settings)
|
||||
// updateWith deep copies the receiving settings, overrides the copy with
|
||||
// fields set in the partialUpdate argument, validates the new settings
|
||||
// and returns them if they are valid, or returns an error otherwise.
|
||||
// In all cases, the receiving settings are unmodified.
|
||||
func (s Settings) updateWith(partialUpdate Settings,
|
||||
forStartup bool) (updated Settings, err error) {
|
||||
updated = s.copy()
|
||||
updated.overrideWith(partialUpdate)
|
||||
err = updated.validate(forStartup)
|
||||
if err != nil {
|
||||
return updated, err
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s Settings) copy() (copied Settings) {
|
||||
copied.VPNIsUp = gosettings.CopyPointer(s.VPNIsUp)
|
||||
copied.Service = s.Service.Copy()
|
||||
return copied
|
||||
}
|
||||
|
||||
func (s *Settings) overrideWith(update Settings) {
|
||||
s.VPNIsUp = gosettings.OverrideWithPointer(s.VPNIsUp, update.VPNIsUp)
|
||||
s.Service.OverrideWith(update.Service)
|
||||
}
|
||||
|
||||
func (s Settings) validate(forStartup bool) (err error) {
|
||||
return s.Service.Validate(forStartup)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package state
|
||||
|
||||
// GetPortForwarded is used by the control HTTP server
|
||||
// to obtain the port currently forwarded.
|
||||
func (s *State) GetPortForwarded() (port uint16) {
|
||||
s.portForwardedMu.RLock()
|
||||
defer s.portForwardedMu.RUnlock()
|
||||
return s.portForwarded
|
||||
}
|
||||
|
||||
// SetPortForwarded is only used from within the OpenVPN loop
|
||||
// to set the port forwarded.
|
||||
func (s *State) SetPortForwarded(port uint16) {
|
||||
s.portForwardedMu.Lock()
|
||||
defer s.portForwardedMu.Unlock()
|
||||
s.portForwarded = port
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
func (s *State) GetSettings() (settings settings.PortForwarding) {
|
||||
s.settingsMu.RLock()
|
||||
defer s.settingsMu.RUnlock()
|
||||
return s.settings
|
||||
}
|
||||
|
||||
func (s *State) SetSettings(ctx context.Context, settings settings.PortForwarding) (
|
||||
outcome string) {
|
||||
s.settingsMu.Lock()
|
||||
|
||||
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
||||
if settingsUnchanged {
|
||||
s.settingsMu.Unlock()
|
||||
return "settings left unchanged"
|
||||
}
|
||||
|
||||
if s.settings.Filepath != settings.Filepath {
|
||||
_ = os.Rename(*s.settings.Filepath, *settings.Filepath)
|
||||
}
|
||||
|
||||
newEnabled := *settings.Enabled
|
||||
previousEnabled := *s.settings.Enabled
|
||||
|
||||
s.settings = settings
|
||||
s.settingsMu.Unlock()
|
||||
|
||||
switch {
|
||||
case !newEnabled && !previousEnabled:
|
||||
case newEnabled && previousEnabled:
|
||||
// no need to restart for now since we os.Rename the file here.
|
||||
case newEnabled && !previousEnabled:
|
||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||
case !newEnabled && previousEnabled:
|
||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||
}
|
||||
|
||||
return "settings updated"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider"
|
||||
)
|
||||
|
||||
type StartData struct {
|
||||
PortForwarder provider.PortForwarder
|
||||
Gateway netip.Addr // needed for PIA
|
||||
ServerName string // needed for PIA
|
||||
Interface string // tun0 for example
|
||||
}
|
||||
|
||||
func (s *State) GetStartData() (startData StartData) {
|
||||
s.startDataMu.RLock()
|
||||
defer s.startDataMu.RUnlock()
|
||||
return s.startData
|
||||
}
|
||||
|
||||
func (s *State) SetStartData(startData StartData) {
|
||||
s.startDataMu.Lock()
|
||||
defer s.startDataMu.Unlock()
|
||||
s.startData = startData
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func New(statusApplier StatusApplier,
|
||||
settings settings.PortForwarding) *State {
|
||||
return &State{
|
||||
statusApplier: statusApplier,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
statusApplier StatusApplier
|
||||
|
||||
settings settings.PortForwarding
|
||||
settingsMu sync.RWMutex
|
||||
|
||||
portForwarded uint16
|
||||
portForwardedMu sync.RWMutex
|
||||
|
||||
startData StartData
|
||||
startDataMu sync.RWMutex
|
||||
}
|
||||
|
||||
type StatusApplier interface {
|
||||
ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/portforward/state"
|
||||
)
|
||||
|
||||
func (l *Loop) GetStatus() (status models.LoopStatus) {
|
||||
return l.statusManager.GetStatus()
|
||||
}
|
||||
|
||||
type StartData = state.StartData
|
||||
|
||||
func (l *Loop) Start(ctx context.Context, data StartData) (
|
||||
outcome string, err error) {
|
||||
l.startMu.Lock()
|
||||
defer l.startMu.Unlock()
|
||||
l.state.SetStartData(data)
|
||||
return l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||
}
|
||||
|
||||
func (l *Loop) Stop(ctx context.Context) (outcome string, err error) {
|
||||
return l.statusManager.ApplyStatus(ctx, constants.Stopped)
|
||||
}
|
||||
@@ -11,12 +11,15 @@ import (
|
||||
|
||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
settings settings.OpenVPN, ipv6Supported bool) (lines []string) {
|
||||
const defaultMTU = 1320 // see https://github.com/qdm12/gluetun/issues/1650#issuecomment-1988298206
|
||||
const defaultMSSFix = defaultMTU - 28 // 28 bytes of IPv4 UDP header size
|
||||
providerSettings := utils.OpenVPNProviderSettings{
|
||||
AuthUserPass: true,
|
||||
RemoteCertTLS: true,
|
||||
Auth: openvpn.SHA512,
|
||||
CA: "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", //nolint:lll
|
||||
TLSCrypt: "a3a7d8f4e778d279d9076a8d47a9aa0c6054aed5752ddefa5efac1ea982740f1ffcabadf0d3709dae18c1fad61e14f72a8eb7cb931ed0209e67c1d325353a657a1198ef649f1c23861a2a19f2c6b27aa5e43be761e0c71e9c2e8d33b75af289effb1b1e4ec603d865f74e2b4348ff631c5c81202d90003ed263dca4022aa9861520e00cc26e40fa171b9985a2763ccb4c63560b7e6b0f897978fb25a2d5889cd6d46a29509fa09830aff18d6e81a8dc1a0182402e3039c3316180e618705ca35f2763f8a62ca5983d145faa2276532ae5e18459a0b729dc67f41b928e592b39467ec3d79c70205595718b1bce56ca4ff58e692ce09c8282d2770d2bf5c217c06", //nolint:lll
|
||||
CAs: []string{"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"}, //nolint:lll
|
||||
TLSCrypt: "a3a7d8f4e778d279d9076a8d47a9aa0c6054aed5752ddefa5efac1ea982740f1ffcabadf0d3709dae18c1fad61e14f72a8eb7cb931ed0209e67c1d325353a657a1198ef649f1c23861a2a19f2c6b27aa5e43be761e0c71e9c2e8d33b75af289effb1b1e4ec603d865f74e2b4348ff631c5c81202d90003ed263dca4022aa9861520e00cc26e40fa171b9985a2763ccb4c63560b7e6b0f897978fb25a2d5889cd6d46a29509fa09830aff18d6e81a8dc1a0182402e3039c3316180e618705ca35f2763f8a62ca5983d145faa2276532ae5e18459a0b729dc67f41b928e592b39467ec3d79c70205595718b1bce56ca4ff58e692ce09c8282d2770d2bf5c217c06", //nolint:lll
|
||||
MssFix: defaultMSSFix,
|
||||
ExtraLines: []string{
|
||||
"comp-lzo no", // Explicitly disable compression
|
||||
"push-peer-info",
|
||||
|
||||
@@ -7,23 +7,20 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/airvpn/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
client *http.Client) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
|
||||
Fetcher: updater.New(client),
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(client),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,28 @@ func getOpenVPNConnection(extractor Extractor,
|
||||
connection.Port = customPort
|
||||
}
|
||||
|
||||
if len(selection.Names) > 0 {
|
||||
// Set the server name for PIA port forwarding code used
|
||||
// together with the custom provider.
|
||||
connection.ServerName = selection.Names[0]
|
||||
}
|
||||
|
||||
return connection, nil
|
||||
}
|
||||
|
||||
func getWireguardConnection(selection settings.ServerSelection) (
|
||||
connection models.Connection) {
|
||||
return models.Connection{
|
||||
connection = models.Connection{
|
||||
Type: vpn.Wireguard,
|
||||
IP: selection.Wireguard.EndpointIP,
|
||||
Port: *selection.Wireguard.EndpointPort,
|
||||
Protocol: constants.UDP,
|
||||
PubKey: selection.Wireguard.PublicKey,
|
||||
}
|
||||
if len(selection.Names) > 0 {
|
||||
// Set the server name for PIA port forwarding code used
|
||||
// together with the custom provider.
|
||||
connection.ServerName = selection.Names[0]
|
||||
}
|
||||
return connection
|
||||
}
|
||||
|
||||
@@ -8,15 +8,13 @@ import (
|
||||
|
||||
type Provider struct {
|
||||
extractor Extractor
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(extractor Extractor) *Provider {
|
||||
return &Provider{
|
||||
extractor: extractor,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Custom),
|
||||
Fetcher: utils.NewNoFetcher(providers.Custom),
|
||||
extractor: extractor,
|
||||
Fetcher: utils.NewNoFetcher(providers.Custom),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
},
|
||||
Auth: openvpn.SHA256,
|
||||
Ping: 10,
|
||||
CA: "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", //nolint:lll
|
||||
CAs: []string{"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"}, //nolint:lll
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
||||
}
|
||||
|
||||
@@ -6,23 +6,20 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/cyberghost/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Cyberghost),
|
||||
Fetcher: updater.New(parallelResolver),
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
},
|
||||
Ping: 5,
|
||||
RemoteCertTLS: true,
|
||||
CA: "MIIDZzCCAk+gAwIBAgIUVwHEFE6geihigDSNkBppm2Zamx0wDQYJKoZIhvcNAQELBQAwQzELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzERMA8GA1UEBwwITW9udHJlYWwxEDAOBgNVBAoMB0dsdWV0dW4wHhcNMjIwNzAxMTY1MzE5WhcNMjcwNjMwMTY1MzE5WjBDMQswCQYDVQQGEwJDQTEPMA0GA1UECAwGUXVlYmVjMREwDwYDVQQHDAhNb250cmVhbDEQMA4GA1UECgwHR2x1ZXR1bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALmJRhTUr+87NFkHL2PWjIz7efHqQgrWuDQt8oOBHvl0Hm72N+ckO+5Q0zG4XtqlpBjFjGUSjfNUWSrRztbXlMmzDcjHKjYHUPepJpoF100fK2q3XPiFRl6sEXzYeOdFgpaTdmGHS6DL9aWeCoYA/k6NV8YqHXujr14gOYOAWG6cRimpTJf8DtEDcxtp1w6fOEoN0b5PvV7dcpLiva8LYyZKPvFYBzlc5BZxOLvq6bvhQm54R6zoHFpaKOf7FeqhxI6KOQu4IPwU12YBlOP5CbkMAQ1cWWVQ4pnh0Hwh71Sjm848jS/OcammNzsp4xWaKt/pzcix3fpJt/MDP/9fxA8CAwEAAaNTMFEwHQYDVR0OBBYEFCIQ9l28Yy1/3qJvFarXjhKdG9tVMB8GA1UdIwQYMBaAFCIQ9l28Yy1/3qJvFarXjhKdG9tVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKLPmLTppXYTTOOxHhTMyHI0oTl7ID2PQfJsref+jDshB3hib98BC17b9ESpLnwx7ugg17NRl7RYutxjuVw/CK/gwAnTMg3D3mdAnKkMRr3UxnD89KprLIpf7WQCmyJaxalsD5jjgl3kuGM7jf2FJNxQz5RrXBGlQHa465ouov+Rp5v/K5Umyt6wsCZXEbOF0SdUhEGU3nxVbFsoPimNYSHHwc29USnQmyW1O/drFDoTcOK4GdHFEVkrHQgqwU8ay1fYGYfIUDhsV/5AAWgQC41r9FWr+VQgyJC94qmDg0c46RE123dL/YifVUl2DKuJ0aOY+OkSgwknKZ+FQd+8d6k=", //nolint:lll
|
||||
TLSAuth: "bc470c93ff9f5602a8abb27dee84a52814d10f20490ad23c47d5d82120c1bf859e93d0696b455d4a1b8d55d40c2685c41ca1d0aef29a3efd27274c4ef09020a3978fe45784b335da6df2d12db97bbb838416515f2a96f04715fd28949c6fe296a925cfada3f8b8928ed7fc963c1563272f5cf46e5e1d9c845d7703ca881497b7e6564a9d1dea9358adffd435295479f47d5298fabf5359613ff5992cb57ff081a04dfb81a26513a6b44a9b5490ad265f8a02384832a59cc3e075ad545461060b7bcab49bac815163cb80983dd51d5b1fd76170ffd904d8291071e96efc3fb777856c717b148d08a510f5687b8a8285dcffe737b98916dd15ef6235dee4266d3b", //nolint:lll
|
||||
CAs: []string{"MIIDZzCCAk+gAwIBAgIUVwHEFE6geihigDSNkBppm2Zamx0wDQYJKoZIhvcNAQELBQAwQzELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzERMA8GA1UEBwwITW9udHJlYWwxEDAOBgNVBAoMB0dsdWV0dW4wHhcNMjIwNzAxMTY1MzE5WhcNMjcwNjMwMTY1MzE5WjBDMQswCQYDVQQGEwJDQTEPMA0GA1UECAwGUXVlYmVjMREwDwYDVQQHDAhNb250cmVhbDEQMA4GA1UECgwHR2x1ZXR1bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALmJRhTUr+87NFkHL2PWjIz7efHqQgrWuDQt8oOBHvl0Hm72N+ckO+5Q0zG4XtqlpBjFjGUSjfNUWSrRztbXlMmzDcjHKjYHUPepJpoF100fK2q3XPiFRl6sEXzYeOdFgpaTdmGHS6DL9aWeCoYA/k6NV8YqHXujr14gOYOAWG6cRimpTJf8DtEDcxtp1w6fOEoN0b5PvV7dcpLiva8LYyZKPvFYBzlc5BZxOLvq6bvhQm54R6zoHFpaKOf7FeqhxI6KOQu4IPwU12YBlOP5CbkMAQ1cWWVQ4pnh0Hwh71Sjm848jS/OcammNzsp4xWaKt/pzcix3fpJt/MDP/9fxA8CAwEAAaNTMFEwHQYDVR0OBBYEFCIQ9l28Yy1/3qJvFarXjhKdG9tVMB8GA1UdIwQYMBaAFCIQ9l28Yy1/3qJvFarXjhKdG9tVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKLPmLTppXYTTOOxHhTMyHI0oTl7ID2PQfJsref+jDshB3hib98BC17b9ESpLnwx7ugg17NRl7RYutxjuVw/CK/gwAnTMg3D3mdAnKkMRr3UxnD89KprLIpf7WQCmyJaxalsD5jjgl3kuGM7jf2FJNxQz5RrXBGlQHa465ouov+Rp5v/K5Umyt6wsCZXEbOF0SdUhEGU3nxVbFsoPimNYSHHwc29USnQmyW1O/drFDoTcOK4GdHFEVkrHQgqwU8ay1fYGYfIUDhsV/5AAWgQC41r9FWr+VQgyJC94qmDg0c46RE123dL/YifVUl2DKuJ0aOY+OkSgwknKZ+FQd+8d6k="}, //nolint:lll
|
||||
TLSAuth: "bc470c93ff9f5602a8abb27dee84a52814d10f20490ad23c47d5d82120c1bf859e93d0696b455d4a1b8d55d40c2685c41ca1d0aef29a3efd27274c4ef09020a3978fe45784b335da6df2d12db97bbb838416515f2a96f04715fd28949c6fe296a925cfada3f8b8928ed7fc963c1563272f5cf46e5e1d9c845d7703ca881497b7e6564a9d1dea9358adffd435295479f47d5298fabf5359613ff5992cb57ff081a04dfb81a26513a6b44a9b5490ad265f8a02384832a59cc3e075ad545461060b7bcab49bac815163cb80983dd51d5b1fd76170ffd904d8291071e96efc3fb777856c717b148d08a510f5687b8a8285dcffe737b98916dd15ef6235dee4266d3b", //nolint:lll
|
||||
}
|
||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,11 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/example/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
@@ -22,10 +20,9 @@ func New(storage common.Storage, randSource rand.Source,
|
||||
updaterWarner common.Warner, client *http.Client,
|
||||
unzipper common.Unzipper, parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
|
||||
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm,
|
||||
},
|
||||
Auth: openvpn.SHA512,
|
||||
CA: "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", //nolint:lll
|
||||
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll
|
||||
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll
|
||||
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll
|
||||
CAs: []string{"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"}, //nolint:lll
|
||||
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll
|
||||
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll
|
||||
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll
|
||||
MssFix: 1200,
|
||||
FastIO: true,
|
||||
Fragment: 1300,
|
||||
|
||||
@@ -6,13 +6,11 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/provider/expressvpn/updater"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
storage common.Storage
|
||||
randSource rand.Source
|
||||
utils.NoPortForwarder
|
||||
common.Fetcher
|
||||
}
|
||||
|
||||
@@ -20,10 +18,9 @@ func New(storage common.Storage, randSource rand.Source,
|
||||
unzipper common.Unzipper, updaterWarner common.Warner,
|
||||
parallelResolver common.ParallelResolver) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Expressvpn),
|
||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
AuthToken: true,
|
||||
KeyDirection: "1",
|
||||
RenegDisabled: true,
|
||||
CA: "MIIFQjCCAyqgAwIBAgIIUfxepT+rr8owDQYJKoZIhvcNAQEMBQAwPzELMAkGA1UEBhMCS1kxEzARBgNVBAoTCkZhc3Rlc3RWUE4xGzAZBgNVBAMTEkZhc3Rlc3RWUE4gUm9vdCBDQTAeFw0xNzA5MTYwMDAxNDZaFw0yNzA5MTQwMDAxNDZaMD8xCzAJBgNVBAYTAktZMRMwEQYDVQQKEwpGYXN0ZXN0VlBOMRswGQYDVQQDExJGYXN0ZXN0VlBOIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1Xj+WfPTozFynFqc+c3CVrggIllaXEl5bY5VgFynXkqCTM6lSrfC4pNjGXUbqWe6RnGJbM4/6kUn+lQDjFSQV1rzP2eDS8+r5+X2WXh4AoeNRUWhvSG+HiHD/B2EFK+Nd5BRSdUjpKWAtsCmT2bBt7nT0jN1OdeNrLJeyF8siAqv/oQzKznF9aIe/N01b2M8ZOFTzoXi2fZAckgGWui8NB/lzkVIJqSkAPRL8qiJLuRCPVOX1PFD8vV//R8/QumtfbcYBMo6vCk2HmWdrh5OQHPxb3KJtbtG+Z1j8x6HGEAe17djYepBiRMyCEQvYgfD6tvFylc4IquhqE9yaP60PJod5TxpWnRQ6HIGSeBm+S+rYSMalTZ8+pUqOOA+IQCYpfpx6EKIJL/VsW2C7cXdvudxDhXPI5lR/QidCb9Ohq3WkfxXaYwzrngdg2avmNqId9R4KESuM9GoHW0dszfyBCh5wYfeaffMElfDam3B92NUwyhZwtIiv623WVXY9PPz+EDjSJsIAu2Vi1vdJyA4nD4k9Lwmx/1zTc/UaYVLsiBqL2WdfvFTeoWoV+dNxQXSEPhB8gwi8x4O4lZW0cwVy/6fa8KMY8gZbcbSTr7U5bRERfW8l+jY+mYKQ/M/ccgpxaHiw1/+4LWfbJQ7VhJJrTyN0C36FQzY1URkSXg+53wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmVEL4x6xdCqiqu2OBLs27EA8xGYwDQYJKoZIhvcNAQEMBQADggIBABCpITvO1+R4T9v2+onHiFxU5JjtCZ0zkXqRCMp/Z0UIYbeo1p07pZCPAUjBfGPCkAaR++OiG9sysALdJf8Y6HQKcyuAcWUqQnaIhoZ2JcAP7EKq7uCqsMhcYZD/j3O/3RPtSW5UOx6ItDU+Ua0t9Edho9whNw0VQXmo1JjYoP3FzPjuKoDWTSO1q5eYlZfwcTcs55O2shNkFafPg/6cCm5j6v9nyHrM3sk4LjkrBPUXVx2m/aoz219t8O9Ha9/CdMKXsPO/8gTUzpgnzSgPnGnBmi5xr1nspVN8X4E2f3D+DKqBim3YgslD68NcuFQvJ0/BxZzWVbrr+QXoyzaiCgXuogpIDc2bB6oRXqFnHNz36d4QJmJdWdSaijiS/peQ6EOPgOZ1GuObLWlDCBZLNeQ+N6QaiJxVO4XUj/s22i1IRtwdz84TRHrbWiIpEymsqmb/Ep5r4xV5d6+791axclfOTH7tQrY/SbPtTJI4OEgNekI8YfadQifpelF82MsFFEZuaQn0lj+fvLGtE/zKh3OdLTxRc5TAgBB+0T81+JQosygNr2aFFG0hxar1eyw/gLeG8H+7Ie50pyPvXO4OgB6Key8rSExpilQXlvAT1qX0qS3/K1i/9QkSE9ftIPT6vtwLV2sVQzfyanI4IZgWC6ryhvNLsRn0NFnQclor0+aq", //nolint:lll //nolint:lll
|
||||
TLSAuth: "697fe793b32cb5091d30f2326d5d124a9412e93d0a44ef7361395d76528fcbfc82c3859dccea70a93cfa8fae409709bff75f844cf5ff0c237f426d0c20969233db0e706edb6bdf195ec3dc11b3f76bc807a77e74662d9a800c8cd1144ebb67b7f0d3f1281d1baf522bfe03b7c3f963b1364fc0769400e413b61ca7b43ab19fac9e0f77e41efd4bda7fd77b1de2d7d7855cbbe3e620cecceac72c21a825b243e651f44d90e290e09c3ad650de8fca99c858bc7caad584bc69b11e5c9fd9381c69c505ec487a65912c672d83ed0113b5a74ddfbd3ab33b3683cec593557520a72c4d6cce46111f56f3396cc3ce7183edce553c68ea0796cf6c4375fad00aaa2a42", //nolint:lll
|
||||
CAs: []string{"MIIFQjCCAyqgAwIBAgIIUfxepT+rr8owDQYJKoZIhvcNAQEMBQAwPzELMAkGA1UEBhMCS1kxEzARBgNVBAoTCkZhc3Rlc3RWUE4xGzAZBgNVBAMTEkZhc3Rlc3RWUE4gUm9vdCBDQTAeFw0xNzA5MTYwMDAxNDZaFw0yNzA5MTQwMDAxNDZaMD8xCzAJBgNVBAYTAktZMRMwEQYDVQQKEwpGYXN0ZXN0VlBOMRswGQYDVQQDExJGYXN0ZXN0VlBOIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1Xj+WfPTozFynFqc+c3CVrggIllaXEl5bY5VgFynXkqCTM6lSrfC4pNjGXUbqWe6RnGJbM4/6kUn+lQDjFSQV1rzP2eDS8+r5+X2WXh4AoeNRUWhvSG+HiHD/B2EFK+Nd5BRSdUjpKWAtsCmT2bBt7nT0jN1OdeNrLJeyF8siAqv/oQzKznF9aIe/N01b2M8ZOFTzoXi2fZAckgGWui8NB/lzkVIJqSkAPRL8qiJLuRCPVOX1PFD8vV//R8/QumtfbcYBMo6vCk2HmWdrh5OQHPxb3KJtbtG+Z1j8x6HGEAe17djYepBiRMyCEQvYgfD6tvFylc4IquhqE9yaP60PJod5TxpWnRQ6HIGSeBm+S+rYSMalTZ8+pUqOOA+IQCYpfpx6EKIJL/VsW2C7cXdvudxDhXPI5lR/QidCb9Ohq3WkfxXaYwzrngdg2avmNqId9R4KESuM9GoHW0dszfyBCh5wYfeaffMElfDam3B92NUwyhZwtIiv623WVXY9PPz+EDjSJsIAu2Vi1vdJyA4nD4k9Lwmx/1zTc/UaYVLsiBqL2WdfvFTeoWoV+dNxQXSEPhB8gwi8x4O4lZW0cwVy/6fa8KMY8gZbcbSTr7U5bRERfW8l+jY+mYKQ/M/ccgpxaHiw1/+4LWfbJQ7VhJJrTyN0C36FQzY1URkSXg+53wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmVEL4x6xdCqiqu2OBLs27EA8xGYwDQYJKoZIhvcNAQEMBQADggIBABCpITvO1+R4T9v2+onHiFxU5JjtCZ0zkXqRCMp/Z0UIYbeo1p07pZCPAUjBfGPCkAaR++OiG9sysALdJf8Y6HQKcyuAcWUqQnaIhoZ2JcAP7EKq7uCqsMhcYZD/j3O/3RPtSW5UOx6ItDU+Ua0t9Edho9whNw0VQXmo1JjYoP3FzPjuKoDWTSO1q5eYlZfwcTcs55O2shNkFafPg/6cCm5j6v9nyHrM3sk4LjkrBPUXVx2m/aoz219t8O9Ha9/CdMKXsPO/8gTUzpgnzSgPnGnBmi5xr1nspVN8X4E2f3D+DKqBim3YgslD68NcuFQvJ0/BxZzWVbrr+QXoyzaiCgXuogpIDc2bB6oRXqFnHNz36d4QJmJdWdSaijiS/peQ6EOPgOZ1GuObLWlDCBZLNeQ+N6QaiJxVO4XUj/s22i1IRtwdz84TRHrbWiIpEymsqmb/Ep5r4xV5d6+791axclfOTH7tQrY/SbPtTJI4OEgNekI8YfadQifpelF82MsFFEZuaQn0lj+fvLGtE/zKh3OdLTxRc5TAgBB+0T81+JQosygNr2aFFG0hxar1eyw/gLeG8H+7Ie50pyPvXO4OgB6Key8rSExpilQXlvAT1qX0qS3/K1i/9QkSE9ftIPT6vtwLV2sVQzfyanI4IZgWC6ryhvNLsRn0NFnQclor0+aq"}, //nolint:lll
|
||||
TLSAuth: "697fe793b32cb5091d30f2326d5d124a9412e93d0a44ef7361395d76528fcbfc82c3859dccea70a93cfa8fae409709bff75f844cf5ff0c237f426d0c20969233db0e706edb6bdf195ec3dc11b3f76bc807a77e74662d9a800c8cd1144ebb67b7f0d3f1281d1baf522bfe03b7c3f963b1364fc0769400e413b61ca7b43ab19fac9e0f77e41efd4bda7fd77b1de2d7d7855cbbe3e620cecceac72c21a825b243e651f44d90e290e09c3ad650de8fca99c858bc7caad584bc69b11e5c9fd9381c69c505ec487a65912c672d83ed0113b5a74ddfbd3ab33b3683cec593557520a72c4d6cce46111f56f3396cc3ce7183edce553c68ea0796cf6c4375fad00aaa2a42", //nolint:lll
|
||||
UDPLines: []string{
|
||||
"tun-mtu 1500",
|
||||
"tun-mtu-extra 32",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user