Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
### 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
|
### 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
|
## 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/)
|
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
- [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:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
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
|
- type: dropdown
|
||||||
id: urgent
|
id: urgent
|
||||||
attributes:
|
attributes:
|
||||||
label: Is this urgent?
|
label: Is this urgent?
|
||||||
description: |
|
description: |
|
||||||
Is this a critical bug, or do you need this fixed urgently?
|
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:
|
options:
|
||||||
- "No"
|
- "No"
|
||||||
- "Yes"
|
- "Yes"
|
||||||
@@ -75,6 +80,7 @@ body:
|
|||||||
- Portainer
|
- Portainer
|
||||||
- Kubernetes
|
- Kubernetes
|
||||||
- Podman
|
- Podman
|
||||||
|
- Unraid
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -84,7 +90,7 @@ body:
|
|||||||
label: What is the version of Gluetun
|
label: What is the version of Gluetun
|
||||||
description: |
|
description: |
|
||||||
Copy paste the version line at the top of your logs.
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -97,7 +103,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
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`.
|
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
|
||||||
render: plain text
|
render: plain text
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,7 @@
|
|||||||
contact_links:
|
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?
|
- name: Configuration help?
|
||||||
url: https://github.com/qdm12/gluetun/discussions/new
|
url: https://github.com/qdm12/gluetun/discussions/new
|
||||||
about: Please create a Github discussion.
|
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,
|
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
||||||
you can only use a custom Openvpn configuration file.
|
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
|
|
||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -45,6 +45,7 @@ jobs:
|
|||||||
level: error
|
level: error
|
||||||
exclude: |
|
exclude: |
|
||||||
./internal/storage/servers.json
|
./internal/storage/servers.json
|
||||||
|
*.md
|
||||||
|
|
||||||
- name: Linting
|
- name: Linting
|
||||||
run: docker build --target lint .
|
run: docker build --target lint .
|
||||||
|
|||||||
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
|
|
||||||
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@v3
|
||||||
|
|
||||||
|
- uses: DavidAnson/markdownlint-cli2-action@v11
|
||||||
|
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
|
||||||
|
}
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,8 +1,8 @@
|
|||||||
ARG ALPINE_VERSION=3.18
|
ARG ALPINE_VERSION=3.18
|
||||||
ARG GO_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 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 MOCKGEN_VERSION=v1.6.0
|
||||||
ARG BUILDPLATFORM=linux/amd64
|
ARG BUILDPLATFORM=linux/amd64
|
||||||
|
|
||||||
@@ -90,12 +90,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
OPENVPN_FLAGS= \
|
OPENVPN_FLAGS= \
|
||||||
OPENVPN_CIPHERS= \
|
OPENVPN_CIPHERS= \
|
||||||
OPENVPN_AUTH= \
|
OPENVPN_AUTH= \
|
||||||
OPENVPN_PROCESS_USER= \
|
OPENVPN_PROCESS_USER=root \
|
||||||
OPENVPN_CUSTOM_CONFIG= \
|
OPENVPN_CUSTOM_CONFIG= \
|
||||||
# Wireguard
|
# Wireguard
|
||||||
WIREGUARD_PRIVATE_KEY= \
|
WIREGUARD_PRIVATE_KEY= \
|
||||||
WIREGUARD_PRESHARED_KEY= \
|
WIREGUARD_PRESHARED_KEY= \
|
||||||
WIREGUARD_PUBLIC_KEY= \
|
WIREGUARD_PUBLIC_KEY= \
|
||||||
|
WIREGUARD_ALLOWED_IPS= \
|
||||||
WIREGUARD_ADDRESSES= \
|
WIREGUARD_ADDRESSES= \
|
||||||
WIREGUARD_MTU=1400 \
|
WIREGUARD_MTU=1400 \
|
||||||
WIREGUARD_IMPLEMENTATION=auto \
|
WIREGUARD_IMPLEMENTATION=auto \
|
||||||
@@ -110,6 +111,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# # Private Internet Access only:
|
# # Private Internet Access only:
|
||||||
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
|
||||||
VPN_PORT_FORWARDING=off \
|
VPN_PORT_FORWARDING=off \
|
||||||
|
VPN_PORT_FORWARDING_PROVIDER= \
|
||||||
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
|
||||||
# # Cyberghost only:
|
# # Cyberghost only:
|
||||||
OPENVPN_CERT= \
|
OPENVPN_CERT= \
|
||||||
@@ -165,6 +167,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
HTTPPROXY= \
|
HTTPPROXY= \
|
||||||
HTTPPROXY_LOG=off \
|
HTTPPROXY_LOG=off \
|
||||||
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
HTTPPROXY_LISTENING_ADDRESS=":8888" \
|
||||||
|
HTTPPROXY_STEALTH=off \
|
||||||
HTTPPROXY_USER= \
|
HTTPPROXY_USER= \
|
||||||
HTTPPROXY_PASSWORD= \
|
HTTPPROXY_PASSWORD= \
|
||||||
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
|
||||||
@@ -177,6 +180,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
|
||||||
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
|
||||||
# Control server
|
# Control server
|
||||||
|
HTTP_CONTROL_SERVER_LOG=on \
|
||||||
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
|
||||||
# Server data updater
|
# Server data updater
|
||||||
UPDATER_PERIOD=0 \
|
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)
|
- [Setup](#setup)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- Problem?
|
- 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)
|
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
|
||||||
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
|
||||||
- Suggestion?
|
- Suggestion?
|
||||||
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
- [Create an issue](https://github.com/qdm12/gluetun/issues)
|
||||||
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
|
||||||
- Happy?
|
- Happy?
|
||||||
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
|
||||||
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||||
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
|
- 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:
|
- Video:
|
||||||
|
|
||||||
[](https://youtu.be/0F6I03LQcI4)
|
[](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: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- Supports Wireguard both kernelspace and userspace
|
||||||
- For **Mullvad**, **Ivpn**, **Surfshark** and **Windscribe**
|
- 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/Custom-provider)
|
- 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/Custom-provider)
|
- 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)
|
- 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 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
|
- 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 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 Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
- 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 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/Connect-a-LAN-device-to-gluetun)
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||||
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
|
||||||
- [Custom VPN server side port forwarding for 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
|
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
|
||||||
- Unbound subprogram drops root privileges once launched
|
- Unbound subprogram drops root privileges once launched
|
||||||
- Can work as a Kubernetes sidecar container, thanks @rorph
|
- Can work as a Kubernetes sidecar container, thanks @rorph
|
||||||
@@ -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!
|
🎉 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:
|
Here's a docker-compose.yml for the laziest:
|
||||||
|
|
||||||
@@ -95,7 +94,8 @@ services:
|
|||||||
gluetun:
|
gluetun:
|
||||||
image: qmcgaw/gluetun
|
image: qmcgaw/gluetun
|
||||||
# container_name: 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:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -107,7 +107,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /yourpath:/gluetun
|
- /yourpath:/gluetun
|
||||||
environment:
|
environment:
|
||||||
# See https://github.com/qdm12/gluetun/wiki
|
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
|
||||||
- VPN_SERVICE_PROVIDER=ivpn
|
- VPN_SERVICE_PROVIDER=ivpn
|
||||||
- VPN_TYPE=openvpn
|
- VPN_TYPE=openvpn
|
||||||
# OpenVPN:
|
# OpenVPN:
|
||||||
@@ -118,13 +118,13 @@ services:
|
|||||||
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
# - WIREGUARD_ADDRESSES=10.64.222.21/32
|
||||||
# Timezone for accurate log times
|
# Timezone for accurate log times
|
||||||
- TZ=
|
- 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_PERIOD=
|
||||||
- UPDATER_VPN_SERVICE_PROVIDERS=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
🆕 Image also available as `ghcr.io/qdm12/gluetun`
|
||||||
|
|
||||||
## License
|
## 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()
|
cli := cli.New()
|
||||||
cmder := command.NewCmder()
|
cmder := command.NewCmder()
|
||||||
|
|
||||||
envReader := env.New(logger)
|
|
||||||
filesReader := files.New()
|
|
||||||
secretsReader := secrets.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)
|
errorCh := make(chan error)
|
||||||
go func() {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
Version: buildInfo.Version,
|
Version: buildInfo.Version,
|
||||||
Commit: buildInfo.Commit,
|
Commit: buildInfo.Commit,
|
||||||
BuildDate: buildInfo.Created,
|
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,
|
AnnounceExp: announcementExp,
|
||||||
// Sponsor information
|
// Sponsor information
|
||||||
PaypalUser: "qmcgaw",
|
PaypalUser: "qmcgaw",
|
||||||
@@ -376,12 +376,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
|
|
||||||
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
|
||||||
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
|
||||||
httpClient, firewallConf, portForwardLogger, puid, pgid)
|
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
|
||||||
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
|
portForwardRunError, err := portForwardLooper.Start(ctx)
|
||||||
"port forwarding", goroutine.OptionTimeout(time.Second))
|
if err != nil {
|
||||||
go portForwardLooper.Run(portForwardCtx, portForwardDone)
|
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,
|
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
|
||||||
unboundLogger)
|
unboundLogger)
|
||||||
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
|
||||||
@@ -399,15 +400,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
publicIPLooper := publicip.NewLoop(ipFetcher,
|
publicIPLooper := publicip.NewLoop(ipFetcher,
|
||||||
logger.New(log.SetComponent("ip getter")),
|
logger.New(log.SetComponent("ip getter")),
|
||||||
allSettings.PublicIP, puid, pgid)
|
allSettings.PublicIP, puid, pgid)
|
||||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
publicIPRunError, err := publicIPLooper.Start(ctx)
|
||||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
if err != nil {
|
||||||
go publicIPLooper.Run(pubIPCtx, pubIPDone)
|
return fmt.Errorf("starting public ip loop: %w", err)
|
||||||
otherGroupHandler.Add(pubIPHandler)
|
}
|
||||||
|
|
||||||
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
|
|
||||||
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
|
|
||||||
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
|
|
||||||
tickersGroupHandler.Add(pubIPTickerHandler)
|
|
||||||
|
|
||||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||||
|
|
||||||
@@ -481,13 +477,31 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
|||||||
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
order.OptionOnSuccess(defaultShutdownOnSuccess),
|
||||||
order.OptionOnFailure(defaultShutdownOnFailure))
|
order.OptionOnFailure(defaultShutdownOnFailure))
|
||||||
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
|
||||||
vpnHandler, portForwardHandler, otherGroupHandler)
|
vpnHandler, otherGroupHandler)
|
||||||
|
|
||||||
// Start VPN for the first time in a blocking call
|
// Start VPN for the first time in a blocking call
|
||||||
// until the VPN is launched
|
// until the VPN is launched
|
||||||
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
|
_, _ = 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())
|
return orderHandler.Shutdown(context.Background())
|
||||||
}
|
}
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -1,14 +1,16 @@
|
|||||||
module github.com/qdm12/gluetun
|
module github.com/qdm12/gluetun
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.2.11
|
github.com/breml/rootcerts v0.2.11
|
||||||
github.com/fatih/color v1.15.0
|
github.com/fatih/color v1.15.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
|
github.com/klauspost/compress v1.16.7
|
||||||
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/qdm12/dns v1.11.0
|
github.com/qdm12/dns v1.11.0
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
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/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.1.0
|
github.com/qdm12/gosplash v0.1.0
|
||||||
github.com/qdm12/gotree v0.2.0
|
github.com/qdm12/gotree v0.2.0
|
||||||
@@ -17,13 +19,15 @@ require (
|
|||||||
github.com/qdm12/ss-server v0.5.0-rc1
|
github.com/qdm12/ss-server v0.5.0-rc1
|
||||||
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
|
||||||
github.com/stretchr/testify v1.8.4
|
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/vishvananda/netlink v1.2.1-beta.2
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.12.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.11.0
|
||||||
golang.org/x/text v0.10.0
|
golang.org/x/text v0.11.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
|
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
|
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,8 +46,8 @@ require (
|
|||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 // indirect
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
|||||||
34
go.sum
34
go.sum
@@ -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/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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||||
|
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
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 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
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/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@@ -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 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 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-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 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
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.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38=
|
||||||
github.com/qdm12/gosettings v0.3.0-rc13/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
|
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 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
|
||||||
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
|
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
|
||||||
@@ -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.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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
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 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||||
@@ -138,6 +146,8 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:
|
|||||||
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-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 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-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-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -146,8 +156,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-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-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.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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
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/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
@@ -165,8 +175,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-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-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.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -195,8 +205,8 @@ 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-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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -204,9 +214,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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
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-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-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
@@ -226,6 +237,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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-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-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-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||||
@@ -236,6 +249,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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
|
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-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 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
||||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
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 {
|
func (c *CLI) FormatServers(args []string) error {
|
||||||
var format, output string
|
var format, output string
|
||||||
allProviders := providers.All()
|
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))
|
providersToFormat := make(map[string]*bool, len(allProviders))
|
||||||
for _, provider := range allProviders {
|
for _, provider := range allProviderFlags {
|
||||||
providersToFormat[provider] = new(bool)
|
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(&format, "format", "markdown", "Format to use which can be: 'markdown'")
|
||||||
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
|
||||||
titleCaser := cases.Title(language.English)
|
titleCaser := cases.Title(language.English)
|
||||||
for _, provider := range allProviders {
|
for _, provider := range allProviderFlags {
|
||||||
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
|
||||||
}
|
}
|
||||||
if err := flagSet.Parse(args); err != nil {
|
if err := flagSet.Parse(args); err != nil {
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ type DNS struct {
|
|||||||
// DoT server. It cannot be the zero value in the internal
|
// DoT server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress netip.Addr
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the Docker DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
// found in /etc/resolv.conf should be kept.
|
// found in /etc/resolv.conf should be used
|
||||||
// Note settings this to true will go around the
|
// Note setting this to true will likely DNS traffic
|
||||||
// DoT server blocking.
|
// 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
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
KeepNameserver *bool
|
||||||
@@ -75,8 +79,11 @@ func (d DNS) String() string {
|
|||||||
|
|
||||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||||
node = gotree.New("DNS settings:")
|
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))
|
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())
|
node.AppendNode(d.DoT.toLinesNode())
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ var (
|
|||||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||||
|
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
|
||||||
|
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
|
||||||
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
||||||
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
||||||
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
if *o.CustomPort != 0 {
|
if *o.CustomPort != 0 {
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
// no restriction on port
|
// no restriction on port
|
||||||
case providers.Cyberghost, providers.HideMyAss,
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
providers.Privatevpn, providers.Torguard:
|
providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
@@ -99,6 +99,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
case providers.Windscribe:
|
case providers.Windscribe:
|
||||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
|
||||||
}
|
}
|
||||||
|
|
||||||
allowedPorts := allowedUDP
|
allowedPorts := allowedUDP
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ type PortForwarding struct {
|
|||||||
// Enabled is true if port forwarding should be activated.
|
// Enabled is true if port forwarding should be activated.
|
||||||
// It cannot be nil for the internal state.
|
// It cannot be nil for the internal state.
|
||||||
Enabled *bool `json:"enabled"`
|
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
|
// Filepath is the port forwarding status file path
|
||||||
// to use. It can be the empty string to indicate not
|
// to use. It can be the empty string to indicate not
|
||||||
// to write to a file. It cannot be nil for the
|
// to write to a file. It cannot be nil for the
|
||||||
@@ -22,14 +30,21 @@ type PortForwarding struct {
|
|||||||
Filepath *string `json:"status_file_path"`
|
Filepath *string `json:"status_file_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PortForwarding) validate(vpnProvider string) (err error) {
|
func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||||
if !*p.Enabled {
|
if !*p.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Enabled
|
// Validate current provider or custom provider specified
|
||||||
validProviders := []string{providers.PrivateInternetAccess}
|
providerSelected := vpnProvider
|
||||||
if err = validate.IsOneOf(vpnProvider, validProviders...); err != nil {
|
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)
|
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,25 +59,29 @@ func (p PortForwarding) validate(vpnProvider string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) copy() (copied PortForwarding) {
|
func (p *PortForwarding) Copy() (copied PortForwarding) {
|
||||||
return PortForwarding{
|
return PortForwarding{
|
||||||
Enabled: gosettings.CopyPointer(p.Enabled),
|
Enabled: gosettings.CopyPointer(p.Enabled),
|
||||||
|
Provider: gosettings.CopyPointer(p.Provider),
|
||||||
Filepath: gosettings.CopyPointer(p.Filepath),
|
Filepath: gosettings.CopyPointer(p.Filepath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) mergeWith(other PortForwarding) {
|
func (p *PortForwarding) mergeWith(other PortForwarding) {
|
||||||
p.Enabled = gosettings.MergeWithPointer(p.Enabled, other.Enabled)
|
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.Filepath = gosettings.MergeWithPointer(p.Filepath, other.Filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) overrideWith(other PortForwarding) {
|
func (p *PortForwarding) OverrideWith(other PortForwarding) {
|
||||||
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
|
||||||
|
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
|
||||||
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PortForwarding) setDefaults() {
|
func (p *PortForwarding) setDefaults() {
|
||||||
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
|
||||||
|
p.Provider = gosettings.DefaultPointer(p.Provider, "")
|
||||||
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +95,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node = gotree.New("Automatic port forwarding settings:")
|
node = gotree.New("Automatic port forwarding settings:")
|
||||||
node.Appendf("Enabled: yes")
|
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
|
filepath := *p.Filepath
|
||||||
if filepath == "" {
|
if filepath == "" {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
|
|||||||
return fmt.Errorf("server selection: %w", err)
|
return fmt.Errorf("server selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.PortForwarding.validate(*p.Name)
|
err = p.PortForwarding.Validate(*p.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("port forwarding: %w", err)
|
return fmt.Errorf("port forwarding: %w", err)
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func (p *Provider) copy() (copied Provider) {
|
|||||||
return Provider{
|
return Provider{
|
||||||
Name: gosettings.CopyPointer(p.Name),
|
Name: gosettings.CopyPointer(p.Name),
|
||||||
ServerSelection: p.ServerSelection.copy(),
|
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) {
|
func (p *Provider) overrideWith(other Provider) {
|
||||||
p.Name = gosettings.OverrideWithPointer(p.Name, other.Name)
|
p.Name = gosettings.OverrideWithPointer(p.Name, other.Name)
|
||||||
p.ServerSelection.overrideWith(other.ServerSelection)
|
p.ServerSelection.overrideWith(other.ServerSelection)
|
||||||
p.PortForwarding.overrideWith(other.PortForwarding)
|
p.PortForwarding.OverrideWith(other.PortForwarding)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) setDefaults() {
|
func (p *Provider) setDefaults() {
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ type PublicIP struct {
|
|||||||
IPFilepath *string
|
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) {
|
func (p PublicIP) validate() (err error) {
|
||||||
const minPeriod = 5 * time.Second
|
const minPeriod = 5 * time.Second
|
||||||
if *p.Period < minPeriod {
|
if *p.Period < minPeriod {
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
| ├── Run OpenVPN as: root
|
| ├── Run OpenVPN as: root
|
||||||
| └── Verbosity level: 1
|
| └── Verbosity level: 1
|
||||||
├── DNS settings:
|
├── DNS settings:
|
||||||
| ├── DNS server address to use: 127.0.0.1
|
|
||||||
| ├── Keep existing nameserver(s): no
|
| ├── Keep existing nameserver(s): no
|
||||||
|
| ├── DNS server address to use: 127.0.0.1
|
||||||
| └── DNS over TLS settings:
|
| └── DNS over TLS settings:
|
||||||
| ├── Enabled: yes
|
| ├── Enabled: yes
|
||||||
| ├── Update period: every 24h0m0s
|
| ├── Update period: every 24h0m0s
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ type Wireguard struct {
|
|||||||
PreSharedKey *string `json:"pre_shared_key"`
|
PreSharedKey *string `json:"pre_shared_key"`
|
||||||
// Addresses are the Wireguard interface addresses.
|
// Addresses are the Wireguard interface addresses.
|
||||||
Addresses []netip.Prefix `json:"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
|
// Interface is the name of the Wireguard interface
|
||||||
// to create. It cannot be the empty string in the
|
// to create. It cannot be the empty string in the
|
||||||
// internal state.
|
// internal state.
|
||||||
@@ -89,13 +93,26 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
|
|||||||
}
|
}
|
||||||
for i, ipNet := range w.Addresses {
|
for i, ipNet := range w.Addresses {
|
||||||
if !ipNet.IsValid() {
|
if !ipNet.IsValid() {
|
||||||
return fmt.Errorf("%w: for address at index %d: %s",
|
return fmt.Errorf("%w: for address at index %d",
|
||||||
ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
|
ErrWireguardInterfaceAddressNotSet, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ipv6Supported && ipNet.Addr().Is6() {
|
if !ipv6Supported && ipNet.Addr().Is6() {
|
||||||
return fmt.Errorf("%w: address %s",
|
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 +135,7 @@ func (w *Wireguard) copy() (copied Wireguard) {
|
|||||||
PrivateKey: gosettings.CopyPointer(w.PrivateKey),
|
PrivateKey: gosettings.CopyPointer(w.PrivateKey),
|
||||||
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey),
|
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey),
|
||||||
Addresses: gosettings.CopySlice(w.Addresses),
|
Addresses: gosettings.CopySlice(w.Addresses),
|
||||||
|
AllowedIPs: gosettings.CopySlice(w.AllowedIPs),
|
||||||
Interface: w.Interface,
|
Interface: w.Interface,
|
||||||
MTU: w.MTU,
|
MTU: w.MTU,
|
||||||
Implementation: w.Implementation,
|
Implementation: w.Implementation,
|
||||||
@@ -128,6 +146,7 @@ func (w *Wireguard) mergeWith(other Wireguard) {
|
|||||||
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey)
|
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey)
|
||||||
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey)
|
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey)
|
||||||
w.Addresses = gosettings.MergeWithSlice(w.Addresses, other.Addresses)
|
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.Interface = gosettings.MergeWithString(w.Interface, other.Interface)
|
||||||
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
|
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
|
||||||
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
|
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
|
||||||
@@ -137,6 +156,7 @@ func (w *Wireguard) overrideWith(other Wireguard) {
|
|||||||
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey)
|
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey)
|
||||||
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey)
|
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey)
|
||||||
w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses)
|
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.Interface = gosettings.OverrideWithString(w.Interface, other.Interface)
|
||||||
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU)
|
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU)
|
||||||
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
|
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
|
||||||
@@ -150,6 +170,11 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
|
|||||||
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
|
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
|
||||||
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
|
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")
|
w.Interface = gosettings.DefaultString(w.Interface, "wg0")
|
||||||
const defaultMTU = 1400
|
const defaultMTU = 1400
|
||||||
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU)
|
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU)
|
||||||
@@ -178,6 +203,11 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
|||||||
addressesNode.Appendf(address.String())
|
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 := node.Appendf("Network interface: %s", w.Interface)
|
||||||
interfaceNode.Appendf("MTU: %d", w.MTU)
|
interfaceNode.Appendf("MTU: %d", w.MTU)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type WireguardSelection struct {
|
|||||||
// It is only used with VPN providers generating Wireguard
|
// It is only used with VPN providers generating Wireguard
|
||||||
// configurations specific to each server and user.
|
// configurations specific to each server and user.
|
||||||
// To indicate it should not be used, it should be set
|
// 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.
|
// in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// EndpointPort is a the server port to use for the VPN server.
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ func (s *Source) readPortForward() (
|
|||||||
return portForwarding, err
|
return portForwarding, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER")
|
||||||
|
|
||||||
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
|
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
|
||||||
env.ForceLowercase(false),
|
env.ForceLowercase(false),
|
||||||
env.RetroKeys(
|
env.RetroKeys(
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return wireguard, err // already wrapped
|
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")
|
mtuPtr, err := s.env.Uint16Ptr("WIREGUARD_MTU")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wireguard, err
|
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"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Source struct{}
|
type Source struct {
|
||||||
|
wireguardConfigPath string
|
||||||
|
}
|
||||||
|
|
||||||
func New() *Source {
|
func New() *Source {
|
||||||
return &Source{}
|
const wireguardConfigPath = "/gluetun/wireguard/wg0.conf"
|
||||||
|
return &Source{
|
||||||
|
wireguardConfigPath: wireguardConfigPath,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) String() string { return "files" }
|
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) {
|
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()
|
vpn.OpenVPN, err = s.readOpenVPN()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vpn, fmt.Errorf("OpenVPN: %w", err)
|
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
|
return vpn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
114
internal/configuration/sources/files/wireguard.go
Normal file
114
internal/configuration/sources/files/wireguard.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.PreSharedKey, err = parseINIWireguardKey(interfaceSection, "PreSharedKey")
|
||||||
|
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
|
||||||
|
}
|
||||||
273
internal/configuration/sources/files/wireguard_test.go
Normal file
273
internal/configuration/sources/files/wireguard_test.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
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=
|
||||||
|
PreSharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||||
|
Address = 10.38.22.35/32
|
||||||
|
DNS = 193.138.218.74
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
`,
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
"pre shared key error": {
|
||||||
|
iniData: `[Interface]
|
||||||
|
PreSharedKey = x
|
||||||
|
`,
|
||||||
|
errMessage: "parsing PreSharedKey: 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=
|
||||||
|
PreSharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
|
||||||
|
Address = 10.38.22.35/32
|
||||||
|
`,
|
||||||
|
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()
|
||||||
|
|
||||||
|
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())
|
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||||
}
|
}
|
||||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
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 {
|
if err != nil {
|
||||||
l.logger.Error(err.Error())
|
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())
|
l.logger.Info("using plaintext DNS at address " + targetIP.String())
|
||||||
}
|
}
|
||||||
nameserver.UseDNSInternally(targetIP.AsSlice())
|
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 {
|
if err != nil {
|
||||||
l.logger.Error(err.Error())
|
l.logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,14 @@ import (
|
|||||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
|
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
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution?
|
l.useUnencryptedDNS(fallback)
|
||||||
// TODO this one is kept if DNS_KEEP_NAMESERVER=on and should be replaced
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-l.start:
|
case <-l.start:
|
||||||
@@ -27,7 +32,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
unboundCancel := func() { waitError <- nil }
|
unboundCancel := func() { waitError <- nil }
|
||||||
closeStreams := func() {}
|
closeStreams := func() {}
|
||||||
|
|
||||||
for *l.GetSettings().DoT.Enabled {
|
settings := l.GetSettings()
|
||||||
|
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
||||||
var err error
|
var err error
|
||||||
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -50,7 +56,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*l.GetSettings().DoT.Enabled {
|
settings = l.GetSettings()
|
||||||
|
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
||||||
const fallback = false
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package firewall
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
|
func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
|
||||||
@@ -147,7 +149,16 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
|
func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
|
||||||
for _, subnet := range c.outboundSubnets {
|
for _, subnet := range c.outboundSubnets {
|
||||||
|
subnetIsIPv6 := subnet.Addr().Is6()
|
||||||
|
firewallUpdated := false
|
||||||
for _, defaultRoute := range c.defaultRoutes {
|
for _, defaultRoute := range c.defaultRoutes {
|
||||||
|
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||||
|
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||||
|
if !ipFamilyMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
firewallUpdated = true
|
||||||
|
|
||||||
const remove = false
|
const remove = false
|
||||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||||
defaultRoute.AssignedIP, subnet, remove)
|
defaultRoute.AssignedIP, subnet, remove)
|
||||||
@@ -155,6 +166,11 @@ func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !firewallUpdated {
|
||||||
|
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
|
||||||
|
"no default route matching its family", subnet))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/netlink"
|
||||||
"github.com/qdm12/gluetun/internal/subnet"
|
"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) {
|
func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Prefix) {
|
||||||
const remove = true
|
const remove = true
|
||||||
for _, subNet := range subnets {
|
for _, subNet := range subnets {
|
||||||
|
subnetIsIPv6 := subNet.Addr().Is6()
|
||||||
|
firewallUpdated := false
|
||||||
for _, defaultRoute := range c.defaultRoutes {
|
for _, defaultRoute := range c.defaultRoutes {
|
||||||
|
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||||
|
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||||
|
if !ipFamilyMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
firewallUpdated = true
|
||||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||||
defaultRoute.AssignedIP, subNet, remove)
|
defaultRoute.AssignedIP, subNet, remove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,6 +55,12 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
|
|||||||
continue
|
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)
|
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 {
|
func (c *Config) addOutboundSubnets(ctx context.Context, subnets []netip.Prefix) error {
|
||||||
const remove = false
|
const remove = false
|
||||||
for _, subnet := range subnets {
|
for _, subnet := range subnets {
|
||||||
|
subnetIsIPv6 := subnet.Addr().Is6()
|
||||||
|
firewallUpdated := false
|
||||||
for _, defaultRoute := range c.defaultRoutes {
|
for _, defaultRoute := range c.defaultRoutes {
|
||||||
|
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
|
||||||
|
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
|
||||||
|
if !ipFamilyMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
firewallUpdated = true
|
||||||
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
|
||||||
defaultRoute.AssignedIP, subnet, remove)
|
defaultRoute.AssignedIP, subnet, remove)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
c.outboundSubnets = append(c.outboundSubnets, subnet)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type vpnHealth struct {
|
|||||||
func (s *Server) onUnhealthyVPN(ctx context.Context) {
|
func (s *Server) onUnhealthyVPN(ctx context.Context) {
|
||||||
s.logger.Info("program has been unhealthy for " +
|
s.logger.Info("program has been unhealthy for " +
|
||||||
s.vpn.healthyWait.String() + ": restarting VPN " +
|
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.Stopped)
|
||||||
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
|
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
|
||||||
s.vpn.healthyWait += *s.config.VPN.Addition
|
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
|
||||||
|
}
|
||||||
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,42 @@ package netlink
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/mod"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
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()
|
families, err := netlink.GenlFamilyList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("listing gen 1 families: %w", err)
|
return false, fmt.Errorf("listing gen 1 families: %w", err)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Your credentials might be wrong 🤨
|
|||||||
That error usually happens because either:
|
That error usually happens because either:
|
||||||
|
|
||||||
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
|
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
|
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:
|
That error usually happens because either:
|
||||||
|
|
||||||
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
|
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
|
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,27 @@ package portforward
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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 {
|
type PortAllower interface {
|
||||||
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
|
||||||
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
RemoveAllowedPort(ctx context.Context, port 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,161 @@
|
|||||||
package portforward
|
package portforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||||
"github.com/qdm12/gluetun/internal/loopstate"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/portforward/state"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Loop struct {
|
type Loop struct {
|
||||||
statusManager *loopstate.State
|
// State
|
||||||
state *state.State
|
settings Settings
|
||||||
// Fixed parameters
|
settingsMutex sync.RWMutex
|
||||||
puid int
|
service Service
|
||||||
pgid int
|
// Fixed injected objets
|
||||||
// Objects
|
routing Routing
|
||||||
client *http.Client
|
client *http.Client
|
||||||
portAllower PortAllower
|
portAllower PortAllower
|
||||||
logger Logger
|
logger Logger
|
||||||
|
// Fixed parameters
|
||||||
|
uid, gid int
|
||||||
// Internal channels and locks
|
// Internal channels and locks
|
||||||
start chan struct{}
|
// runCtx is used to detect when the loop has exited
|
||||||
running chan models.LoopStatus
|
// when performing an update
|
||||||
stop chan struct{}
|
runCtx context.Context //nolint:containedctx
|
||||||
stopped chan struct{}
|
runCancel context.CancelFunc
|
||||||
startMu sync.Mutex
|
runDone <-chan struct{}
|
||||||
backoffTime time.Duration
|
updateTrigger chan<- Settings
|
||||||
userTrigger bool
|
updatedResult <-chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBackoffTime = 5 * time.Second
|
func NewLoop(settings settings.PortForwarding, routing Routing,
|
||||||
|
|
||||||
func NewLoop(settings settings.PortForwarding,
|
|
||||||
client *http.Client, portAllower PortAllower,
|
client *http.Client, portAllower PortAllower,
|
||||||
logger Logger, puid, pgid int) *Loop {
|
logger Logger, uid, gid 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)
|
|
||||||
|
|
||||||
return &Loop{
|
return &Loop{
|
||||||
statusManager: statusManager,
|
settings: Settings{
|
||||||
state: state,
|
VPNIsUp: ptrTo(false),
|
||||||
puid: puid,
|
Service: service.Settings{
|
||||||
pgid: pgid,
|
Enabled: settings.Enabled,
|
||||||
// Objects
|
Filepath: *settings.Filepath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: routing,
|
||||||
client: client,
|
client: client,
|
||||||
portAllower: portAllower,
|
portAllower: portAllower,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
start: start,
|
uid: uid,
|
||||||
running: running,
|
gid: gid,
|
||||||
stop: stop,
|
|
||||||
stopped: stopped,
|
|
||||||
userTrigger: true,
|
|
||||||
backoffTime: defaultBackoffTime,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
31
internal/portforward/service/interfaces.go
Normal file
31
internal/portforward/service/interfaces.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
65
internal/portforward/service/settings.go
Normal file
65
internal/portforward/service/settings.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
74
internal/portforward/service/start.go
Normal file
74
internal/portforward/service/start.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
48
internal/portforward/service/stop.go
Normal file
48
internal/portforward/service/stop.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
package portforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||||
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *Loop) GetSettings() (settings settings.PortForwarding) {
|
type Settings struct {
|
||||||
return l.state.GetSettings()
|
// 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) (
|
// updateWith deep copies the receiving settings, overrides the copy with
|
||||||
outcome string) {
|
// fields set in the partialUpdate argument, validates the new settings
|
||||||
return l.state.SetSettings(ctx, 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)
|
|
||||||
}
|
|
||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/airvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/airvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
|
|
||||||
Fetcher: updater.New(client),
|
Fetcher: updater.New(client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import (
|
|||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
extractor Extractor
|
extractor Extractor
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(extractor Extractor) *Provider {
|
func New(extractor Extractor) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
extractor: extractor,
|
extractor: extractor,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Custom),
|
|
||||||
Fetcher: utils.NewNoFetcher(providers.Custom),
|
Fetcher: utils.NewNoFetcher(providers.Custom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/cyberghost/updater"
|
"github.com/qdm12/gluetun/internal/provider/cyberghost/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Cyberghost),
|
|
||||||
Fetcher: updater.New(parallelResolver),
|
Fetcher: updater.New(parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/example/updater"
|
"github.com/qdm12/gluetun/internal/provider/example/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +22,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
|
|
||||||
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
|
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/expressvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/expressvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Expressvpn),
|
|
||||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/fastestvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/fastestvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Fastestvpn),
|
|
||||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/hidemyass/updater"
|
"github.com/qdm12/gluetun/internal/provider/hidemyass/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.HideMyAss),
|
|
||||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/ipvanish/updater"
|
"github.com/qdm12/gluetun/internal/provider/ipvanish/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Ipvanish),
|
|
||||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/ivpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/ivpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Ivpn),
|
|
||||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/mullvad/updater"
|
"github.com/qdm12/gluetun/internal/provider/mullvad/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Mullvad),
|
|
||||||
Fetcher: updater.New(client),
|
Fetcher: updater.New(client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/nordvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/nordvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Nordvpn),
|
|
||||||
Fetcher: updater.New(client, updaterWarner),
|
Fetcher: updater.New(client, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/perfectprivacy/updater"
|
"github.com/qdm12/gluetun/internal/provider/perfectprivacy/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Perfectprivacy),
|
|
||||||
Fetcher: updater.New(unzipper, updaterWarner),
|
Fetcher: updater.New(unzipper, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privado/updater"
|
"github.com/qdm12/gluetun/internal/provider/privado/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Privado),
|
|
||||||
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,29 +23,32 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrServerNameNotFound = errors.New("server name not found in servers")
|
ErrServerNameNotFound = errors.New("server name not found in servers")
|
||||||
ErrGatewayIPIsNotValid = errors.New("gateway IP address is not valid")
|
|
||||||
ErrServerNameEmpty = errors.New("server name is empty")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PortForward obtains a VPN server side port forwarded from PIA.
|
// PortForward obtains a VPN server side port forwarded from PIA.
|
||||||
func (p *Provider) PortForward(ctx context.Context, client *http.Client,
|
func (p *Provider) PortForward(ctx context.Context,
|
||||||
logger utils.Logger, gateway netip.Addr, serverName string) (
|
objects utils.PortForwardObjects) (port uint16, err error) {
|
||||||
port uint16, err error) {
|
switch {
|
||||||
|
case objects.ServerName == "":
|
||||||
|
panic("server name cannot be empty")
|
||||||
|
case !objects.Gateway.IsValid():
|
||||||
|
panic("gateway is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName := objects.ServerName
|
||||||
|
|
||||||
server, ok := p.storage.GetServerByName(providers.PrivateInternetAccess, serverName)
|
server, ok := p.storage.GetServerByName(providers.PrivateInternetAccess, serverName)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, fmt.Errorf("%w: %s", ErrServerNameNotFound, serverName)
|
return 0, fmt.Errorf("%w: %s", ErrServerNameNotFound, serverName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger := objects.Logger
|
||||||
|
|
||||||
if !server.PortForward {
|
if !server.PortForward {
|
||||||
logger.Error("The server " + serverName +
|
logger.Error("The server " + serverName +
|
||||||
" (region " + server.Region + ") does not support port forwarding")
|
" (region " + server.Region + ") does not support port forwarding")
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
if !gateway.IsValid() {
|
|
||||||
return 0, fmt.Errorf("%w: %s", ErrGatewayIPIsNotValid, gateway)
|
|
||||||
} else if serverName == "" {
|
|
||||||
return 0, ErrServerNameEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
privateIPClient, err := newHTTPClient(serverName)
|
privateIPClient, err := newHTTPClient(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -70,7 +73,8 @@ func (p *Provider) PortForward(ctx context.Context, client *http.Client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !dataFound || expired {
|
if !dataFound || expired {
|
||||||
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, gateway,
|
client := objects.Client
|
||||||
|
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, objects.Gateway,
|
||||||
p.portForwardPath, p.authFilePath)
|
p.portForwardPath, p.authFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("refreshing port forward data: %w", err)
|
return 0, fmt.Errorf("refreshing port forward data: %w", err)
|
||||||
@@ -80,7 +84,7 @@ func (p *Provider) PortForward(ctx context.Context, client *http.Client,
|
|||||||
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
|
logger.Info("Port forwarded data expires in " + format.FriendlyDuration(durationToExpiration))
|
||||||
|
|
||||||
// First time binding
|
// First time binding
|
||||||
if err := bindPort(ctx, privateIPClient, gateway, data); err != nil {
|
if err := bindPort(ctx, privateIPClient, objects.Gateway, data); err != nil {
|
||||||
return 0, fmt.Errorf("binding port: %w", err)
|
return 0, fmt.Errorf("binding port: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +96,15 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||||
gateway netip.Addr, serverName string) (err error) {
|
objects utils.PortForwardObjects) (err error) {
|
||||||
privateIPClient, err := newHTTPClient(serverName)
|
switch {
|
||||||
|
case objects.ServerName == "":
|
||||||
|
panic("server name cannot be empty")
|
||||||
|
case !objects.Gateway.IsValid():
|
||||||
|
panic("gateway is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
privateIPClient, err := newHTTPClient(objects.ServerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating custom HTTP client: %w", err)
|
return fmt.Errorf("creating custom HTTP client: %w", err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +131,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-keepAliveTimer.C:
|
case <-keepAliveTimer.C:
|
||||||
err := bindPort(ctx, privateIPClient, gateway, data)
|
err = bindPort(ctx, privateIPClient, objects.Gateway, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("binding port: %w", err)
|
return fmt.Errorf("binding port: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privatevpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/privatevpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Privatevpn),
|
|
||||||
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|||||||
AuthUserPass: true,
|
AuthUserPass: true,
|
||||||
Ciphers: []string{
|
Ciphers: []string{
|
||||||
openvpn.AES256cbc,
|
openvpn.AES256cbc,
|
||||||
|
openvpn.AES256gcm,
|
||||||
},
|
},
|
||||||
Auth: openvpn.SHA512,
|
Auth: openvpn.SHA512,
|
||||||
MssFix: 1450,
|
MssFix: 1450,
|
||||||
|
|||||||
117
internal/provider/protonvpn/portforward.go
Normal file
117
internal/provider/protonvpn/portforward.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package protonvpn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/gluetun/internal/natpmp"
|
||||||
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
||||||
|
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||||
|
port uint16, err error) {
|
||||||
|
client := natpmp.New()
|
||||||
|
_, externalIPv4Address, err := client.ExternalAddress(ctx,
|
||||||
|
objects.Gateway)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "connection refused") {
|
||||||
|
err = fmt.Errorf("%w - make sure you have +pmp at the end of your OpenVPN username", err)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("getting external IPv4 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := objects.Logger
|
||||||
|
|
||||||
|
logger.Info("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||||
|
const internalPort, externalPort = 0, 1
|
||||||
|
const lifetime = 60 * time.Second
|
||||||
|
|
||||||
|
_, _, assignedUDPExternalPort, assignedLifetime, err :=
|
||||||
|
client.AddPortMapping(ctx, objects.Gateway, "udp",
|
||||||
|
internalPort, externalPort, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("adding UDP port mapping: %w", err)
|
||||||
|
}
|
||||||
|
checkLifetime(logger, "UDP", lifetime, assignedLifetime)
|
||||||
|
|
||||||
|
_, _, assignedTCPExternalPort, assignedLifetime, err :=
|
||||||
|
client.AddPortMapping(ctx, objects.Gateway, "tcp",
|
||||||
|
internalPort, externalPort, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("adding TCP port mapping: %w", err)
|
||||||
|
}
|
||||||
|
checkLifetime(logger, "TCP", lifetime, assignedLifetime)
|
||||||
|
|
||||||
|
checkExternalPorts(logger, assignedUDPExternalPort, assignedTCPExternalPort)
|
||||||
|
port = assignedTCPExternalPort
|
||||||
|
|
||||||
|
p.portForwarded = port
|
||||||
|
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLifetime(logger utils.Logger, protocol string,
|
||||||
|
requested, actual time.Duration) {
|
||||||
|
if requested != actual {
|
||||||
|
logger.Warn(fmt.Sprintf("assigned %s port lifetime %s differs"+
|
||||||
|
" from requested lifetime %s", strings.ToUpper(protocol),
|
||||||
|
actual, requested))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkExternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
||||||
|
if udpPort != tcpPort {
|
||||||
|
logger.Warn(fmt.Sprintf("UDP external port %d differs from TCP external port %d",
|
||||||
|
udpPort, tcpPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrExternalPortChanged = errors.New("external port changed")
|
||||||
|
|
||||||
|
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||||
|
objects utils.PortForwardObjects) (err error) {
|
||||||
|
client := natpmp.New()
|
||||||
|
const refreshTimeout = 45 * time.Second
|
||||||
|
timer := time.NewTimer(refreshTimeout)
|
||||||
|
logger := objects.Logger
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
objects.Logger.Debug("refreshing port forward since 45 seconds have elapsed")
|
||||||
|
networkProtocols := []string{"udp", "tcp"}
|
||||||
|
const internalPort = 0
|
||||||
|
const lifetime = 60 * time.Second
|
||||||
|
|
||||||
|
for _, networkProtocol := range networkProtocols {
|
||||||
|
_, _, assignedExternalPort, assignedLiftetime, err :=
|
||||||
|
client.AddPortMapping(ctx, objects.Gateway, networkProtocol,
|
||||||
|
internalPort, p.portForwarded, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding port mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignedLiftetime != lifetime {
|
||||||
|
logger.Warn(fmt.Sprintf("assigned lifetime %s differs"+
|
||||||
|
" from requested lifetime %s",
|
||||||
|
assignedLiftetime, lifetime))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.portForwarded != assignedExternalPort {
|
||||||
|
return fmt.Errorf("%w: %d changed to %d",
|
||||||
|
ErrExternalPortChanged, p.portForwarded, assignedExternalPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", p.portForwarded))
|
||||||
|
|
||||||
|
timer.Reset(refreshTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,13 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/protonvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/protonvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
|
portForwarded uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
@@ -22,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Protonvpn),
|
|
||||||
Fetcher: updater.New(client, updaterWarner),
|
Fetcher: updater.New(client, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider contains methods to read and modify the openvpn configuration to connect as a client.
|
// Provider contains methods to read and modify the openvpn configuration to connect as a client.
|
||||||
@@ -15,15 +12,6 @@ type Provider interface {
|
|||||||
GetConnection(selection settings.ServerSelection, ipv6Supported bool) (connection models.Connection, err error)
|
GetConnection(selection settings.ServerSelection, ipv6Supported bool) (connection models.Connection, err error)
|
||||||
OpenVPNConfig(connection models.Connection, settings settings.OpenVPN, ipv6Supported bool) (lines []string)
|
OpenVPNConfig(connection models.Connection, settings settings.OpenVPN, ipv6Supported bool) (lines []string)
|
||||||
Name() string
|
Name() string
|
||||||
PortForwarder
|
|
||||||
FetchServers(ctx context.Context, minServers int) (
|
FetchServers(ctx context.Context, minServers int) (
|
||||||
servers []models.Server, err error)
|
servers []models.Server, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortForwarder interface {
|
|
||||||
PortForward(ctx context.Context, client *http.Client,
|
|
||||||
logger utils.Logger, gateway netip.Addr, serverName string) (
|
|
||||||
port uint16, err error)
|
|
||||||
KeepPortForward(ctx context.Context, gateway netip.Addr,
|
|
||||||
serverName string) (err error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/purevpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/purevpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.Purevpn),
|
|
||||||
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
|
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
"github.com/qdm12/gluetun/internal/provider/common"
|
||||||
"github.com/qdm12/gluetun/internal/provider/slickvpn/updater"
|
"github.com/qdm12/gluetun/internal/provider/slickvpn/updater"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
storage common.Storage
|
storage common.Storage
|
||||||
randSource rand.Source
|
randSource rand.Source
|
||||||
utils.NoPortForwarder
|
|
||||||
common.Fetcher
|
common.Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
|
|||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
NoPortForwarder: utils.NewNoPortForwarding(providers.SlickVPN),
|
|
||||||
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
Fetcher: updater.New(client, updaterWarner, parallelResolver),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user