Compare commits

...

62 Commits

Author SHA1 Message Date
Quentin McGaw
1c43a1d55b fix(portforward): service start error not treated as critical
A service start error can happen if the service is started after the Wireguard VPN tunnel is up, but the tunnel does not work. The VPN is then internally restarted, causing the service start error, so it should not be treated as a critical error.
2023-10-07 13:21:32 +00:00
Quentin McGaw
6c639fcf7f fix(publicip): do not retry on too many requests 2023-10-07 12:59:43 +00:00
Quentin McGaw
ec1f252528 fix(portforward): different validation when vpn is up or not 2023-10-07 12:43:36 +00:00
Quentin McGaw
ee413f59a2 fix(protonvpn): set natpmp external port to 1 2023-10-06 16:09:05 +00:00
Quentin McGaw
d4df87286e fix(portforward): trigger after VPN restart 2023-09-28 14:00:58 +00:00
Quentin McGaw
a194906bdd chore(protonvpn): add debug logs for keeping port forwarded 2023-09-28 07:08:07 +00:00
Quentin McGaw
9b00763a69 feat(config): add /32 if not present for Wireguard addresses 2023-09-24 16:50:34 +00:00
Quentin McGaw
4d627bb7b1 feat(protonvpn): port forwarding connection refused error points to add +pmp to OpenVPN user 2023-09-24 15:15:05 +00:00
Quentin McGaw
dc8fc5f81f feat(updater): log warning about using -minratio 2023-09-24 15:05:39 +00:00
Quentin McGaw
b787e12e25 feat(surfshark): update servers data 2023-09-24 15:02:08 +00:00
Quentin McGaw
f96448947f fix(publicip): rework run loop and fix restarts
- Clearing IP data on VPN disconnection clears file
- More efficient partial updates
- Fix loop exit
- Validate settings before updating
2023-09-24 14:55:51 +00:00
Quentin McGaw
e64e5af4c3 chore(portforward): improve loop reliability
- handle settings update within run function
- signal back start result to update call
- update loop settings only when service is started
2023-09-24 10:28:10 +00:00
Quentin McGaw
aa6dc786a4 chore(provider): use type assertion for port forwarders 2023-09-23 13:02:09 +00:00
Quentin McGaw
84300db7c1 fix(portforward): restart service on run error
- fix when port assigned changes
2023-09-23 12:39:49 +00:00
Quentin McGaw
2ac0f35060 fix(protonvpn): crash service if port assigned changes 2023-09-23 12:36:13 +00:00
Quentin McGaw
1a865f56d5 chore(vpn): fix typo portForwader 2023-09-23 12:03:56 +00:00
Quentin McGaw
0406de399d chore(portforward): move vpn gateway obtention within port forwarding service 2023-09-23 12:03:06 +00:00
Quentin McGaw
71201411f4 fix(portforward): rework run loop and fix deadlocks (#1874) 2023-09-23 12:57:12 +02:00
Quentin McGaw
c435bbb32c docs(issue): provide minimum requirements for an issue
- title must be filled
- at least 10 lines of log provided
- Gluetun version must be provided
2023-09-22 09:22:13 +00:00
Quentin McGaw
4cbfea41f2 docs(issues): add Unraid as option in bug template 2023-09-22 09:16:44 +00:00
Quentin McGaw
f9c9ad34f7 feat(protonvpn): check udp vs tcp port forwarded 2023-09-22 08:50:19 +00:00
Quentin McGaw
4ea474b896 fix(routing): change firewall only for matching ip families 2023-09-20 10:45:13 +00:00
Quentin McGaw
6aa4a93665 change(format): use dashes instead of spaces for provider names
- `-private\ internet\ access` -> `private-internet-access`
- `-perfect\ privacy` -> `-perfect-privacy`
- `-vpn\ unlimited` -> `-vpn-unlimited`
2023-09-20 10:24:32 +00:00
Quentin McGaw
ea25a0ff89 fix(protonvpn): natpmp assigned ports logs removed 2023-09-20 09:51:13 +00:00
Quentin McGaw
659da67ed5 feat(cyberghost): update servers data 2023-09-20 09:35:28 +00:00
Quentin McGaw
ffc6d2e593 chore(lint): upgrade linter to v1.54.1 2023-09-20 09:34:32 +00:00
Quentin McGaw
03ce08e23d chore(build): upgrade Go to 1.21 2023-09-20 09:34:29 +00:00
Aleksa Siriški
3449e7a0e1 fix(publicip): IPv6 endpoint for ipinfo (#1853) 2023-09-13 16:37:39 +02:00
Quentin McGaw
c0062fb807 fix(protonvpn): natpmp check for assigned internal port 2023-09-13 14:18:35 +00:00
dependabot[bot]
1ac031e78c Chore(deps): Bump golang.org/x/sys from 0.10.0 to 0.11.0 (#1786) 2023-08-24 02:04:07 -07:00
Quentin McGaw
e556871e8b change(dns): DNS_KEEP_NAMESERVER leaves DNS fully untouched 2023-08-11 11:03:40 +00:00
Quentin McGaw
082a38b769 fix(netlink): try loading Wireguard module if not found (#1741) 2023-08-04 13:09:56 +02:00
Quentin McGaw
39ae57f49d fix(routing): add outbound subnets routes only for matching ip families 2023-07-28 07:24:26 +00:00
Quentin McGaw
9024912e17 fix(custom): allow custom endpoint port setting 2023-07-27 10:32:08 +00:00
Quentin McGaw
eecfb3952f chore(settings): change source precedence order
1. Secret files (program scope)
2. Files (program scope)
3. Environment variables (OS scope)
Fix #1759
2023-07-22 16:02:32 +00:00
Quentin McGaw
0ebfe534d3 feat(settings): parse Wireguard settings from /gluetun/wireguard/wg0.conf (#1120) 2023-07-22 17:25:30 +02:00
eiqnepm
c5cc240a6c feat(surfshark): update API endpoint and servers data (#1560) 2023-07-21 20:21:46 +02:00
Quentin McGaw
1a5a0148ea feat(torguard): update severs data 2023-07-18 16:02:06 +00:00
Quentin McGaw
abe2aceb18 feat(wireguard): clarify wireguard is up message 2023-07-18 15:53:39 +00:00
Quentin McGaw
fa541b8fc2 chore(deps): bump gosettings to v0.4.0-rc1 2023-07-11 13:26:55 +00:00
dependabot[bot]
a681d38dfb Chore(deps): Bump golang.org/x/net from 0.10.0 to 0.12.0 (#1729) 2023-07-09 14:22:14 +02:00
dependabot[bot]
a7b96e3f4d Chore(deps): Bump golang.org/x/sys from 0.8.0 to 0.10.0 (#1732) 2023-07-07 15:32:23 +02:00
dependabot[bot]
04ef92edab Chore(deps): Bump golang.org/x/text from 0.10.0 to 0.11.0 (#1726) 2023-07-07 12:56:47 +02:00
Quentin McGaw
919b55c3aa feat(wireguard): WIREGUARD_ALLOWED_IPS variable (#1291) 2023-07-06 09:08:59 +02:00
Quentin McGaw
9c0f187a12 chore(natpmp): more robust tests with longer connection durations 2023-07-06 06:54:01 +00:00
Quentin McGaw
075a1e2a80 chore(natpmp): initialRetry -> initialConnectionDuration 2023-07-06 06:50:17 +00:00
Quentin McGaw
f31a846cda chore(ci): add markdown-skip workflow 2023-07-05 15:45:46 +00:00
Quentin McGaw
9bef46db77 chore(ci): trigger markdown on pull requests
- Verification steps
- Publishing step to Docker Hub is reserved for pushes to the master branch
2023-07-05 15:44:33 +00:00
Quentin McGaw
d83217f7ac chore(ci): add markdown dead link checking 2023-07-05 14:47:52 +00:00
Quentin McGaw
1cd2fec796 chore(ci): add markdown linting to markdown workflow 2023-07-05 14:31:09 +00:00
Quentin McGaw
235f24ee5b chore(ci): add misspell action to markdown job 2023-07-05 14:28:56 +00:00
Quentin McGaw
2e34c6009e chore(ci): Markdown workflow triggers on *.md files 2023-07-05 14:28:50 +00:00
Quentin McGaw
c0eb2f2315 chore(ci): rename workflow to Markdown 2023-07-05 14:27:14 +00:00
Quentin McGaw
8ad16cdc12 feat(protonvpn): port forwarding support with NAT-PMP (#1543)
Co-authored-by: Nicholas Xavier <nicho@nicho.dev>
2023-06-30 20:09:44 +02:00
Quentin McGaw
fae6544431 feat(pf): VPN_PORT_FORWARDING_PROVIDER variable (#1616) 2023-06-30 19:24:01 +02:00
Quentin McGaw
f8a41b2133 fix(protonvpn): add aes-256-gcm cipher for openvpn 2023-06-30 17:14:44 +00:00
Quentin McGaw
ff9b56d6d8 docs(all): update to use newer wiki repository
- Update URLs logged by program
- Update README.md links
- Update contributing guide link
- Update issue templates links
- Replace Wiki issue template by link to Gluetun Wiki repository issue creation
- Set program announcement about Github wiki new location
2023-06-30 10:31:26 +00:00
Quentin McGaw
99d5a591b9 docs(readme): fixes and small changes
- remove `UPDATER_VPN_SERVICE_PROVIDERS` in docker-compose config
- remove Slack channel link (don't have time to check it)
- Update Wireguard native integrations support list
2023-06-29 16:28:24 +00:00
Quentin McGaw
fbe252a9b6 chore(Docker): add missing environment variables
- `OPENVPN_PROCESS_USER` defaults to `root`
- Add `HTTPPROXY_STEALTH=off`
- Add `HTTP_CONTROL_SERVER_LOG=on`
2023-06-29 16:20:25 +00:00
Quentin McGaw
76a92b90e3 fix(routing): VPNLocalGatewayIP Wireguard support 2023-06-28 14:23:34 +00:00
Quentin McGaw
2873b06275 fix(wireguard): wrap setupIPv6 rule error correctly 2023-06-28 13:08:23 +00:00
Quentin McGaw
9cdd6294d2 feat(mullvad): update servers data 2023-06-28 13:06:40 +00:00
145 changed files with 9550 additions and 6630 deletions

View File

@@ -47,7 +47,7 @@ You can customize **settings** and **extensions** in the [devcontainer.json](dev
### Entrypoint script
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/godevcontainer/blob/master/shell/.welcome.sh).
### Publish a port

View File

@@ -13,6 +13,6 @@ Contributions are [released](https://help.github.com/articles/github-terms-of-se
## Resources
- [Gluetun guide on development](https://github.com/qdm12/gluetun/wiki/Development)
- [Gluetun guide on development](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)

View File

@@ -7,13 +7,18 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
⚠️ Your issue will be instantly closed as not planned WITHOUT explanation if:
- you do not fill out **the title of the issue** ☝️
- you do not provide the **Gluetun version** as requested below
- you provide **less than 10 lines of logs** as requested below
- type: dropdown
id: urgent
attributes:
label: Is this urgent?
description: |
Is this a critical bug, or do you need this fixed urgently?
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun/wiki/Docker-image-tags) if that can help.
If this is urgent, note you can use one of the [image tags available](https://github.com/qdm12/gluetun-wiki/blob/main/setup/docker-image-tags.md) if that can help.
options:
- "No"
- "Yes"
@@ -75,6 +80,7 @@ body:
- Portainer
- Kubernetes
- Podman
- Unraid
- Other
validations:
required: true
@@ -84,7 +90,7 @@ body:
label: What is the version of Gluetun
description: |
Copy paste the version line at the top of your logs.
It should be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`.
validations:
required: true
- type: textarea
@@ -97,7 +103,7 @@ body:
- type: textarea
id: logs
attributes:
label: Share your logs
label: Share your logs (at least 10 lines)
description: No sensitive information is logged out except when running with `LOG_LEVEL=debug`.
render: plain text
validations:

View File

@@ -1,4 +1,7 @@
contact_links:
- name: Report a Wiki issue
url: https://github.com/qdm12/gluetun-wiki/issues/new
about: Please create an issue on the gluetun-wiki repository.
- name: Configuration help?
url: https://github.com/qdm12/gluetun/discussions/new
about: Please create a Github discussion.

View File

@@ -14,4 +14,4 @@ One of the following is required:
If the list of servers requires to login **or** is hidden behind an interactive configurator,
you can only use a custom Openvpn configuration file.
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to do so.
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.

View File

@@ -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

View File

@@ -45,6 +45,7 @@ jobs:
level: error
exclude: |
./internal/storage/servers.json
*.md
- name: Linting
run: docker build --target lint .

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,3 @@
{
"MD013": false
}

View File

@@ -1,8 +1,8 @@
ARG ALPINE_VERSION=3.18
ARG GO_ALPINE_VERSION=3.18
ARG GO_VERSION=1.20
ARG GO_VERSION=1.21
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.53.2
ARG GOLANGCI_LINT_VERSION=v1.54.1
ARG MOCKGEN_VERSION=v1.6.0
ARG BUILDPLATFORM=linux/amd64
@@ -90,12 +90,13 @@ ENV VPN_SERVICE_PROVIDER=pia \
OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \
OPENVPN_AUTH= \
OPENVPN_PROCESS_USER= \
OPENVPN_PROCESS_USER=root \
OPENVPN_CUSTOM_CONFIG= \
# Wireguard
WIREGUARD_PRIVATE_KEY= \
WIREGUARD_PRESHARED_KEY= \
WIREGUARD_PUBLIC_KEY= \
WIREGUARD_ALLOWED_IPS= \
WIREGUARD_ADDRESSES= \
WIREGUARD_MTU=1400 \
WIREGUARD_IMPLEMENTATION=auto \
@@ -110,6 +111,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
# # Private Internet Access only:
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET= \
VPN_PORT_FORWARDING=off \
VPN_PORT_FORWARDING_PROVIDER= \
VPN_PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \
# # Cyberghost only:
OPENVPN_CERT= \
@@ -165,6 +167,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
HTTPPROXY= \
HTTPPROXY_LOG=off \
HTTPPROXY_LISTENING_ADDRESS=":8888" \
HTTPPROXY_STEALTH=off \
HTTPPROXY_USER= \
HTTPPROXY_PASSWORD= \
HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \
@@ -177,6 +180,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password \
SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305 \
# Control server
HTTP_CONTROL_SERVER_LOG=on \
HTTP_CONTROL_SERVER_ADDRESS=":8000" \
# Server data updater
UPDATER_PERIOD=0 \

View File

@@ -38,17 +38,16 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- [Setup](#setup)
- [Features](#features)
- Problem?
- [Check the Wiki](https://github.com/qdm12/gluetun/wiki)
- Check the Wiki [common errors](https://github.com/qdm12/gluetun-wiki/tree/main/errors) and [faq](https://github.com/qdm12/gluetun-wiki/tree/main/faq)
- [Start a discussion](https://github.com/qdm12/gluetun/discussions)
- [Fix the Unraid template](https://github.com/qdm12/gluetun/discussions/550)
- Suggestion?
- [Create an issue](https://github.com/qdm12/gluetun/issues)
- [Join the Slack channel](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
- Happy?
- Sponsor me on [github.com/sponsors/qdm12](https://github.com/sponsors/qdm12)
- Donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
- Drop me [an email](mailto:quentin.mcgaw@gmail.com)
- **Want to add a VPN provider?** check [Development](https://github.com/qdm12/gluetun/wiki/Development) and [Add a provider](https://github.com/qdm12/gluetun/wiki/Add-a-provider)
- **Want to add a VPN provider?** check [the development page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/development.md) and [add a provider page](https://github.com/qdm12/gluetun-wiki/blob/main/contributing/add-a-provider.md)
- Video:
[![Video Gif](https://i.imgur.com/CetWunc.gif)](https://youtu.be/0F6I03LQcI4)
@@ -61,9 +60,9 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace
- For **Mullvad**, **Ivpn**, **Surfshark** and **Windscribe**
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Surfshark** and **Windscribe**
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
@@ -71,10 +70,10 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
- Built in Shadowsocks proxy (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
- [Connect other containers to it](https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun/wiki/Connect-a-LAN-device-to-gluetun)
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun/wiki/Private-internet-access#vpn-server-port-forwarding)
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Unbound subprogram drops root privileges once launched
- Can work as a Kubernetes sidecar container, thanks @rorph
@@ -83,9 +82,9 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
🎉 There are now instructions specific to each VPN provider with examples to help you get started as quickly as possible!
Go to the [Wiki](https://github.com/qdm12/gluetun/wiki)!
Go to the [Wiki](https://github.com/qdm12/gluetun-wiki)!
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun/issues/new?assignees=&labels=%F0%9F%93%84+Wiki+issue&template=wiki+issue.yml&title=Wiki+issue%3A+)
[🐛 Found a bug in the Wiki?!](https://github.com/qdm12/gluetun-wiki/issues/new)
Here's a docker-compose.yml for the laziest:
@@ -95,7 +94,8 @@ services:
gluetun:
image: qmcgaw/gluetun
# container_name: gluetun
# line above must be uncommented to allow external containers to connect. See https://github.com/qdm12/gluetun/wiki/Connect-a-container-to-gluetun#external-container-to-gluetun
# line above must be uncommented to allow external containers to connect.
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md#external-container-to-gluetun
cap_add:
- NET_ADMIN
devices:
@@ -107,7 +107,7 @@ services:
volumes:
- /yourpath:/gluetun
environment:
# See https://github.com/qdm12/gluetun/wiki
# See https://github.com/qdm12/gluetun-wiki/tree/main/setup#setup
- VPN_SERVICE_PROVIDER=ivpn
- VPN_TYPE=openvpn
# OpenVPN:
@@ -118,13 +118,13 @@ services:
# - WIREGUARD_ADDRESSES=10.64.222.21/32
# Timezone for accurate log times
- TZ=
# Server list updater. See https://github.com/qdm12/gluetun/wiki/Updating-Servers#periodic-update
# Server list updater
# See https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
- UPDATER_PERIOD=
- UPDATER_VPN_SERVICE_PROVIDERS=
```
🆕 Image also available as `ghcr.io/qdm12/gluetun`
## License
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/master/LICENSE)
[![MIT](https://img.shields.io/github/license/qdm12/gluetun)](https://github.com/qdm12/gluetun/blob/master/LICENSE)

View File

@@ -82,10 +82,10 @@ func main() {
cli := cli.New()
cmder := command.NewCmder()
envReader := env.New(logger)
filesReader := files.New()
secretsReader := secrets.New()
muxReader := mux.New(envReader, filesReader, secretsReader)
filesReader := files.New()
envReader := env.New(logger)
muxReader := mux.New(secretsReader, filesReader, envReader)
errorCh := make(chan error)
go func() {
@@ -159,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
}
}
announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2023-07-01T00:00:00Z")
if err != nil {
return err
}
@@ -170,7 +170,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Created,
Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!",
Announcement: "Wiki moved to https://github.com/qdm12/gluetun-wiki",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
@@ -376,12 +376,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
portForwardLogger := logger.New(log.SetComponent("port forwarding"))
portForwardLooper := portforward.NewLoop(allSettings.VPN.Provider.PortForwarding,
httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardHandler, portForwardCtx, portForwardDone := goshutdown.NewGoRoutineHandler(
"port forwarding", goroutine.OptionTimeout(time.Second))
go portForwardLooper.Run(portForwardCtx, portForwardDone)
routingConf, httpClient, firewallConf, portForwardLogger, puid, pgid)
portForwardRunError, err := portForwardLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting port forwarding loop: %w", err)
}
unboundLogger := logger.New(log.SetComponent("dns over tls"))
unboundLogger := logger.New(log.SetComponent("dns"))
unboundLooper := dns.NewLoop(dnsConf, allSettings.DNS, httpClient,
unboundLogger)
dnsHandler, dnsCtx, dnsDone := goshutdown.NewGoRoutineHandler(
@@ -399,15 +400,10 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
publicIPLooper := publicip.NewLoop(ipFetcher,
logger.New(log.SetComponent("ip getter")),
allSettings.PublicIP, puid, pgid)
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.Run(pubIPCtx, pubIPDone)
otherGroupHandler.Add(pubIPHandler)
pubIPTickerHandler, pubIPTickerCtx, pubIPTickerDone := goshutdown.NewGoRoutineHandler(
"public IP", goroutine.OptionTimeout(defaultShutdownTimeout))
go publicIPLooper.RunRestartTicker(pubIPTickerCtx, pubIPTickerDone)
tickersGroupHandler.Add(pubIPTickerHandler)
publicIPRunError, err := publicIPLooper.Start(ctx)
if err != nil {
return fmt.Errorf("starting public ip loop: %w", err)
}
updaterLogger := logger.New(log.SetComponent("updater"))
@@ -481,13 +477,31 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
order.OptionOnSuccess(defaultShutdownOnSuccess),
order.OptionOnFailure(defaultShutdownOnFailure))
orderHandler.Append(controlGroupHandler, tickersGroupHandler, healthServerHandler,
vpnHandler, portForwardHandler, otherGroupHandler)
vpnHandler, otherGroupHandler)
// Start VPN for the first time in a blocking call
// until the VPN is launched
_, _ = vpnLooper.ApplyStatus(ctx, constants.Running) // TODO option to disable with variable
<-ctx.Done()
select {
case <-ctx.Done():
stoppers := []interface {
String() string
Stop() error
}{
portForwardLooper, publicIPLooper,
}
for _, stopper := range stoppers {
err := stopper.Stop()
if err != nil {
logger.Error(fmt.Sprintf("stopping %s: %s", stopper, err))
}
}
case err := <-portForwardRunError:
logger.Errorf("port forwarding loop crashed: %s", err)
case err := <-publicIPRunError:
logger.Errorf("public IP loop crashed: %s", err)
}
return orderHandler.Shutdown(context.Background())
}

18
go.mod
View File

@@ -1,14 +1,16 @@
module github.com/qdm12/gluetun
go 1.20
go 1.21
require (
github.com/breml/rootcerts v0.2.11
github.com/fatih/color v1.15.0
github.com/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/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/gosettings v0.3.0-rc13
github.com/qdm12/gosettings v0.4.0-rc1
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/gotree v0.2.0
@@ -17,13 +19,15 @@ require (
github.com/qdm12/ss-server v0.5.0-rc1
github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e
github.com/stretchr/testify v1.8.4
github.com/ulikunitz/xz v0.5.11
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/net v0.10.0
golang.org/x/sys v0.8.0
golang.org/x/text v0.10.0
golang.org/x/net v0.12.0
golang.org/x/sys v0.11.0
golang.org/x/text v0.11.0
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
gopkg.in/ini.v1 v1.67.0
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
)
@@ -42,8 +46,8 @@ require (
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 // indirect
golang.org/x/crypto v0.9.0 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect

34
go.sum
View File

@@ -37,6 +37,7 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
@@ -49,6 +50,10 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
@@ -91,8 +97,8 @@ github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8
github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/gosettings v0.3.0-rc13 h1:fag+/hFPBUcNk3a5ifUbwNS2VgXFpxindkl8mQNk76U=
github.com/qdm12/gosettings v0.3.0-rc13/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
github.com/qdm12/gosettings v0.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38=
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
@@ -119,6 +125,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
@@ -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-20230221090011-e4bae7ad2296 h1:QJ/xcIANMLApehfgPCHnfK1hZiaMmbaTVmPv7DAoTbo=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -146,8 +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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
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/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
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-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -195,8 +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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -226,6 +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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
@@ -236,6 +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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=

View File

@@ -33,15 +33,20 @@ func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool,
func (c *CLI) FormatServers(args []string) error {
var format, output string
allProviders := providers.All()
allProviderFlags := make([]string, len(allProviders))
for i, provider := range allProviders {
allProviderFlags[i] = strings.ReplaceAll(provider, " ", "-")
}
providersToFormat := make(map[string]*bool, len(allProviders))
for _, provider := range allProviders {
for _, provider := range allProviderFlags {
providersToFormat[provider] = new(bool)
}
flagSet := flag.NewFlagSet("markdown", flag.ExitOnError)
flagSet := flag.NewFlagSet("format-servers", flag.ExitOnError)
flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'")
flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to")
titleCaser := cases.Title(language.English)
for _, provider := range allProviders {
for _, provider := range allProviderFlags {
addProviderFlag(flagSet, providersToFormat, provider, titleCaser)
}
if err := flagSet.Parse(args); err != nil {

View File

@@ -16,10 +16,14 @@ type DNS struct {
// DoT server. It cannot be the zero value in the internal
// state.
ServerAddress netip.Addr
// KeepNameserver is true if the Docker DNS server
// found in /etc/resolv.conf should be kept.
// Note settings this to true will go around the
// DoT server blocking.
// KeepNameserver is true if the existing DNS server
// found in /etc/resolv.conf should be used
// Note setting this to true will likely DNS traffic
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS over TLS server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
@@ -75,8 +79,11 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:")
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.AppendNode(d.DoT.toLinesNode())
return node
}

View File

@@ -34,6 +34,8 @@ var (
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")

View File

@@ -56,7 +56,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
if *o.CustomPort != 0 {
switch vpnProvider {
// no restriction on port
case providers.Cyberghost, providers.HideMyAss,
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
@@ -99,6 +99,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Windscribe:
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
default:
panic(fmt.Sprintf("VPN provider %s has no registered allowed ports", vpnProvider))
}
allowedPorts := allowedUDP

View File

@@ -15,6 +15,14 @@ type PortForwarding struct {
// Enabled is true if port forwarding should be activated.
// It cannot be nil for the internal state.
Enabled *bool `json:"enabled"`
// Provider is set to specify which custom port forwarding code
// should be used. This is especially necessary for the custom
// provider using Wireguard for a provider where Wireguard is not
// natively supported but custom port forwading code is available.
// It defaults to the empty string, meaning the current provider
// should be the one used for port forwarding.
// It cannot be nil for the internal state.
Provider *string `json:"provider"`
// Filepath is the port forwarding status file path
// to use. It can be the empty string to indicate not
// to write to a file. It cannot be nil for the
@@ -22,14 +30,21 @@ type PortForwarding struct {
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 {
return nil
}
// Validate Enabled
validProviders := []string{providers.PrivateInternetAccess}
if err = validate.IsOneOf(vpnProvider, validProviders...); err != nil {
// Validate current provider or custom provider specified
providerSelected := vpnProvider
if *p.Provider != "" {
providerSelected = *p.Provider
}
validProviders := []string{
providers.PrivateInternetAccess,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
}
@@ -44,25 +59,29 @@ func (p PortForwarding) validate(vpnProvider string) (err error) {
return nil
}
func (p *PortForwarding) copy() (copied PortForwarding) {
func (p *PortForwarding) Copy() (copied PortForwarding) {
return PortForwarding{
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
}
}
func (p *PortForwarding) mergeWith(other PortForwarding) {
p.Enabled = gosettings.MergeWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.MergeWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.MergeWithPointer(p.Filepath, other.Filepath)
}
func (p *PortForwarding) overrideWith(other PortForwarding) {
func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
p.Provider = gosettings.OverrideWithPointer(p.Provider, other.Provider)
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
}
func (p *PortForwarding) setDefaults() {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
}
@@ -76,7 +95,11 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
}
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
if filepath == "" {

View File

@@ -49,7 +49,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
return fmt.Errorf("server selection: %w", err)
}
err = p.PortForwarding.validate(*p.Name)
err = p.PortForwarding.Validate(*p.Name)
if err != nil {
return fmt.Errorf("port forwarding: %w", err)
}
@@ -61,7 +61,7 @@ func (p *Provider) copy() (copied Provider) {
return Provider{
Name: gosettings.CopyPointer(p.Name),
ServerSelection: p.ServerSelection.copy(),
PortForwarding: p.PortForwarding.copy(),
PortForwarding: p.PortForwarding.Copy(),
}
}
@@ -74,7 +74,7 @@ func (p *Provider) mergeWith(other Provider) {
func (p *Provider) overrideWith(other Provider) {
p.Name = gosettings.OverrideWithPointer(p.Name, other.Name)
p.ServerSelection.overrideWith(other.ServerSelection)
p.PortForwarding.overrideWith(other.PortForwarding)
p.PortForwarding.OverrideWith(other.PortForwarding)
}
func (p *Provider) setDefaults() {

View File

@@ -23,6 +23,20 @@ type PublicIP struct {
IPFilepath *string
}
// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
updatedSettings = p.copy()
updatedSettings.overrideWith(partialUpdate)
err = updatedSettings.validate()
if err != nil {
return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
}
return updatedSettings, nil
}
func (p PublicIP) validate() (err error) {
const minPeriod = 5 * time.Second
if *p.Period < minPeriod {

View File

@@ -38,8 +38,8 @@ func Test_Settings_String(t *testing.T) {
| ├── Run OpenVPN as: root
| └── Verbosity level: 1
├── DNS settings:
| ├── DNS server address to use: 127.0.0.1
| ├── Keep existing nameserver(s): no
| ├── DNS server address to use: 127.0.0.1
| └── DNS over TLS settings:
| ├── Enabled: yes
| ├── Update period: every 24h0m0s

View File

@@ -25,6 +25,10 @@ type Wireguard struct {
PreSharedKey *string `json:"pre_shared_key"`
// Addresses are the Wireguard interface addresses.
Addresses []netip.Prefix `json:"addresses"`
// AllowedIPs are the Wireguard allowed IPs.
// If left unset, they default to "0.0.0.0/0"
// and, if IPv6 is supported, "::0".
AllowedIPs []netip.Prefix `json:"allowed_ips"`
// Interface is the name of the Wireguard interface
// to create. It cannot be the empty string in the
// internal state.
@@ -89,13 +93,26 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
}
for i, ipNet := range w.Addresses {
if !ipNet.IsValid() {
return fmt.Errorf("%w: for address at index %d: %s",
ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
return fmt.Errorf("%w: for address at index %d",
ErrWireguardInterfaceAddressNotSet, i)
}
if !ipv6Supported && ipNet.Addr().Is6() {
return fmt.Errorf("%w: address %s",
ErrWireguardInterfaceAddressIPv6, ipNet)
ErrWireguardInterfaceAddressIPv6, ipNet.String())
}
}
// Validate AllowedIPs
// WARNING: do not check for IPv6 networks in the allowed IPs,
// the wireguard code will take care to ignore it.
if len(w.AllowedIPs) == 0 {
return fmt.Errorf("%w", ErrWireguardAllowedIPsNotSet)
}
for i, allowedIP := range w.AllowedIPs {
if !allowedIP.IsValid() {
return fmt.Errorf("%w: for allowed ip %d of %d",
ErrWireguardAllowedIPNotSet, i+1, len(w.AllowedIPs))
}
}
@@ -118,6 +135,7 @@ func (w *Wireguard) copy() (copied Wireguard) {
PrivateKey: gosettings.CopyPointer(w.PrivateKey),
PreSharedKey: gosettings.CopyPointer(w.PreSharedKey),
Addresses: gosettings.CopySlice(w.Addresses),
AllowedIPs: gosettings.CopySlice(w.AllowedIPs),
Interface: w.Interface,
MTU: w.MTU,
Implementation: w.Implementation,
@@ -128,6 +146,7 @@ func (w *Wireguard) mergeWith(other Wireguard) {
w.PrivateKey = gosettings.MergeWithPointer(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.MergeWithPointer(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.MergeWithSlice(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.MergeWithSlice(w.AllowedIPs, other.AllowedIPs)
w.Interface = gosettings.MergeWithString(w.Interface, other.Interface)
w.MTU = gosettings.MergeWithNumber(w.MTU, other.MTU)
w.Implementation = gosettings.MergeWithString(w.Implementation, other.Implementation)
@@ -137,6 +156,7 @@ func (w *Wireguard) overrideWith(other Wireguard) {
w.PrivateKey = gosettings.OverrideWithPointer(w.PrivateKey, other.PrivateKey)
w.PreSharedKey = gosettings.OverrideWithPointer(w.PreSharedKey, other.PreSharedKey)
w.Addresses = gosettings.OverrideWithSlice(w.Addresses, other.Addresses)
w.AllowedIPs = gosettings.OverrideWithSlice(w.AllowedIPs, other.AllowedIPs)
w.Interface = gosettings.OverrideWithString(w.Interface, other.Interface)
w.MTU = gosettings.OverrideWithNumber(w.MTU, other.MTU)
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
@@ -150,6 +170,11 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
}
defaultAllowedIPs := []netip.Prefix{
netip.PrefixFrom(netip.IPv4Unspecified(), 0),
netip.PrefixFrom(netip.IPv6Unspecified(), 0),
}
w.AllowedIPs = gosettings.DefaultSlice(w.AllowedIPs, defaultAllowedIPs)
w.Interface = gosettings.DefaultString(w.Interface, "wg0")
const defaultMTU = 1400
w.MTU = gosettings.DefaultNumber(w.MTU, defaultMTU)
@@ -178,6 +203,11 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
addressesNode.Appendf(address.String())
}
allowedIPsNode := node.Appendf("Allowed IPs:")
for _, allowedIP := range w.AllowedIPs {
allowedIPsNode.Appendf(allowedIP.String())
}
interfaceNode := node.Appendf("Network interface: %s", w.Interface)
interfaceNode.Appendf("MTU: %d", w.MTU)

View File

@@ -16,7 +16,7 @@ type WireguardSelection struct {
// It is only used with VPN providers generating Wireguard
// configurations specific to each server and user.
// To indicate it should not be used, it should be set
// to netaddr.IPv4Unspecified(). It can never be the zero value
// to netip.IPv4Unspecified(). It can never be the zero value
// in the internal state.
EndpointIP netip.Addr `json:"endpoint_ip"`
// EndpointPort is a the server port to use for the VPN server.

View File

@@ -16,6 +16,8 @@ func (s *Source) readPortForward() (
return portForwarding, err
}
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER")
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
env.ForceLowercase(false),
env.RetroKeys(

View File

@@ -19,6 +19,10 @@ func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
if err != nil {
return wireguard, err // already wrapped
}
wireguard.AllowedIPs, err = s.env.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
if err != nil {
return wireguard, err // already wrapped
}
mtuPtr, err := s.env.Uint16Ptr("WIREGUARD_MTU")
if err != nil {
return wireguard, err

View File

@@ -0,0 +1,3 @@
package files
func ptrTo[T any](x T) *T { return &x }

View 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
}

View File

@@ -4,10 +4,15 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
type Source struct{}
type Source struct {
wireguardConfigPath string
}
func New() *Source {
return &Source{}
const wireguardConfigPath = "/gluetun/wireguard/wg0.conf"
return &Source{
wireguardConfigPath: wireguardConfigPath,
}
}
func (s *Source) String() string { return "files" }

View 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
}

View File

@@ -7,10 +7,20 @@ import (
)
func (s *Source) readVPN() (vpn settings.VPN, err error) {
vpn.Provider, err = s.readProvider()
if err != nil {
return vpn, fmt.Errorf("provider: %w", err)
}
vpn.OpenVPN, err = s.readOpenVPN()
if err != nil {
return vpn, fmt.Errorf("OpenVPN: %w", err)
}
vpn.Wireguard, err = s.readWireguard()
if err != nil {
return vpn, fmt.Errorf("wireguard: %w", err)
}
return vpn, nil
}

View 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
}

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}

View File

@@ -19,7 +19,8 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
l.logger.Info("using plaintext DNS at address " + targetIP.String())
}
nameserver.UseDNSInternally(targetIP.AsSlice())
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), *settings.KeepNameserver)
const keepNameserver = false
err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
if err != nil {
l.logger.Error(err.Error())
}
@@ -39,7 +40,8 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
l.logger.Info("using plaintext DNS at address " + targetIP.String())
}
nameserver.UseDNSInternally(targetIP.AsSlice())
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), *settings.KeepNameserver)
const keepNameserver = false
err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP.AsSlice(), keepNameserver)
if err != nil {
l.logger.Error(err.Error())
}

View File

@@ -10,9 +10,14 @@ import (
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
if *l.GetSettings().KeepNameserver {
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
"this will likely leak DNS traffic outside the VPN " +
"and go through your container network DNS outside the VPN tunnel!")
} else {
const fallback = false
l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution?
// TODO this one is kept if DNS_KEEP_NAMESERVER=on and should be replaced
l.useUnencryptedDNS(fallback)
}
select {
case <-l.start:
@@ -27,7 +32,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
unboundCancel := func() { waitError <- nil }
closeStreams := func() {}
for *l.GetSettings().DoT.Enabled {
settings := l.GetSettings()
for !*settings.KeepNameserver && *settings.DoT.Enabled {
var err error
unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx)
if err == nil {
@@ -50,7 +56,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.logAndWait(ctx, err)
}
if !*l.GetSettings().DoT.Enabled {
settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
const fallback = false
l.useUnencryptedDNS(fallback)
}

View File

@@ -3,6 +3,8 @@ package firewall
import (
"context"
"fmt"
"github.com/qdm12/gluetun/internal/netlink"
)
func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
@@ -147,7 +149,16 @@ func (c *Config) allowVPNIP(ctx context.Context) (err error) {
func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
for _, subnet := range c.outboundSubnets {
subnetIsIPv6 := subnet.Addr().Is6()
firewallUpdated := false
for _, defaultRoute := range c.defaultRoutes {
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
if !ipFamilyMatch {
continue
}
firewallUpdated = true
const remove = false
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subnet, remove)
@@ -155,6 +166,11 @@ func (c *Config) allowOutboundSubnets(ctx context.Context) (err error) {
return err
}
}
if !firewallUpdated {
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
"no default route matching its family", subnet))
}
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/subnet"
)
@@ -37,7 +38,16 @@ func (c *Config) SetOutboundSubnets(ctx context.Context, subnets []netip.Prefix)
func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Prefix) {
const remove = true
for _, subNet := range subnets {
subnetIsIPv6 := subNet.Addr().Is6()
firewallUpdated := false
for _, defaultRoute := range c.defaultRoutes {
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
if !ipFamilyMatch {
continue
}
firewallUpdated = true
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subNet, remove)
if err != nil {
@@ -45,6 +55,12 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
continue
}
}
if !firewallUpdated {
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
"no default route matching its family", subNet))
continue
}
c.outboundSubnets = subnet.RemoveSubnetFromSubnets(c.outboundSubnets, subNet)
}
}
@@ -52,13 +68,28 @@ func (c *Config) removeOutboundSubnets(ctx context.Context, subnets []netip.Pref
func (c *Config) addOutboundSubnets(ctx context.Context, subnets []netip.Prefix) error {
const remove = false
for _, subnet := range subnets {
subnetIsIPv6 := subnet.Addr().Is6()
firewallUpdated := false
for _, defaultRoute := range c.defaultRoutes {
defaultRouteIsIPv6 := defaultRoute.Family == netlink.FamilyV6
ipFamilyMatch := subnetIsIPv6 == defaultRouteIsIPv6
if !ipFamilyMatch {
continue
}
firewallUpdated = true
err := c.acceptOutputFromIPToSubnet(ctx, defaultRoute.NetInterface,
defaultRoute.AssignedIP, subnet, remove)
if err != nil {
return err
}
}
if !firewallUpdated {
c.logger.Info(fmt.Sprintf("ignoring subnet %s which has "+
"no default route matching its family", subnet))
continue
}
c.outboundSubnets = append(c.outboundSubnets, subnet)
}
return nil

View File

@@ -16,7 +16,7 @@ type vpnHealth struct {
func (s *Server) onUnhealthyVPN(ctx context.Context) {
s.logger.Info("program has been unhealthy for " +
s.vpn.healthyWait.String() + ": restarting VPN " +
"(see https://github.com/qdm12/gluetun/wiki/Healthcheck)")
"(see https://github.com/qdm12/gluetun-wiki/blob/main/faq/healthcheck.md)")
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Stopped)
_, _ = s.vpn.loop.ApplyStatus(ctx, constants.Running)
s.vpn.healthyWait += *s.config.VPN.Addition

207
internal/mod/info.go Normal file
View 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
View 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
View 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
View 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)
}
}

View 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)
}
})
}
}

View 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
}

View 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)
})
}
}

View 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
View 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,
}
}

View 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)
}

View 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
}

View 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
View 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
View 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)
})
}
}

View File

@@ -5,10 +5,42 @@ package netlink
import (
"fmt"
"github.com/qdm12/gluetun/internal/mod"
"github.com/vishvananda/netlink"
)
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
// Check for Wireguard family without loading the wireguard module.
// Some kernels have the wireguard module built-in, and don't have a
// modules directory, such as WSL2 kernels.
ok, err = hasWireguardFamily()
if err != nil {
return false, fmt.Errorf("checking for wireguard family: %w", err)
}
if ok {
return true, nil
}
// Try loading the wireguard module, since some systems do not load
// it after a boot. If this fails, wireguard is assumed to not be supported.
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
err = mod.Probe("wireguard")
if err != nil {
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
return false, nil
}
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
// Re-check if the Wireguard family is now available, after loading
// the wireguard kernel module.
ok, err = hasWireguardFamily()
if err != nil {
return false, fmt.Errorf("checking for wireguard family: %w", err)
}
return ok, nil
}
func hasWireguardFamily() (ok bool, err error) {
families, err := netlink.GenlFamilyList()
if err != nil {
return false, fmt.Errorf("listing gen 1 families: %w", err)

View File

@@ -52,7 +52,7 @@ Your credentials might be wrong 🤨
That error usually happens because either:
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
Update your server information using https://github.com/qdm12/gluetun/wiki/Updating-Servers
Check out https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as SERVER_REGIONS

View File

@@ -52,7 +52,7 @@ func Test_processLogLine(t *testing.T) {
That error usually happens because either:
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
Update your server information using https://github.com/qdm12/gluetun/wiki/Updating-Servers
Check out https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as SERVER_REGIONS

View File

@@ -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())
}
}

View File

@@ -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
}

View File

@@ -1,5 +0,0 @@
package portforward
func (l *Loop) GetPortForwarded() (port uint16) {
return l.state.GetPortForwarded()
}

View File

@@ -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
}
}
}

View File

@@ -2,9 +2,27 @@ package portforward
import (
"context"
"net/netip"
)
type Service interface {
Start(ctx context.Context) (runError <-chan error, err error)
Stop() (err error)
GetPortForwarded() (port uint16)
}
type Routing interface {
VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error)
}
type PortAllower interface {
SetAllowedPort(ctx context.Context, port uint16, intf string) (err error)
RemoveAllowedPort(ctx context.Context, port uint16) (err error)
}
type Logger interface {
Debug(s string)
Info(s string)
Warn(s string)
Error(s string)
}

View File

@@ -1,7 +0,0 @@
package portforward
type Logger interface {
Info(s string)
Warn(s string)
Error(s string)
}

View File

@@ -1,64 +1,161 @@
package portforward
import (
"context"
"fmt"
"net/http"
"sync"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/loopstate"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/portforward/state"
"github.com/qdm12/gluetun/internal/portforward/service"
)
type Loop struct {
statusManager *loopstate.State
state *state.State
// Fixed parameters
puid int
pgid int
// Objects
// State
settings Settings
settingsMutex sync.RWMutex
service Service
// Fixed injected objets
routing Routing
client *http.Client
portAllower PortAllower
logger Logger
// Fixed parameters
uid, gid int
// Internal channels and locks
start chan struct{}
running chan models.LoopStatus
stop chan struct{}
stopped chan struct{}
startMu sync.Mutex
backoffTime time.Duration
userTrigger bool
// runCtx is used to detect when the loop has exited
// when performing an update
runCtx context.Context //nolint:containedctx
runCancel context.CancelFunc
runDone <-chan struct{}
updateTrigger chan<- Settings
updatedResult <-chan error
}
const defaultBackoffTime = 5 * time.Second
func NewLoop(settings settings.PortForwarding,
func NewLoop(settings settings.PortForwarding, routing Routing,
client *http.Client, portAllower PortAllower,
logger Logger, puid, pgid int) *Loop {
start := make(chan struct{})
running := make(chan models.LoopStatus)
stop := make(chan struct{})
stopped := make(chan struct{})
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
state := state.New(statusManager, settings)
logger Logger, uid, gid int) *Loop {
return &Loop{
statusManager: statusManager,
state: state,
puid: puid,
pgid: pgid,
// Objects
settings: Settings{
VPNIsUp: ptrTo(false),
Service: service.Settings{
Enabled: settings.Enabled,
Filepath: *settings.Filepath,
},
},
routing: routing,
client: client,
portAllower: portAllower,
logger: logger,
start: start,
running: running,
stop: stop,
stopped: stopped,
userTrigger: true,
backoffTime: defaultBackoffTime,
uid: uid,
gid: gid,
}
}
func (l *Loop) String() string {
return "port forwarding loop"
}
func (l *Loop) Start(_ context.Context) (runError <-chan error, _ error) {
l.runCtx, l.runCancel = context.WithCancel(context.Background())
runDone := make(chan struct{})
l.runDone = runDone
updateTrigger := make(chan Settings)
l.updateTrigger = updateTrigger
updateResult := make(chan error)
l.updatedResult = updateResult
runErrorCh := make(chan error)
go l.run(l.runCtx, runDone, runErrorCh, updateTrigger, updateResult)
return runErrorCh, nil
}
func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
runErrorCh chan<- error, updateTrigger <-chan Settings,
updateResult chan<- error) {
defer close(runDone)
var serviceRunError <-chan error
for {
updateReceived := false
select {
case <-runCtx.Done():
// Stop call takes care of stopping the service
return
case partialUpdate := <-updateTrigger:
updatedSettings, err := l.settings.updateWith(partialUpdate, *l.settings.VPNIsUp)
if err != nil {
updateResult <- err
continue
}
updateReceived = true
l.settingsMutex.Lock()
l.settings = updatedSettings
l.settingsMutex.Unlock()
case err := <-serviceRunError:
l.logger.Error(err.Error())
}
firstRun := serviceRunError == nil
if !firstRun {
err := l.service.Stop()
if err != nil {
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
return
}
}
serviceSettings := l.settings.Service.Copy()
// Only enable port forward if the VPN tunnel is up
*serviceSettings.Enabled = *serviceSettings.Enabled && *l.settings.VPNIsUp
l.service = service.New(serviceSettings, l.routing, l.client,
l.portAllower, l.logger, l.uid, l.gid)
var err error
serviceRunError, err = l.service.Start(runCtx)
if updateReceived {
// Signal to the Update call that the service has started
// and if it failed to start.
updateResult <- err
}
}
}
func (l *Loop) UpdateWith(partialUpdate Settings) (err error) {
select {
case l.updateTrigger <- partialUpdate:
select {
case err = <-l.updatedResult:
return err
case <-l.runCtx.Done():
return l.runCtx.Err()
}
case <-l.runCtx.Done():
// loop has been stopped, no update can be done
return l.runCtx.Err()
}
}
func (l *Loop) Stop() (err error) {
l.runCancel()
<-l.runDone
if l.service != nil {
return l.service.Stop()
}
return nil
}
func (l *Loop) GetPortForwarded() (port uint16) {
if l.service == nil {
return 0
}
return l.service.GetPortForwarded()
}
func ptrTo[T any](value T) *T {
return &value
}

View File

@@ -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
}
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -1,16 +1,44 @@
package portforward
import (
"context"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/portforward/service"
"github.com/qdm12/gosettings"
)
func (l *Loop) GetSettings() (settings settings.PortForwarding) {
return l.state.GetSettings()
type Settings struct {
// VPNIsUp can be optionally set to signal the loop
// the VPN is up (true) or down (false). If left to nil,
// it is assumed the VPN is in the same previous state.
VPNIsUp *bool
Service service.Settings
}
func (l *Loop) SetSettings(ctx context.Context, settings settings.PortForwarding) (
outcome string) {
return l.state.SetSettings(ctx, settings)
// updateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (s Settings) updateWith(partialUpdate Settings,
forStartup bool) (updated Settings, err error) {
updated = s.copy()
updated.overrideWith(partialUpdate)
err = updated.validate(forStartup)
if err != nil {
return updated, err
}
return updated, nil
}
func (s Settings) copy() (copied Settings) {
copied.VPNIsUp = gosettings.CopyPointer(s.VPNIsUp)
copied.Service = s.Service.Copy()
return copied
}
func (s *Settings) overrideWith(update Settings) {
s.VPNIsUp = gosettings.OverrideWithPointer(s.VPNIsUp, update.VPNIsUp)
s.Service.OverrideWith(update.Service)
}
func (s Settings) validate(forStartup bool) (err error) {
return s.Service.Validate(forStartup)
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/airvpn/updater"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
Fetcher: updater.New(client),
}
}

View File

@@ -8,14 +8,12 @@ import (
type Provider struct {
extractor Extractor
utils.NoPortForwarder
common.Fetcher
}
func New(extractor Extractor) *Provider {
return &Provider{
extractor: extractor,
NoPortForwarder: utils.NewNoPortForwarding(providers.Custom),
Fetcher: utils.NewNoFetcher(providers.Custom),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/cyberghost/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -21,7 +19,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Cyberghost),
Fetcher: updater.New(parallelResolver),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/example/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -24,7 +22,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Example),
Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/expressvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Expressvpn),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/fastestvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Fastestvpn),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/hidemyass/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.HideMyAss),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/ipvanish/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Ipvanish),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/ivpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Ivpn),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/mullvad/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Mullvad),
Fetcher: updater.New(client),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/nordvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Nordvpn),
Fetcher: updater.New(client, updaterWarner),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/perfectprivacy/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -21,7 +19,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Perfectprivacy),
Fetcher: updater.New(unzipper, updaterWarner),
}
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/privado/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Privado),
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -23,29 +23,32 @@ import (
var (
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.
func (p *Provider) PortForward(ctx context.Context, client *http.Client,
logger utils.Logger, gateway netip.Addr, serverName string) (
port uint16, err error) {
func (p *Provider) PortForward(ctx context.Context,
objects utils.PortForwardObjects) (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)
if !ok {
return 0, fmt.Errorf("%w: %s", ErrServerNameNotFound, serverName)
}
logger := objects.Logger
if !server.PortForward {
logger.Error("The server " + serverName +
" (region " + server.Region + ") does not support port forwarding")
return 0, nil
}
if !gateway.IsValid() {
return 0, fmt.Errorf("%w: %s", ErrGatewayIPIsNotValid, gateway)
} else if serverName == "" {
return 0, ErrServerNameEmpty
}
privateIPClient, err := newHTTPClient(serverName)
if err != nil {
@@ -70,7 +73,8 @@ func (p *Provider) PortForward(ctx context.Context, client *http.Client,
}
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)
if err != nil {
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))
// 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)
}
@@ -92,8 +96,15 @@ var (
)
func (p *Provider) KeepPortForward(ctx context.Context,
gateway netip.Addr, serverName string) (err error) {
privateIPClient, err := newHTTPClient(serverName)
objects utils.PortForwardObjects) (err error) {
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 {
return fmt.Errorf("creating custom HTTP client: %w", err)
}
@@ -120,7 +131,7 @@ func (p *Provider) KeepPortForward(ctx context.Context,
}
return ctx.Err()
case <-keepAliveTimer.C:
err := bindPort(ctx, privateIPClient, gateway, data)
err = bindPort(ctx, privateIPClient, objects.Gateway, data)
if err != nil {
return fmt.Errorf("binding port: %w", err)
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/privatevpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Privatevpn),
Fetcher: updater.New(unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -15,6 +15,7 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
AuthUserPass: true,
Ciphers: []string{
openvpn.AES256cbc,
openvpn.AES256gcm,
},
Auth: openvpn.SHA512,
MssFix: 1450,

View 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)
}
}

View File

@@ -7,14 +7,13 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/protonvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
portForwarded uint16
}
func New(storage common.Storage, randSource rand.Source,
@@ -22,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Protonvpn),
Fetcher: updater.New(client, updaterWarner),
}
}

View File

@@ -2,12 +2,9 @@ package provider
import (
"context"
"net/http"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
"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.
@@ -15,15 +12,6 @@ type Provider interface {
GetConnection(selection settings.ServerSelection, ipv6Supported bool) (connection models.Connection, err error)
OpenVPNConfig(connection models.Connection, settings settings.OpenVPN, ipv6Supported bool) (lines []string)
Name() string
PortForwarder
FetchServers(ctx context.Context, minServers int) (
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)
}

View File

@@ -6,13 +6,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/purevpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -22,7 +20,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.Purevpn),
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
}
}

View File

@@ -7,13 +7,11 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/slickvpn/updater"
"github.com/qdm12/gluetun/internal/provider/utils"
)
type Provider struct {
storage common.Storage
randSource rand.Source
utils.NoPortForwarder
common.Fetcher
}
@@ -23,7 +21,6 @@ func New(storage common.Storage, randSource rand.Source,
return &Provider{
storage: storage,
randSource: randSource,
NoPortForwarder: utils.NewNoPortForwarding(providers.SlickVPN),
Fetcher: updater.New(client, updaterWarner, parallelResolver),
}
}

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