mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-04 01:29:54 +02:00
Compare commits
31 Commits
09c47838f1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 93cc5a4b2c | |||
| a17591dcdb | |||
| c31c566282 | |||
| 496458ca4e | |||
| 52a41cb891 | |||
| 6c76273ef6 | |||
| 366062dc12 | |||
| 8abb05567c | |||
| a53a0267e4 | |||
| 4e986c8af7 | |||
| 6d84462f00 | |||
| acab89b91a | |||
| 48c1f2bf6a | |||
| c599e7fd2c | |||
| ff6e45fae0 | |||
| 17f24343d6 | |||
| ebbc630b31 | |||
| 39ac8b3432 | |||
| f65ee3dcb1 | |||
| 7e8d81b161 | |||
| 21e868c89c | |||
| 2e20e2df66 | |||
| 6f5f518d1d | |||
| 1998e0d04f | |||
| 14f30bc641 | |||
| f89e55b8ff | |||
| 7ad6af0947 | |||
| d3e089ccd7 | |||
| 3eebbf65a8 | |||
| a1ef736b0f | |||
| 46edfe49e3 |
@@ -56,7 +56,6 @@ body:
|
|||||||
- IVPN
|
- IVPN
|
||||||
- Mullvad
|
- Mullvad
|
||||||
- NordVPN
|
- NordVPN
|
||||||
- OVPN
|
|
||||||
- Privado
|
- Privado
|
||||||
- Private Internet Access
|
- Private Internet Access
|
||||||
- PrivateVPN
|
- PrivateVPN
|
||||||
|
|||||||
+31
-5
@@ -4,12 +4,38 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
- package-ecosystem: gomod
|
- # Servers data dependency that should be updated as soon as
|
||||||
directory: /
|
# possible when a new version is released, to have the latest
|
||||||
|
# servers available
|
||||||
|
package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
|
ignore:
|
||||||
|
- # In particular avoid amneziawg-go which have v1.x.y versions available
|
||||||
|
# on the Go modules proxy, but are not in the Github repository tags
|
||||||
|
# and are not the latest releases either. Most likely a mistake from the
|
||||||
|
# maintainers, which is persisted on the Go proxy.
|
||||||
|
dependency-name: "github.com/amnezia-vpn/amneziawg-go"
|
||||||
|
versions: ["1.x"]
|
||||||
|
groups:
|
||||||
|
low-importance:
|
||||||
|
patterns:
|
||||||
|
- "github.com/breml/rootcerts"
|
||||||
|
- "github.com/fatih/color"
|
||||||
|
- "github.com/golang/mock"
|
||||||
|
- "github.com/klauspost/compress"
|
||||||
|
- "github.com/klauspost/pgzip"
|
||||||
|
- "github.com/pelletier/go-toml/v2"
|
||||||
|
- "github.com/qdm12/goshutdown"
|
||||||
|
- "github.com/qdm12/gosplash"
|
||||||
|
- "github.com/qdm12/gotree"
|
||||||
|
- "github.com/qdm12/log"
|
||||||
|
- "github.com/stretchr/testify"
|
||||||
|
- "github.com/ulikunitz/xz"
|
||||||
|
- "gopkg.in/ini.v1"
|
||||||
|
|||||||
@@ -64,8 +64,6 @@
|
|||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ NordVPN"
|
- name: "☁️ NordVPN"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ OVPN"
|
|
||||||
color: "cfe8d4"
|
|
||||||
- name: "☁️ Perfect Privacy"
|
- name: "☁️ Perfect Privacy"
|
||||||
color: "cfe8d4"
|
color: "cfe8d4"
|
||||||
- name: "☁️ PIA"
|
- name: "☁️ PIA"
|
||||||
|
|||||||
@@ -8,5 +8,4 @@
|
|||||||
|
|
||||||
# Assertions
|
# Assertions
|
||||||
|
|
||||||
* [ ] I am aware that we do not accept manual changes to the servers.json file <!-- If this is your goal, please consult https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-using-the-command-line -->
|
|
||||||
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
* [ ] I am aware that any changes to settings should be reflected in the [wiki](https://github.com/qdm12/gluetun-wiki/)
|
||||||
|
|||||||
+16
-10
@@ -28,6 +28,10 @@ on:
|
|||||||
- go.mod
|
- go.mod
|
||||||
- go.sum
|
- go.sum
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -44,7 +48,6 @@ jobs:
|
|||||||
locale: "US"
|
locale: "US"
|
||||||
level: error
|
level: error
|
||||||
exclude: |
|
exclude: |
|
||||||
./internal/storage/servers.json
|
|
||||||
./.golangci.yml
|
./.golangci.yml
|
||||||
*.md
|
*.md
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ jobs:
|
|||||||
github.event_name == 'release' ||
|
github.event_name == 'release' ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
)
|
)
|
||||||
needs: [ verify ]
|
needs: [verify]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: secrets
|
environment: secrets
|
||||||
steps:
|
steps:
|
||||||
@@ -120,7 +123,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Gluetun container with ProtonVPN Wireguard and port forwarding
|
- name: Run Gluetun container with ProtonVPN Wireguard and port forwarding
|
||||||
configuration
|
configuration
|
||||||
run: echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner
|
run:
|
||||||
|
echo -e "${{ secrets.PROTONVPN_WIREGUARD_PRIVATE_KEY }}" | ./ci/runner
|
||||||
protonvpn-wireguard-port-forwarding
|
protonvpn-wireguard-port-forwarding
|
||||||
|
|
||||||
- name: Run Gluetun container with ProtonVPN OpenVPN and port forwarding
|
- name: Run Gluetun container with ProtonVPN OpenVPN and port forwarding
|
||||||
@@ -129,11 +133,12 @@ jobs:
|
|||||||
secrets.PROTONVPN_OPENVPN_PASSWORD }}" | ./ci/runner
|
secrets.PROTONVPN_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||||
protonvpn-openvpn-port-forwarding
|
protonvpn-openvpn-port-forwarding
|
||||||
|
|
||||||
- name: Run Gluetun container with Private Internet Access OpenVPN and port
|
# - name:
|
||||||
forwarding configuration
|
# Run Gluetun container with Private Internet Access OpenVPN and port
|
||||||
run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
|
# forwarding configuration
|
||||||
secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
|
# run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
|
||||||
private-internet-access-openvpn-port-forwarding
|
# secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||||
|
# private-internet-access-openvpn-port-forwarding
|
||||||
|
|
||||||
- name: Run Gluetun container with AirVPN Wireguard configuration
|
- name: Run Gluetun container with AirVPN Wireguard configuration
|
||||||
run: echo -e "${{ secrets.AIRVPN_WIREGUARD_PRIVATE_KEY }}\n${{
|
run: echo -e "${{ secrets.AIRVPN_WIREGUARD_PRIVATE_KEY }}\n${{
|
||||||
@@ -141,7 +146,8 @@ jobs:
|
|||||||
secrets.AIRVPN_WIREGUARD_ADDRESSES }}" | ./ci/runner airvpn-wireguard
|
secrets.AIRVPN_WIREGUARD_ADDRESSES }}" | ./ci/runner airvpn-wireguard
|
||||||
|
|
||||||
- name: Run Gluetun container with AirVPN OpenVPN configuration
|
- name: Run Gluetun container with AirVPN OpenVPN configuration
|
||||||
run: echo -e "${{ secrets.AIRVPN_OPENVPN_KEY }}\n${{ secrets.AIRVPN_OPENVPN_CERT
|
run:
|
||||||
|
echo -e "${{ secrets.AIRVPN_OPENVPN_KEY }}\n${{ secrets.AIRVPN_OPENVPN_CERT
|
||||||
}}" | ./ci/runner airvpn-openvpn
|
}}" | ./ci/runner airvpn-openvpn
|
||||||
|
|
||||||
codeql:
|
codeql:
|
||||||
@@ -169,7 +175,7 @@ jobs:
|
|||||||
github.event_name == 'release' ||
|
github.event_name == 'release' ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||||
)
|
)
|
||||||
needs: [ verify, verify-private, codeql ]
|
needs: [verify, verify-private, codeql]
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ on:
|
|||||||
- "**.md"
|
- "**.md"
|
||||||
- .github/workflows/markdown.yml
|
- .github/workflows/markdown.yml
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
markdown:
|
markdown:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
name: Update servers list
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
provider:
|
|
||||||
description: "VPN Provider to update"
|
|
||||||
required: true
|
|
||||||
default: "all"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- all
|
|
||||||
- airvpn
|
|
||||||
- cyberghost
|
|
||||||
- expressvpn
|
|
||||||
- fastestvpn
|
|
||||||
- giganews
|
|
||||||
- hidemyass
|
|
||||||
- ipvanish
|
|
||||||
- ivpn
|
|
||||||
- mullvad
|
|
||||||
- nordvpn
|
|
||||||
- perfect privacy
|
|
||||||
- privado
|
|
||||||
- private internet access
|
|
||||||
- privatevpn
|
|
||||||
- protonvpn
|
|
||||||
- purevpn
|
|
||||||
- slickvpn
|
|
||||||
- surfshark
|
|
||||||
- torguard
|
|
||||||
- vpnsecure
|
|
||||||
- vpn unlimited
|
|
||||||
- vyprvpn
|
|
||||||
- windscribe
|
|
||||||
schedule:
|
|
||||||
- cron: "11 3 1 */2 *" # Run at 03:11 on the 1st of every 2nd month
|
|
||||||
jobs:
|
|
||||||
update-servers-list:
|
|
||||||
if: github.repository == 'passteque/gluetun'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- name: Update servers list
|
|
||||||
run: |
|
|
||||||
SELECTED_PROVIDER="${{ github.event.inputs.provider || 'all' }}"
|
|
||||||
|
|
||||||
if [ "$SELECTED_PROVIDER" = "all" ]; then
|
|
||||||
FLAGS="-all"
|
|
||||||
else
|
|
||||||
FLAGS="-providers $SELECTED_PROVIDER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
go run ./cmd/gluetun/main.go update $FLAGS \
|
|
||||||
-maintainer \
|
|
||||||
-proton-email "${{ secrets.PROTON_EMAIL }}" \
|
|
||||||
-proton-password "${{ secrets.PROTON_PASSWORD }}"
|
|
||||||
|
|
||||||
- name: Check for changes
|
|
||||||
run: |
|
|
||||||
if git diff --exit-code internal/storage/servers.json >/dev/null; then
|
|
||||||
echo "Error: internal/storage/servers.json was not modified."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check no other file changes
|
|
||||||
run: |
|
|
||||||
if ! git diff --exit-code --quiet ':!internal/storage/servers.json'; then
|
|
||||||
echo "Error: Unexpected changes detected in files other than servers.json"
|
|
||||||
git status --short
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: createpr
|
|
||||||
uses: peter-evans/create-pull-request@v8
|
|
||||||
with:
|
|
||||||
branch-suffix: timestamp
|
|
||||||
branch: bot/update-servers-list
|
|
||||||
base: master
|
|
||||||
delete-branch: true
|
|
||||||
title: "feat(providers/${{ github.event.inputs.provider || 'all' }}): servers data update"
|
|
||||||
body: |
|
|
||||||
This PR was automatically generated by the [Update servers list](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow run.
|
|
||||||
|
|
||||||
# - name: Merge Pull Request
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# run: |
|
|
||||||
# gh pr merge ${{ steps.createpr.outputs.pull-request-number }} --auto -m -d
|
|
||||||
@@ -12,6 +12,10 @@ formatters:
|
|||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
|
||||||
|
run:
|
||||||
|
build-tags:
|
||||||
|
- integration
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
settings:
|
settings:
|
||||||
misspell:
|
misspell:
|
||||||
|
|||||||
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
// to develop this project.
|
// to develop this project.
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"go.buildTags": "linux",
|
"go.buildTags": "linux,integration",
|
||||||
"go.toolsEnvVars": {
|
"go.toolsEnvVars": {
|
||||||
"CGO_ENABLED": "0"
|
"CGO_ENABLED": "0"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-5
@@ -186,14 +186,12 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# # ProtonVPN only:
|
# # ProtonVPN only:
|
||||||
SECURE_CORE_ONLY= \
|
SECURE_CORE_ONLY= \
|
||||||
TOR_ONLY= \
|
TOR_ONLY= \
|
||||||
# # Surfshark and ovpn only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
# # VPN Secure only:
|
# # VPN Secure only:
|
||||||
PREMIUM_ONLY= \
|
PREMIUM_ONLY= \
|
||||||
# # PIA and ProtonVPN only:
|
# # PIA and ProtonVPN only:
|
||||||
PORT_FORWARD_ONLY= \
|
PORT_FORWARD_ONLY= \
|
||||||
# # Ovpn only:
|
|
||||||
SERVER_DEDICATED=no \
|
|
||||||
# Firewall
|
# Firewall
|
||||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \
|
||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
@@ -220,7 +218,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
DNS_CACHING=on \
|
DNS_CACHING=on \
|
||||||
DNS_UPSTREAM_IPV6=off \
|
DNS_UPSTREAM_IPV6=off \
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
BLOCK_SURVEILLANCE=off \
|
|
||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
DNS_UNBLOCK_HOSTNAMES= \
|
DNS_UNBLOCK_HOSTNAMES= \
|
||||||
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
|
||||||
@@ -278,7 +275,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
PUID=1000 \
|
PUID=1000 \
|
||||||
PGID=1000
|
PGID=1000
|
||||||
ENTRYPOINT ["/gluetun-entrypoint"]
|
ENTRYPOINT ["/gluetun-entrypoint"]
|
||||||
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp 1080/tcp
|
EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp 1080/tcp 1080/udp
|
||||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
RUN apk add --no-cache --update -l wget && \
|
RUN apk add --no-cache --update -l wget && \
|
||||||
|
|||||||
@@ -60,20 +60,20 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Alpine 3.23 for a small Docker image of 43.1MB
|
- Based on Alpine 3.23 for a small Docker image of 43.1MB
|
||||||
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Ovpn**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
|
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **Giganews**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad** (Wireguard only), **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers
|
||||||
- Supports OpenVPN for all providers listed
|
- Supports OpenVPN for all providers listed
|
||||||
- Supports Wireguard both kernelspace and userspace
|
- Supports Wireguard both kernelspace and userspace
|
||||||
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Ovpn**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
- For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **ProtonVPN**, **Surfshark** and **Windscribe**
|
||||||
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- For **Cyberghost**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **VyprVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
- 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/passteque/gluetun/issues/134)
|
- More in progress, see [#134](https://github.com/passteque/gluetun/issues/134)
|
||||||
- Supports AmneziaWG only with the custom provider for now
|
- Supports AmneziaWG only with the custom provider for now
|
||||||
- DNS over TLS baked in with service provider(s) of your choice
|
- DNS over TLS baked in with service provider(s) of your choice
|
||||||
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours
|
- DNS fine blocking of malicious/ads hostnames and IP addresses, with live update every 24 hours
|
||||||
- Choose the vpn network protocol, `udp` or `tcp`
|
- Choose the vpn network protocol, `udp` or `tcp`
|
||||||
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
- Built in firewall kill switch to allow traffic only with needed the VPN servers and LAN devices
|
||||||
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
- Built in Shadowsocks proxy server (protocol based on SOCKS5 with an encryption layer, tunnels TCP+UDP)
|
||||||
- Built in Socks5 proxy server (tunnels TCP) - partial credits to @angelakis and @adjscent
|
- Built in Socks5 proxy server (tunnels TCP+UDP) - partial credits to @angelakis and @adjscent
|
||||||
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
- Built in HTTP proxy (tunnels HTTP and HTTPS through TCP)
|
||||||
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
|
- [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)
|
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-srp v0.0.7
|
github.com/ProtonMail/go-srp v0.0.7
|
||||||
github.com/amnezia-vpn/amneziawg-go v0.2.16
|
github.com/amnezia-vpn/amneziawg-go v0.2.18
|
||||||
github.com/breml/rootcerts v0.3.4
|
github.com/breml/rootcerts v0.3.4
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2
|
github.com/jsimonetti/rtnetlink v1.4.2
|
||||||
github.com/klauspost/compress v1.18.4
|
github.com/klauspost/compress v1.18.4
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/mdlayher/genetlink v1.3.2
|
github.com/mdlayher/genetlink v1.4.0
|
||||||
github.com/mdlayher/netlink v1.9.0
|
github.com/mdlayher/netlink v1.9.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||||
github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82
|
github.com/qdm12/gluetun-servers v0.1.0
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
@@ -27,10 +27,9 @@ require (
|
|||||||
github.com/ti-mo/netfilter v0.5.3
|
github.com/ti-mo/netfilter v0.5.3
|
||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/net v0.55.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/sys v0.45.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/text v0.38.0
|
||||||
golang.org/x/text v0.35.0
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
gopkg.in/ini.v1 v1.67.1
|
gopkg.in/ini.v1 v1.67.1
|
||||||
@@ -47,7 +46,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.6.0 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
@@ -57,10 +56,11 @@ require (
|
|||||||
github.com/prometheus/common v0.60.1 // indirect
|
github.com/prometheus/common v0.60.1 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.35.1 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ github.com/ProtonMail/go-crypto v1.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYS
|
|||||||
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/amnezia-vpn/amneziawg-go v0.2.16 h1:XY6HOq/xtqH8ZXMncRWkjFs85EKdN10NLNnw23kTpE0=
|
github.com/amnezia-vpn/amneziawg-go v0.2.18 h1:pUn7/P8qdGmHd6JmE3bCQXPblZs3vruWR98nLODQLJg=
|
||||||
github.com/amnezia-vpn/amneziawg-go v0.2.16/go.mod h1:nRkPpIzjCxMW8pZKXTRkpqAQVlmFJdVOGkeQSC7wbms=
|
github.com/amnezia-vpn/amneziawg-go v0.2.18/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
|
github.com/breml/rootcerts v0.3.4 h1:9i7WNl/ctd9OEAOaTfLy//Wrlfxq/tRQ7v4okYFN9Ys=
|
||||||
@@ -48,12 +48,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
||||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
|
||||||
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
|
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
|
||||||
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
|
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
@@ -76,8 +76,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
|||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||||
github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82 h1:tE44IEW7o9yPQaO8HBeoO9RxtTTxqhboIypegrQlVt8=
|
github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY=
|
||||||
github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
|
github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
@@ -120,15 +120,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -136,14 +136,14 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -156,8 +156,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -167,8 +167,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -176,8 +176,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -104,6 +105,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
return fmt.Errorf("creating DoH dialer: %w", err)
|
||||||
}
|
}
|
||||||
|
net.DefaultResolver = &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: dnsDialer.Dial,
|
||||||
|
}
|
||||||
|
|
||||||
const clientTimeout = 10 * time.Second
|
const clientTimeout = 10 * time.Second
|
||||||
httpClient := &http.Client{Timeout: clientTimeout}
|
httpClient := &http.Client{Timeout: clientTimeout}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Start launches a command and streams stdout and stderr to channels.
|
// Start launches a command and streams stdout and stderr to channels.
|
||||||
// All the channels returned are ready only and won't be closed
|
// stdoutLines and stderrLines channels will be closed when there is no more
|
||||||
// if the command fails later.
|
// output to read, in order for the caller to catch all lines even after the
|
||||||
|
// command has finished. The waitError channel returned will never be closed.
|
||||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||||
stdoutLines, stderrLines <-chan string,
|
stdoutLines, stderrLines <-chan string,
|
||||||
waitError <-chan error, startErr error,
|
waitError <-chan error, startErr error,
|
||||||
@@ -38,6 +39,7 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
|
close(stdoutLinesCh)
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
||||||
@@ -45,9 +47,11 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
_ = stderr.Close()
|
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
|
close(stdoutLinesCh)
|
||||||
|
_ = stderr.Close()
|
||||||
<-stderrDone
|
<-stderrDone
|
||||||
|
close(stderrLinesCh)
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +59,10 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
|||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
<-stdoutDone
|
<-stdoutDone
|
||||||
<-stderrDone
|
close(stdoutLinesCh)
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
|
<-stderrDone
|
||||||
|
close(stderrLinesCh)
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
waitErrorCh <- err
|
waitErrorCh <- err
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -89,30 +89,48 @@ func Test_start(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var stdoutIndex, stderrIndex int
|
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
|
||||||
|
testCase.stdout, testCase.stderr, testCase.waitErr)
|
||||||
done := false
|
|
||||||
for !done {
|
|
||||||
select {
|
|
||||||
case line := <-stdoutLines:
|
|
||||||
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
|
||||||
stdoutIndex++
|
|
||||||
case line := <-stderrLines:
|
|
||||||
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
|
||||||
stderrIndex++
|
|
||||||
case err := <-waitError:
|
|
||||||
if testCase.waitErr != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
|
||||||
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
|
||||||
|
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
stdoutIndex := 0
|
||||||
|
stderrIndex := 0
|
||||||
|
|
||||||
|
done := false
|
||||||
|
for !done {
|
||||||
|
select {
|
||||||
|
case line, ok := <-stdoutLines:
|
||||||
|
if !ok {
|
||||||
|
stdoutLines = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedStdout[stdoutIndex], line)
|
||||||
|
stdoutIndex++
|
||||||
|
case line, ok := <-stderrLines:
|
||||||
|
if !ok {
|
||||||
|
stderrLines = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedStderr[stderrIndex], line)
|
||||||
|
stderrIndex++
|
||||||
|
case err := <-waitError:
|
||||||
|
if expectedWaitErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, expectedWaitErr.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(expectedStdout), stdoutIndex)
|
||||||
|
assert.Equal(t, len(expectedStderr), stderrIndex)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,31 +18,39 @@ func (c *Cmder) RunAndLog(ctx context.Context, command string, logger Logger) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
|
||||||
streamDone := make(chan struct{})
|
streamDone := make(chan struct{})
|
||||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
go streamLines(streamDone, logger, stdout, stderr)
|
||||||
|
|
||||||
err = <-waitError
|
err = <-waitError
|
||||||
streamCancel()
|
|
||||||
<-streamDone
|
<-streamDone
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamLines(ctx context.Context, done chan<- struct{},
|
func streamLines(done chan<- struct{}, logger Logger,
|
||||||
logger Logger, stdout, stderr <-chan string,
|
stdout, stderr <-chan string,
|
||||||
) {
|
) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
var line string
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case line, ok := <-stdout:
|
||||||
return
|
if ok {
|
||||||
case line = <-stdout:
|
logger.Info(line)
|
||||||
logger.Info(line)
|
break
|
||||||
case line = <-stderr:
|
}
|
||||||
logger.Error(line)
|
if stderr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stdout = nil
|
||||||
|
case line, ok := <-stderr:
|
||||||
|
if ok {
|
||||||
|
logger.Error(line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if stdout == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stderr = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||||
@@ -16,8 +16,9 @@ func readObsolete(r *reader.Reader) (warnings []string) {
|
|||||||
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
|
||||||
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because you should use the built-in server which now " +
|
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because you should use the built-in server which now " +
|
||||||
"forwards local names to private DNS resolvers found in /etc/resolv.conf at container start",
|
"forwards local names to private DNS resolvers found in /etc/resolv.conf at container start",
|
||||||
|
"BLOCK_SURVEILLANCE": "BLOCK_SURVEILLANCE is obsolete because its DNS block lists are not longer maintained",
|
||||||
}
|
}
|
||||||
sortedKeys := maps.Keys(keyToMessage)
|
sortedKeys := slices.Collect(maps.Keys(keyToMessage))
|
||||||
slices.Sort(sortedKeys)
|
slices.Sort(sortedKeys)
|
||||||
warnings = make([]string, 0, len(keyToMessage))
|
warnings = make([]string, 0, len(keyToMessage))
|
||||||
for _, key := range sortedKeys {
|
for _, key := range sortedKeys {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
type DNSBlacklist struct {
|
type DNSBlacklist struct {
|
||||||
BlockMalicious *bool
|
BlockMalicious *bool
|
||||||
BlockAds *bool
|
BlockAds *bool
|
||||||
BlockSurveillance *bool
|
|
||||||
AllowedHosts []string
|
AllowedHosts []string
|
||||||
AddBlockedHosts []string
|
AddBlockedHosts []string
|
||||||
AddBlockedIPs []netip.Addr
|
AddBlockedIPs []netip.Addr
|
||||||
@@ -31,7 +30,6 @@ type DNSBlacklist struct {
|
|||||||
func (b *DNSBlacklist) setDefaults() {
|
func (b *DNSBlacklist) setDefaults() {
|
||||||
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
|
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
|
||||||
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
|
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
|
||||||
b.BlockSurveillance = gosettings.DefaultPointer(b.BlockSurveillance, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||||
@@ -65,7 +63,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
|||||||
return DNSBlacklist{
|
return DNSBlacklist{
|
||||||
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
|
||||||
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
BlockAds: gosettings.CopyPointer(b.BlockAds),
|
||||||
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
|
|
||||||
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
|
||||||
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
|
||||||
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
|
||||||
@@ -77,7 +74,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
|||||||
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||||
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
|
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
|
||||||
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
|
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
|
||||||
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
|
|
||||||
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
|
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
|
||||||
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||||
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||||
@@ -93,7 +89,6 @@ func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
|
|||||||
Client: client,
|
Client: client,
|
||||||
BlockMalicious: b.BlockMalicious,
|
BlockMalicious: b.BlockMalicious,
|
||||||
BlockAds: b.BlockAds,
|
BlockAds: b.BlockAds,
|
||||||
BlockSurveillance: b.BlockSurveillance,
|
|
||||||
AllowedHosts: b.AllowedHosts,
|
AllowedHosts: b.AllowedHosts,
|
||||||
AddBlockedHosts: b.AddBlockedHosts,
|
AddBlockedHosts: b.AddBlockedHosts,
|
||||||
AddBlockedIPs: b.AddBlockedIPs,
|
AddBlockedIPs: b.AddBlockedIPs,
|
||||||
@@ -110,7 +105,6 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
|||||||
|
|
||||||
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
|
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
|
||||||
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
|
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
|
||||||
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
|
|
||||||
|
|
||||||
if len(b.AllowedHosts) > 0 {
|
if len(b.AllowedHosts) > 0 {
|
||||||
allowedHostsNode := node.Append("Allowed hosts:")
|
allowedHostsNode := node.Append("Allowed hosts:")
|
||||||
@@ -156,12 +150,6 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
|
|
||||||
reader.RetroKeys("BLOCK_NSA"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
|
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
|||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
// no restriction on port
|
// no restriction on port
|
||||||
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
case providers.Custom, providers.Cyberghost, providers.HideMyAss,
|
||||||
providers.Ovpn, providers.Privatevpn, providers.Torguard:
|
providers.Privatevpn, providers.Torguard:
|
||||||
// no custom port allowed
|
// no custom port allowed
|
||||||
case providers.Expressvpn, providers.Fastestvpn,
|
case providers.Expressvpn, providers.Fastestvpn,
|
||||||
providers.Giganews, providers.Ipvanish,
|
providers.Giganews, providers.Ipvanish,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
|||||||
return errors.New("port forwarding password is empty")
|
return errors.New("port forwarding password is empty")
|
||||||
}
|
}
|
||||||
case providers.Protonvpn:
|
case providers.Protonvpn:
|
||||||
const maxPortsCount = 4
|
const maxPortsCount = 5
|
||||||
if p.PortsCount > maxPortsCount {
|
if p.PortsCount > maxPortsCount {
|
||||||
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
|
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
|||||||
providers.Ivpn,
|
providers.Ivpn,
|
||||||
providers.Mullvad,
|
providers.Mullvad,
|
||||||
providers.Nordvpn,
|
providers.Nordvpn,
|
||||||
providers.Ovpn,
|
|
||||||
providers.Protonvpn,
|
providers.Protonvpn,
|
||||||
providers.Surfshark,
|
providers.Surfshark,
|
||||||
providers.Windscribe,
|
providers.Windscribe,
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ type ServerSelection struct {
|
|||||||
// TorOnly is true if VPN servers without tor should
|
// TorOnly is true if VPN servers without tor should
|
||||||
// be filtered. This is used with ProtonVPN.
|
// be filtered. This is used with ProtonVPN.
|
||||||
TorOnly *bool `json:"tor_only"`
|
TorOnly *bool `json:"tor_only"`
|
||||||
// Dedicated is true if dedicated VPN servers should be chosen only.
|
|
||||||
// This is used with OVPN.
|
|
||||||
Dedicated *bool `json:"dedicated"`
|
|
||||||
// OpenVPN contains settings to select OpenVPN servers
|
// OpenVPN contains settings to select OpenVPN servers
|
||||||
// and the final connection.
|
// and the final connection.
|
||||||
OpenVPN OpenVPNSelection `json:"openvpn"`
|
OpenVPN OpenVPNSelection `json:"openvpn"`
|
||||||
@@ -275,8 +272,6 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
|
|||||||
return errors.New("secure core only filter is not supported")
|
return errors.New("secure core only filter is not supported")
|
||||||
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
|
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
|
||||||
return errors.New("tor only filter is not supported")
|
return errors.New("tor only filter is not supported")
|
||||||
case *settings.Dedicated && vpnServiceProvider != providers.Ovpn:
|
|
||||||
return errors.New("dedicated filter is not supported")
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -301,7 +296,6 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
|||||||
TorOnly: gosettings.CopyPointer(ss.TorOnly),
|
TorOnly: gosettings.CopyPointer(ss.TorOnly),
|
||||||
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
|
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
|
||||||
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
|
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
|
||||||
Dedicated: gosettings.CopyPointer(ss.Dedicated),
|
|
||||||
OpenVPN: ss.OpenVPN.copy(),
|
OpenVPN: ss.OpenVPN.copy(),
|
||||||
Wireguard: ss.Wireguard.copy(),
|
Wireguard: ss.Wireguard.copy(),
|
||||||
}
|
}
|
||||||
@@ -325,7 +319,6 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
|||||||
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
|
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
|
||||||
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
|
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
|
||||||
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
|
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
|
||||||
ss.Dedicated = gosettings.OverrideWithPointer(ss.Dedicated, other.Dedicated)
|
|
||||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||||
ss.Wireguard.overrideWith(other.Wireguard)
|
ss.Wireguard.overrideWith(other.Wireguard)
|
||||||
}
|
}
|
||||||
@@ -342,7 +335,6 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled
|
|||||||
defaultPortForwardOnly := portForwardingEnabled &&
|
defaultPortForwardOnly := portForwardingEnabled &&
|
||||||
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn)
|
||||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly)
|
||||||
ss.Dedicated = gosettings.DefaultPointer(ss.Dedicated, false)
|
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
ss.Wireguard.setDefaults()
|
ss.Wireguard.setDefaults()
|
||||||
}
|
}
|
||||||
@@ -418,10 +410,6 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Multi-hop only servers: yes")
|
node.Appendf("Multi-hop only servers: yes")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *ss.Dedicated {
|
|
||||||
node.Appendf("Dedicated servers: yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *ss.PortForwardOnly {
|
if *ss.PortForwardOnly {
|
||||||
node.Appendf("Port forwarding only servers: yes")
|
node.Appendf("Port forwarding only servers: yes")
|
||||||
}
|
}
|
||||||
@@ -513,12 +501,6 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ovpn only
|
|
||||||
ss.Dedicated, err = r.BoolPtr("SERVER_DEDICATED")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ss.OpenVPN.read(r)
|
err = ss.OpenVPN.read(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
| ├── Update period: every 24h0m0s
|
| ├── Update period: every 24h0m0s
|
||||||
| └── DNS filtering settings:
|
| └── DNS filtering settings:
|
||||||
| ├── Block malicious: yes
|
| ├── Block malicious: yes
|
||||||
| ├── Block ads: no
|
| └── Block ads: no
|
||||||
| └── Block surveillance: yes
|
|
||||||
├── Firewall settings:
|
├── Firewall settings:
|
||||||
| ├── Enabled: yes
|
| ├── Enabled: yes
|
||||||
| └── Iptables settings:
|
| └── Iptables settings:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
@@ -23,7 +22,7 @@ type WireguardSelection struct {
|
|||||||
// It can never be the zero value in the internal state.
|
// It can never be the zero value in the internal state.
|
||||||
EndpointIP netip.Addr `json:"endpoint_ip"`
|
EndpointIP netip.Addr `json:"endpoint_ip"`
|
||||||
// EndpointPort is a the server port to use for the VPN server.
|
// EndpointPort is a the server port to use for the VPN server.
|
||||||
// It is optional for VPN providers IVPN, Mullvad, Ovpn, Surfshark
|
// It is optional for VPN providers IVPN, Mullvad, Surfshark
|
||||||
// and Windscribe, and compulsory for the others.
|
// and Windscribe, and compulsory for the others.
|
||||||
// When optional, it can be set to 0 to indicate not use
|
// When optional, it can be set to 0 to indicate not use
|
||||||
// a custom endpoint port. It cannot be nil in the internal
|
// a custom endpoint port. It cannot be nil in the internal
|
||||||
@@ -41,9 +40,8 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate EndpointIP
|
// Validate EndpointIP
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
|
||||||
providers.Mullvad, providers.Nordvpn, providers.Ovpn,
|
providers.Mullvad, providers.Nordvpn, providers.Protonvpn,
|
||||||
providers.Protonvpn, providers.Surfshark,
|
providers.Surfshark, providers.Windscribe:
|
||||||
providers.Windscribe:
|
|
||||||
// endpoint IP addresses are baked in
|
// endpoint IP addresses are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
|
||||||
@@ -65,16 +63,12 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
if *w.EndpointPort != 0 {
|
if *w.EndpointPort != 0 {
|
||||||
return errors.New("endpoint port is set")
|
return errors.New("endpoint port is set")
|
||||||
}
|
}
|
||||||
case providers.Airvpn, providers.Ivpn, providers.Mullvad,
|
case providers.Airvpn, providers.Ivpn, providers.Mullvad, providers.Windscribe:
|
||||||
providers.Ovpn, providers.Windscribe:
|
|
||||||
// EndpointPort is optional and can be 0
|
// EndpointPort is optional and can be 0
|
||||||
if *w.EndpointPort == 0 {
|
if *w.EndpointPort == 0 {
|
||||||
break // no custom endpoint port set
|
break // no custom endpoint port set
|
||||||
}
|
}
|
||||||
if helpers.IsOneOf(vpnProvider,
|
if vpnProvider == providers.Mullvad {
|
||||||
providers.Mullvad,
|
|
||||||
providers.Ovpn,
|
|
||||||
) {
|
|
||||||
break // no restriction on custom endpoint port value
|
break // no restriction on custom endpoint port value
|
||||||
}
|
}
|
||||||
var allowed []uint16
|
var allowed []uint16
|
||||||
@@ -98,7 +92,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
|||||||
// Validate PublicKey
|
// Validate PublicKey
|
||||||
switch vpnProvider {
|
switch vpnProvider {
|
||||||
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
|
||||||
providers.Ovpn, providers.Surfshark, providers.Windscribe:
|
providers.Surfshark, providers.Windscribe:
|
||||||
// public keys are baked in
|
// public keys are baked in
|
||||||
case providers.Custom:
|
case providers.Custom:
|
||||||
if w.PublicKey == "" {
|
if w.PublicKey == "" {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const (
|
|||||||
Ivpn = "ivpn"
|
Ivpn = "ivpn"
|
||||||
Mullvad = "mullvad"
|
Mullvad = "mullvad"
|
||||||
Nordvpn = "nordvpn"
|
Nordvpn = "nordvpn"
|
||||||
Ovpn = "ovpn"
|
|
||||||
Perfectprivacy = "perfect privacy"
|
Perfectprivacy = "perfect privacy"
|
||||||
Privado = "privado"
|
Privado = "privado"
|
||||||
PrivateInternetAccess = "private internet access"
|
PrivateInternetAccess = "private internet access"
|
||||||
@@ -44,7 +43,6 @@ func All() []string {
|
|||||||
Ivpn,
|
Ivpn,
|
||||||
Mullvad,
|
Mullvad,
|
||||||
Nordvpn,
|
Nordvpn,
|
||||||
Ovpn,
|
|
||||||
Perfectprivacy,
|
Perfectprivacy,
|
||||||
Privado,
|
Privado,
|
||||||
PrivateInternetAccess,
|
PrivateInternetAccess,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -97,25 +96,24 @@ func saveData(ctx context.Context, binary string) (data string, err error) {
|
|||||||
}
|
}
|
||||||
return "", fmt.Errorf("running %s-save: %w", binary, err)
|
return "", fmt.Errorf("running %s-save: %w", binary, err)
|
||||||
}
|
}
|
||||||
err = checkData(string(output))
|
return filterData(output)
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("checking saved data: %w", err)
|
|
||||||
}
|
|
||||||
return string(output), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkData(data string) error {
|
func filterData(cmdOutput []byte) (filtered string, err error) {
|
||||||
scanner := bufio.NewScanner(strings.NewReader(data))
|
lines := strings.Split(string(cmdOutput), "\n")
|
||||||
i := 0
|
filteredLines := make([]string, 0, len(lines))
|
||||||
for scanner.Scan() {
|
for _, line := range lines {
|
||||||
line := scanner.Text()
|
switch {
|
||||||
if strings.HasPrefix(line, "[unsupported") {
|
case strings.HasPrefix(line, ":DOCKER_OUTPUT"),
|
||||||
return fmt.Errorf("unsupported revision marker found in line %d: %s", i+1, line)
|
strings.HasPrefix(line, ":DOCKER_POSTROUTING"),
|
||||||
|
strings.HasPrefix(line, "-A DOCKER_OUTPUT"),
|
||||||
|
strings.HasPrefix(line, "-A DOCKER_POSTROUTING"):
|
||||||
|
// Do not touch (aka save and restore) NAT rules added by Docker
|
||||||
|
continue
|
||||||
|
case strings.Contains(line, "[unsupported revision]"):
|
||||||
|
return "", fmt.Errorf("mismatch container iptables-save and kernel: %s", line)
|
||||||
}
|
}
|
||||||
i++
|
filteredLines = append(filteredLines, line)
|
||||||
}
|
}
|
||||||
if scanner.Err() != nil {
|
return strings.Join(filteredLines, "\n"), nil
|
||||||
return fmt.Errorf("scanning data: %w", scanner.Err())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,7 +278,6 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
|
|||||||
appendOrDelete(remove), interfaceFlag, destinationPort),
|
appendOrDelete(remove), interfaceFlag, destinationPort),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
restore(ctx) // just in case
|
|
||||||
errMessage := err.Error()
|
errMessage := err.Error()
|
||||||
if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") {
|
if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") {
|
||||||
if !remove {
|
if !remove {
|
||||||
@@ -286,6 +285,7 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
restore(ctx)
|
||||||
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
|
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
|
||||||
sourcePort, destinationPort, intf, err)
|
sourcePort, destinationPort, intf, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,8 @@ type Server struct {
|
|||||||
SecureCore bool `json:"secure_core,omitempty"`
|
SecureCore bool `json:"secure_core,omitempty"`
|
||||||
Tor bool `json:"tor,omitempty"`
|
Tor bool `json:"tor,omitempty"`
|
||||||
PortForward bool `json:"port_forward,omitempty"`
|
PortForward bool `json:"port_forward,omitempty"`
|
||||||
Dedicated bool `json:"dedicated,omitempty"`
|
|
||||||
Keep bool `json:"keep,omitempty"`
|
Keep bool `json:"keep,omitempty"`
|
||||||
IPs []netip.Addr `json:"ips,omitempty"`
|
IPs []netip.Addr `json:"ips,omitempty"`
|
||||||
PortsTCP []uint16 `json:"ports_tcp,omitempty"`
|
|
||||||
PortsUDP []uint16 `json:"ports_udp,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HasMinimumInformation() (err error) {
|
func (s *Server) HasMinimumInformation() (err error) {
|
||||||
|
|||||||
@@ -29,19 +29,16 @@ func (r *Runner) Run(ctx context.Context, errCh chan<- error, ready chan<- struc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
|
||||||
streamDone := make(chan struct{})
|
streamDone := make(chan struct{})
|
||||||
go streamLines(streamCtx, streamDone, r.logger,
|
go streamLines(streamDone, r.logger,
|
||||||
stdoutLines, stderrLines, ready)
|
stdoutLines, stderrLines, ready)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
<-waitError
|
<-waitError
|
||||||
streamCancel()
|
|
||||||
<-streamDone
|
<-streamDone
|
||||||
errCh <- ctx.Err()
|
errCh <- ctx.Err()
|
||||||
case err := <-waitError:
|
case err := <-waitError:
|
||||||
streamCancel()
|
|
||||||
<-streamDone
|
<-streamDone
|
||||||
errCh <- err
|
errCh <- err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
package openvpn
|
package openvpn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func streamLines(ctx context.Context, done chan<- struct{},
|
func streamLines(done chan<- struct{},
|
||||||
logger Logger, stdout, stderr <-chan string,
|
logger Logger, stdout, stderr <-chan string,
|
||||||
tunnelReady chan<- struct{},
|
tunnelReady chan<- struct{},
|
||||||
) {
|
) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
var line string
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
var line string
|
||||||
|
var ok bool
|
||||||
errLine := false
|
errLine := false
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case line, ok = <-stdout:
|
||||||
return
|
if ok {
|
||||||
case line = <-stdout:
|
break
|
||||||
case line = <-stderr:
|
}
|
||||||
errLine = true
|
if stderr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stdout = nil
|
||||||
|
case line, ok = <-stderr:
|
||||||
|
if ok {
|
||||||
|
errLine = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if stdout == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stderr = nil
|
||||||
}
|
}
|
||||||
line, level := processLogLine(line)
|
line, level := processLogLine(line)
|
||||||
if line == "" {
|
if line == "" {
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
|||||||
}
|
}
|
||||||
portsString := strings.Join(portStrings, ",")
|
portsString := strings.Join(portStrings, ",")
|
||||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||||
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
|
var firstPort string
|
||||||
|
if len(portStrings) > 0 {
|
||||||
|
firstPort = portStrings[0]
|
||||||
|
}
|
||||||
|
commandString = strings.ReplaceAll(commandString, "{{PORT}}", firstPort)
|
||||||
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
|
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
|
||||||
return cmder.RunAndLog(ctx, commandString, logger)
|
return cmder.RunAndLog(ctx, commandString, logger)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -59,6 +60,10 @@ func (s *Service) SetPortsForwarded(ctx context.Context, ports []uint16) (err er
|
|||||||
s.portMutex.Lock()
|
s.portMutex.Lock()
|
||||||
defer s.portMutex.Unlock()
|
defer s.portMutex.Unlock()
|
||||||
|
|
||||||
|
if s.settings.PortForwarder != nil {
|
||||||
|
return errors.New("setting port forwarded at runtime is not supported with internally running port forwarding code")
|
||||||
|
}
|
||||||
|
|
||||||
slices.Sort(ports)
|
slices.Sort(ports)
|
||||||
if slices.Equal(s.ports, ports) {
|
if slices.Equal(s.ports, ports) {
|
||||||
return nil
|
return nil
|
||||||
@@ -78,7 +83,5 @@ func (s *Service) SetPortsForwarded(ctx context.Context, ports []uint16) (err er
|
|||||||
return fmt.Errorf("handling new ports: %w", err)
|
return fmt.Errorf("handling new ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("updated: " + portsToString(s.ports))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (s *Settings) Validate(forStartup bool) (err error) {
|
|||||||
return errors.New("password not set")
|
return errors.New("password not set")
|
||||||
}
|
}
|
||||||
case providers.Protonvpn:
|
case providers.Protonvpn:
|
||||||
const maxPortsCount = 4
|
const maxPortsCount = 5
|
||||||
if s.PortsCount > maxPortsCount {
|
if s.PortsCount > maxPortsCount {
|
||||||
return fmt.Errorf("ports count too high: %d > %d", s.PortsCount, maxPortsCount)
|
return fmt.Errorf("ports count too high: %d > %d", s.PortsCount, maxPortsCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,13 +92,9 @@ func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[ui
|
|||||||
s.logger.Info(portPairsToString(internalToExternalPorts))
|
s.logger.Info(portPairsToString(internalToExternalPorts))
|
||||||
|
|
||||||
externalPorts := slices.Collect(maps.Values(internalToExternalPorts))
|
externalPorts := slices.Collect(maps.Values(internalToExternalPorts))
|
||||||
autoRedirectionNeeded := false
|
|
||||||
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
||||||
for internal, external := range internalToExternalPorts {
|
for internal, external := range internalToExternalPorts {
|
||||||
externalToInternalPorts[external] = internal
|
externalToInternalPorts[external] = internal
|
||||||
if internal != external {
|
|
||||||
autoRedirectionNeeded = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
slices.Sort(externalPorts)
|
slices.Sort(externalPorts)
|
||||||
userRedirectionEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
userRedirectionEnabled := !slices.Equal(s.settings.ListeningPorts, []uint16{0})
|
||||||
@@ -109,23 +105,21 @@ func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[ui
|
|||||||
return fmt.Errorf("allowing port in firewall: %w", err)
|
return fmt.Errorf("allowing port in firewall: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourcePort, destinationPort uint16
|
var destinationPort uint16
|
||||||
switch {
|
switch {
|
||||||
case userRedirectionEnabled: // precedence over auto redirection
|
case userRedirectionEnabled: // precedence over auto redirection
|
||||||
sourcePort = externalToInternalPorts[port]
|
|
||||||
destinationPort = s.settings.ListeningPorts[i]
|
destinationPort = s.settings.ListeningPorts[i]
|
||||||
case autoRedirectionNeeded:
|
case port != internalPort: // auto redirection needed, source and destination ports differ
|
||||||
sourcePort = externalToInternalPorts[port]
|
|
||||||
destinationPort = port
|
destinationPort = port
|
||||||
default:
|
default:
|
||||||
// No redirection needed, source and destination ports are the same.
|
// No redirection needed, source and destination ports are the same.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, sourcePort, destinationPort)
|
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, internalPort, destinationPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("redirecting port %d to %d in firewall: %w",
|
return fmt.Errorf("redirecting port %d to %d in firewall: %w",
|
||||||
sourcePort, destinationPort, err)
|
internalPort, destinationPort, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
@@ -65,7 +66,11 @@ func modifyConfig(lines []string, connection models.Connection,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add values
|
// Add values
|
||||||
modified = append(modified, "proto "+connection.Protocol)
|
protocol := connection.Protocol
|
||||||
|
if protocol == constants.TCP {
|
||||||
|
protocol = "tcp-client"
|
||||||
|
}
|
||||||
|
modified = append(modified, "proto "+protocol)
|
||||||
modified = append(modified, fmt.Sprintf("remote %s %d", connection.IP, connection.Port))
|
modified = append(modified, fmt.Sprintf("remote %s %d", connection.IP, connection.Port))
|
||||||
modified = append(modified, "dev "+settings.Interface)
|
modified = append(modified, "dev "+settings.Interface)
|
||||||
modified = append(modified, "mute-replay-warnings")
|
modified = append(modified, "mute-replay-warnings")
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
|
|
||||||
connection models.Connection, err error,
|
|
||||||
) {
|
|
||||||
defaults := utils.NewConnectionDefaults(443, 1194, 9929) //nolint:mnd
|
|
||||||
return utils.GetConnection(p.Name(),
|
|
||||||
p.storage, selection, defaults, ipv6Supported, p.connPicker)
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Provider_GetConnection(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const provider = providers.Ovpn
|
|
||||||
|
|
||||||
errTest := errors.New("test error")
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
filteredServers []models.Server
|
|
||||||
storageErr error
|
|
||||||
selection settings.ServerSelection
|
|
||||||
ipv6Supported bool
|
|
||||||
connection models.Connection
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"error": {
|
|
||||||
storageErr: errTest,
|
|
||||||
errWrapped: errTest,
|
|
||||||
errMessage: "filtering servers: test error",
|
|
||||||
},
|
|
||||||
"default_openvpn_tcp_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.OpenVPN,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 443,
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_openvpn_udp_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.OpenVPN,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 1194,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_wireguard_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x"},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.Wireguard,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 9929,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
PubKey: "x",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"default_multihop_port": {
|
|
||||||
filteredServers: []models.Server{
|
|
||||||
{IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})}, WgPubKey: "x", PortsUDP: []uint16{30044}},
|
|
||||||
},
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
}.WithDefaults(provider),
|
|
||||||
connection: models.Connection{
|
|
||||||
Type: vpn.Wireguard,
|
|
||||||
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
|
|
||||||
Port: 30044,
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
PubKey: "x",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
|
|
||||||
storage := common.NewMockStorage(ctrl)
|
|
||||||
storage.EXPECT().FilterServers(provider, testCase.selection).
|
|
||||||
Return(testCase.filteredServers, testCase.storageErr)
|
|
||||||
|
|
||||||
client := (*http.Client)(nil)
|
|
||||||
provider := New(storage, client)
|
|
||||||
|
|
||||||
connection, err := provider.GetConnection(testCase.selection, testCase.ipv6Supported)
|
|
||||||
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.EqualError(t, err, testCase.errMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.connection, connection)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) OpenVPNConfig(connection models.Connection,
|
|
||||||
settings settings.OpenVPN, ipv6Supported bool,
|
|
||||||
) (lines []string) {
|
|
||||||
providerSettings := utils.OpenVPNProviderSettings{
|
|
||||||
AuthUserPass: true,
|
|
||||||
RemoteCertTLS: true,
|
|
||||||
Ciphers: []string{
|
|
||||||
openvpn.AES256gcm,
|
|
||||||
openvpn.AES256cbc,
|
|
||||||
openvpn.AES128gcm,
|
|
||||||
openvpn.Chacha20Poly1305,
|
|
||||||
},
|
|
||||||
CAs: []string{
|
|
||||||
"MIIEfTCCA2WgAwIBAgIJAK2aIWqpLj1/MA0GCSqGSIb3DQEBBQUAMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTAeFw0xNDA4MTcxODIxMjlaFw0zNDA4MTIxODIxMjlaMIGFMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMRIwEAYDVQQHEwlTdG9ja2hvbG0xHDAaBgNVBAsTE0Zpcm1hIERhdmlkIFdpYmVyZ2gxEzARBgNVBAMTCm92cG4uc2UgY2ExGzAZBgkqhkiG9w0BCQEWDGluZm9Ab3Zwbi5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR+aP4GTuZwurZuOA2NYzMfqKyZi/TJcLEPlGTB/b4CWA9bTd8f0pHPrDAZsXIEayxxB58BIFNDNiybnbO15JN/QwlsqmA+aZX6mCSkScs/rRwasM6LDo8iGx+KmYEqAgzziONGbCMnlO+OaarXte7LhZ9X6Z/bryu4xq/i1v3raak13kXsrogtu4iDzxqJE/QhbNOi0yhCdlm5RYQjmlKGdPB9pNTgcakVI4HcngRYMzBlrGin0YkvWCdpx5FrDNeld7BSWrJMNYyvd+buaid0Fu1T9/P/Srj/8AiabKoaDyiGFbZdTnGfK+04lWRvwAmvazpqbUt5Omw634jJDuMCAwEAAaOB7TCB6jAdBgNVHQ4EFgQUEvJcHHcTiDtu7bAyZw+xaqg+xdIwgboGA1UdIwSBsjCBr4AUEvJcHHcTiDtu7bAyZw+xaqg+xdKhgYukgYgwgYUxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0xEjAQBgNVBAcTCVN0b2NraG9sbTEcMBoGA1UECxMTRmlybWEgRGF2aWQgV2liZXJnaDETMBEGA1UEAxMKb3Zwbi5zZSBjYTEbMBkGCSqGSIb3DQEJARYMaW5mb0BvdnBuLnNlggkArZohaqkuPX8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJmID6OyBJbV7ayPPgquojF+FICuDdOfGVKP828cyISxcbVA04VpD0QLYVb0k9pFUx0NbgX2SvRTiFhP7LcyS1HV9s+XLCb2WItPPsrdRTwtqU2n3TlCEzWA3WOcOCtT6JSkv1eelmx1JnP0gYJrDvDvRYBFctwWhtE0bineSQkZwN6980zkknADLAiHpeZSu/AMx7CGTwA6SmoFvpNBmHXDcfe/9ZqbbYfUfyPNe+0JbMrcv1elKi+6wlEkHFaEBphiZwGEbOX1CjUMcQFgW/cIp3n50Eiyx6ktuqimhyb59P4Nw8gqH452tTtE4MM/brA5y0Q0WFBRBojfZIbGWWQ==", //nolint:lll
|
|
||||||
},
|
|
||||||
TLSAuth: "81782767e4d59c4464cc5d1896f1cf6015017d53ac62e2e3b94b889e00b2c69ddc01944fe1c6d895b4d80540502eb71910b8d785c9efa9e3182343532adffe1cfbb7bb6eae39c502da2748edf0fb89b8a20b0a1085cc1f06135037881bc0c4ad8f2c0f4f72d2ab466fb54af3d8264c5fddeb0f21aa0ca41863678f5fc4c44de4ca0926b36dfddc42c6f2fabd1694bdc8215b2d223b9c21dc6734c2c778093187afb8c33403b228b9af68b540c284f6d183bcc88bd41d47bd717996e499ce1cbbfa768a9723c19c58314c4d19cfed82e543ee92e73d38ad26d4fbec231c0f9f3b30773a5c87792e9bc7c34e8d7611002ebedd044e48a0f1f96527bfdcc940aa09", //nolint:lll
|
|
||||||
KeyDirection: "1",
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(connection.Hostname, "singapore.ovpn.com") {
|
|
||||||
providerSettings.TLSCrypt = providerSettings.TLSAuth
|
|
||||||
providerSettings.TLSAuth = ""
|
|
||||||
providerSettings.KeyDirection = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package ovpn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/ovpn/updater"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Provider struct {
|
|
||||||
storage common.Storage
|
|
||||||
connPicker *utils.ConnectionPicker
|
|
||||||
common.Fetcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(storage common.Storage, client *http.Client) *Provider {
|
|
||||||
return &Provider{
|
|
||||||
storage: storage,
|
|
||||||
connPicker: utils.NewConnectionPicker(),
|
|
||||||
Fetcher: updater.New(client),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Name() string {
|
|
||||||
return providers.Ovpn
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type apiData struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
DataCenters []apiDataCenter `json:"datacenters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiDataCenter struct {
|
|
||||||
City string `json:"city"`
|
|
||||||
CountryName string `json:"country_name"`
|
|
||||||
Servers []apiServer `json:"servers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiServer struct {
|
|
||||||
IP netip.Addr `json:"ip"`
|
|
||||||
Ptr string `json:"ptr"` // hostname
|
|
||||||
Online bool `json:"online"`
|
|
||||||
// PublicKey is for the Standard Shared Entry Point
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
// PublicKeyIPv4 is for the Public / Dedicated IP Entry Point
|
|
||||||
PublicKeyIPv4 string `json:"public_key_ipv4"`
|
|
||||||
WireguardPorts []uint16 `json:"wireguard_ports"`
|
|
||||||
MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"`
|
|
||||||
MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAPI(ctx context.Context, client *http.Client) (
|
|
||||||
data apiData, err error,
|
|
||||||
) {
|
|
||||||
const url = "https://www.ovpn.com/v2/api/client/entry"
|
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return data, fmt.Errorf("HTTP response status code is not OK: %d %s",
|
|
||||||
response.StatusCode, response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(response.Body)
|
|
||||||
err = decoder.Decode(&data)
|
|
||||||
if err != nil {
|
|
||||||
_ = response.Body.Close()
|
|
||||||
return data, fmt.Errorf("decoding response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = response.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return data, fmt.Errorf("closing response body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *apiDataCenter) validate() (err error) {
|
|
||||||
conditionalErrors := []conditionalError{
|
|
||||||
{err: "city is not set", condition: a.City == ""},
|
|
||||||
{err: "country name is not set", condition: a.CountryName == ""},
|
|
||||||
{err: "servers array is not set", condition: len(a.Servers) == 0},
|
|
||||||
}
|
|
||||||
err = collectErrors(conditionalErrors)
|
|
||||||
if err != nil {
|
|
||||||
var dataCenterSetFields []string
|
|
||||||
if a.CountryName != "" {
|
|
||||||
dataCenterSetFields = append(dataCenterSetFields, a.CountryName)
|
|
||||||
}
|
|
||||||
if a.City != "" {
|
|
||||||
dataCenterSetFields = append(dataCenterSetFields, a.City)
|
|
||||||
}
|
|
||||||
if len(dataCenterSetFields) == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fmt.Errorf("data center %s: %w",
|
|
||||||
strings.Join(dataCenterSetFields, ", "), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, server := range a.Servers {
|
|
||||||
err = server.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("datacenter %s, %s: server %d of %d: %w",
|
|
||||||
a.CountryName, a.City, i+1, len(a.Servers), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *apiServer) validate() (err error) {
|
|
||||||
const defaultWireguardPort = 9929
|
|
||||||
conditionalErrors := []conditionalError{
|
|
||||||
{err: "ip address is not set", condition: !a.IP.IsValid()},
|
|
||||||
{err: "hostname field is not set", condition: a.Ptr == ""},
|
|
||||||
{err: "public key field is not set", condition: a.PublicKey == ""},
|
|
||||||
{err: "public key IPv4 field is not set", condition: a.PublicKeyIPv4 == ""},
|
|
||||||
{err: "wireguard ports array is not set", condition: len(a.WireguardPorts) == 0},
|
|
||||||
{
|
|
||||||
err: "wireguard port is not the default 9929",
|
|
||||||
condition: len(a.WireguardPorts) != 1 || a.WireguardPorts[0] != defaultWireguardPort,
|
|
||||||
},
|
|
||||||
{err: "multihop OpenVPN port is not set", condition: a.MultiHopOpenvpnPort == 0},
|
|
||||||
{err: "multihop WireGuard port is not set", condition: a.MultiHopWireguardPort == 0},
|
|
||||||
}
|
|
||||||
err = collectErrors(conditionalErrors)
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return nil
|
|
||||||
case a.Ptr != "":
|
|
||||||
return fmt.Errorf("server %s: %w", a.Ptr, err)
|
|
||||||
case a.IP.IsValid():
|
|
||||||
return fmt.Errorf("server %s: %w", a.IP.String(), err)
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type conditionalError struct {
|
|
||||||
err string
|
|
||||||
condition bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectErrors(conditionalErrors []conditionalError) (err error) {
|
|
||||||
errs := make([]string, 0, len(conditionalErrors))
|
|
||||||
for _, conditionalError := range conditionalErrors {
|
|
||||||
if !conditionalError.condition {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errs = append(errs, conditionalError.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(strings.Join(errs, "; "))
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_fetchAPI(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
responseStatus int
|
|
||||||
responseBody io.ReadCloser
|
|
||||||
data apiData
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
"http response status not ok": {
|
|
||||||
responseStatus: http.StatusNoContent,
|
|
||||||
err: errors.New("HTTP response status code is not OK: 204 No Content"),
|
|
||||||
},
|
|
||||||
"nil body": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
err: errors.New("decoding response body: EOF"),
|
|
||||||
},
|
|
||||||
"no server": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: io.NopCloser(strings.NewReader(`{}`)),
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: io.NopCloser(strings.NewReader(`{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"slug": "vienna",
|
|
||||||
"city": "Vienna",
|
|
||||||
"country": "AT",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"pools": [
|
|
||||||
"pool-1.prd.at.vienna.ovpn.com"
|
|
||||||
],
|
|
||||||
"ping_address": "37.120.212.227",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"name": "VPN44 - Vienna",
|
|
||||||
"online": true,
|
|
||||||
"load": 8,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [
|
|
||||||
9929
|
|
||||||
],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`)),
|
|
||||||
data: apiData{
|
|
||||||
Success: true,
|
|
||||||
DataCenters: []apiDataCenter{
|
|
||||||
{CountryName: "Austria", City: "Vienna", Servers: []apiServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("37.120.212.227"),
|
|
||||||
Ptr: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
Online: true,
|
|
||||||
PublicKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
PublicKeyIPv4: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
WireguardPorts: []uint16{9929},
|
|
||||||
MultiHopOpenvpnPort: 20044,
|
|
||||||
MultiHopWireguardPort: 30044,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
assert.Equal(t, http.MethodGet, r.Method)
|
|
||||||
assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry")
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: testCase.responseStatus,
|
|
||||||
Status: http.StatusText(testCase.responseStatus),
|
|
||||||
Body: testCase.responseBody,
|
|
||||||
}, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := fetchAPI(ctx, client)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.data, data)
|
|
||||||
if testCase.err != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
|
||||||
|
|
||||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
return f(r)
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|
||||||
servers []models.Server, err error,
|
|
||||||
) {
|
|
||||||
data, err := fetchAPI(ctx, u.client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fetching API: %w", err)
|
|
||||||
} else if !data.Success {
|
|
||||||
return nil, errors.New("response success field is false")
|
|
||||||
}
|
|
||||||
|
|
||||||
for dataCenterIndex, dataCenter := range data.DataCenters {
|
|
||||||
err = dataCenter.validate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("validating data center %d of %d: %w",
|
|
||||||
dataCenterIndex+1, len(data.DataCenters), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, apiServer := range dataCenter.Servers {
|
|
||||||
if !apiServer.Online {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
baseServer := models.Server{
|
|
||||||
Country: dataCenter.CountryName,
|
|
||||||
City: dataCenter.City,
|
|
||||||
Hostname: apiServer.Ptr,
|
|
||||||
IPs: []netip.Addr{apiServer.IP},
|
|
||||||
}
|
|
||||||
openVPNServer := baseServer
|
|
||||||
openVPNServer.VPN = vpn.OpenVPN
|
|
||||||
openVPNServer.TCP = true
|
|
||||||
openVPNServer.UDP = true
|
|
||||||
multiHopOpenVPNServer := openVPNServer
|
|
||||||
multiHopOpenVPNServer.MultiHop = true
|
|
||||||
multiHopOpenVPNServer.PortsTCP = []uint16{apiServer.MultiHopOpenvpnPort}
|
|
||||||
multiHopOpenVPNServer.PortsUDP = []uint16{apiServer.MultiHopOpenvpnPort}
|
|
||||||
servers = append(servers, openVPNServer, multiHopOpenVPNServer)
|
|
||||||
|
|
||||||
wireguardServer := baseServer
|
|
||||||
wireguardServer.VPN = vpn.Wireguard
|
|
||||||
wireguardServer.WgPubKey = apiServer.PublicKey
|
|
||||||
multiHopWireguardServer := wireguardServer
|
|
||||||
multiHopWireguardServer.MultiHop = true
|
|
||||||
multiHopWireguardServer.PortsUDP = []uint16{apiServer.MultiHopWireguardPort}
|
|
||||||
dedicatedWireguardServer := wireguardServer
|
|
||||||
dedicatedWireguardServer.WgPubKey = apiServer.PublicKeyIPv4
|
|
||||||
dedicatedWireguardServer.Dedicated = true
|
|
||||||
dedicatedMultiHopWireguardServer := multiHopWireguardServer
|
|
||||||
dedicatedMultiHopWireguardServer.WgPubKey = apiServer.PublicKeyIPv4
|
|
||||||
dedicatedMultiHopWireguardServer.Dedicated = true
|
|
||||||
servers = append(servers,
|
|
||||||
wireguardServer,
|
|
||||||
multiHopWireguardServer,
|
|
||||||
dedicatedWireguardServer,
|
|
||||||
dedicatedMultiHopWireguardServer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(servers) < minServers {
|
|
||||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
|
||||||
common.ErrNotEnoughServers, len(servers), minServers)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(models.SortableServers(servers))
|
|
||||||
|
|
||||||
return servers, nil
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/common"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Updater_FetchServers(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
|
||||||
// Inputs
|
|
||||||
minServers int
|
|
||||||
|
|
||||||
// From API
|
|
||||||
responseStatus int
|
|
||||||
responseBody string
|
|
||||||
|
|
||||||
// Output
|
|
||||||
servers []models.Server
|
|
||||||
errWrapped error
|
|
||||||
errMessage string
|
|
||||||
}{
|
|
||||||
"http_response_error": {
|
|
||||||
responseStatus: http.StatusNoContent,
|
|
||||||
errMessage: "fetching API: HTTP response status code is not OK: 204 No Content",
|
|
||||||
},
|
|
||||||
"success_field_false": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{"success": false}`,
|
|
||||||
errMessage: "response success field is false",
|
|
||||||
},
|
|
||||||
"validation_failed": {
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"city": "Vienna",
|
|
||||||
"servers": [
|
|
||||||
{}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
errMessage: "validating data center 1 of 1: data center Vienna: country name is not set",
|
|
||||||
},
|
|
||||||
"not_enough_servers": {
|
|
||||||
minServers: 7,
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"city": "Vienna",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"online": true,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [9929],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
errWrapped: common.ErrNotEnoughServers,
|
|
||||||
// Wireguard + dedicated Wireguard + Wireguard multi-hop +
|
|
||||||
// dedicated Wireguard multi-hop + OpenVPN + OpenVPN multi-hop
|
|
||||||
errMessage: "not enough servers found: 6 and expected at least 7",
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
minServers: 4,
|
|
||||||
responseBody: `{
|
|
||||||
"success": true,
|
|
||||||
"datacenters": [
|
|
||||||
{
|
|
||||||
"slug": "vienna",
|
|
||||||
"city": "Vienna",
|
|
||||||
"country": "AT",
|
|
||||||
"country_name": "Austria",
|
|
||||||
"pools": [
|
|
||||||
"pool-1.prd.at.vienna.ovpn.com"
|
|
||||||
],
|
|
||||||
"ping_address": "37.120.212.227",
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.227",
|
|
||||||
"ptr": "vpn44.prd.vienna.ovpn.com",
|
|
||||||
"name": "VPN44 - Vienna",
|
|
||||||
"online": true,
|
|
||||||
"load": 8,
|
|
||||||
"public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [
|
|
||||||
9929
|
|
||||||
],
|
|
||||||
"multihop_openvpn_port": 20044,
|
|
||||||
"multihop_wireguard_port": 30044
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ip": "37.120.212.228",
|
|
||||||
"ptr": "vpn45.prd.vienna.ovpn.com",
|
|
||||||
"online": false,
|
|
||||||
"public_key": "r93LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
"public_key_ipv4": "wGbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
"wireguard_ports": [9929],
|
|
||||||
"multihop_openvpn_port": 20045,
|
|
||||||
"multihop_wireguard_port": 30045
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
responseStatus: http.StatusOK,
|
|
||||||
servers: []models.Server{
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
UDP: true,
|
|
||||||
TCP: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
UDP: true,
|
|
||||||
TCP: true,
|
|
||||||
MultiHop: true,
|
|
||||||
PortsTCP: []uint16{20044},
|
|
||||||
PortsUDP: []uint16{20044},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
WgPubKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=",
|
|
||||||
MultiHop: true,
|
|
||||||
PortsUDP: []uint16{30044},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
Dedicated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Country: "Austria",
|
|
||||||
City: "Vienna",
|
|
||||||
Hostname: "vpn44.prd.vienna.ovpn.com",
|
|
||||||
IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")},
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=",
|
|
||||||
MultiHop: true,
|
|
||||||
Dedicated: true,
|
|
||||||
PortsUDP: []uint16{30044},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, testCase := range testCases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
assert.Equal(t, http.MethodGet, r.Method)
|
|
||||||
assert.Equal(t, r.URL.String(), "https://www.ovpn.com/v2/api/client/entry")
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: testCase.responseStatus,
|
|
||||||
Status: http.StatusText(testCase.responseStatus),
|
|
||||||
Body: io.NopCloser(strings.NewReader(testCase.responseBody)),
|
|
||||||
}, nil
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
updater := &Updater{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
servers, err := updater.FetchServers(ctx, testCase.minServers)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.servers, servers)
|
|
||||||
if testCase.errMessage == "" {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
assert.Contains(t, err.Error(), testCase.errMessage)
|
|
||||||
}
|
|
||||||
if testCase.errWrapped != nil {
|
|
||||||
assert.ErrorIs(t, err, testCase.errWrapped)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Updater struct {
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(client *http.Client) *Updater {
|
|
||||||
return &Updater{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,14 +13,14 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const nonSymmetricPortStart uint16 = 56789
|
|
||||||
|
|
||||||
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
||||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||||
internalToExternalPorts map[uint16]uint16, err error,
|
internalToExternalPorts map[uint16]uint16, err error,
|
||||||
) {
|
) {
|
||||||
if !objects.CanPortForward {
|
if !objects.CanPortForward {
|
||||||
return nil, errors.New("server does not support port forwarding")
|
return nil, errors.New("server does not support port forwarding")
|
||||||
|
} else if objects.PortsCount == 0 {
|
||||||
|
return nil, nil //nolint:nilnil
|
||||||
}
|
}
|
||||||
|
|
||||||
client := natpmp.New()
|
client := natpmp.New()
|
||||||
@@ -39,38 +40,75 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
|||||||
logger := objects.Logger
|
logger := objects.Logger
|
||||||
|
|
||||||
logger.Debug("gateway external IPv4 address is " + externalIPv4Address.String())
|
logger.Debug("gateway external IPv4 address is " + externalIPv4Address.String())
|
||||||
const externalPort = 0
|
|
||||||
const lifetime = 60 * time.Second
|
|
||||||
|
|
||||||
p.internalToExternalPorts = make(map[uint16]uint16, objects.PortsCount)
|
p.internalToExternalPorts = make(map[uint16]uint16, objects.PortsCount)
|
||||||
for i := range objects.PortsCount {
|
const lifetime = 60 * time.Second
|
||||||
internalPort := nonSymmetricPortStart + i
|
|
||||||
protoToInternalPort := map[string]uint16{
|
|
||||||
"udp": 0,
|
|
||||||
"tcp": 0,
|
|
||||||
}
|
|
||||||
protoToExternalPort := maps.Clone(protoToInternalPort)
|
|
||||||
for protocol := range protoToExternalPort {
|
|
||||||
_, assignedInternalPort, assignedExternalPort, assignedLifetime, err := client.AddPortMapping(
|
|
||||||
ctx, objects.Gateway, protocol, internalPort, externalPort, lifetime)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("adding %d/%d %s port mapping: %w",
|
|
||||||
i+1, objects.PortsCount, strings.ToUpper(protocol), err)
|
|
||||||
}
|
|
||||||
checkLifetime(logger, strings.ToUpper(protocol), lifetime, assignedLifetime)
|
|
||||||
checkInternalPort(logger, internalPort, assignedInternalPort)
|
|
||||||
protoToInternalPort[protocol] = assignedInternalPort
|
|
||||||
protoToExternalPort[protocol] = assignedExternalPort
|
|
||||||
}
|
|
||||||
|
|
||||||
checkInternalPorts(logger, protoToInternalPort["udp"], protoToInternalPort["tcp"])
|
// Only one port can be a symmetric mapping
|
||||||
checkExternalPorts(logger, protoToExternalPort["udp"], protoToExternalPort["tcp"])
|
const internalPort, externalPort = 0, 1
|
||||||
p.internalToExternalPorts[protoToInternalPort["tcp"]] = protoToExternalPort["tcp"]
|
_, assignedExternalPort, err := addPortMappingTCPUDP(ctx,
|
||||||
|
client, logger, objects.Gateway, internalPort, externalPort, lifetime)
|
||||||
|
// Note the returned assignedInternalPort is always 0 in this case
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adding first port mapping: %w", err)
|
||||||
|
}
|
||||||
|
p.internalToExternalPorts[assignedExternalPort] = assignedExternalPort
|
||||||
|
|
||||||
|
// Extra ports must be non-symmetric, meaning that the internal port is
|
||||||
|
// different from the external port.
|
||||||
|
const nonSymmetricPortStart = uint16(56789)
|
||||||
|
nonSymmetricPortStartMinusOne := nonSymmetricPortStart - 1
|
||||||
|
if _, ok := p.internalToExternalPorts[nonSymmetricPortStart]; ok {
|
||||||
|
nonSymmetricPortStartMinusOne++
|
||||||
|
}
|
||||||
|
for i := uint16(1); i < objects.PortsCount; i++ {
|
||||||
|
internalPort := nonSymmetricPortStartMinusOne + i
|
||||||
|
const externalPort = 0
|
||||||
|
assignedInternalPort, assignedExternalPort, err := addPortMappingTCPUDP(ctx,
|
||||||
|
client, logger, objects.Gateway, internalPort, externalPort, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adding %d/%d port mapping: %w", i+1, objects.PortsCount, err)
|
||||||
|
}
|
||||||
|
p.internalToExternalPorts[assignedInternalPort] = assignedExternalPort
|
||||||
}
|
}
|
||||||
|
|
||||||
return maps.Clone(p.internalToExternalPorts), nil
|
return maps.Clone(p.internalToExternalPorts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addPortMappingTCPUDP(ctx context.Context, client *natpmp.Client, logger utils.Logger,
|
||||||
|
gateway netip.Addr, internalPort, externalPort uint16, lifetime time.Duration,
|
||||||
|
) (assignedInternalPort, assignedExternalPort uint16, err error) {
|
||||||
|
var assignedLifetime time.Duration
|
||||||
|
protocolToExternalPort := map[string]uint16{
|
||||||
|
"tcp": 0,
|
||||||
|
"udp": 0,
|
||||||
|
}
|
||||||
|
for _, protocol := range [...]string{"udp", "tcp"} {
|
||||||
|
protocolStr := strings.ToUpper(protocol)
|
||||||
|
_, assignedInternalPort, assignedExternalPort, assignedLifetime, err = client.AddPortMapping(
|
||||||
|
ctx, gateway, protocol, internalPort, externalPort, lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("adding %s port mapping: %w", protocolStr, err)
|
||||||
|
}
|
||||||
|
protocolToExternalPort[protocol] = assignedExternalPort
|
||||||
|
checkLifetime(logger, protocolStr, lifetime, assignedLifetime)
|
||||||
|
if internalPort != assignedInternalPort {
|
||||||
|
return 0, 0, fmt.Errorf("%s internal port requested as %d but received %d",
|
||||||
|
protocolStr, internalPort, assignedInternalPort)
|
||||||
|
} else if externalPort != 0 && externalPort != 1 && externalPort != assignedExternalPort {
|
||||||
|
return 0, 0, fmt.Errorf("%s external port requested as %d but received %d",
|
||||||
|
protocolStr, externalPort, assignedExternalPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if protocolToExternalPort["tcp"] != protocolToExternalPort["udp"] {
|
||||||
|
return 0, 0, fmt.Errorf("TCP and UDP external ports differ: %d and %d",
|
||||||
|
protocolToExternalPort["tcp"], protocolToExternalPort["udp"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return assignedInternalPort, assignedExternalPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkLifetime(logger utils.Logger, protocol string,
|
func checkLifetime(logger utils.Logger, protocol string,
|
||||||
requested, actual time.Duration,
|
requested, actual time.Duration,
|
||||||
) {
|
) {
|
||||||
@@ -81,27 +119,6 @@ func checkLifetime(logger utils.Logger, protocol string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkInternalPort(logger utils.Logger, sent, received uint16) {
|
|
||||||
if sent != received {
|
|
||||||
logger.Warn(fmt.Sprintf("internal port assigned %d differs from requested internal port %d",
|
|
||||||
sent, received))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkInternalPorts(logger utils.Logger, udpPort, tcpPort uint16) {
|
|
||||||
if udpPort != tcpPort {
|
|
||||||
logger.Warn(fmt.Sprintf("UDP internal port %d differs from TCP internal port %d",
|
|
||||||
udpPort, tcpPort))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) KeepPortForward(ctx context.Context,
|
func (p *Provider) KeepPortForward(ctx context.Context,
|
||||||
objects utils.PortForwardObjects,
|
objects utils.PortForwardObjects,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
@@ -117,22 +134,12 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
objects.Logger.Debug("refreshing forwarded ports since 45 seconds have elapsed")
|
objects.Logger.Debug("refreshing forwarded ports since 45 seconds have elapsed")
|
||||||
networkProtocols := [...]string{"udp", "tcp"}
|
|
||||||
const lifetime = 60 * time.Second
|
const lifetime = 60 * time.Second
|
||||||
for internalPort, externalPort := range p.internalToExternalPorts {
|
for internalPort, externalPort := range p.internalToExternalPorts {
|
||||||
for _, networkProtocol := range networkProtocols {
|
_, _, err := addPortMappingTCPUDP(ctx, client, logger, objects.Gateway, internalPort, externalPort, lifetime)
|
||||||
_, assignedInternalPort, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(
|
if err != nil {
|
||||||
ctx, objects.Gateway, networkProtocol, internalPort, externalPort, lifetime)
|
return fmt.Errorf("refreshing port mapping for internal port %d and external port %d: %w",
|
||||||
if err != nil {
|
internalPort, externalPort, err)
|
||||||
return fmt.Errorf("adding port mapping: %w", err)
|
|
||||||
}
|
|
||||||
checkLifetime(logger, networkProtocol, lifetime, assignedLiftetime)
|
|
||||||
if externalPort != assignedExternalPort {
|
|
||||||
return fmt.Errorf("external port changed from %d to %d", externalPort, assignedExternalPort)
|
|
||||||
} else if internalPort != assignedInternalPort {
|
|
||||||
return fmt.Errorf("internal port changed from %d (for external port %d) to %d",
|
|
||||||
internalPort, externalPort, assignedInternalPort)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", externalPort))
|
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", externalPort))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
"github.com/qdm12/gluetun/internal/provider/ivpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
"github.com/qdm12/gluetun/internal/provider/mullvad"
|
||||||
"github.com/qdm12/gluetun/internal/provider/nordvpn"
|
"github.com/qdm12/gluetun/internal/provider/nordvpn"
|
||||||
"github.com/qdm12/gluetun/internal/provider/ovpn"
|
|
||||||
"github.com/qdm12/gluetun/internal/provider/perfectprivacy"
|
"github.com/qdm12/gluetun/internal/provider/perfectprivacy"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privado"
|
"github.com/qdm12/gluetun/internal/provider/privado"
|
||||||
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
|
||||||
@@ -68,7 +67,6 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providers.Ivpn: ivpn.New(storage, client, updaterWarner, parallelResolver),
|
providers.Ivpn: ivpn.New(storage, client, updaterWarner, parallelResolver),
|
||||||
providers.Mullvad: mullvad.New(storage, client),
|
providers.Mullvad: mullvad.New(storage, client),
|
||||||
providers.Nordvpn: nordvpn.New(storage, client, updaterWarner),
|
providers.Nordvpn: nordvpn.New(storage, client, updaterWarner),
|
||||||
providers.Ovpn: ovpn.New(storage, client),
|
|
||||||
providers.Perfectprivacy: perfectprivacy.New(storage, unzipper, updaterWarner),
|
providers.Perfectprivacy: perfectprivacy.New(storage, unzipper, updaterWarner),
|
||||||
providers.Privado: privado.New(storage, client, updaterWarner),
|
providers.Privado: privado.New(storage, client, updaterWarner),
|
||||||
providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client),
|
providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client),
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ func GetConnection(provider string,
|
|||||||
})
|
})
|
||||||
|
|
||||||
protocol := getProtocol(selection)
|
protocol := getProtocol(selection)
|
||||||
|
port := getPort(selection, defaults.OpenVPNTCPPort,
|
||||||
|
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
||||||
|
|
||||||
connections := make([]models.Connection, 0, len(servers))
|
connections := make([]models.Connection, 0, len(servers))
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
@@ -67,9 +69,6 @@ func GetConnection(provider string,
|
|||||||
hostname = server.OvpnX509
|
hostname = server.OvpnX509
|
||||||
}
|
}
|
||||||
|
|
||||||
port := getPort(selection, server, defaults.OpenVPNTCPPort,
|
|
||||||
defaults.OpenVPNUDPPort, defaults.WireguardPort)
|
|
||||||
|
|
||||||
connection := models.Connection{
|
connection := models.Connection{
|
||||||
Type: selection.VPN,
|
Type: selection.VPN,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
|
|||||||
@@ -6,44 +6,29 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPort(selection settings.ServerSelection, server models.Server,
|
func getPort(selection settings.ServerSelection,
|
||||||
defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16,
|
defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16,
|
||||||
) (port uint16) {
|
) (port uint16) {
|
||||||
switch selection.VPN {
|
switch selection.VPN {
|
||||||
case vpn.Wireguard:
|
case vpn.Wireguard:
|
||||||
customPort := *selection.Wireguard.EndpointPort
|
customPort := *selection.Wireguard.EndpointPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
// Note: servers filtering ensures the custom port is within the
|
|
||||||
// server ports defined if any is set.
|
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(server.PortsUDP) > 0 {
|
|
||||||
defaultWireguard = server.PortsUDP[0]
|
|
||||||
}
|
|
||||||
checkDefined("Wireguard", defaultWireguard)
|
checkDefined("Wireguard", defaultWireguard)
|
||||||
return defaultWireguard
|
return defaultWireguard
|
||||||
default: // OpenVPN
|
default: // OpenVPN
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
customPort := *selection.OpenVPN.CustomPort
|
||||||
if customPort > 0 {
|
if customPort > 0 {
|
||||||
// Note: servers filtering ensures the custom port is within the
|
|
||||||
// server ports defined if any is set.
|
|
||||||
return customPort
|
return customPort
|
||||||
}
|
}
|
||||||
if selection.OpenVPN.Protocol == constants.TCP {
|
if selection.OpenVPN.Protocol == constants.TCP {
|
||||||
if len(server.PortsTCP) > 0 {
|
|
||||||
defaultOpenVPNTCP = server.PortsTCP[0]
|
|
||||||
}
|
|
||||||
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
checkDefined("OpenVPN TCP", defaultOpenVPNTCP)
|
||||||
return defaultOpenVPNTCP
|
return defaultOpenVPNTCP
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(server.PortsUDP) > 0 {
|
|
||||||
defaultOpenVPNUDP = server.PortsUDP[0]
|
|
||||||
}
|
|
||||||
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
checkDefined("OpenVPN UDP", defaultOpenVPNUDP)
|
||||||
return defaultOpenVPNUDP
|
return defaultOpenVPNUDP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
selection settings.ServerSelection
|
selection settings.ServerSelection
|
||||||
server models.Server
|
|
||||||
defaultOpenVPNTCP uint16
|
defaultOpenVPNTCP uint16
|
||||||
defaultOpenVPNUDP uint16
|
defaultOpenVPNUDP uint16
|
||||||
defaultWireguard uint16
|
defaultWireguard uint16
|
||||||
@@ -50,20 +48,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: defaultOpenVPNUDP,
|
port: defaultOpenVPNUDP,
|
||||||
},
|
},
|
||||||
"OpenVPN_server_port_udp": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
CustomPort: uint16Ptr(0),
|
|
||||||
Protocol: constants.UDP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsUDP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultOpenVPNUDP: defaultOpenVPNUDP,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"OpenVPN UDP no default port defined": {
|
"OpenVPN UDP no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.OpenVPN,
|
VPN: vpn.OpenVPN,
|
||||||
@@ -104,20 +88,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
},
|
},
|
||||||
port: 1234,
|
port: 1234,
|
||||||
},
|
},
|
||||||
"OpenVPN_server_port_tcp": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.OpenVPN,
|
|
||||||
OpenVPN: settings.OpenVPNSelection{
|
|
||||||
CustomPort: uint16Ptr(0),
|
|
||||||
Protocol: constants.TCP,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsTCP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultOpenVPNTCP: defaultOpenVPNTCP,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"Wireguard": {
|
"Wireguard": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -135,19 +105,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
defaultWireguard: defaultWireguard,
|
defaultWireguard: defaultWireguard,
|
||||||
port: 1234,
|
port: 1234,
|
||||||
},
|
},
|
||||||
"Wireguard_server_port": {
|
|
||||||
selection: settings.ServerSelection{
|
|
||||||
VPN: vpn.Wireguard,
|
|
||||||
Wireguard: settings.WireguardSelection{
|
|
||||||
EndpointPort: uint16Ptr(0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: models.Server{
|
|
||||||
PortsUDP: []uint16{1234},
|
|
||||||
},
|
|
||||||
defaultWireguard: defaultWireguard,
|
|
||||||
port: 1234,
|
|
||||||
},
|
|
||||||
"Wireguard no default port defined": {
|
"Wireguard no default port defined": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
VPN: vpn.Wireguard,
|
VPN: vpn.Wireguard,
|
||||||
@@ -163,7 +120,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
if testCase.panics != "" {
|
if testCase.panics != "" {
|
||||||
assert.PanicsWithValue(t, testCase.panics, func() {
|
assert.PanicsWithValue(t, testCase.panics, func() {
|
||||||
_ = getPort(testCase.selection,
|
_ = getPort(testCase.selection,
|
||||||
testCase.server,
|
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
@@ -172,7 +128,6 @@ func Test_GetPort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
port := getPort(testCase.selection,
|
port := getPort(testCase.selection,
|
||||||
testCase.server,
|
|
||||||
testCase.defaultOpenVPNTCP,
|
testCase.defaultOpenVPNTCP,
|
||||||
testCase.defaultOpenVPNUDP,
|
testCase.defaultOpenVPNUDP,
|
||||||
testCase.defaultWireguard)
|
testCase.defaultWireguard)
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ type cmdType byte
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
connect cmdType = 1
|
connect cmdType = 1
|
||||||
bind cmdType = 2
|
|
||||||
udpAssociate cmdType = 3
|
udpAssociate cmdType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,8 +50,6 @@ func (c cmdType) String() string {
|
|||||||
switch c {
|
switch c {
|
||||||
case connect:
|
case connect:
|
||||||
return "connect"
|
return "connect"
|
||||||
case bind:
|
|
||||||
return "bind"
|
|
||||||
case udpAssociate:
|
case udpAssociate:
|
||||||
return "UDP associate"
|
return "UDP associate"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
|
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
|
||||||
func (c *socksConn) encodeFailedResponse(writer io.Writer, socksVersion byte, reply replyCode) { //nolint:unparam
|
func (c *socksConn) encodeFailedResponse(writer io.Writer, socksVersion byte, reply replyCode) {
|
||||||
_, err := writer.Write([]byte{
|
_, err := writer.Write([]byte{
|
||||||
socksVersion,
|
socksVersion,
|
||||||
byte(reply),
|
byte(reply),
|
||||||
|
|||||||
+89
-49
@@ -2,6 +2,7 @@ package socks5
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -15,12 +16,13 @@ type server struct {
|
|||||||
logger Logger
|
logger Logger
|
||||||
|
|
||||||
// internal fields
|
// internal fields
|
||||||
listener net.Listener
|
tcpListener net.Listener
|
||||||
|
udpRouter *udpRouter
|
||||||
listening atomic.Bool
|
listening atomic.Bool
|
||||||
socksConnCtx context.Context //nolint:containedctx
|
socksConnCtx context.Context //nolint:containedctx
|
||||||
socksConnCancel context.CancelFunc
|
socksConnCancel context.CancelFunc
|
||||||
done <-chan struct{}
|
done <-chan error
|
||||||
stopping atomic.Bool
|
stopCh chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(settings Settings) *server {
|
func newServer(settings Settings) *server {
|
||||||
@@ -39,19 +41,28 @@ func (s *server) String() string {
|
|||||||
func (s *server) Start(ctx context.Context) (runErr <-chan error, err error) {
|
func (s *server) Start(ctx context.Context) (runErr <-chan error, err error) {
|
||||||
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
|
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
|
||||||
config := &net.ListenConfig{}
|
config := &net.ListenConfig{}
|
||||||
s.listener, err = config.Listen(ctx, "tcp", s.address)
|
s.tcpListener, err = config.Listen(ctx, "tcp", s.address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("listening on %s: %w", s.address, err)
|
return nil, fmt.Errorf("TCP listening on %s: %w", s.address, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.udpRouter, err = newUDPRouter(ctx, s.address, s.logger)
|
||||||
|
if err != nil {
|
||||||
|
_ = s.tcpListener.Close()
|
||||||
|
return nil, fmt.Errorf("creating UDP router: %w", err)
|
||||||
}
|
}
|
||||||
s.listening.Store(true)
|
s.listening.Store(true)
|
||||||
s.logger.Infof("SOCKS5 server listening on %s", s.listener.Addr())
|
s.logger.Infof("SOCKS5 TCP server listening on %s", s.tcpListener.Addr())
|
||||||
|
s.logger.Infof("SOCKS5 UDP server listening on %s", s.udpRouter.localAddress())
|
||||||
|
|
||||||
ready := make(chan struct{})
|
ready := make(chan struct{})
|
||||||
runErrCh := make(chan error)
|
runErrCh := make(chan error)
|
||||||
runErr = runErrCh
|
runErr = runErrCh
|
||||||
done := make(chan struct{})
|
done := make(chan error)
|
||||||
s.done = done
|
s.done = done
|
||||||
go s.runServer(ready, runErrCh, done)
|
stop := make(chan struct{})
|
||||||
|
s.stopCh = stop
|
||||||
|
go s.runServer(ready, runErrCh, stop, done)
|
||||||
select {
|
select {
|
||||||
case <-ready:
|
case <-ready:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -62,61 +73,90 @@ func (s *server) Start(ctx context.Context) (runErr <-chan error, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) runServer(ready chan<- struct{},
|
func (s *server) runServer(ready chan<- struct{},
|
||||||
runErrCh chan<- error, done chan<- struct{},
|
runErrCh chan<- error, stop <-chan struct{}, done chan<- error,
|
||||||
) {
|
) {
|
||||||
close(ready)
|
close(ready)
|
||||||
defer close(done)
|
defer close(done)
|
||||||
wg := new(sync.WaitGroup)
|
|
||||||
defer wg.Wait()
|
|
||||||
|
|
||||||
dialer := &net.Dialer{}
|
udpErrCh := make(chan error)
|
||||||
for {
|
go func() {
|
||||||
connection, err := s.listener.Accept()
|
udpErrCh <- s.udpRouter.run(s.socksConnCtx)
|
||||||
if err != nil {
|
}()
|
||||||
if !s.stopping.Load() {
|
|
||||||
_ = s.stop()
|
tcpErrCh := make(chan error)
|
||||||
runErrCh <- fmt.Errorf("accepting connection: %w", err)
|
go func() {
|
||||||
}
|
var wg sync.WaitGroup
|
||||||
return
|
defer wg.Wait()
|
||||||
}
|
|
||||||
wg.Add(1)
|
dialer := &net.Dialer{}
|
||||||
go func(ctx context.Context, connection net.Conn,
|
for {
|
||||||
dialer *net.Dialer, wg *sync.WaitGroup,
|
connection, err := s.tcpListener.Accept()
|
||||||
) {
|
|
||||||
defer wg.Done()
|
|
||||||
socksConn := &socksConn{
|
|
||||||
dialer: dialer,
|
|
||||||
username: s.username,
|
|
||||||
password: s.password,
|
|
||||||
clientConn: connection,
|
|
||||||
logger: s.logger,
|
|
||||||
}
|
|
||||||
err := socksConn.run(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Infof("running socks connection: %s", err)
|
s.socksConnCancel() // stop ongoing TCP socks connections - no impact on UDP
|
||||||
|
tcpErrCh <- fmt.Errorf("accepting connection: %w", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}(s.socksConnCtx, connection, dialer, wg)
|
wg.Go(func() {
|
||||||
|
connection := connection // capture loop variable
|
||||||
|
socksConn := &socksConn{
|
||||||
|
dialer: dialer,
|
||||||
|
username: s.username,
|
||||||
|
password: s.password,
|
||||||
|
clientConn: connection,
|
||||||
|
udpRouter: s.udpRouter,
|
||||||
|
logger: s.logger,
|
||||||
|
}
|
||||||
|
err := socksConn.run(s.socksConnCtx)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Infof("running socks connection: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
s.listening.Store(false)
|
||||||
|
var errs []error
|
||||||
|
err := s.tcpListener.Close()
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("closing TCP listener: %w", err))
|
||||||
|
}
|
||||||
|
// stop ongoing TCP socks connections. This impacts the udpRouter run error when it is being closed.
|
||||||
|
s.socksConnCancel()
|
||||||
|
<-tcpErrCh // wait for TCP server to stop
|
||||||
|
err = s.udpRouter.close()
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("closing UDP router: %w", err))
|
||||||
|
}
|
||||||
|
<-udpErrCh // wait for UDP router to stop
|
||||||
|
if len(errs) > 0 {
|
||||||
|
// Only write to the done channel if the [server.Stop] method is waiting to read from it
|
||||||
|
done <- errors.Join(errs...)
|
||||||
|
}
|
||||||
|
// If no error, the done channel is closed so the error is effectively `nil`
|
||||||
|
// Note: do NOT write an error the runError channel, since we are stopping the server gracefully.
|
||||||
|
case err := <-udpErrCh:
|
||||||
|
_ = s.tcpListener.Close() // stop accepting new TCP connections
|
||||||
|
s.socksConnCancel() // stop ongoing TCP socks connections
|
||||||
|
<-tcpErrCh // wait for TCP server to stop
|
||||||
|
runErrCh <- fmt.Errorf("running UDP router: %w", err)
|
||||||
|
case err := <-tcpErrCh:
|
||||||
|
s.socksConnCancel()
|
||||||
|
_ = s.udpRouter.close() // stop UDP router
|
||||||
|
<-udpErrCh // wait for UDP router to stop
|
||||||
|
runErrCh <- fmt.Errorf("running TCP server: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) Stop() (err error) {
|
func (s *server) Stop() (err error) {
|
||||||
s.stopping.Store(true)
|
close(s.stopCh)
|
||||||
err = s.stop()
|
return <-s.done
|
||||||
<-s.done // wait for run goroutine to finish
|
|
||||||
s.stopping.Store(false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) stop() error {
|
|
||||||
s.listening.Store(false)
|
|
||||||
err := s.listener.Close()
|
|
||||||
s.socksConnCancel() // stop ongoing socks connections
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) listeningAddress() net.Addr {
|
func (s *server) listeningAddress() net.Addr {
|
||||||
if s.listening.Load() {
|
if s.listening.Load() {
|
||||||
return s.listener.Addr()
|
return s.tcpListener.Addr()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package socks5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Server_UDPResolution(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
server := newServer(Settings{
|
||||||
|
Address: "127.0.0.1:0",
|
||||||
|
Logger: noopLogger{},
|
||||||
|
})
|
||||||
|
runErr, err := server.Start(ctx)
|
||||||
|
require.NoError(t, err, "starting SOCKS5 server")
|
||||||
|
|
||||||
|
const timeout = 3 * time.Second
|
||||||
|
|
||||||
|
// Connect to the SOCKS5 server via TCP to negotiate UDP associate
|
||||||
|
dialer := &net.Dialer{Timeout: timeout}
|
||||||
|
tcpConn, err := dialer.DialContext(ctx, "tcp", server.listeningAddress().String())
|
||||||
|
require.NoError(t, err, "tcp connecting to SOCKS5 server")
|
||||||
|
t.Cleanup(func() { tcpConn.Close() })
|
||||||
|
|
||||||
|
negotiateSOCKS5(t, tcpConn, "", "")
|
||||||
|
|
||||||
|
// UDP Associate Command: [VERSION (5), CMD (3 = UDP ASSOC), RSV (0), ATYP (1 = IPv4), ADDR (0.0.0.0), PORT (0)]
|
||||||
|
_, err = tcpConn.Write([]byte{5, 3, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||||
|
require.NoError(t, err, "sending UDP ASSOC request")
|
||||||
|
|
||||||
|
relayAddressString, err := readSOCKS5ResponseAddress(t, tcpConn)
|
||||||
|
require.NoError(t, err, "reading UDP ASSOC reply")
|
||||||
|
relayAddress, err := net.ResolveUDPAddr("udp", relayAddressString)
|
||||||
|
require.NoError(t, err, "resolving udp relay address")
|
||||||
|
|
||||||
|
// Dial the relay using IPv4 so source IP family matches the control connection.
|
||||||
|
udpConn, err := net.DialUDP("udp4", nil, relayAddress)
|
||||||
|
require.NoError(t, err, "dialing UDP relay")
|
||||||
|
t.Cleanup(func() { _ = udpConn.Close() })
|
||||||
|
|
||||||
|
queryID := uint16(rand.Uint32()) //nolint:gosec
|
||||||
|
dnsRequest := &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: queryID,
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{{
|
||||||
|
Name: dns.Fqdn("github.com"),
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
dnsQuery, err := dnsRequest.Pack()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Encapsulate DNS payload into SOCKS5 UDP Request Header
|
||||||
|
// [RSV (0,0), FRAG (0), ATYP (1 = IPv4), DST.ADDR (1.1.1.1), DST.PORT (53)]
|
||||||
|
packet := append([]byte{0, 0, 0, 1, 1, 1, 1, 1, 0, 53}, dnsQuery...)
|
||||||
|
|
||||||
|
// Send encapsulated packet to the proxy's UDP relay address
|
||||||
|
_, err = udpConn.Write(packet)
|
||||||
|
require.NoError(t, err, "sending UDP packet to relay")
|
||||||
|
|
||||||
|
// Read response from the proxy relay
|
||||||
|
err = udpConn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
require.NoError(t, err, "setting read deadline on UDP connection")
|
||||||
|
buffer := make([]byte, 2048)
|
||||||
|
n, err := udpConn.Read(buffer)
|
||||||
|
require.NoError(t, err, "receiving UDP response from relay")
|
||||||
|
const minimumHeaderSize = 10
|
||||||
|
require.GreaterOrEqual(t, n, minimumHeaderSize, "received UDP packet too short to contain valid SOCKS5 header")
|
||||||
|
|
||||||
|
// Verify header layout and slice out the raw DNS response
|
||||||
|
// Header format: RSV(2) FRAG(1) ATYP(1) DST.ADDR(variable) DST.PORT(2)
|
||||||
|
atyp := buffer[3]
|
||||||
|
var headerSize int
|
||||||
|
switch atyp {
|
||||||
|
case 1: // IPv4
|
||||||
|
headerSize = 10
|
||||||
|
case 3: // Domain name
|
||||||
|
headerSize = 4 + 1 + int(buffer[4]) + 2
|
||||||
|
case 4: // IPv6
|
||||||
|
headerSize = 22
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unknown ATYP in SOCKS5 UDP header: %d", atyp)
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsResponse := new(dns.Msg)
|
||||||
|
err = dnsResponse.Unpack(buffer[headerSize:n])
|
||||||
|
require.NoError(t, err, "unpacking DNS response from SOCKS5 UDP packet")
|
||||||
|
|
||||||
|
assert.Equal(t, queryID, dnsResponse.Id, "DNS response ID should match query ID")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-runErr:
|
||||||
|
require.NoError(t, err, "SOCKS5 server run error")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.Stop()
|
||||||
|
require.NoError(t, err, "stopping SOCKS5 server")
|
||||||
|
}
|
||||||
+249
-1
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,6 +24,7 @@ type socksConn struct {
|
|||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
clientConn net.Conn
|
clientConn net.Conn
|
||||||
|
udpRouter *udpRouter
|
||||||
logger Logger
|
logger Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +111,29 @@ func (c *socksConn) handleRequest(ctx context.Context) error {
|
|||||||
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.command != connect {
|
|
||||||
|
switch request.command {
|
||||||
|
case connect:
|
||||||
|
err = c.handleConnectRequest(ctx, socksVersion, request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handling %s request: %w", request.command, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case udpAssociate:
|
||||||
|
err = c.handleUDPAssociateRequest(ctx, socksVersion, request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handling %s request: %w", request.command, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
c.encodeFailedResponse(c.clientConn, socksVersion, commandNotSupported)
|
c.encodeFailedResponse(c.clientConn, socksVersion, commandNotSupported)
|
||||||
return fmt.Errorf("command %s is not supported", request.command)
|
return fmt.Errorf("command %s is not supported", request.command)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socksConn) handleConnectRequest(ctx context.Context,
|
||||||
|
socksVersion byte, request request,
|
||||||
|
) error {
|
||||||
destinationAddress := net.JoinHostPort(request.destination, fmt.Sprint(request.port))
|
destinationAddress := net.JoinHostPort(request.destination, fmt.Sprint(request.port))
|
||||||
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
|
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -176,6 +196,234 @@ func (c *socksConn) handleRequest(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *socksConn) handleUDPAssociateRequest(ctx context.Context,
|
||||||
|
socksVersion byte, request request,
|
||||||
|
) error {
|
||||||
|
expectedAddrPort, err := udpAssociateExpectedClientEndpoint(request)
|
||||||
|
if err != nil {
|
||||||
|
c.encodeFailedResponse(c.clientConn, socksVersion, addressTypeNotSupported)
|
||||||
|
return fmt.Errorf("deriving expected client address and port from request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAddress, bindPort, bindAddrType, err := c.udpAssociationAddresses()
|
||||||
|
if err != nil {
|
||||||
|
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||||
|
return fmt.Errorf("getting udp association addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
association, err := c.udpRouter.registerAssociation(c.clientConn, expectedAddrPort)
|
||||||
|
if err != nil {
|
||||||
|
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||||
|
return fmt.Errorf("registering udp association: %w", err)
|
||||||
|
}
|
||||||
|
defer c.udpRouter.unregisterAssociation(association)
|
||||||
|
|
||||||
|
err = c.encodeSuccessResponse(c.clientConn, socksVersion, succeeded,
|
||||||
|
bindAddrType, bindAddress, bindPort)
|
||||||
|
if err != nil {
|
||||||
|
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||||
|
return fmt.Errorf("writing successful %s response: %w", udpAssociate, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
associationCtx, associationCancel := context.WithCancel(ctx)
|
||||||
|
defer associationCancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
c.udpRouter.runAssociationHandler(associationCtx, association)
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
_, _ = io.Copy(io.Discard, c.clientConn)
|
||||||
|
associationCancel()
|
||||||
|
})
|
||||||
|
<-associationCtx.Done()
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpAssociateExpectedClientEndpoint(request request) (expectedAddrPort netip.AddrPort, err error) {
|
||||||
|
switch request.addressType {
|
||||||
|
case ipv4, ipv6:
|
||||||
|
expectedClientAddress, parseErr := netip.ParseAddr(request.destination)
|
||||||
|
if parseErr != nil {
|
||||||
|
return netip.AddrPort{}, fmt.Errorf("parsing destination address: %w", parseErr)
|
||||||
|
}
|
||||||
|
expectedClientAddress = expectedClientAddress.Unmap()
|
||||||
|
if !expectedClientAddress.IsUnspecified() {
|
||||||
|
return netip.AddrPortFrom(expectedClientAddress, request.port), nil
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(netip.Addr{}, request.port), nil
|
||||||
|
case domainName:
|
||||||
|
// For UDP associate, client endpoint matching is based on observed UDP source
|
||||||
|
// address/port. A hostname is not directly matchable at this stage, so we
|
||||||
|
// ignore the domain name request destination entirely.
|
||||||
|
return netip.AddrPortFrom(netip.Addr{}, request.port), nil
|
||||||
|
default:
|
||||||
|
return netip.AddrPort{}, fmt.Errorf("address type %d is not supported", request.addressType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socksConn) udpAssociationAddresses() (bindAddress string,
|
||||||
|
bindPort uint16, bindAddrType addrType, err error,
|
||||||
|
) {
|
||||||
|
localAddress := c.udpRouter.localAddress().String()
|
||||||
|
host, portString, err := net.SplitHostPort(localAddress)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, 0, fmt.Errorf("splitting local address: %w", err)
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseUint(portString, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, 0, fmt.Errorf("parsing local port: %w", err)
|
||||||
|
}
|
||||||
|
bindAddress = host
|
||||||
|
bindPort = uint16(port)
|
||||||
|
if isUnspecifiedIPAddress(bindAddress) {
|
||||||
|
controlLocalAddress := c.clientConn.LocalAddr().String()
|
||||||
|
controlLocalHost, _, splitErr := net.SplitHostPort(controlLocalAddress)
|
||||||
|
if splitErr != nil {
|
||||||
|
return "", 0, 0, fmt.Errorf("splitting control connection local address: %w", splitErr)
|
||||||
|
}
|
||||||
|
bindAddress = controlLocalHost
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddress := net.ParseIP(bindAddress)
|
||||||
|
if ipAddress == nil {
|
||||||
|
bindAddrType = domainName
|
||||||
|
return bindAddress, bindPort, bindAddrType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipAddress.To4() != nil {
|
||||||
|
bindAddrType = ipv4
|
||||||
|
} else {
|
||||||
|
bindAddrType = ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindAddress, bindPort, bindAddrType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnspecifiedIPAddress(address string) bool {
|
||||||
|
ipAddress, err := netip.ParseAddr(address)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ipAddress.IsUnspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUDPDatagram(packet []byte) (destination string, payload []byte, err error) {
|
||||||
|
const minimumPacketLength = 4
|
||||||
|
if len(packet) < minimumPacketLength {
|
||||||
|
return "", nil, fmt.Errorf("packet is too short: %d", len(packet))
|
||||||
|
}
|
||||||
|
if packet[0] != 0 || packet[1] != 0 {
|
||||||
|
return "", nil, fmt.Errorf("reserved bytes are invalid: %x %x", packet[0], packet[1])
|
||||||
|
}
|
||||||
|
if packet[2] != 0 {
|
||||||
|
return "", nil, fmt.Errorf("fragmentation is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 3
|
||||||
|
addressType := addrType(packet[offset])
|
||||||
|
offset++
|
||||||
|
|
||||||
|
switch addressType {
|
||||||
|
case ipv4:
|
||||||
|
const ipv4Length = 4
|
||||||
|
if len(packet) < offset+ipv4Length+2 {
|
||||||
|
return "", nil, fmt.Errorf("packet is too short for IPv4 address")
|
||||||
|
}
|
||||||
|
var ip [ipv4Length]byte
|
||||||
|
copy(ip[:], packet[offset:offset+ipv4Length])
|
||||||
|
destination = netip.AddrFrom4(ip).String()
|
||||||
|
offset += ipv4Length
|
||||||
|
case ipv6:
|
||||||
|
const ipv6Length = 16
|
||||||
|
if len(packet) < offset+ipv6Length+2 {
|
||||||
|
return "", nil, fmt.Errorf("packet is too short for IPv6 address")
|
||||||
|
}
|
||||||
|
var ip [ipv6Length]byte
|
||||||
|
copy(ip[:], packet[offset:offset+ipv6Length])
|
||||||
|
destination = netip.AddrFrom16(ip).String()
|
||||||
|
offset += ipv6Length
|
||||||
|
case domainName:
|
||||||
|
if len(packet) < offset+1 {
|
||||||
|
return "", nil, fmt.Errorf("packet is too short for domain name length")
|
||||||
|
}
|
||||||
|
domainNameLength := int(packet[offset])
|
||||||
|
offset++
|
||||||
|
if len(packet) < offset+domainNameLength+2 {
|
||||||
|
return "", nil, fmt.Errorf("packet is too short for domain name")
|
||||||
|
}
|
||||||
|
destination = string(packet[offset : offset+domainNameLength])
|
||||||
|
offset += domainNameLength
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("address type is not supported: %d", addressType)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := binary.BigEndian.Uint16(packet[offset : offset+2])
|
||||||
|
destination = net.JoinHostPort(destination, fmt.Sprint(port))
|
||||||
|
offset += 2
|
||||||
|
payload = packet[offset:]
|
||||||
|
|
||||||
|
return destination, payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUDPDatagramToBuffer(writer io.Writer, sourceAddrPort netip.AddrPort,
|
||||||
|
payload []byte,
|
||||||
|
) error {
|
||||||
|
address := sourceAddrPort.Addr()
|
||||||
|
if !address.IsValid() {
|
||||||
|
return errors.New("source address is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeUDPDatagramSourceAddress(writer, address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing source address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var portBytes [2]byte
|
||||||
|
binary.BigEndian.PutUint16(portBytes[:], sourceAddrPort.Port())
|
||||||
|
_, err = writer.Write(portBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing destination port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUDPDatagramSourceAddress(writer io.Writer, address netip.Addr) error {
|
||||||
|
var addrType addrType
|
||||||
|
var addressBytes []byte
|
||||||
|
switch {
|
||||||
|
case address.Is4():
|
||||||
|
addrType = ipv4
|
||||||
|
array := address.As4()
|
||||||
|
addressBytes = array[:]
|
||||||
|
case address.Is6():
|
||||||
|
addrType = ipv6
|
||||||
|
array := address.As16()
|
||||||
|
addressBytes = array[:]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("address type is not supported: %v", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := writer.Write([]byte{0, 0, 0, byte(addrType)})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing header: %w", err)
|
||||||
|
}
|
||||||
|
_, err = writer.Write(addressBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing IP address: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
|
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
|
||||||
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
|
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
|
||||||
const headerLength = 2 // version + nMethods bytes
|
const headerLength = 2 // version + nMethods bytes
|
||||||
|
|||||||
+502
-38
@@ -2,9 +2,13 @@ package socks5
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -96,6 +100,178 @@ func TestServerProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerProxyTCPAndUDPParallel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testCases := map[string]struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
"no_auth": {},
|
||||||
|
"with_auth": {
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
backendTCPListener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
backendTCPConnChannel := make(chan net.Conn, 1)
|
||||||
|
go func() {
|
||||||
|
connection, err := backendTCPListener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backendTCPConnChannel <- connection
|
||||||
|
}()
|
||||||
|
|
||||||
|
backendUDPPacketConn, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server := newServer(Settings{
|
||||||
|
Username: testCase.username,
|
||||||
|
Password: testCase.password,
|
||||||
|
Address: "127.0.0.1:0",
|
||||||
|
Logger: noopLogger{},
|
||||||
|
})
|
||||||
|
_, err = server.Start(t.Context())
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = server.Stop()
|
||||||
|
_ = backendTCPListener.Close()
|
||||||
|
_ = backendUDPPacketConn.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
clientTCPConn := dialSOCKS5(t, server.listeningAddress().String(),
|
||||||
|
backendTCPListener.Addr().String(), testCase.username, testCase.password)
|
||||||
|
defer clientTCPConn.Close()
|
||||||
|
|
||||||
|
backendTCPConn := <-backendTCPConnChannel
|
||||||
|
defer backendTCPConn.Close()
|
||||||
|
|
||||||
|
udpControlConn, clientUDPConn := dialSOCKS5UDPAssociate(t,
|
||||||
|
server.listeningAddress().String(), testCase.username, testCase.password)
|
||||||
|
defer udpControlConn.Close()
|
||||||
|
defer clientUDPConn.Close()
|
||||||
|
|
||||||
|
tcpErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
tcpErrCh <- runTCPProxyRoundTrip(clientTCPConn, backendTCPConn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
udpErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
udpErrCh <- runUDPProxyRoundTrip(t.Context(), clientUDPConn, backendUDPPacketConn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = <-tcpErrCh
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = <-udpErrCh
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTCPProxyRoundTrip(clientTCPConn net.Conn, backendTCPConn net.Conn) error {
|
||||||
|
clientMessage := []byte("hello from client")
|
||||||
|
_, err := clientTCPConn.Write(clientMessage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
received := make([]byte, len(clientMessage))
|
||||||
|
_, err = io.ReadFull(backendTCPConn, received)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(clientMessage, received) {
|
||||||
|
return errors.New("backend did not receive expected TCP payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
backendMessage := []byte("hello from backend")
|
||||||
|
_, err = backendTCPConn.Write(backendMessage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedByClient := make([]byte, len(backendMessage))
|
||||||
|
_, err = io.ReadFull(clientTCPConn, receivedByClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(backendMessage, receivedByClient) {
|
||||||
|
return errors.New("client did not receive expected TCP payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUDPProxyRoundTrip(ctx context.Context, clientUDPConn *net.UDPConn, backendUDPPacketConn net.PacketConn) error {
|
||||||
|
udpPayload := []byte("hello from udp client")
|
||||||
|
udpRequest, err := makeSOCKS5UDPDatagram(backendUDPPacketConn.LocalAddr().String(), udpPayload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = clientUDPConn.Write(udpRequest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
err = backendUDPPacketConn.SetReadDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting read deadline on backend connection: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bufferSize = 512
|
||||||
|
backendReadBuffer := make([]byte, bufferSize)
|
||||||
|
packetLength, proxyAddress, err := backendUDPPacketConn.ReadFrom(backendReadBuffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(udpPayload, backendReadBuffer[:packetLength]) {
|
||||||
|
return errors.New("backend did not receive expected UDP payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
backendUDPReply := []byte("hello from udp backend")
|
||||||
|
_, err = backendUDPPacketConn.WriteTo(backendUDPReply, proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasDeadline {
|
||||||
|
err = clientUDPConn.SetReadDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting read deadline on client connection: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
udpResponseBuffer := make([]byte, 1024)
|
||||||
|
responseLength, err := clientUDPConn.Read(udpResponseBuffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationAddress, udpResponsePayload, err := parseSOCKS5UDPDatagram(udpResponseBuffer[:responseLength])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(backendUDPReply, udpResponsePayload) {
|
||||||
|
return errors.New("client did not receive expected UDP payload")
|
||||||
|
}
|
||||||
|
if destinationAddress != backendUDPPacketConn.LocalAddr().String() {
|
||||||
|
return errors.New("udp response destination address mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// dialSOCKS5 performs the full SOCKS5 handshake (with optional username/password
|
// dialSOCKS5 performs the full SOCKS5 handshake (with optional username/password
|
||||||
// subnegotiation) and returns a connected net.Conn ready for data exchange.
|
// subnegotiation) and returns a connected net.Conn ready for data exchange.
|
||||||
func dialSOCKS5(t *testing.T, proxyAddr, targetAddr, username, password string) net.Conn {
|
func dialSOCKS5(t *testing.T, proxyAddr, targetAddr, username, password string) net.Conn {
|
||||||
@@ -109,6 +285,55 @@ func dialSOCKS5(t *testing.T, proxyAddr, targetAddr, username, password string)
|
|||||||
conn, err := (&net.Dialer{}).DialContext(t.Context(), "tcp", proxyAddr)
|
conn, err := (&net.Dialer{}).DialContext(t.Context(), "tcp", proxyAddr)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
negotiateSOCKS5(t, conn, username, password)
|
||||||
|
|
||||||
|
var connectRequest []byte
|
||||||
|
if ip := net.ParseIP(host).To4(); ip != nil {
|
||||||
|
connectRequest = []byte{socks5Version, byte(connect), 0, byte(ipv4)}
|
||||||
|
connectRequest = append(connectRequest, ip...)
|
||||||
|
} else {
|
||||||
|
connectRequest = []byte{socks5Version, byte(connect), 0, byte(domainName), byte(len(host))}
|
||||||
|
connectRequest = append(connectRequest, []byte(host)...)
|
||||||
|
}
|
||||||
|
connectRequest = binary.BigEndian.AppendUint16(connectRequest, uint16(targetPort)) //nolint:gosec
|
||||||
|
_, err = conn.Write(connectRequest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = readSOCKS5ResponseAddress(t, conn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialSOCKS5UDPAssociate(t *testing.T, proxyAddr, username, password string) (net.Conn, *net.UDPConn) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
controlConn, err := (&net.Dialer{}).DialContext(t.Context(), "tcp", proxyAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
negotiateSOCKS5(t, controlConn, username, password)
|
||||||
|
|
||||||
|
udpAssociateRequest := []byte{socks5Version, byte(udpAssociate), 0, byte(ipv4), 0, 0, 0, 0, 0, 0}
|
||||||
|
_, err = controlConn.Write(udpAssociateRequest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
udpProxyAddress, err := readSOCKS5ResponseAddress(t, controlConn)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
udpProxyResolvedAddress, err := net.ResolveUDPAddr("udp", udpProxyAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
udpConn, err := net.DialUDP("udp", nil, udpProxyResolvedAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return controlConn, udpConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func negotiateSOCKS5(t *testing.T, conn net.Conn, username, password string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
var method authMethod
|
var method authMethod
|
||||||
if username != "" || password != "" {
|
if username != "" || password != "" {
|
||||||
method = authUsernamePassword
|
method = authUsernamePassword
|
||||||
@@ -138,45 +363,146 @@ func dialSOCKS5(t *testing.T, proxyAddr, targetAddr, username, password string)
|
|||||||
require.Equal(t, authUsernamePasswordSubNegotiation1, subnegResp[0])
|
require.Equal(t, authUsernamePasswordSubNegotiation1, subnegResp[0])
|
||||||
require.Equal(t, byte(0), subnegResp[1])
|
require.Equal(t, byte(0), subnegResp[1])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var connectRequest []byte
|
func readSOCKS5ResponseAddress(t *testing.T, conn net.Conn) (address string, err error) {
|
||||||
if ip := net.ParseIP(host).To4(); ip != nil {
|
t.Helper()
|
||||||
connectRequest = []byte{socks5Version, byte(connect), 0, byte(ipv4)}
|
|
||||||
connectRequest = append(connectRequest, ip...)
|
|
||||||
} else {
|
|
||||||
connectRequest = []byte{socks5Version, byte(connect), 0, byte(domainName), byte(len(host))}
|
|
||||||
connectRequest = append(connectRequest, []byte(host)...)
|
|
||||||
}
|
|
||||||
connectRequest = binary.BigEndian.AppendUint16(connectRequest, uint16(targetPort)) //nolint:gosec
|
|
||||||
_, err = conn.Write(connectRequest)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var responseHeader [4]byte
|
var responseHeader [4]byte
|
||||||
_, err = io.ReadFull(conn, responseHeader[:])
|
_, err = io.ReadFull(conn, responseHeader[:])
|
||||||
require.NoError(t, err)
|
if err != nil {
|
||||||
require.Equal(t, socks5Version, responseHeader[0])
|
return "", err
|
||||||
require.Equal(t, byte(succeeded), responseHeader[1])
|
}
|
||||||
|
if responseHeader[0] != socks5Version {
|
||||||
// Consume BND.ADDR and BND.PORT (their values are irrelevant to the caller).
|
return "", errors.New("version mismatch")
|
||||||
switch addrType(responseHeader[3]) {
|
}
|
||||||
case ipv4:
|
if responseHeader[1] != byte(succeeded) {
|
||||||
var addrPort [net.IPv4len + 2]byte
|
return "", errors.New("request was not successful")
|
||||||
_, err = io.ReadFull(conn, addrPort[:])
|
|
||||||
require.NoError(t, err)
|
|
||||||
case ipv6:
|
|
||||||
var addrPort [net.IPv6len + 2]byte
|
|
||||||
_, err = io.ReadFull(conn, addrPort[:])
|
|
||||||
require.NoError(t, err)
|
|
||||||
case domainName:
|
|
||||||
var lenBuf [1]byte
|
|
||||||
_, err = io.ReadFull(conn, lenBuf[:])
|
|
||||||
require.NoError(t, err)
|
|
||||||
addrPort := make([]byte, int(lenBuf[0])+2)
|
|
||||||
_, err = io.ReadFull(conn, addrPort)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn
|
var host string
|
||||||
|
switch addrType(responseHeader[3]) {
|
||||||
|
case ipv4:
|
||||||
|
addressAndPort := make([]byte, net.IPv4len+2)
|
||||||
|
_, err = io.ReadFull(conn, addressAndPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
host = net.IP(addressAndPort[:net.IPv4len]).String()
|
||||||
|
port := binary.BigEndian.Uint16(addressAndPort[net.IPv4len:])
|
||||||
|
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), nil
|
||||||
|
case ipv6:
|
||||||
|
addressAndPort := make([]byte, net.IPv6len+2)
|
||||||
|
_, err = io.ReadFull(conn, addressAndPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
host = net.IP(addressAndPort[:net.IPv6len]).String()
|
||||||
|
port := binary.BigEndian.Uint16(addressAndPort[net.IPv6len:])
|
||||||
|
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), nil
|
||||||
|
case domainName:
|
||||||
|
var lengthBuffer [1]byte
|
||||||
|
_, err = io.ReadFull(conn, lengthBuffer[:])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
domainAndPort := make([]byte, int(lengthBuffer[0])+2)
|
||||||
|
_, err = io.ReadFull(conn, domainAndPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
host = string(domainAndPort[:len(domainAndPort)-2])
|
||||||
|
port := binary.BigEndian.Uint16(domainAndPort[len(domainAndPort)-2:])
|
||||||
|
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("unknown address type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSOCKS5UDPDatagram(targetAddress string, payload []byte) ([]byte, error) {
|
||||||
|
host, portString, err := net.SplitHostPort(targetAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseUint(portString, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
datagram := []byte{0, 0, 0}
|
||||||
|
ipAddress := net.ParseIP(host)
|
||||||
|
if ipAddress != nil {
|
||||||
|
if ipAddress.To4() != nil {
|
||||||
|
datagram = append(datagram, byte(ipv4))
|
||||||
|
datagram = append(datagram, ipAddress.To4()...)
|
||||||
|
} else {
|
||||||
|
datagram = append(datagram, byte(ipv6))
|
||||||
|
datagram = append(datagram, ipAddress.To16()...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(host) > 255 {
|
||||||
|
return nil, errors.New("domain name too long")
|
||||||
|
}
|
||||||
|
datagram = append(datagram, byte(domainName), byte(len(host)))
|
||||||
|
datagram = append(datagram, []byte(host)...)
|
||||||
|
}
|
||||||
|
datagram = binary.BigEndian.AppendUint16(datagram, uint16(port))
|
||||||
|
datagram = append(datagram, payload...)
|
||||||
|
|
||||||
|
return datagram, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSOCKS5UDPDatagram(datagram []byte) (destinationAddress string, payload []byte, err error) {
|
||||||
|
if len(datagram) < 4 {
|
||||||
|
return "", nil, errors.New("datagram too short")
|
||||||
|
}
|
||||||
|
if datagram[0] != 0 || datagram[1] != 0 {
|
||||||
|
return "", nil, errors.New("invalid reserved header")
|
||||||
|
}
|
||||||
|
if datagram[2] != 0 {
|
||||||
|
return "", nil, errors.New("fragments are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 3
|
||||||
|
var host string
|
||||||
|
switch addrType(datagram[offset]) {
|
||||||
|
case ipv4:
|
||||||
|
offset++
|
||||||
|
if len(datagram) < offset+net.IPv4len+2 {
|
||||||
|
return "", nil, errors.New("datagram too short for IPv4")
|
||||||
|
}
|
||||||
|
host = net.IP(datagram[offset : offset+net.IPv4len]).String()
|
||||||
|
offset += net.IPv4len
|
||||||
|
case ipv6:
|
||||||
|
offset++
|
||||||
|
if len(datagram) < offset+net.IPv6len+2 {
|
||||||
|
return "", nil, errors.New("datagram too short for IPv6")
|
||||||
|
}
|
||||||
|
host = net.IP(datagram[offset : offset+net.IPv6len]).String()
|
||||||
|
offset += net.IPv6len
|
||||||
|
case domainName:
|
||||||
|
offset++
|
||||||
|
if len(datagram) < offset+1 {
|
||||||
|
return "", nil, errors.New("datagram too short for domain length")
|
||||||
|
}
|
||||||
|
domainLength := int(datagram[offset])
|
||||||
|
offset++
|
||||||
|
if len(datagram) < offset+domainLength+2 {
|
||||||
|
return "", nil, errors.New("datagram too short for domain")
|
||||||
|
}
|
||||||
|
host = string(datagram[offset : offset+domainLength])
|
||||||
|
offset += domainLength
|
||||||
|
default:
|
||||||
|
return "", nil, errors.New("unknown address type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(datagram) < offset+2 {
|
||||||
|
return "", nil, errors.New("datagram too short for port")
|
||||||
|
}
|
||||||
|
port := binary.BigEndian.Uint16(datagram[offset : offset+2])
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), datagram[offset:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newServer(t *testing.T) {
|
func Test_newServer(t *testing.T) {
|
||||||
@@ -224,7 +550,8 @@ func Test_Server_StartStop(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
logger := NewMockLogger(ctrl)
|
logger := NewMockLogger(ctrl)
|
||||||
logger.EXPECT().Infof("SOCKS5 server listening on %s", gomock.Any())
|
logger.EXPECT().Infof("SOCKS5 TCP server listening on %s", gomock.Any())
|
||||||
|
logger.EXPECT().Infof("SOCKS5 UDP server listening on %s", gomock.Any())
|
||||||
|
|
||||||
server := newServer(Settings{
|
server := newServer(Settings{
|
||||||
Address: "127.0.0.1:0",
|
Address: "127.0.0.1:0",
|
||||||
@@ -377,6 +704,70 @@ func Test_decodeRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_udpAssociateExpectedClientEndpoint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
request request
|
||||||
|
expected netip.AddrPort
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
"ipv4_endpoint": {
|
||||||
|
request: request{
|
||||||
|
addressType: ipv4,
|
||||||
|
destination: "192.0.2.10",
|
||||||
|
port: 5555,
|
||||||
|
},
|
||||||
|
expected: netip.MustParseAddrPort("192.0.2.10:5555"),
|
||||||
|
},
|
||||||
|
"ipv4_unspecified_address": {
|
||||||
|
request: request{
|
||||||
|
addressType: ipv4,
|
||||||
|
destination: "0.0.0.0",
|
||||||
|
port: 6000,
|
||||||
|
},
|
||||||
|
expected: netip.AddrPortFrom(netip.Addr{}, 6000),
|
||||||
|
},
|
||||||
|
"domain_name_with_port": {
|
||||||
|
request: request{
|
||||||
|
addressType: domainName,
|
||||||
|
destination: "client.example",
|
||||||
|
port: 7000,
|
||||||
|
},
|
||||||
|
expected: netip.AddrPortFrom(netip.Addr{}, 7000),
|
||||||
|
},
|
||||||
|
"domain_name_without_port": {
|
||||||
|
request: request{
|
||||||
|
addressType: domainName,
|
||||||
|
destination: "client.example",
|
||||||
|
},
|
||||||
|
expected: netip.AddrPort{},
|
||||||
|
},
|
||||||
|
"unsupported_address_type": {
|
||||||
|
request: request{
|
||||||
|
addressType: 255,
|
||||||
|
},
|
||||||
|
expectedErr: "address type 255 is not supported",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
result, err := udpAssociateExpectedClientEndpoint(testCase.request)
|
||||||
|
|
||||||
|
if testCase.expectedErr != "" {
|
||||||
|
assert.ErrorContains(t, err, testCase.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testCase.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_verifyFirstNegotiation(t *testing.T) {
|
func Test_verifyFirstNegotiation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
@@ -598,10 +989,6 @@ func Test_cmdType_String(t *testing.T) {
|
|||||||
cmd: connect,
|
cmd: connect,
|
||||||
expectedName: "connect",
|
expectedName: "connect",
|
||||||
},
|
},
|
||||||
"bind": {
|
|
||||||
cmd: bind,
|
|
||||||
expectedName: "bind",
|
|
||||||
},
|
|
||||||
"udp_associate": {
|
"udp_associate": {
|
||||||
cmd: udpAssociate,
|
cmd: udpAssociate,
|
||||||
expectedName: "UDP associate",
|
expectedName: "UDP associate",
|
||||||
@@ -620,3 +1007,80 @@ func Test_cmdType_String(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_socksConn_udpAssociationAddresses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
routerAddress string
|
||||||
|
expectAddressFromConn bool
|
||||||
|
expectedAddress string
|
||||||
|
}{
|
||||||
|
"wildcard_router_address_uses_control_connection_local_ip": {
|
||||||
|
routerAddress: ":0",
|
||||||
|
expectAddressFromConn: true,
|
||||||
|
},
|
||||||
|
"concrete_router_address_is_kept": {
|
||||||
|
routerAddress: "127.0.0.1:0",
|
||||||
|
expectedAddress: "127.0.0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
router, err := newUDPRouter(t.Context(), testCase.routerAddress, noopLogger{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := router.close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
controlListener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := controlListener.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
acceptedConnCh := make(chan net.Conn, 1)
|
||||||
|
go func() {
|
||||||
|
acceptedConn, acceptErr := controlListener.Accept()
|
||||||
|
if acceptErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
acceptedConnCh <- acceptedConn
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientControlConn, err := (&net.Dialer{}).DialContext(t.Context(), "tcp", controlListener.Addr().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer clientControlConn.Close()
|
||||||
|
|
||||||
|
serverControlConn := <-acceptedConnCh
|
||||||
|
defer serverControlConn.Close()
|
||||||
|
|
||||||
|
socksConnection := &socksConn{
|
||||||
|
clientConn: clientControlConn,
|
||||||
|
udpRouter: router,
|
||||||
|
}
|
||||||
|
bindAddress, bindPort, bindAddrType, err := socksConnection.udpAssociationAddresses()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if testCase.expectAddressFromConn {
|
||||||
|
clientLocalHost, _, err := net.SplitHostPort(clientControlConn.LocalAddr().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, clientLocalHost, bindAddress)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, testCase.expectedAddress, bindAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, routerPortString, err := net.SplitHostPort(router.localAddress().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
routerPort, err := strconv.ParseUint(routerPortString, 10, 16)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint16(routerPort), bindPort)
|
||||||
|
assert.Equal(t, ipv4, bindAddrType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
package socks5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type udpAssociation struct {
|
||||||
|
id uint64
|
||||||
|
clientAddrPort netip.AddrPort
|
||||||
|
expectedAddrPort netip.AddrPort
|
||||||
|
controlConnAddr netip.Addr
|
||||||
|
packetCh chan *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpRouter struct {
|
||||||
|
logger Logger
|
||||||
|
|
||||||
|
listener net.PacketConn
|
||||||
|
mutex sync.Mutex
|
||||||
|
bufferPool sync.Pool
|
||||||
|
nextAssociationID uint64
|
||||||
|
clientAddrPortToAssociation map[netip.AddrPort]udpAssociation
|
||||||
|
clientIPToPendingAssociations map[netip.Addr][]udpAssociation
|
||||||
|
associationIDToClientAddrPort map[uint64]netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxUDPPacketLength = 65535
|
||||||
|
maxSOCKS5UDPDatagramOverhead = 3 + 1 + 16 + 2
|
||||||
|
pooledUDPPacketBufferCapacity = maxUDPPacketLength + maxSOCKS5UDPDatagramOverhead
|
||||||
|
)
|
||||||
|
|
||||||
|
func newUDPRouter(ctx context.Context, address string, logger Logger) (router *udpRouter, err error) {
|
||||||
|
config := &net.ListenConfig{}
|
||||||
|
listener, err := config.ListenPacket(ctx, "udp", address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UDP listening: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &udpRouter{
|
||||||
|
logger: logger,
|
||||||
|
listener: listener,
|
||||||
|
bufferPool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, pooledUDPPacketBufferCapacity))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextAssociationID: 1,
|
||||||
|
clientAddrPortToAssociation: make(map[netip.AddrPort]udpAssociation),
|
||||||
|
clientIPToPendingAssociations: make(map[netip.Addr][]udpAssociation),
|
||||||
|
associationIDToClientAddrPort: make(map[uint64]netip.AddrPort),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) localAddress() net.Addr {
|
||||||
|
return r.listener.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) close() error {
|
||||||
|
return r.listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) registerAssociation(controlConn net.Conn, expectedAddrPort netip.AddrPort) (udpAssociation, error) {
|
||||||
|
controlConnAddrPort, err := netip.ParseAddrPort(controlConn.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return udpAssociation{}, fmt.Errorf("parsing control connection address: %w", err)
|
||||||
|
}
|
||||||
|
controlConnAddr := controlConnAddrPort.Addr().Unmap()
|
||||||
|
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
const udpPacketChannelBuffer = 64
|
||||||
|
associationID := r.nextAssociationID
|
||||||
|
r.nextAssociationID++
|
||||||
|
|
||||||
|
association := udpAssociation{
|
||||||
|
id: associationID,
|
||||||
|
expectedAddrPort: expectedAddrPort,
|
||||||
|
controlConnAddr: controlConnAddr,
|
||||||
|
packetCh: make(chan *bytes.Buffer, udpPacketChannelBuffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedAddrPort.Addr().IsValid() && expectedAddrPort.Port() != 0 {
|
||||||
|
association.clientAddrPort = expectedAddrPort
|
||||||
|
r.clientAddrPortToAssociation[association.clientAddrPort] = association
|
||||||
|
r.associationIDToClientAddrPort[association.id] = association.clientAddrPort
|
||||||
|
return association, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingAssociations := r.clientIPToPendingAssociations[controlConnAddr]
|
||||||
|
pendingAssociations = append(pendingAssociations, association)
|
||||||
|
r.clientIPToPendingAssociations[controlConnAddr] = pendingAssociations
|
||||||
|
|
||||||
|
return association, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) unregisterAssociation(association udpAssociation) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
clientAddrPort, hasClientAddress := r.associationIDToClientAddrPort[association.id]
|
||||||
|
if hasClientAddress {
|
||||||
|
delete(r.associationIDToClientAddrPort, association.id)
|
||||||
|
delete(r.clientAddrPortToAssociation, clientAddrPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingAssociations := r.clientIPToPendingAssociations[association.controlConnAddr]
|
||||||
|
for i, pendingAssociation := range pendingAssociations {
|
||||||
|
if pendingAssociation.id == association.id {
|
||||||
|
pendingAssociations = append(pendingAssociations[:i], pendingAssociations[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(pendingAssociations) == 0 {
|
||||||
|
delete(r.clientIPToPendingAssociations, association.controlConnAddr)
|
||||||
|
} else {
|
||||||
|
r.clientIPToPendingAssociations[association.controlConnAddr] = pendingAssociations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) run(ctx context.Context) error {
|
||||||
|
packetBuffer := make([]byte, maxUDPPacketLength)
|
||||||
|
|
||||||
|
for {
|
||||||
|
packetLength, sourceAddress, err := r.listener.ReadFrom(packetBuffer)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil && errors.Is(err, net.ErrClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("reading UDP packet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAddrPort, err := netAddrToNetipAddrPort(sourceAddress)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("parsing source address: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buffer := r.bufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert
|
||||||
|
buffer.Reset()
|
||||||
|
_, err = buffer.Write(packetBuffer[:packetLength])
|
||||||
|
if err != nil {
|
||||||
|
r.bufferPool.Put(buffer)
|
||||||
|
r.logger.Warnf("buffering packet: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = r.routePacket(sourceAddrPort, buffer)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("failed routing UDP packet: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) routePacket(sourceAddrPort netip.AddrPort, packet *bytes.Buffer) error {
|
||||||
|
r.mutex.Lock()
|
||||||
|
association, packetFromClient := r.findClientAssociation(sourceAddrPort)
|
||||||
|
r.mutex.Unlock()
|
||||||
|
|
||||||
|
if !packetFromClient {
|
||||||
|
r.bufferPool.Put(packet)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case association.packetCh <- packet:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
r.bufferPool.Put(packet)
|
||||||
|
return errors.New("association packet queue full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) findClientAssociation(sourceAddrPort netip.AddrPort) (
|
||||||
|
association udpAssociation, ok bool,
|
||||||
|
) {
|
||||||
|
association, ok = r.clientAddrPortToAssociation[sourceAddrPort]
|
||||||
|
if ok {
|
||||||
|
return association, true
|
||||||
|
}
|
||||||
|
sourceAddr := sourceAddrPort.Addr()
|
||||||
|
|
||||||
|
pendingAssociations := r.clientIPToPendingAssociations[sourceAddr]
|
||||||
|
if len(pendingAssociations) == 0 {
|
||||||
|
return udpAssociation{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
index := -1
|
||||||
|
for i, pendingAssociation := range pendingAssociations {
|
||||||
|
if matchesExpectedClientEndpoint(pendingAssociation, sourceAddrPort) {
|
||||||
|
association = pendingAssociation
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index == -1 {
|
||||||
|
return udpAssociation{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
r.clientIPToPendingAssociations[sourceAddr] = append(pendingAssociations[:index], pendingAssociations[index+1:]...)
|
||||||
|
if len(r.clientIPToPendingAssociations[sourceAddr]) == 0 {
|
||||||
|
delete(r.clientIPToPendingAssociations, sourceAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
association.clientAddrPort = sourceAddrPort
|
||||||
|
r.clientAddrPortToAssociation[sourceAddrPort] = association
|
||||||
|
r.associationIDToClientAddrPort[association.id] = sourceAddrPort
|
||||||
|
|
||||||
|
return association, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesExpectedClientEndpoint(association udpAssociation, sourceAddrPort netip.AddrPort) bool {
|
||||||
|
switch {
|
||||||
|
case association.expectedAddrPort.Addr().IsValid() && sourceAddrPort.Addr() != association.expectedAddrPort.Addr():
|
||||||
|
return false
|
||||||
|
case association.expectedAddrPort.Port() != 0 && sourceAddrPort.Port() != association.expectedAddrPort.Port():
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) clientAddrPortForAssociation(associationID uint64) (
|
||||||
|
clientAddrPort netip.AddrPort, ok bool,
|
||||||
|
) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
clientAddrPort, ok = r.associationIDToClientAddrPort[associationID]
|
||||||
|
return clientAddrPort, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) runAssociationHandler(ctx context.Context, association udpAssociation) {
|
||||||
|
config := &net.ListenConfig{}
|
||||||
|
socket, err := config.ListenPacket(ctx, "udp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("creating per-association UDP socket: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer socket.Close()
|
||||||
|
|
||||||
|
go closeSocketOnContextDone(ctx, socket)
|
||||||
|
|
||||||
|
packetBuffer := make([]byte, maxUDPPacketLength)
|
||||||
|
|
||||||
|
forwardDoneCh := make(chan struct{})
|
||||||
|
go r.forwardClientPackets(ctx, socket, association.packetCh, forwardDoneCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
packetLength, sourceAddress, err := socket.ReadFrom(packetBuffer)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
|
||||||
|
<-forwardDoneCh
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.logger.Warnf("reading from per-association UDP socket: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAddrPort, err := netAddrToNetipAddrPort(sourceAddress)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("parsing source address from destination: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := r.bufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert
|
||||||
|
buffer.Reset()
|
||||||
|
err = encodeUDPDatagramToBuffer(buffer, sourceAddrPort, packetBuffer[:packetLength])
|
||||||
|
if err != nil {
|
||||||
|
r.bufferPool.Put(buffer)
|
||||||
|
r.logger.Warnf("encoding response datagram: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientAddrPort, found := r.clientAddrPortForAssociation(association.id)
|
||||||
|
if !found {
|
||||||
|
r.bufferPool.Put(buffer)
|
||||||
|
r.logger.Warnf("client address not found for association id %d", association.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientUDPAddress := &net.UDPAddr{
|
||||||
|
IP: clientAddrPort.Addr().AsSlice(),
|
||||||
|
Port: int(clientAddrPort.Port()),
|
||||||
|
}
|
||||||
|
_, err = r.listener.WriteTo(buffer.Bytes(), clientUDPAddress)
|
||||||
|
r.bufferPool.Put(buffer)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("writing response to client: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeSocketOnContextDone(ctx context.Context, socket net.PacketConn) {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = socket.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) forwardClientPackets(ctx context.Context, socket net.PacketConn,
|
||||||
|
packetCh <-chan *bytes.Buffer, done chan<- struct{},
|
||||||
|
) {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case buffer, ok := <-packetCh:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.writeClientPacketToDestination(ctx, socket, buffer)
|
||||||
|
r.bufferPool.Put(buffer)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("forwarding client packet to destination: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *udpRouter) writeClientPacketToDestination(ctx context.Context,
|
||||||
|
socket net.PacketConn, packet *bytes.Buffer,
|
||||||
|
) error {
|
||||||
|
destination, payload, err := decodeUDPDatagram(packet.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decoding UDP datagram: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(destination)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("splitting destination host and port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := netip.ParseAddr(host); err != nil { // domain name
|
||||||
|
addrs, err := net.DefaultResolver.LookupHost(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving destination host: %w", err)
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return fmt.Errorf("resolving destination host: no addresses found for %q", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = net.JoinHostPort(addrs[0], portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDestinationUDPAddress, err := net.ResolveUDPAddr("udp", destination)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving destination UDP address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = socket.WriteTo(payload, resolvedDestinationUDPAddress)
|
||||||
|
if err != nil && ctx.Err() == nil {
|
||||||
|
return fmt.Errorf("writing payload to destination: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func netAddrToNetipAddrPort(addr net.Addr) (netip.AddrPort, error) {
|
||||||
|
addrPort, err := netip.ParseAddrPort(addr.String())
|
||||||
|
if err != nil {
|
||||||
|
return netip.AddrPort{}, fmt.Errorf("parsing address: %w", err)
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package socks5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_udpRouter_ResolveGithubFromCloudflareDNS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
const deadlineBuffer = 500 * time.Millisecond
|
||||||
|
deadline = deadline.Add(-deadlineBuffer)
|
||||||
|
} else {
|
||||||
|
const defaultTimeout = 10 * time.Second
|
||||||
|
deadline = time.Now().Add(defaultTimeout)
|
||||||
|
}
|
||||||
|
ctx, cancel = context.WithDeadline(ctx, deadline)
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
logger := NewMockLogger(ctrl)
|
||||||
|
|
||||||
|
router, err := newUDPRouter(ctx, "127.0.0.1:0", logger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
routerRunErrCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
routerRunErrCh <- router.run(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cancel()
|
||||||
|
err := router.close()
|
||||||
|
assert.NoError(t, err, "closing router")
|
||||||
|
runErr := <-routerRunErrCh
|
||||||
|
assert.NoError(t, runErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
controlListener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := controlListener.Close()
|
||||||
|
assert.NoError(t, err, "closing control listener")
|
||||||
|
})
|
||||||
|
|
||||||
|
acceptedConnCh := make(chan net.Conn)
|
||||||
|
go func() {
|
||||||
|
acceptedConn, acceptErr := controlListener.Accept()
|
||||||
|
assert.NoError(t, acceptErr, "accepting control connection")
|
||||||
|
if acceptErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
acceptedConnCh <- acceptedConn
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientControlConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", controlListener.Addr().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err = clientControlConn.Close()
|
||||||
|
assert.NoError(t, err, "closing client control connection")
|
||||||
|
})
|
||||||
|
|
||||||
|
serverControlConn := <-acceptedConnCh
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := serverControlConn.Close()
|
||||||
|
assert.NoError(t, err, "closing server control connection")
|
||||||
|
})
|
||||||
|
|
||||||
|
association, err := router.registerAssociation(serverControlConn, netip.AddrPort{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
router.unregisterAssociation(association)
|
||||||
|
})
|
||||||
|
|
||||||
|
associationCtx, associationCancel := context.WithCancel(ctx)
|
||||||
|
handlerDoneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
router.runAssociationHandler(associationCtx, association)
|
||||||
|
close(handlerDoneCh)
|
||||||
|
}()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
associationCancel()
|
||||||
|
<-handlerDoneCh
|
||||||
|
})
|
||||||
|
|
||||||
|
udpRouterAddress, err := net.ResolveUDPAddr("udp", router.localAddress().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
clientUDPConn, err := net.DialUDP("udp", nil, udpRouterAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := clientUDPConn.Close()
|
||||||
|
assert.NoError(t, err, "closing client UDP connection")
|
||||||
|
})
|
||||||
|
|
||||||
|
queryID := uint16(rand.Uint32()) //nolint:gosec
|
||||||
|
dnsRequest := &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: queryID,
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{{
|
||||||
|
Name: dns.Fqdn("github.com"),
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
dnsQuery, err := dnsRequest.Pack()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
targetAddrPort := netip.MustParseAddrPort("1.1.1.1:53")
|
||||||
|
socksDatagramBuffer := bytes.NewBuffer(nil)
|
||||||
|
err = encodeUDPDatagramToBuffer(socksDatagramBuffer, targetAddrPort, dnsQuery)
|
||||||
|
require.NoError(t, err)
|
||||||
|
socksDatagram := socksDatagramBuffer.Bytes()
|
||||||
|
|
||||||
|
err = clientUDPConn.SetDeadline(deadline)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = clientUDPConn.Write(socksDatagram)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
responseBuffer := make([]byte, maxUDPPacketLength)
|
||||||
|
responseLength, err := clientUDPConn.Read(responseBuffer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
responseDestination, responsePayload, err := decodeUDPDatagram(responseBuffer[:responseLength])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
responseHost, responsePortString, err := net.SplitHostPort(responseDestination)
|
||||||
|
require.NoError(t, err)
|
||||||
|
responsePort, err := strconv.ParseUint(responsePortString, 10, 16)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint64(53), responsePort)
|
||||||
|
assert.NotEmpty(t, responseHost)
|
||||||
|
|
||||||
|
dnsResponse := new(dns.Msg)
|
||||||
|
err = dnsResponse.Unpack(responsePayload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, queryID, dnsResponse.Id)
|
||||||
|
assert.True(t, dnsResponse.Response)
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, dnsResponse.Rcode)
|
||||||
|
require.NotEmpty(t, dnsResponse.Question)
|
||||||
|
assert.Equal(t, dns.Fqdn("github.com"), dnsResponse.Question[0].Name)
|
||||||
|
assert.Equal(t, dns.TypeA, dnsResponse.Question[0].Qtype)
|
||||||
|
assert.NotEmpty(t, dnsResponse.Answer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func copyServer(server models.Server) (serverCopy models.Server) {
|
func copyServer(server models.Server) (serverCopy models.Server) {
|
||||||
serverCopy = server
|
serverCopy = server
|
||||||
serverCopy.IPs = slices.Clone(server.IPs)
|
serverCopy.IPs = copyIPs(server.IPs)
|
||||||
serverCopy.PortsTCP = slices.Clone(server.PortsTCP)
|
|
||||||
serverCopy.PortsUDP = slices.Clone(server.PortsUDP)
|
|
||||||
return serverCopy
|
return serverCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyIPs(toCopy []netip.Addr) (copied []netip.Addr) {
|
||||||
|
if toCopy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copied = make([]netip.Addr, len(toCopy))
|
||||||
|
copy(copied, toCopy)
|
||||||
|
return copied
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,9 +21,43 @@ func Test_copyServer(t *testing.T) {
|
|||||||
assert.Equal(t, server, serverCopy)
|
assert.Equal(t, server, serverCopy)
|
||||||
// Check for mutation
|
// Check for mutation
|
||||||
serverCopy.IPs[0] = netip.AddrFrom4([4]byte{9, 9, 9, 9})
|
serverCopy.IPs[0] = netip.AddrFrom4([4]byte{9, 9, 9, 9})
|
||||||
serverCopy.PortsTCP = []uint16{80}
|
assert.NotEqual(t, server, serverCopy)
|
||||||
serverCopy.PortsUDP = []uint16{53}
|
}
|
||||||
assert.NotEqual(t, server.IPs, serverCopy.IPs)
|
|
||||||
assert.NotEqual(t, server.PortsTCP, serverCopy.PortsTCP)
|
func Test_copyIPs(t *testing.T) {
|
||||||
assert.NotEqual(t, server.PortsUDP, serverCopy.PortsUDP)
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
toCopy []netip.Addr
|
||||||
|
copied []netip.Addr
|
||||||
|
}{
|
||||||
|
"nil": {},
|
||||||
|
"empty": {
|
||||||
|
toCopy: []netip.Addr{},
|
||||||
|
copied: []netip.Addr{},
|
||||||
|
},
|
||||||
|
"single IP": {
|
||||||
|
toCopy: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})},
|
||||||
|
copied: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1})},
|
||||||
|
},
|
||||||
|
"two IPs": {
|
||||||
|
toCopy: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
|
||||||
|
copied: []netip.Addr{netip.AddrFrom4([4]byte{1, 1, 1, 1}), netip.AddrFrom4([4]byte{2, 2, 2, 2})},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
copied := copyIPs(testCase.toCopy)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.copied, copied)
|
||||||
|
|
||||||
|
if len(copied) > 0 {
|
||||||
|
testCase.toCopy[0] = netip.AddrFrom4([4]byte{9, 9, 9, 9})
|
||||||
|
assert.NotEqual(t, testCase.toCopy[0], testCase.copied[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
@@ -49,7 +48,6 @@ func (s *Storage) FilterServers(provider string, selection settings.ServerSelect
|
|||||||
return servers, nil
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocognit,gocyclo
|
|
||||||
func filterServer(server models.Server,
|
func filterServer(server models.Server,
|
||||||
selection settings.ServerSelection,
|
selection settings.ServerSelection,
|
||||||
) (filtered bool) {
|
) (filtered bool) {
|
||||||
@@ -92,11 +90,6 @@ func filterServer(server models.Server,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*selection.Dedicated && !server.Dedicated) ||
|
|
||||||
(!*selection.Dedicated && server.Dedicated) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if filterByPossibilities(server.Country, selection.Countries) {
|
if filterByPossibilities(server.Country, selection.Countries) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -129,14 +122,6 @@ func filterServer(server models.Server,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
serverPorts := server.PortsUDP
|
|
||||||
if server.VPN == vpn.OpenVPN && server.TCP {
|
|
||||||
serverPorts = server.PortsTCP
|
|
||||||
}
|
|
||||||
if filterByPorts(selection, serverPorts) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO filter port forward server for PIA
|
// TODO filter port forward server for PIA
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -180,21 +165,3 @@ func filterByProtocol(selection settings.ServerSelection,
|
|||||||
return (wantTCP && !serverTCP) || (wantUDP && !serverUDP)
|
return (wantTCP && !serverTCP) || (wantUDP && !serverUDP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterByPorts(selection settings.ServerSelection,
|
|
||||||
serverPorts []uint16,
|
|
||||||
) (filtered bool) {
|
|
||||||
if len(serverPorts) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
|
||||||
if selection.VPN == vpn.Wireguard {
|
|
||||||
customPort = *selection.Wireguard.EndpointPort
|
|
||||||
}
|
|
||||||
if customPort == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return !slices.Contains(serverPorts, customPort)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func commaJoin(slice []string) string {
|
|||||||
return strings.Join(slice, ", ")
|
return strings.Join(slice, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func noServerFoundError(selection settings.ServerSelection) (err error) { //nolint:gocyclo
|
func noServerFoundError(selection settings.ServerSelection) (err error) {
|
||||||
var messageParts []string
|
var messageParts []string
|
||||||
|
|
||||||
messageParts = append(messageParts, "VPN "+selection.VPN)
|
messageParts = append(messageParts, "VPN "+selection.VPN)
|
||||||
@@ -155,15 +155,6 @@ func noServerFoundError(selection settings.ServerSelection) (err error) { //noli
|
|||||||
"target ip address "+targetIP.String())
|
"target ip address "+targetIP.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
customPort := *selection.OpenVPN.CustomPort
|
|
||||||
if selection.VPN == vpn.Wireguard {
|
|
||||||
customPort = *selection.Wireguard.EndpointPort
|
|
||||||
}
|
|
||||||
if customPort > 0 {
|
|
||||||
messageParts = append(messageParts,
|
|
||||||
fmt.Sprintf("%s endpoint port %d", selection.VPN, customPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
message := "for " + strings.Join(messageParts, "; ")
|
message := "for " + strings.Join(messageParts, "; ")
|
||||||
|
|
||||||
return fmt.Errorf("no server found: %s", message)
|
return fmt.Errorf("no server found: %s", message)
|
||||||
|
|||||||
@@ -1,39 +1,26 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
|
||||||
serversmodule "github.com/qdm12/gluetun-servers/pkg/servers"
|
serversmodule "github.com/qdm12/gluetun-servers/pkg/servers"
|
||||||
|
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed servers.json
|
|
||||||
var allServersEmbedFS embed.FS
|
|
||||||
|
|
||||||
func parseHardcodedServers() (allServers models.AllServers) {
|
func parseHardcodedServers() (allServers models.AllServers) {
|
||||||
f, err := allServersEmbedFS.Open("servers.json")
|
allProviders := providers.All()
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer f.Close() // no-op
|
|
||||||
decoder := json.NewDecoder(f)
|
|
||||||
err = decoder.Decode(&allServers)
|
|
||||||
if err != nil {
|
|
||||||
panic("decoding servers.json: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
for provider, metadata := range allServers.ProviderToServers {
|
const version = 1
|
||||||
if metadata.Filepath == "" {
|
allServers.ProviderToServers = make(map[string]models.Servers, len(allProviders))
|
||||||
panic(fmt.Sprintf("embedded manifest file servers.json should have the filepath field set for %s", provider))
|
allServers.Version = version
|
||||||
}
|
for _, provider := range allProviders {
|
||||||
filename := path.Base(metadata.Filepath)
|
filename := provider + ".json"
|
||||||
providerFile, err := serversmodule.Files.Open(filename)
|
providerFile, err := serversmodule.Files.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
const rootURL = "https://github.com/qdm12/gluetun-servers/blob/main/pkg/servers"
|
panic(fmt.Sprintf("reading embedded provider file %s for %s: %s", filename, provider, err))
|
||||||
panic(fmt.Sprintf("reading embedded provider file defined at %s/%s: %s", rootURL, filename, err))
|
|
||||||
}
|
}
|
||||||
defer providerFile.Close() // no-op
|
defer providerFile.Close() // no-op
|
||||||
|
|
||||||
@@ -48,7 +35,8 @@ func parseHardcodedServers() (allServers models.AllServers) {
|
|||||||
filename, provider))
|
filename, provider))
|
||||||
}
|
}
|
||||||
|
|
||||||
providerServers.Filepath = metadata.Filepath // inherit filepath from servers.json
|
const serversPath = "/gluetun/servers/"
|
||||||
|
providerServers.Filepath = filepath.Join(serversPath, filename)
|
||||||
allServers.ProviderToServers[provider] = providerServers
|
allServers.ProviderToServers[provider] = providerServers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ func Test_parseHardcodedServers(t *testing.T) {
|
|||||||
func Test_parseHardcodedServers_filepathsAndEmbeddedProviderFiles(t *testing.T) {
|
func Test_parseHardcodedServers_filepathsAndEmbeddedProviderFiles(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var hardcodedServers models.AllServers
|
hardcodedServers := parseHardcodedServers()
|
||||||
require.NotPanics(t, func() {
|
|
||||||
hardcodedServers = parseHardcodedServers()
|
|
||||||
})
|
|
||||||
|
|
||||||
allProviders := providers.All()
|
allProviders := providers.All()
|
||||||
for _, provider := range allProviders {
|
for _, provider := range allProviders {
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ package storage
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
func panicOnProviderMissingHardcoded(provider string) {
|
func panicOnProviderMissingHardcoded(provider string) {
|
||||||
panic(fmt.Sprintf("provider %s not found in hardcoded servers map; "+
|
panic(fmt.Sprintf("provider %s not found in hardcoded servers map", provider))
|
||||||
"did you add the provider key in the embedded servers.json?", provider))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,8 +152,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
|||||||
allProviders[0]: 1,
|
allProviders[0]: 1,
|
||||||
// Missing provider allProviders[1]
|
// Missing provider allProviders[1]
|
||||||
}
|
}
|
||||||
expectedPanicValue := fmt.Sprintf("provider %s not found in hardcoded servers map; "+
|
expectedPanicValue := fmt.Sprintf("provider %s not found in hardcoded servers map", allProviders[1])
|
||||||
"did you add the provider key in the embedded servers.json?", allProviders[1])
|
|
||||||
assert.PanicsWithValue(t, expectedPanicValue, func() {
|
assert.PanicsWithValue(t, expectedPanicValue, func() {
|
||||||
_, _ = s.extractServersFromBytes(b, hardcodedVersions)
|
_, _ = s.extractServersFromBytes(b, hardcodedVersions)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"airvpn": {
|
|
||||||
"filepath": "/gluetun/servers/airvpn.json"
|
|
||||||
},
|
|
||||||
"cyberghost": {
|
|
||||||
"filepath": "/gluetun/servers/cyberghost.json"
|
|
||||||
},
|
|
||||||
"expressvpn": {
|
|
||||||
"filepath": "/gluetun/servers/expressvpn.json"
|
|
||||||
},
|
|
||||||
"fastestvpn": {
|
|
||||||
"filepath": "/gluetun/servers/fastestvpn.json"
|
|
||||||
},
|
|
||||||
"giganews": {
|
|
||||||
"filepath": "/gluetun/servers/giganews.json"
|
|
||||||
},
|
|
||||||
"hidemyass": {
|
|
||||||
"filepath": "/gluetun/servers/hidemyass.json"
|
|
||||||
},
|
|
||||||
"ipvanish": {
|
|
||||||
"filepath": "/gluetun/servers/ipvanish.json"
|
|
||||||
},
|
|
||||||
"ivpn": {
|
|
||||||
"filepath": "/gluetun/servers/ivpn.json"
|
|
||||||
},
|
|
||||||
"mullvad": {
|
|
||||||
"filepath": "/gluetun/servers/mullvad.json"
|
|
||||||
},
|
|
||||||
"nordvpn": {
|
|
||||||
"filepath": "/gluetun/servers/nordvpn.json"
|
|
||||||
},
|
|
||||||
"ovpn": {
|
|
||||||
"filepath": "/gluetun/servers/ovpn.json"
|
|
||||||
},
|
|
||||||
"perfect privacy": {
|
|
||||||
"filepath": "/gluetun/servers/perfect privacy.json"
|
|
||||||
},
|
|
||||||
"privado": {
|
|
||||||
"filepath": "/gluetun/servers/privado.json"
|
|
||||||
},
|
|
||||||
"private internet access": {
|
|
||||||
"filepath": "/gluetun/servers/private internet access.json"
|
|
||||||
},
|
|
||||||
"privatevpn": {
|
|
||||||
"filepath": "/gluetun/servers/privatevpn.json"
|
|
||||||
},
|
|
||||||
"protonvpn": {
|
|
||||||
"filepath": "/gluetun/servers/protonvpn.json"
|
|
||||||
},
|
|
||||||
"purevpn": {
|
|
||||||
"filepath": "/gluetun/servers/purevpn.json"
|
|
||||||
},
|
|
||||||
"slickvpn": {
|
|
||||||
"filepath": "/gluetun/servers/slickvpn.json"
|
|
||||||
},
|
|
||||||
"surfshark": {
|
|
||||||
"filepath": "/gluetun/servers/surfshark.json"
|
|
||||||
},
|
|
||||||
"torguard": {
|
|
||||||
"filepath": "/gluetun/servers/torguard.json"
|
|
||||||
},
|
|
||||||
"vpn unlimited": {
|
|
||||||
"filepath": "/gluetun/servers/vpn unlimited.json"
|
|
||||||
},
|
|
||||||
"vpnsecure": {
|
|
||||||
"filepath": "/gluetun/servers/vpnsecure.json"
|
|
||||||
},
|
|
||||||
"vyprvpn": {
|
|
||||||
"filepath": "/gluetun/servers/vyprvpn.json"
|
|
||||||
},
|
|
||||||
"windscribe": {
|
|
||||||
"filepath": "/gluetun/servers/windscribe.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
# Maintenance
|
# Maintenance
|
||||||
|
|
||||||
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
|
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
|
||||||
- Go 1.18
|
|
||||||
- gofumpt
|
|
||||||
- Use netip
|
|
||||||
- Split servers.json
|
|
||||||
- Common slice of Wireguard providers in config settings
|
- Common slice of Wireguard providers in config settings
|
||||||
- DNS block lists as LFS and built in image
|
- DNS block lists as LFS and built in image
|
||||||
- Add HTTP server v3 as json rpc
|
- Add HTTP server v3 as json rpc
|
||||||
- Use `github.com/qdm12/ddns-updater/pkg/publicip`
|
- Use `github.com/qdm12/ddns-updater/pkg/publicip`
|
||||||
- Windows and Darwin development support
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Authentication with the control server
|
|
||||||
- Get announcement from Github file
|
- Get announcement from Github file
|
||||||
- Support multiple connections in custom ovpn
|
- Support multiple connections in custom ovpn
|
||||||
- Automate IPv6 detection for OpenVPN
|
- Automate IPv6 detection for OpenVPN
|
||||||
|
|||||||
Reference in New Issue
Block a user