mirror of
https://github.com/qdm12/gluetun.git
synced 2026-06-27 22:37:33 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a670635c4 | |||
| b5f815640e | |||
| 506c9140ba | |||
| 80763145dc | |||
| cfb8fcf00f | |||
| b9421221da | |||
| 83feb533de | |||
| 1b12dcb5b4 | |||
| 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 |
+31
-5
@@ -4,12 +4,38 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
interval: "weekly"
|
||||
- # Servers data dependency that should be updated as soon as
|
||||
# possible when a new version is released, to have the latest
|
||||
# servers available
|
||||
package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
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"
|
||||
|
||||
@@ -8,5 +8,4 @@
|
||||
|
||||
# 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/)
|
||||
|
||||
+16
-10
@@ -28,6 +28,10 @@ on:
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,7 +48,6 @@ jobs:
|
||||
locale: "US"
|
||||
level: error
|
||||
exclude: |
|
||||
./internal/storage/servers.json
|
||||
./.golangci.yml
|
||||
*.md
|
||||
|
||||
@@ -98,7 +101,7 @@ jobs:
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
needs: [ verify ]
|
||||
needs: [verify]
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
@@ -120,7 +123,8 @@ jobs:
|
||||
|
||||
- name: Run Gluetun container with ProtonVPN Wireguard and port forwarding
|
||||
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
|
||||
|
||||
- name: Run Gluetun container with ProtonVPN OpenVPN and port forwarding
|
||||
@@ -129,11 +133,12 @@ jobs:
|
||||
secrets.PROTONVPN_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||
protonvpn-openvpn-port-forwarding
|
||||
|
||||
- name: Run Gluetun container with Private Internet Access OpenVPN and port
|
||||
forwarding configuration
|
||||
run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
|
||||
secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||
private-internet-access-openvpn-port-forwarding
|
||||
# - name:
|
||||
# Run Gluetun container with Private Internet Access OpenVPN and port
|
||||
# forwarding configuration
|
||||
# run: echo -e "${{ secrets.PRIVATEINTERNETACCESS_OPENVPN_USER }}\n${{
|
||||
# secrets.PRIVATEINTERNETACCESS_OPENVPN_PASSWORD }}" | ./ci/runner
|
||||
# private-internet-access-openvpn-port-forwarding
|
||||
|
||||
- name: Run Gluetun container with AirVPN Wireguard configuration
|
||||
run: echo -e "${{ secrets.AIRVPN_WIREGUARD_PRIVATE_KEY }}\n${{
|
||||
@@ -141,7 +146,8 @@ jobs:
|
||||
secrets.AIRVPN_WIREGUARD_ADDRESSES }}" | ./ci/runner airvpn-wireguard
|
||||
|
||||
- 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
|
||||
|
||||
codeql:
|
||||
@@ -169,7 +175,7 @@ jobs:
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
needs: [ verify, verify-private, codeql ]
|
||||
needs: [verify, verify-private, codeql]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
- "**.md"
|
||||
- .github/workflows/markdown.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
markdown:
|
||||
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$
|
||||
- examples$
|
||||
|
||||
run:
|
||||
build-tags:
|
||||
- integration
|
||||
|
||||
linters:
|
||||
settings:
|
||||
misspell:
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@
|
||||
// to develop this project.
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"go.buildTags": "linux",
|
||||
"go.buildTags": "linux,integration",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
||||
PUID=1000 \
|
||||
PGID=1000
|
||||
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
|
||||
ARG TARGETPLATFORM
|
||||
RUN apk add --no-cache --update -l wget && \
|
||||
|
||||
@@ -73,7 +73,7 @@ Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
|
||||
- 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 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)
|
||||
- [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)
|
||||
|
||||
@@ -4,15 +4,16 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
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/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/jsimonetti/rtnetlink v1.4.2
|
||||
github.com/klauspost/compress v1.18.4
|
||||
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/miekg/dns v1.1.62
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||
github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82
|
||||
@@ -27,10 +28,9 @@ require (
|
||||
github.com/ti-mo/netfilter v0.5.3
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/text v0.35.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
@@ -47,8 +47,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mdlayher/socket v0.6.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -57,10 +56,11 @@ require (
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/mod v0.36.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
|
||||
google.golang.org/protobuf v1.35.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-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16 h1:XY6HOq/xtqH8ZXMncRWkjFs85EKdN10NLNnw23kTpE0=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16/go.mod h1:nRkPpIzjCxMW8pZKXTRkpqAQVlmFJdVOGkeQSC7wbms=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.18 h1:pUn7/P8qdGmHd6JmE3bCQXPblZs3vruWR98nLODQLJg=
|
||||
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
|
||||
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/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
||||
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/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
@@ -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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
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/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
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-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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
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-20190412213103-97732733099d/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.5.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.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
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/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
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-20191011141410-1b5146add898/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"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -104,6 +105,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
||||
}
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: dnsDialer.Dial,
|
||||
}
|
||||
|
||||
const clientTimeout = 10 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
)
|
||||
|
||||
// Start launches a command and streams stdout and stderr to channels.
|
||||
// All the channels returned are ready only and won't be closed
|
||||
// if the command fails later.
|
||||
// stdoutLines and stderrLines channels will be closed when there is no more
|
||||
// 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) (
|
||||
stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
@@ -38,6 +39,7 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
||||
@@ -45,9 +47,11 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stderr.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
@@ -55,8 +59,10 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
<-stdoutDone
|
||||
<-stderrDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stdout.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
_ = stderr.Close()
|
||||
waitErrorCh <- err
|
||||
}()
|
||||
|
||||
@@ -89,30 +89,48 @@ func Test_start(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var stdoutIndex, stderrIndex int
|
||||
|
||||
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)
|
||||
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
|
||||
testCase.stdout, testCase.stderr, testCase.waitErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||
go streamLines(streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
func streamLines(done chan<- struct{}, logger Logger,
|
||||
stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
logger.Info(line)
|
||||
case line = <-stderr:
|
||||
logger.Error(line)
|
||||
case line, ok := <-stdout:
|
||||
if ok {
|
||||
logger.Info(line)
|
||||
break
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func readObsolete(r *reader.Reader) (warnings []string) {
|
||||
@@ -17,7 +17,7 @@ func readObsolete(r *reader.Reader) (warnings []string) {
|
||||
"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",
|
||||
}
|
||||
sortedKeys := maps.Keys(keyToMessage)
|
||||
sortedKeys := slices.Collect(maps.Keys(keyToMessage))
|
||||
slices.Sort(sortedKeys)
|
||||
warnings = make([]string, 0, len(keyToMessage))
|
||||
for _, key := range sortedKeys {
|
||||
|
||||
@@ -95,7 +95,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
|
||||
return errors.New("port forwarding password is empty")
|
||||
}
|
||||
case providers.Protonvpn:
|
||||
const maxPortsCount = 4
|
||||
const maxPortsCount = 5
|
||||
if p.PortsCount > maxPortsCount {
|
||||
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
@@ -84,11 +85,14 @@ func triggerDNSQuery(ctx context.Context, client *http.Client, session string) (
|
||||
IP map[string]uint `json:"ip"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data ipLeakData
|
||||
err = decoder.Decode(&data)
|
||||
rawData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding response: %w", err)
|
||||
return nil, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
var data ipLeakData
|
||||
err = json.Unmarshal(rawData, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding response %q: %w", rawData, err)
|
||||
} else if data.Session != session {
|
||||
return nil, fmt.Errorf("ipleak.net session mismatch: expected %s, got %s", session, data.Session)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package iptables
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"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)
|
||||
}
|
||||
err = checkData(string(output))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking saved data: %w", err)
|
||||
}
|
||||
return string(output), nil
|
||||
return filterData(output)
|
||||
}
|
||||
|
||||
func checkData(data string) error {
|
||||
scanner := bufio.NewScanner(strings.NewReader(data))
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "[unsupported") {
|
||||
return fmt.Errorf("unsupported revision marker found in line %d: %s", i+1, line)
|
||||
func filterData(cmdOutput []byte) (filtered string, err error) {
|
||||
lines := strings.Split(string(cmdOutput), "\n")
|
||||
filteredLines := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasPrefix(line, ":DOCKER_OUTPUT"),
|
||||
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 fmt.Errorf("scanning data: %w", scanner.Err())
|
||||
}
|
||||
return nil
|
||||
return strings.Join(filteredLines, "\n"), nil
|
||||
}
|
||||
|
||||
@@ -278,7 +278,6 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
|
||||
appendOrDelete(remove), interfaceFlag, destinationPort),
|
||||
})
|
||||
if err != nil {
|
||||
restore(ctx) // just in case
|
||||
errMessage := err.Error()
|
||||
if strings.Contains(errMessage, "can't initialize ip6tables table `nat': Table does not exist") {
|
||||
if !remove {
|
||||
@@ -286,6 +285,7 @@ func (c *Config) RedirectPort(ctx context.Context, intf string,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
restore(ctx)
|
||||
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
|
||||
@@ -29,19 +29,16 @@ func (r *Runner) Run(ctx context.Context, errCh chan<- error, ready chan<- struc
|
||||
return
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, r.logger,
|
||||
go streamLines(streamDone, r.logger,
|
||||
stdoutLines, stderrLines, ready)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
errCh <- ctx.Err()
|
||||
case err := <-waitError:
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
func streamLines(done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
tunnelReady chan<- struct{},
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
var line string
|
||||
var ok bool
|
||||
errLine := false
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
case line = <-stderr:
|
||||
errLine = true
|
||||
case line, ok = <-stdout:
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
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)
|
||||
if line == "" {
|
||||
|
||||
@@ -15,7 +15,11 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||
}
|
||||
portsString := strings.Join(portStrings, ",")
|
||||
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)
|
||||
return cmder.RunAndLog(ctx, commandString, logger)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -59,6 +60,10 @@ func (s *Service) SetPortsForwarded(ctx context.Context, ports []uint16) (err er
|
||||
s.portMutex.Lock()
|
||||
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)
|
||||
if slices.Equal(s.ports, ports) {
|
||||
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)
|
||||
}
|
||||
|
||||
s.logger.Info("updated: " + portsToString(s.ports))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s *Settings) Validate(forStartup bool) (err error) {
|
||||
return errors.New("password not set")
|
||||
}
|
||||
case providers.Protonvpn:
|
||||
const maxPortsCount = 4
|
||||
const maxPortsCount = 5
|
||||
if 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))
|
||||
|
||||
externalPorts := slices.Collect(maps.Values(internalToExternalPorts))
|
||||
autoRedirectionNeeded := false
|
||||
externalToInternalPorts := make(map[uint16]uint16, len(internalToExternalPorts))
|
||||
for internal, external := range internalToExternalPorts {
|
||||
externalToInternalPorts[external] = internal
|
||||
if internal != external {
|
||||
autoRedirectionNeeded = true
|
||||
}
|
||||
}
|
||||
slices.Sort(externalPorts)
|
||||
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)
|
||||
}
|
||||
|
||||
var sourcePort, destinationPort uint16
|
||||
var destinationPort uint16
|
||||
switch {
|
||||
case userRedirectionEnabled: // precedence over auto redirection
|
||||
sourcePort = externalToInternalPorts[port]
|
||||
destinationPort = s.settings.ListeningPorts[i]
|
||||
case autoRedirectionNeeded:
|
||||
sourcePort = externalToInternalPorts[port]
|
||||
case port != internalPort: // auto redirection needed, source and destination ports differ
|
||||
destinationPort = port
|
||||
default:
|
||||
// No redirection needed, source and destination ports are the same.
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, sourcePort, destinationPort)
|
||||
err = s.portAllower.RedirectPort(ctx, s.settings.Interface, internalPort, destinationPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting port %d to %d in firewall: %w",
|
||||
sourcePort, destinationPort, err)
|
||||
internalPort, destinationPort, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
@@ -65,7 +66,11 @@ func modifyConfig(lines []string, connection models.Connection,
|
||||
}
|
||||
|
||||
// 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, "dev "+settings.Interface)
|
||||
modified = append(modified, "mute-replay-warnings")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -12,14 +13,14 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
)
|
||||
|
||||
const nonSymmetricPortStart uint16 = 56789
|
||||
|
||||
// PortForward obtains a VPN server side port forwarded from ProtonVPN gateway.
|
||||
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
|
||||
internalToExternalPorts map[uint16]uint16, err error,
|
||||
) {
|
||||
if !objects.CanPortForward {
|
||||
return nil, errors.New("server does not support port forwarding")
|
||||
} else if objects.PortsCount == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
client := natpmp.New()
|
||||
@@ -39,38 +40,75 @@ func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObj
|
||||
logger := objects.Logger
|
||||
|
||||
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)
|
||||
for i := range objects.PortsCount {
|
||||
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
|
||||
}
|
||||
const lifetime = 60 * time.Second
|
||||
|
||||
checkInternalPorts(logger, protoToInternalPort["udp"], protoToInternalPort["tcp"])
|
||||
checkExternalPorts(logger, protoToExternalPort["udp"], protoToExternalPort["tcp"])
|
||||
p.internalToExternalPorts[protoToInternalPort["tcp"]] = protoToExternalPort["tcp"]
|
||||
// Only one port can be a symmetric mapping
|
||||
const internalPort, externalPort = 0, 1
|
||||
_, 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
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
objects utils.PortForwardObjects,
|
||||
) (err error) {
|
||||
@@ -117,22 +134,12 @@ func (p *Provider) KeepPortForward(ctx context.Context,
|
||||
}
|
||||
|
||||
objects.Logger.Debug("refreshing forwarded ports since 45 seconds have elapsed")
|
||||
networkProtocols := [...]string{"udp", "tcp"}
|
||||
const lifetime = 60 * time.Second
|
||||
for internalPort, externalPort := range p.internalToExternalPorts {
|
||||
for _, networkProtocol := range networkProtocols {
|
||||
_, assignedInternalPort, assignedExternalPort, assignedLiftetime, err := client.AddPortMapping(
|
||||
ctx, objects.Gateway, networkProtocol, internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
_, _, err := addPortMappingTCPUDP(ctx, client, logger, objects.Gateway, internalPort, externalPort, lifetime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing port mapping for internal port %d and external port %d: %w",
|
||||
internalPort, externalPort, err)
|
||||
}
|
||||
objects.Logger.Debug(fmt.Sprintf("port forwarded %d maintained", externalPort))
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ type cmdType byte
|
||||
|
||||
const (
|
||||
connect cmdType = 1
|
||||
bind cmdType = 2
|
||||
udpAssociate cmdType = 3
|
||||
)
|
||||
|
||||
@@ -51,8 +50,6 @@ func (c cmdType) String() string {
|
||||
switch c {
|
||||
case connect:
|
||||
return "connect"
|
||||
case bind:
|
||||
return "bind"
|
||||
case udpAssociate:
|
||||
return "UDP associate"
|
||||
default:
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// 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{
|
||||
socksVersion,
|
||||
byte(reply),
|
||||
|
||||
+89
-49
@@ -2,6 +2,7 @@ package socks5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
@@ -15,12 +16,13 @@ type server struct {
|
||||
logger Logger
|
||||
|
||||
// internal fields
|
||||
listener net.Listener
|
||||
tcpListener net.Listener
|
||||
udpRouter *udpRouter
|
||||
listening atomic.Bool
|
||||
socksConnCtx context.Context //nolint:containedctx
|
||||
socksConnCancel context.CancelFunc
|
||||
done <-chan struct{}
|
||||
stopping atomic.Bool
|
||||
done <-chan error
|
||||
stopCh chan<- struct{}
|
||||
}
|
||||
|
||||
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) {
|
||||
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
|
||||
config := &net.ListenConfig{}
|
||||
s.listener, err = config.Listen(ctx, "tcp", s.address)
|
||||
s.tcpListener, err = config.Listen(ctx, "tcp", s.address)
|
||||
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.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{})
|
||||
runErrCh := make(chan error)
|
||||
runErr = runErrCh
|
||||
done := make(chan struct{})
|
||||
done := make(chan error)
|
||||
s.done = done
|
||||
go s.runServer(ready, runErrCh, done)
|
||||
stop := make(chan struct{})
|
||||
s.stopCh = stop
|
||||
go s.runServer(ready, runErrCh, stop, done)
|
||||
select {
|
||||
case <-ready:
|
||||
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{},
|
||||
runErrCh chan<- error, done chan<- struct{},
|
||||
runErrCh chan<- error, stop <-chan struct{}, done chan<- error,
|
||||
) {
|
||||
close(ready)
|
||||
defer close(done)
|
||||
wg := new(sync.WaitGroup)
|
||||
defer wg.Wait()
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
for {
|
||||
connection, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
if !s.stopping.Load() {
|
||||
_ = s.stop()
|
||||
runErrCh <- fmt.Errorf("accepting connection: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(ctx context.Context, connection net.Conn,
|
||||
dialer *net.Dialer, wg *sync.WaitGroup,
|
||||
) {
|
||||
defer wg.Done()
|
||||
socksConn := &socksConn{
|
||||
dialer: dialer,
|
||||
username: s.username,
|
||||
password: s.password,
|
||||
clientConn: connection,
|
||||
logger: s.logger,
|
||||
}
|
||||
err := socksConn.run(ctx)
|
||||
udpErrCh := make(chan error)
|
||||
go func() {
|
||||
udpErrCh <- s.udpRouter.run(s.socksConnCtx)
|
||||
}()
|
||||
|
||||
tcpErrCh := make(chan error)
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
for {
|
||||
connection, err := s.tcpListener.Accept()
|
||||
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) {
|
||||
s.stopping.Store(true)
|
||||
err = s.stop()
|
||||
<-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
|
||||
close(s.stopCh)
|
||||
return <-s.done
|
||||
}
|
||||
|
||||
func (s *server) listeningAddress() net.Addr {
|
||||
if s.listening.Load() {
|
||||
return s.listener.Addr()
|
||||
return s.tcpListener.Addr()
|
||||
}
|
||||
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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,6 +24,7 @@ type socksConn struct {
|
||||
username string
|
||||
password string
|
||||
clientConn net.Conn
|
||||
udpRouter *udpRouter
|
||||
logger Logger
|
||||
}
|
||||
|
||||
@@ -109,11 +111,29 @@ func (c *socksConn) handleRequest(ctx context.Context) error {
|
||||
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
|
||||
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)
|
||||
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))
|
||||
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
|
||||
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
|
||||
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
|
||||
const headerLength = 2 // version + nMethods bytes
|
||||
|
||||
+502
-38
@@ -2,9 +2,13 @@ package socks5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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
|
||||
// subnegotiation) and returns a connected net.Conn ready for data exchange.
|
||||
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)
|
||||
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
|
||||
if username != "" || password != "" {
|
||||
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, byte(0), subnegResp[1])
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func readSOCKS5ResponseAddress(t *testing.T, conn net.Conn) (address string, err error) {
|
||||
t.Helper()
|
||||
|
||||
var responseHeader [4]byte
|
||||
_, err = io.ReadFull(conn, responseHeader[:])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, socks5Version, responseHeader[0])
|
||||
require.Equal(t, byte(succeeded), responseHeader[1])
|
||||
|
||||
// Consume BND.ADDR and BND.PORT (their values are irrelevant to the caller).
|
||||
switch addrType(responseHeader[3]) {
|
||||
case ipv4:
|
||||
var addrPort [net.IPv4len + 2]byte
|
||||
_, 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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if responseHeader[0] != socks5Version {
|
||||
return "", errors.New("version mismatch")
|
||||
}
|
||||
if responseHeader[1] != byte(succeeded) {
|
||||
return "", errors.New("request was not successful")
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -224,7 +550,8 @@ func Test_Server_StartStop(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
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{
|
||||
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) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
@@ -598,10 +989,6 @@ func Test_cmdType_String(t *testing.T) {
|
||||
cmd: connect,
|
||||
expectedName: "connect",
|
||||
},
|
||||
"bind": {
|
||||
cmd: bind,
|
||||
expectedName: "bind",
|
||||
},
|
||||
"udp_associate": {
|
||||
cmd: udpAssociate,
|
||||
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,35 +1,23 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
serversmodule "github.com/qdm12/gluetun-servers/pkg/servers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
//go:embed servers.json
|
||||
var allServersEmbedFS embed.FS
|
||||
|
||||
func parseHardcodedServers() (allServers models.AllServers) {
|
||||
f, err := allServersEmbedFS.Open("servers.json")
|
||||
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())
|
||||
}
|
||||
allProviders := providers.All()
|
||||
|
||||
for provider, metadata := range allServers.ProviderToServers {
|
||||
if metadata.Filepath == "" {
|
||||
panic(fmt.Sprintf("embedded manifest file servers.json should have the filepath field set for %s", provider))
|
||||
}
|
||||
filename := path.Base(metadata.Filepath)
|
||||
const version = 1
|
||||
allServers.ProviderToServers = make(map[string]models.Servers, len(allProviders))
|
||||
allServers.Version = version
|
||||
for _, provider := range allProviders {
|
||||
filename := provider + ".json"
|
||||
providerFile, err := serversmodule.Files.Open(filename)
|
||||
if err != nil {
|
||||
const rootURL = "https://github.com/qdm12/gluetun-servers/blob/main/pkg/servers"
|
||||
@@ -48,7 +36,8 @@ func parseHardcodedServers() (allServers models.AllServers) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,5 @@ package storage
|
||||
import "fmt"
|
||||
|
||||
func panicOnProviderMissingHardcoded(provider string) {
|
||||
panic(fmt.Sprintf("provider %s not found in hardcoded servers map; "+
|
||||
"did you add the provider key in the embedded servers.json?", provider))
|
||||
panic(fmt.Sprintf("provider %s not found in hardcoded servers map", provider))
|
||||
}
|
||||
|
||||
@@ -152,8 +152,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
allProviders[0]: 1,
|
||||
// Missing provider allProviders[1]
|
||||
}
|
||||
expectedPanicValue := fmt.Sprintf("provider %s not found in hardcoded servers map; "+
|
||||
"did you add the provider key in the embedded servers.json?", allProviders[1])
|
||||
expectedPanicValue := fmt.Sprintf("provider %s not found in hardcoded servers map", allProviders[1])
|
||||
assert.PanicsWithValue(t, expectedPanicValue, func() {
|
||||
_, _ = 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
|
||||
|
||||
- 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
|
||||
- DNS block lists as LFS and built in image
|
||||
- Add HTTP server v3 as json rpc
|
||||
- Use `github.com/qdm12/ddns-updater/pkg/publicip`
|
||||
- Windows and Darwin development support
|
||||
|
||||
## Features
|
||||
|
||||
- Authentication with the control server
|
||||
- Get announcement from Github file
|
||||
- Support multiple connections in custom ovpn
|
||||
- Automate IPv6 detection for OpenVPN
|
||||
|
||||
Reference in New Issue
Block a user