From 7d824a51798f3e635e116dfece6056fd2ad4a2c6 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 6 Jan 2022 06:40:23 -0500 Subject: [PATCH] chore(settings): refactor settings processing (#756) - Better settings tree structure logged using `qdm12/gotree` - Read settings from environment variables, then files, then secret files - Settings methods to default them, merge them and override them - `DNS_PLAINTEXT_ADDRESS` default changed to `127.0.0.1` to use DoT. Warning added if set to something else. - `HTTPPROXY_LISTENING_ADDRESS` instead of `HTTPPROXY_PORT` (with retro-compatibility) --- .golangci.yml | 8 + Dockerfile | 6 +- README.md | 2 +- cmd/gluetun/main.go | 57 ++- go.mod | 4 +- go.sum | 8 +- internal/cli/healthcheck.go | 17 +- internal/cli/openvpnconfig.go | 17 +- internal/cli/update.go | 52 +- internal/cli/warner.go | 5 + internal/configuration/configuration.go | 3 - internal/configuration/constants.go | 6 - internal/configuration/custom.go | 95 ---- internal/configuration/cyberghost.go | 47 -- internal/configuration/dns.go | 117 ----- internal/configuration/dns_test.go | 76 --- internal/configuration/dnsblacklist.go | 87 ---- internal/configuration/expressvpn.go | 40 -- internal/configuration/fastestvpn.go | 30 -- internal/configuration/firewall.go | 99 ---- internal/configuration/health.go | 72 --- internal/configuration/health_test.go | 272 ----------- internal/configuration/healthwait_test.go | 55 --- internal/configuration/healthywait.go | 30 -- internal/configuration/hidemyass.go | 40 -- internal/configuration/httpproxy.go | 106 ---- internal/configuration/ipvanish.go | 35 -- internal/configuration/ipvanish_test.go | 170 ------- internal/configuration/ivpn.go | 73 --- internal/configuration/ivpn_test.go | 274 ----------- internal/configuration/keys.go | 29 -- internal/configuration/lines.go | 22 - internal/configuration/log.go | 30 -- internal/configuration/mullvad.go | 77 --- internal/configuration/nordvpn.go | 58 --- internal/configuration/openvpn.go | 207 -------- internal/configuration/openvpn_test.go | 40 -- internal/configuration/perfectprivacy.go | 43 -- internal/configuration/privado.go | 39 -- .../configuration/privateinternetaccess.go | 75 --- internal/configuration/privatevpn.go | 35 -- internal/configuration/protonvpn.go | 51 -- internal/configuration/provider.go | 248 ---------- internal/configuration/provider_test.go | 462 ------------------ internal/configuration/publicip.go | 47 -- internal/configuration/purevpn.go | 39 -- internal/configuration/reader.go | 134 ----- internal/configuration/secrets.go | 119 ----- internal/configuration/selection.go | 189 ------- internal/configuration/server.go | 50 -- internal/configuration/settings.go | 125 ----- internal/configuration/settings/dns.go | 82 ++++ .../configuration/settings/dnsblacklist.go | 138 ++++++ internal/configuration/settings/dot.go | 113 +++++ internal/configuration/settings/errors.go | 51 ++ internal/configuration/settings/firewall.go | 117 +++++ internal/configuration/settings/health.go | 83 ++++ .../configuration/settings/healthywait.go | 66 +++ .../configuration/settings/helpers/belong.go | 45 ++ .../configuration/settings/helpers/copy.go | 190 +++++++ .../configuration/settings/helpers/default.go | 93 ++++ .../configuration/settings/helpers/files.go | 31 ++ .../configuration/settings/helpers/merge.go | 226 +++++++++ .../settings/helpers/messages.go | 29 ++ .../settings/helpers/obfuscate.go | 25 + .../settings/helpers/override.go | 133 +++++ .../settings/helpers/pointers.go | 11 + .../configuration/settings/helpers/string.go | 15 + .../configuration/settings/helpers_test.go | 4 + internal/configuration/settings/httpproxy.go | 112 +++++ internal/configuration/settings/log.go | 51 ++ internal/configuration/settings/openvpn.go | 318 ++++++++++++ .../settings/openvpnselection.go | 170 +++++++ .../configuration/settings/portforward.go | 89 ++++ .../settings/portforward_test.go | 19 + internal/configuration/settings/provider.go | 92 ++++ internal/configuration/settings/publicip.go | 89 ++++ internal/configuration/settings/server.go | 71 +++ .../configuration/settings/serverselection.go | 386 +++++++++++++++ internal/configuration/settings/settings.go | 144 ++++++ .../configuration/settings/settings_test.go | 101 ++++ .../configuration/settings/shadowsocks.go | 68 +++ .../configuration/settings/surfshark_retro.go | 56 +++ internal/configuration/settings/system.go | 61 +++ internal/configuration/settings/unbound.go | 193 ++++++++ .../configuration/settings/unbound_test.go | 43 ++ internal/configuration/settings/updater.go | 113 +++++ internal/configuration/settings/version.go | 53 ++ internal/configuration/settings/vpn.go | 98 ++++ internal/configuration/settings/wireguard.go | 138 ++++++ .../settings/wireguardselection.go | 144 ++++++ internal/configuration/settings_test.go | 74 --- internal/configuration/shadowsocks.go | 109 ----- internal/configuration/sources/env/dns.go | 51 ++ .../configuration/sources/env/dnsblacklist.go | 90 ++++ internal/configuration/sources/env/dot.go | 31 ++ .../configuration/sources/env/firewall.go | 96 ++++ internal/configuration/sources/env/health.go | 52 ++ internal/configuration/sources/env/helpers.go | 134 +++++ .../configuration/sources/env/httproxy.go | 186 +++++++ internal/configuration/sources/env/log.go | 54 ++ internal/configuration/sources/env/openvpn.go | 125 +++++ .../sources/env/openvpnselection.go | 78 +++ .../configuration/sources/env/portforward.go | 19 + .../configuration/sources/env/provider.go | 40 ++ .../configuration/sources/env/publicip.go | 51 ++ internal/configuration/sources/env/reader.go | 87 ++++ .../sources/env/serverselection.go | 115 +++++ .../configuration/sources/env/shadowsocks.go | 54 ++ internal/configuration/sources/env/system.go | 63 +++ internal/configuration/sources/env/unbound.go | 38 ++ internal/configuration/sources/env/updater.go | 50 ++ internal/configuration/sources/env/version.go | 33 ++ internal/configuration/sources/env/vpn.go | 29 ++ .../configuration/sources/env/wireguard.go | 44 ++ .../sources/env/wireguardselection.go | 65 +++ .../configuration/sources/files/health.go | 5 + .../configuration/sources/files/helpers.go | 31 ++ .../configuration/sources/files/openvpn.go | 22 + .../configuration/sources/files/reader.go | 28 ++ .../configuration/sources/files/system.go | 10 + internal/configuration/sources/files/vpn.go | 16 + internal/configuration/sources/mux/reader.go | 57 +++ .../configuration/sources/secrets/health.go | 5 + .../configuration/sources/secrets/helpers.go | 31 ++ .../sources/secrets/httpproxy.go | 27 + .../configuration/sources/secrets/openvpn.go | 44 ++ .../configuration/sources/secrets/reader.go | 34 ++ .../sources/secrets/shadowsocks.go | 19 + internal/configuration/sources/secrets/vpn.go | 16 + internal/configuration/sources/source.go | 8 + internal/configuration/surfshark.go | 102 ---- internal/configuration/surfshark_test.go | 305 ------------ internal/configuration/system.go | 55 --- internal/configuration/torguard.go | 35 -- internal/configuration/unbound.go | 79 --- internal/configuration/unbound_test.go | 80 --- internal/configuration/updater.go | 90 ---- internal/configuration/vpn.go | 79 --- internal/configuration/vpnunlimited.go | 60 --- internal/configuration/vyprvpn.go | 24 - internal/configuration/warner_mock_test.go | 46 -- internal/configuration/wevpn.go | 57 --- internal/configuration/windscribe.go | 69 --- internal/configuration/wireguard.go | 88 ---- internal/constants/providers.go | 27 + internal/dns/loop.go | 6 +- internal/dns/plaintext.go | 24 +- internal/dns/run.go | 4 +- internal/dns/settings.go | 10 +- internal/dns/setup.go | 9 +- internal/dns/state/settings.go | 16 +- internal/dns/state/state.go | 6 +- internal/dns/ticker.go | 8 +- internal/dns/update.go | 22 +- internal/healthcheck/health.go | 2 +- internal/healthcheck/openvpn.go | 2 +- internal/healthcheck/server.go | 8 +- internal/httpproxy/loop.go | 4 +- internal/httpproxy/run.go | 8 +- internal/httpproxy/settings.go | 6 +- internal/httpproxy/state/settings.go | 14 +- internal/httpproxy/state/state.go | 6 +- internal/openvpn/run.go | 6 +- internal/portforward/fs.go | 4 +- internal/portforward/loop.go | 4 +- internal/portforward/settings.go | 6 +- internal/portforward/state/settings.go | 16 +- internal/portforward/state/state.go | 6 +- internal/provider/custom/connection.go | 14 +- internal/provider/custom/openvpnconf.go | 28 +- internal/provider/custom/openvpnconf_test.go | 19 +- internal/provider/cyberghost/connection.go | 6 +- internal/provider/cyberghost/filter.go | 4 +- internal/provider/cyberghost/filter_test.go | 25 +- internal/provider/cyberghost/openvpnconf.go | 39 +- internal/provider/expressvpn/connection.go | 6 +- .../provider/expressvpn/connection_test.go | 22 +- internal/provider/expressvpn/filter.go | 4 +- internal/provider/expressvpn/filter_test.go | 33 +- internal/provider/expressvpn/openvpnconf.go | 25 +- internal/provider/fastestvpn/connection.go | 6 +- internal/provider/fastestvpn/filter.go | 4 +- internal/provider/fastestvpn/openvpnconf.go | 25 +- internal/provider/hidemyass/connection.go | 10 +- internal/provider/hidemyass/filter.go | 4 +- internal/provider/hidemyass/openvpnconf.go | 18 +- internal/provider/ipvanish/connection.go | 6 +- internal/provider/ipvanish/filter.go | 4 +- internal/provider/ipvanish/openvpnconf.go | 21 +- internal/provider/ivpn/connection.go | 6 +- internal/provider/ivpn/connection_test.go | 40 +- internal/provider/ivpn/filter.go | 4 +- internal/provider/ivpn/filter_test.go | 89 ++-- internal/provider/ivpn/openvpnconf.go | 18 +- internal/provider/mullvad/connection.go | 6 +- internal/provider/mullvad/connection_test.go | 40 +- internal/provider/mullvad/filter.go | 6 +- internal/provider/mullvad/filter_test.go | 91 ++-- internal/provider/mullvad/openvpnconf.go | 20 +- internal/provider/nordvpn/connection.go | 6 +- internal/provider/nordvpn/filter.go | 4 +- internal/provider/nordvpn/openvpnconf.go | 24 +- .../provider/perfectprivacy/connection.go | 8 +- internal/provider/perfectprivacy/filter.go | 4 +- .../provider/perfectprivacy/openvpnconf.go | 24 +- internal/provider/privado/connection.go | 6 +- internal/provider/privado/filter.go | 4 +- internal/provider/privado/openvpnconf.go | 21 +- .../privateinternetaccess/connection.go | 6 +- .../provider/privateinternetaccess/filter.go | 4 +- .../privateinternetaccess/openvpnconf.go | 26 +- .../provider/privateinternetaccess/port.go | 14 +- internal/provider/privatevpn/connection.go | 10 +- internal/provider/privatevpn/filter.go | 4 +- internal/provider/privatevpn/openvpnconf.go | 21 +- internal/provider/protonvpn/connection.go | 8 +- internal/provider/protonvpn/filter.go | 6 +- internal/provider/protonvpn/openvpnconf.go | 26 +- internal/provider/provider.go | 8 +- internal/provider/purevpn/connection.go | 6 +- internal/provider/purevpn/filter.go | 4 +- internal/provider/purevpn/openvpnconf.go | 18 +- internal/provider/surfshark/connection.go | 6 +- internal/provider/surfshark/filter.go | 6 +- internal/provider/surfshark/filter_test.go | 43 +- internal/provider/surfshark/openvpnconf.go | 26 +- internal/provider/torguard/connection.go | 10 +- internal/provider/torguard/filter.go | 4 +- internal/provider/torguard/openvpnconf.go | 26 +- internal/provider/utils/formatting.go | 14 +- internal/provider/utils/pick.go | 8 +- internal/provider/utils/port.go | 10 +- internal/provider/utils/port_test.go | 39 +- internal/provider/utils/protocol.go | 10 +- internal/provider/utils/protocol_test.go | 47 +- internal/provider/utils/wireguard.go | 8 +- internal/provider/utils/wireguard_test.go | 14 +- internal/provider/vpnunlimited/connection.go | 6 +- internal/provider/vpnunlimited/filter.go | 8 +- internal/provider/vpnunlimited/openvpnconf.go | 36 +- internal/provider/vyprvpn/connection.go | 6 +- internal/provider/vyprvpn/filter.go | 4 +- internal/provider/vyprvpn/openvpnconf.go | 19 +- internal/provider/wevpn/connection.go | 6 +- internal/provider/wevpn/connection_test.go | 20 +- internal/provider/wevpn/filter.go | 4 +- internal/provider/wevpn/filter_test.go | 27 +- internal/provider/wevpn/openvpnconf.go | 31 +- internal/provider/windscribe/connection.go | 6 +- .../provider/windscribe/connection_test.go | 40 +- internal/provider/windscribe/filter.go | 4 +- internal/provider/windscribe/filter_test.go | 63 ++- internal/provider/windscribe/openvpnconf.go | 21 +- internal/publicip/loop.go | 4 +- internal/publicip/runner.go | 4 +- internal/publicip/settings.go | 6 +- internal/publicip/state/settings.go | 10 +- internal/publicip/state/state.go | 6 +- internal/publicip/ticker.go | 6 +- internal/shadowsocks/loop.go | 10 +- internal/shadowsocks/state.go | 12 +- internal/updater/loop.go | 14 +- internal/updater/providers.go | 32 +- internal/updater/state.go | 8 +- internal/updater/updater.go | 263 +++------- internal/vpn/loop.go | 4 +- internal/vpn/openvpn.go | 4 +- internal/vpn/run.go | 4 +- internal/vpn/settings.go | 6 +- internal/vpn/state/state.go | 10 +- internal/vpn/state/vpn.go | 10 +- internal/vpn/tunnelup.go | 2 +- internal/vpn/wireguard.go | 4 +- internal/wireguard/settings.go | 2 +- 275 files changed, 7167 insertions(+), 6328 deletions(-) create mode 100644 internal/cli/warner.go delete mode 100644 internal/configuration/configuration.go delete mode 100644 internal/configuration/constants.go delete mode 100644 internal/configuration/custom.go delete mode 100644 internal/configuration/cyberghost.go delete mode 100644 internal/configuration/dns.go delete mode 100644 internal/configuration/dns_test.go delete mode 100644 internal/configuration/dnsblacklist.go delete mode 100644 internal/configuration/expressvpn.go delete mode 100644 internal/configuration/fastestvpn.go delete mode 100644 internal/configuration/firewall.go delete mode 100644 internal/configuration/health.go delete mode 100644 internal/configuration/health_test.go delete mode 100644 internal/configuration/healthwait_test.go delete mode 100644 internal/configuration/healthywait.go delete mode 100644 internal/configuration/hidemyass.go delete mode 100644 internal/configuration/httpproxy.go delete mode 100644 internal/configuration/ipvanish.go delete mode 100644 internal/configuration/ipvanish_test.go delete mode 100644 internal/configuration/ivpn.go delete mode 100644 internal/configuration/ivpn_test.go delete mode 100644 internal/configuration/keys.go delete mode 100644 internal/configuration/lines.go delete mode 100644 internal/configuration/log.go delete mode 100644 internal/configuration/mullvad.go delete mode 100644 internal/configuration/nordvpn.go delete mode 100644 internal/configuration/openvpn.go delete mode 100644 internal/configuration/openvpn_test.go delete mode 100644 internal/configuration/perfectprivacy.go delete mode 100644 internal/configuration/privado.go delete mode 100644 internal/configuration/privateinternetaccess.go delete mode 100644 internal/configuration/privatevpn.go delete mode 100644 internal/configuration/protonvpn.go delete mode 100644 internal/configuration/provider.go delete mode 100644 internal/configuration/provider_test.go delete mode 100644 internal/configuration/publicip.go delete mode 100644 internal/configuration/purevpn.go delete mode 100644 internal/configuration/reader.go delete mode 100644 internal/configuration/secrets.go delete mode 100644 internal/configuration/selection.go delete mode 100644 internal/configuration/server.go delete mode 100644 internal/configuration/settings.go create mode 100644 internal/configuration/settings/dns.go create mode 100644 internal/configuration/settings/dnsblacklist.go create mode 100644 internal/configuration/settings/dot.go create mode 100644 internal/configuration/settings/errors.go create mode 100644 internal/configuration/settings/firewall.go create mode 100644 internal/configuration/settings/health.go create mode 100644 internal/configuration/settings/healthywait.go create mode 100644 internal/configuration/settings/helpers/belong.go create mode 100644 internal/configuration/settings/helpers/copy.go create mode 100644 internal/configuration/settings/helpers/default.go create mode 100644 internal/configuration/settings/helpers/files.go create mode 100644 internal/configuration/settings/helpers/merge.go create mode 100644 internal/configuration/settings/helpers/messages.go create mode 100644 internal/configuration/settings/helpers/obfuscate.go create mode 100644 internal/configuration/settings/helpers/override.go create mode 100644 internal/configuration/settings/helpers/pointers.go create mode 100644 internal/configuration/settings/helpers/string.go create mode 100644 internal/configuration/settings/helpers_test.go create mode 100644 internal/configuration/settings/httpproxy.go create mode 100644 internal/configuration/settings/log.go create mode 100644 internal/configuration/settings/openvpn.go create mode 100644 internal/configuration/settings/openvpnselection.go create mode 100644 internal/configuration/settings/portforward.go create mode 100644 internal/configuration/settings/portforward_test.go create mode 100644 internal/configuration/settings/provider.go create mode 100644 internal/configuration/settings/publicip.go create mode 100644 internal/configuration/settings/server.go create mode 100644 internal/configuration/settings/serverselection.go create mode 100644 internal/configuration/settings/settings.go create mode 100644 internal/configuration/settings/settings_test.go create mode 100644 internal/configuration/settings/shadowsocks.go create mode 100644 internal/configuration/settings/surfshark_retro.go create mode 100644 internal/configuration/settings/system.go create mode 100644 internal/configuration/settings/unbound.go create mode 100644 internal/configuration/settings/unbound_test.go create mode 100644 internal/configuration/settings/updater.go create mode 100644 internal/configuration/settings/version.go create mode 100644 internal/configuration/settings/vpn.go create mode 100644 internal/configuration/settings/wireguard.go create mode 100644 internal/configuration/settings/wireguardselection.go delete mode 100644 internal/configuration/settings_test.go delete mode 100644 internal/configuration/shadowsocks.go create mode 100644 internal/configuration/sources/env/dns.go create mode 100644 internal/configuration/sources/env/dnsblacklist.go create mode 100644 internal/configuration/sources/env/dot.go create mode 100644 internal/configuration/sources/env/firewall.go create mode 100644 internal/configuration/sources/env/health.go create mode 100644 internal/configuration/sources/env/helpers.go create mode 100644 internal/configuration/sources/env/httproxy.go create mode 100644 internal/configuration/sources/env/log.go create mode 100644 internal/configuration/sources/env/openvpn.go create mode 100644 internal/configuration/sources/env/openvpnselection.go create mode 100644 internal/configuration/sources/env/portforward.go create mode 100644 internal/configuration/sources/env/provider.go create mode 100644 internal/configuration/sources/env/publicip.go create mode 100644 internal/configuration/sources/env/reader.go create mode 100644 internal/configuration/sources/env/serverselection.go create mode 100644 internal/configuration/sources/env/shadowsocks.go create mode 100644 internal/configuration/sources/env/system.go create mode 100644 internal/configuration/sources/env/unbound.go create mode 100644 internal/configuration/sources/env/updater.go create mode 100644 internal/configuration/sources/env/version.go create mode 100644 internal/configuration/sources/env/vpn.go create mode 100644 internal/configuration/sources/env/wireguard.go create mode 100644 internal/configuration/sources/env/wireguardselection.go create mode 100644 internal/configuration/sources/files/health.go create mode 100644 internal/configuration/sources/files/helpers.go create mode 100644 internal/configuration/sources/files/openvpn.go create mode 100644 internal/configuration/sources/files/reader.go create mode 100644 internal/configuration/sources/files/system.go create mode 100644 internal/configuration/sources/files/vpn.go create mode 100644 internal/configuration/sources/mux/reader.go create mode 100644 internal/configuration/sources/secrets/health.go create mode 100644 internal/configuration/sources/secrets/helpers.go create mode 100644 internal/configuration/sources/secrets/httpproxy.go create mode 100644 internal/configuration/sources/secrets/openvpn.go create mode 100644 internal/configuration/sources/secrets/reader.go create mode 100644 internal/configuration/sources/secrets/shadowsocks.go create mode 100644 internal/configuration/sources/secrets/vpn.go create mode 100644 internal/configuration/sources/source.go delete mode 100644 internal/configuration/surfshark.go delete mode 100644 internal/configuration/surfshark_test.go delete mode 100644 internal/configuration/system.go delete mode 100644 internal/configuration/torguard.go delete mode 100644 internal/configuration/unbound.go delete mode 100644 internal/configuration/unbound_test.go delete mode 100644 internal/configuration/updater.go delete mode 100644 internal/configuration/vpn.go delete mode 100644 internal/configuration/vpnunlimited.go delete mode 100644 internal/configuration/vyprvpn.go delete mode 100644 internal/configuration/warner_mock_test.go delete mode 100644 internal/configuration/wevpn.go delete mode 100644 internal/configuration/windscribe.go delete mode 100644 internal/configuration/wireguard.go create mode 100644 internal/constants/providers.go diff --git a/.golangci.yml b/.golangci.yml index 3c1311d3..b612403c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,6 +27,14 @@ issues: - text: "mnd: Magic number: 0400*" linters: - gomnd + - text: "variable 'mssFix' is only used in the if-statement*" + path: "openvpnconf.go" + linters: + - ifshort + - text: "variable 'auth' is only used in the if-statement*" + path: "openvpnconf.go" + linters: + - ifshort linters: enable: # - cyclop diff --git a/Dockerfile b/Dockerfile index 7da6426c..3e1ab724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,7 +102,7 @@ ENV VPNSP=pia \ ISP= \ OWNED=no \ # # Private Internet Access only: - PIA_ENCRYPTION=strong \ + PIA_ENCRYPTION= \ PORT_FORWARDING=off \ PORT_FORWARDING_STATUS_FILE="/tmp/gluetun/forwarded_port" \ # # Cyberghost only: @@ -143,12 +143,12 @@ ENV VPNSP=pia \ BLOCK_ADS=off \ UNBLOCK= \ DNS_UPDATE_PERIOD=24h \ - DNS_PLAINTEXT_ADDRESS=1.1.1.1 \ + DNS_PLAINTEXT_ADDRESS=127.0.0.1 \ DNS_KEEP_NAMESERVER=off \ # HTTP proxy HTTPPROXY= \ HTTPPROXY_LOG=off \ - HTTPPROXY_PORT=8888 \ + HTTPPROXY_LISTENING_ADDRESS=":8888" \ HTTPPROXY_USER= \ HTTPPROXY_PASSWORD= \ HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user \ diff --git a/README.md b/README.md index 83d5c34c..d8f0b6c2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private I ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* -**ANNOUNCEMENT**: Wireguard is now supported for all providers supporting it! +**ANNOUNCEMENT**: Large settings refactor merged on 2022-06-01, please file issues if you find any problem! ![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg) diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 628fc33a..09fb97b8 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "os/signal" - "strconv" "strings" "syscall" "time" @@ -17,7 +16,11 @@ import ( "github.com/qdm12/dns/pkg/unbound" "github.com/qdm12/gluetun/internal/alpine" "github.com/qdm12/gluetun/internal/cli" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/sources" + "github.com/qdm12/gluetun/internal/configuration/sources/env" + "github.com/qdm12/gluetun/internal/configuration/sources/files" + "github.com/qdm12/gluetun/internal/configuration/sources/mux" + "github.com/qdm12/gluetun/internal/configuration/sources/secrets" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/dns" "github.com/qdm12/gluetun/internal/firewall" @@ -37,7 +40,6 @@ import ( "github.com/qdm12/gluetun/internal/vpn" "github.com/qdm12/golibs/command" "github.com/qdm12/golibs/logging" - "github.com/qdm12/golibs/params" "github.com/qdm12/goshutdown" "github.com/qdm12/goshutdown/goroutine" "github.com/qdm12/goshutdown/group" @@ -77,12 +79,16 @@ func main() { tun := tun.New() netLinker := netlink.New() cli := cli.New() - env := params.New() cmder := command.NewCmder() + envReader := env.New(logger) + filesReader := files.New() + secretsReader := secrets.New() + muxReader := mux.New(envReader, filesReader, secretsReader) + errorCh := make(chan error) go func() { - errorCh <- _main(ctx, buildInfo, args, logger, env, tun, netLinker, cmder, cli) + errorCh <- _main(ctx, buildInfo, args, logger, muxReader, tun, netLinker, cmder, cli) }() select { @@ -122,17 +128,17 @@ var ( //nolint:gocognit,gocyclo func _main(ctx context.Context, buildInfo models.BuildInformation, - args []string, logger logging.ParentLogger, env params.Interface, + args []string, logger logging.ParentLogger, source sources.Source, tun tun.Interface, netLinker netlink.NetLinker, cmder command.RunStarter, cli cli.CLIer) error { if len(args) > 1 { // cli operation switch args[1] { case "healthcheck": - return cli.HealthCheck(ctx, env, logger) + return cli.HealthCheck(ctx, source, logger) case "clientkey": return cli.ClientKey(args[2:]) case "openvpnconfig": - return cli.OpenvpnConfig(logger, env) + return cli.OpenvpnConfig(logger, source) case "update": return cli.Update(ctx, args[2:], logger) case "format-servers": @@ -142,7 +148,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, } } - announcementExp, err := time.Parse(time.RFC3339, "2021-10-02T00:00:00Z") + announcementExp, err := time.Parse(time.RFC3339, "2021-02-15T00:00:00Z") if err != nil { return err } @@ -153,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, Version: buildInfo.Version, Commit: buildInfo.Commit, BuildDate: buildInfo.Created, - Announcement: "Wireguard is now supported for Mullvad, IVPN and Windscribe!", + Announcement: "Large settings parsing refactoring merged on 2022-01-06, please report any issue!", AnnounceExp: announcementExp, // Sponsor information PaypalUser: "qmcgaw", @@ -163,23 +169,28 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, fmt.Println(line) } + allSettings, err := source.Read() + if err != nil { + return err + } + // TODO run this in a loop or in openvpn to reload from file without restarting storageLogger := logger.NewChild(logging.Settings{Prefix: "storage: "}) storage, err := storage.New(storageLogger, constants.ServersData) if err != nil { return err } + allServers := storage.GetServers() - var allSettings configuration.Settings - err = allSettings.Read(env, allServers, - logger.NewChild(logging.Settings{Prefix: "configuration: "})) + err = allSettings.Validate(allServers) if err != nil { return err } - logger.PatchLevel(allSettings.Log.Level) - puid, pgid := allSettings.System.PUID, allSettings.System.PGID + logger.PatchLevel(*allSettings.Log.Level) + + puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID) const clientTimeout = 15 * time.Second httpClient := &http.Client{Timeout: clientTimeout} @@ -225,15 +236,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, } // set it for Unbound // TODO remove this when migrating to qdm12/dns v2 - allSettings.DNS.Unbound.Username = nonRootUsername + allSettings.DNS.DoT.Unbound.Username = nonRootUsername allSettings.VPN.OpenVPN.ProcUser = nonRootUsername if err := os.Chown("/etc/unbound", puid, pgid); err != nil { return err } - firewallLogLevel := allSettings.Log.Level - if allSettings.Firewall.Debug { + firewallLogLevel := *allSettings.Log.Level + if *allSettings.Firewall.Debug { firewallLogLevel = logging.LevelDebug } routingLogger := logger.NewChild(logging.Settings{ @@ -292,7 +303,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, } } - if allSettings.Firewall.Enabled { + if *allSettings.Firewall.Enabled { err := firewallConf.SetEnabled(ctx, true) // disabled by default if err != nil { return err @@ -361,7 +372,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, vpnLooper := vpn.NewLoop(allSettings.VPN, allSettings.Firewall.VPNInputPorts, allServers, ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper, unboundLooper, vpnLogger, httpClient, - buildInfo, allSettings.VersionInformation) + buildInfo, *allSettings.Version.Enabled) vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler( "vpn", goroutine.OptionTimeout(time.Second)) go vpnLooper.Run(vpnCtx, vpnDone) @@ -388,15 +399,15 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, go httpProxyLooper.Run(httpProxyCtx, httpProxyDone) otherGroupHandler.Add(httpProxyHandler) - shadowsocksLooper := shadowsocks.NewLooper(allSettings.ShadowSocks, + shadowsocksLooper := shadowsocks.NewLooper(allSettings.Shadowsocks, logger.NewChild(logging.Settings{Prefix: "shadowsocks: "})) shadowsocksHandler, shadowsocksCtx, shadowsocksDone := goshutdown.NewGoRoutineHandler( "shadowsocks proxy", goroutine.OptionTimeout(defaultShutdownTimeout)) go shadowsocksLooper.Run(shadowsocksCtx, shadowsocksDone) otherGroupHandler.Add(shadowsocksHandler) - controlServerAddress := ":" + strconv.Itoa(int(allSettings.ControlServer.Port)) - controlServerLogging := allSettings.ControlServer.Log + controlServerAddress := fmt.Sprintf(":%d", allSettings.ControlServer.Port) + controlServerLogging := *allSettings.ControlServer.Log httpServerHandler, httpServerCtx, httpServerDone := goshutdown.NewGoRoutineHandler( "http server", goroutine.OptionTimeout(defaultShutdownTimeout)) httpServer := server.New(httpServerCtx, controlServerAddress, controlServerLogging, diff --git a/go.mod b/go.mod index 82d1c927..d1b4ff84 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,9 @@ require ( github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 github.com/qdm12/goshutdown v0.3.0 github.com/qdm12/gosplash v0.1.0 - github.com/qdm12/ss-server v0.3.0 + github.com/qdm12/gotree v0.2.0 + github.com/qdm12/govalid v0.1.0 + github.com/qdm12/ss-server v0.4.0 github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e github.com/stretchr/testify v1.7.0 github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 diff --git a/go.sum b/go.sum index d1083403..d4dbb5c8 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,12 @@ github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSE github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM= github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g= github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw= -github.com/qdm12/ss-server v0.3.0 h1:BfKv4OU6dYb2KcDMYpTc7LIuO2jB73g3JCzy988GrLI= -github.com/qdm12/ss-server v0.3.0/go.mod h1:ug+nWfuzKw/h5fxL1B6e9/OhkVuWJX4i2V1Pf0pJU1o= +github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c= +github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4= +github.com/qdm12/govalid v0.1.0 h1:UIFVmuaAg0Q+h0GeyfcFEZ5sQ5KJPvRQwycC1/cqDN8= +github.com/qdm12/govalid v0.1.0/go.mod h1:CyS/OEQdOvunBgrtIsW93fjd4jBkwZPBjGSpxq3NwA4= +github.com/qdm12/ss-server v0.4.0 h1:lMMYfDGc9P86Lyvd3+p8lK4hhgHUKDzjZC91FqJYkDU= +github.com/qdm12/ss-server v0.4.0/go.mod h1:AY0p4huvPUPW+/CiWsJcDgT6sneDryk26VXSccPNCxY= github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e h1:4q+uFLawkaQRq3yARYLsjJPZd2wYwxn4g6G/5v0xW1g= github.com/qdm12/updated v0.0.0-20210603204757-205acfe6937e/go.mod h1:UvJRGkZ9XL3/D7e7JiTTVLm1F3Cymd3/gFpD6frEpBo= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= diff --git a/internal/cli/healthcheck.go b/internal/cli/healthcheck.go index 2176e916..2dcf8e17 100644 --- a/internal/cli/healthcheck.go +++ b/internal/cli/healthcheck.go @@ -6,23 +6,26 @@ import ( "net/http" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/sources" "github.com/qdm12/gluetun/internal/healthcheck" - "github.com/qdm12/golibs/params" ) type HealthChecker interface { - HealthCheck(ctx context.Context, env params.Interface, warner configuration.Warner) error + HealthCheck(ctx context.Context, source sources.Source, warner Warner) error } -func (c *CLI) HealthCheck(ctx context.Context, env params.Interface, - warner configuration.Warner) error { +func (c *CLI) HealthCheck(ctx context.Context, source sources.Source, warner Warner) error { // Extract the health server port from the configuration. - config := configuration.Health{} - err := config.Read(env, warner) + config, err := source.ReadHealth() if err != nil { return err } + + err = config.Validate() + if err != nil { + return err + } + _, port, err := net.SplitHostPort(config.ServerAddress) if err != nil { return err diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 3985915f..0c8ade07 100644 --- a/internal/cli/openvpnconfig.go +++ b/internal/cli/openvpnconfig.go @@ -5,15 +5,14 @@ import ( "strings" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/sources" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/storage" - "github.com/qdm12/golibs/params" ) type OpenvpnConfigMaker interface { - OpenvpnConfig(logger OpenvpnConfigLogger, env params.Interface) error + OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error } type OpenvpnConfigLogger interface { @@ -21,19 +20,23 @@ type OpenvpnConfigLogger interface { Warn(s string) } -func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, env params.Interface) error { +func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, source sources.Source) error { storage, err := storage.New(logger, constants.ServersData) if err != nil { return err } allServers := storage.GetServers() - var allSettings configuration.Settings - err = allSettings.Read(env, allServers, logger) + allSettings, err := source.Read() if err != nil { return err } - providerConf := provider.New(allSettings.VPN.Provider.Name, allServers, time.Now) + + if err = allSettings.Validate(allServers); err != nil { + return err + } + + providerConf := provider.New(*allSettings.VPN.Provider.Name, allServers, time.Now) connection, err := providerConf.GetConnection(allSettings.VPN.Provider.ServerSelection) if err != nil { return err diff --git a/internal/cli/update.go b/internal/cli/update.go index b2843d0b..268c261e 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -6,11 +6,13 @@ import ( "errors" "flag" "fmt" + "net" "net/http" "os" + "strings" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/storage" @@ -19,6 +21,8 @@ import ( var ( ErrModeUnspecified = errors.New("at least one of -enduser or -maintainer must be specified") + ErrDNSAddress = errors.New("DNS address is not valid") + ErrNoProviderSpecified = errors.New("no provider was specified") ErrNewStorage = errors.New("cannot create storage") ErrUpdateServerInformation = errors.New("cannot update server information") ErrWriteToFile = errors.New("cannot write updated information to file") @@ -34,44 +38,44 @@ type UpdaterLogger interface { Error(s string) } +func boolPtr(b bool) *bool { return &b } + func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { - options := configuration.Updater{CLI: true} + options := settings.Updater{CLI: boolPtr(true)} var endUserMode, maintainerMode, updateAll bool + var dnsAddress, csvProviders string flagSet := flag.NewFlagSet("update", flag.ExitOnError) flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)") flagSet.BoolVar(&maintainerMode, "maintainer", false, "Write results to ./internal/storage/servers.json to modify the program (for maintainers)") - flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use") + flagSet.StringVar(&dnsAddress, "dns", "8.8.8.8", "DNS resolver address to use") flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers") - flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers") - flagSet.BoolVar(&options.Expressvpn, "expressvpn", false, "Update ExpressVPN servers") - flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers") - flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers") - flagSet.BoolVar(&options.Ipvanish, "ipvanish", false, "Update IpVanish servers") - flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers") - flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers") - flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers") - flagSet.BoolVar(&options.Perfectprivacy, "perfectprivacy", false, "Update Perfect Privacy servers") - flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers") - flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers") - flagSet.BoolVar(&options.Privatevpn, "privatevpn", false, "Update Private VPN servers") - flagSet.BoolVar(&options.Protonvpn, "protonvpn", false, "Update Protonvpn servers") - flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers") - flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers") - flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers") - flagSet.BoolVar(&options.VPNUnlimited, "vpnunlimited", false, "Update VPN Unlimited servers") - flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers") - flagSet.BoolVar(&options.Wevpn, "wevpn", false, "Update WeVPN servers") - flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers") + flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for") if err := flagSet.Parse(args); err != nil { return err } + if !endUserMode && !maintainerMode { return ErrModeUnspecified } + options.DNSAddress = net.ParseIP(dnsAddress) + if options.DNSAddress == nil { + return fmt.Errorf("%w: %s", ErrDNSAddress, dnsAddress) + } + if updateAll { - options.EnableAll() + options.Providers = constants.AllProviders() + } else { + if csvProviders == "" { + return ErrNoProviderSpecified + } + options.Providers = strings.Split(csvProviders, ",") + } + + err := options.Validate() + if err != nil { + return fmt.Errorf("options validation failed: %w", err) } const clientTimeout = 10 * time.Second diff --git a/internal/cli/warner.go b/internal/cli/warner.go new file mode 100644 index 00000000..a00e13d8 --- /dev/null +++ b/internal/cli/warner.go @@ -0,0 +1,5 @@ +package cli + +type Warner interface { + Warn(s string) +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go deleted file mode 100644 index 2db2e5dc..00000000 --- a/internal/configuration/configuration.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package configuration reads initial settings from environment variables -// and secret files. -package configuration diff --git a/internal/configuration/constants.go b/internal/configuration/constants.go deleted file mode 100644 index 7610d498..00000000 --- a/internal/configuration/constants.go +++ /dev/null @@ -1,6 +0,0 @@ -package configuration - -const ( - lastIndent = "|--" - indent = " " -) diff --git a/internal/configuration/custom.go b/internal/configuration/custom.go deleted file mode 100644 index 45577685..00000000 --- a/internal/configuration/custom.go +++ /dev/null @@ -1,95 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "net" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -var ( - errCustomNotSupported = errors.New("custom provider is not supported") - errCustomExtractFromFile = errors.New("cannot extract configuration from file") -) - -func (settings *Provider) readCustom(r reader, vpnType string) (err error) { - settings.Name = constants.Custom - - switch vpnType { - case constants.OpenVPN: - return settings.ServerSelection.OpenVPN.readCustom(r) - case constants.Wireguard: - return settings.ServerSelection.Wireguard.readCustom(r) - default: - return fmt.Errorf("%w: for VPN type %s", errCustomNotSupported, vpnType) - } -} - -func (settings *OpenVPNSelection) readCustom(r reader) (err error) { - configFile, err := r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue(), params.Compulsory()) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) - } - settings.ConfFile = configFile - - // For display and consistency purposes only, - // these values are not actually used since the file is re-read - // before each OpenVPN start. - _, connection, err := r.ovpnExt.Data(configFile) - if err != nil { - return fmt.Errorf("%w: %s", errCustomExtractFromFile, err) - } - settings.TCP = connection.Protocol == constants.TCP - - return nil -} - -func (settings *OpenVPN) readCustom(r reader) (err error) { - settings.ConfFile, err = r.env.Path("OPENVPN_CUSTOM_CONFIG", - params.Compulsory(), params.CaseSensitiveValue()) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) - } - - return nil -} - -func (settings *WireguardSelection) readCustom(r reader) (err error) { - settings.PublicKey, err = r.env.Get("WIREGUARD_PUBLIC_KEY", - params.CaseSensitiveValue(), params.Compulsory()) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_PUBLIC_KEY: %w", err) - } - - settings.EndpointIP, err = readWireguardEndpointIP(r.env) - if err != nil { - return err - } - - settings.EndpointPort, err = r.env.Port("WIREGUARD_ENDPOINT_PORT", params.Compulsory(), - params.RetroKeys([]string{"WIREGUARD_PORT"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_ENDPOINT_PORT: %w", err) - } - - return nil -} - -// readWireguardEndpointIP reads and parses the server endpoint IP -// address from the environment variable WIREGUARD_ENDPOINT_IP. -func readWireguardEndpointIP(env params.Interface) (endpointIP net.IP, err error) { - s, err := env.Get("WIREGUARD_ENDPOINT_IP", params.Compulsory()) - if err != nil { - return nil, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_IP: %w", err) - } - - endpointIP = net.ParseIP(s) - if endpointIP == nil { - return nil, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_IP: %w: %s", - ErrInvalidIP, s) - } - - return endpointIP, nil -} diff --git a/internal/configuration/cyberghost.go b/internal/configuration/cyberghost.go deleted file mode 100644 index 6628a61b..00000000 --- a/internal/configuration/cyberghost.go +++ /dev/null @@ -1,47 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readCyberghost(r reader) (err error) { - settings.Name = constants.Cyberghost - servers := r.servers.GetCyberghost() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", - constants.CyberghostCountryChoices(servers), - params.RetroKeys([]string{"REGION"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.CyberghostHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolAndPort(r) -} - -func (settings *OpenVPN) readCyberghost(r reader) (err error) { - settings.ClientKey, err = readClientKey(r) - if err != nil { - return fmt.Errorf("%w: %s", errClientKey, err) - } - - settings.ClientCrt, err = readClientCertificate(r) - if err != nil { - return fmt.Errorf("%w: %s", errClientCert, err) - } - - return nil -} diff --git a/internal/configuration/dns.go b/internal/configuration/dns.go deleted file mode 100644 index 21e94909..00000000 --- a/internal/configuration/dns.go +++ /dev/null @@ -1,117 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "net" - "strings" - "time" - - "github.com/qdm12/dns/pkg/blacklist" - "github.com/qdm12/dns/pkg/unbound" - "github.com/qdm12/golibs/params" -) - -// DNS contains settings to configure Unbound for DNS over TLS operation. -type DNS struct { //nolint:maligned - Enabled bool - PlaintextAddress net.IP - KeepNameserver bool - UpdatePeriod time.Duration - Unbound unbound.Settings - BlacklistBuild blacklist.BuilderSettings -} - -func (settings *DNS) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *DNS) lines() (lines []string) { - lines = append(lines, lastIndent+"DNS:") - - if settings.PlaintextAddress != nil { - lines = append(lines, indent+lastIndent+"Plaintext address: "+settings.PlaintextAddress.String()) - } - - if settings.KeepNameserver { - lines = append(lines, indent+lastIndent+"Keep nameserver (disabled blocking): yes") - } - - if !settings.Enabled { - return lines - } - - lines = append(lines, indent+lastIndent+"DNS over TLS:") - - lines = append(lines, indent+indent+lastIndent+"Unbound:") - for _, line := range settings.Unbound.Lines() { - lines = append(lines, indent+indent+indent+line) - } - - lines = append(lines, indent+indent+lastIndent+"Blacklist:") - for _, line := range settings.BlacklistBuild.Lines(indent, lastIndent) { - lines = append(lines, indent+indent+indent+line) - } - - if settings.UpdatePeriod > 0 { - lines = append(lines, indent+indent+lastIndent+"Update: every "+settings.UpdatePeriod.String()) - } - - return lines -} - -var ( - ErrUnboundSettings = errors.New("failed getting Unbound settings") - ErrBlacklistSettings = errors.New("failed getting DNS blacklist settings") -) - -func (settings *DNS) read(r reader) (err error) { - settings.Enabled, err = r.env.OnOff("DOT", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable DOT: %w", err) - } - - // Plain DNS settings - if err := settings.readDNSPlaintext(r.env); err != nil { - return err - } - settings.KeepNameserver, err = r.env.OnOff("DNS_KEEP_NAMESERVER", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err) - } - - // DNS over TLS external settings - if err := settings.readBlacklistBuilding(r); err != nil { - return fmt.Errorf("%w: %s", ErrBlacklistSettings, err) - } - - settings.UpdatePeriod, err = r.env.Duration("DNS_UPDATE_PERIOD", params.Default("24h")) - if err != nil { - return fmt.Errorf("environment variable DNS_UPDATE_PERIOD: %w", err) - } - - // Unbound settings - if err := settings.readUnbound(r); err != nil { - return fmt.Errorf("%w: %s", ErrUnboundSettings, err) - } - - return nil -} - -var ( - ErrDNSAddressNotAnIP = errors.New("DNS plaintext address is not an IP address") -) - -func (settings *DNS) readDNSPlaintext(env params.Interface) error { - s, err := env.Get("DNS_PLAINTEXT_ADDRESS", params.Default("1.1.1.1")) - if err != nil { - return fmt.Errorf("environment variable DNS_PLAINTEXT_ADDRESS: %w", err) - } - - settings.PlaintextAddress = net.ParseIP(s) - if settings.PlaintextAddress == nil { - return fmt.Errorf("%w: %s", ErrDNSAddressNotAnIP, s) - } - - return nil -} diff --git a/internal/configuration/dns_test.go b/internal/configuration/dns_test.go deleted file mode 100644 index f407a85e..00000000 --- a/internal/configuration/dns_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package configuration - -import ( - "net" - "testing" - "time" - - "github.com/qdm12/dns/pkg/blacklist" - "github.com/qdm12/dns/pkg/provider" - "github.com/qdm12/dns/pkg/unbound" - "github.com/stretchr/testify/assert" -) - -func Test_DNS_Lines(t *testing.T) { - t.Parallel() - testCases := map[string]struct { - settings DNS - lines []string - }{ - "disabled DOT": { - settings: DNS{ - PlaintextAddress: net.IP{1, 1, 1, 1}, - }, - lines: []string{ - "|--DNS:", - " |--Plaintext address: 1.1.1.1", - }, - }, - "enabled DOT": { - settings: DNS{ - Enabled: true, - KeepNameserver: true, - Unbound: unbound.Settings{ - Providers: []provider.Provider{ - provider.Cloudflare(), - }, - }, - BlacklistBuild: blacklist.BuilderSettings{ - BlockMalicious: true, - BlockAds: true, - BlockSurveillance: true, - }, - UpdatePeriod: time.Hour, - }, - lines: []string{ - "|--DNS:", - " |--Keep nameserver (disabled blocking): yes", - " |--DNS over TLS:", - " |--Unbound:", - " |--DNS over TLS providers:", - " |--Cloudflare", - " |--Listening port: 0", - " |--Access control:", - " |--Allowed:", - " |--Caching: disabled", - " |--IPv4 resolution: disabled", - " |--IPv6 resolution: disabled", - " |--Verbosity level: 0/5", - " |--Verbosity details level: 0/4", - " |--Validation log level: 0/2", - " |--Username: ", - " |--Blacklist:", - " |--Blocked categories: malicious, surveillance, ads", - " |--Update: every 1h0m0s", - }, - }, - } - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - lines := testCase.settings.lines() - assert.Equal(t, testCase.lines, lines) - }) - } -} diff --git a/internal/configuration/dnsblacklist.go b/internal/configuration/dnsblacklist.go deleted file mode 100644 index 31bb4788..00000000 --- a/internal/configuration/dnsblacklist.go +++ /dev/null @@ -1,87 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - - "github.com/qdm12/golibs/params" - "inet.af/netaddr" -) - -func (settings *DNS) readBlacklistBuilding(r reader) (err error) { - settings.BlacklistBuild.BlockMalicious, err = r.env.OnOff("BLOCK_MALICIOUS", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err) - } - - settings.BlacklistBuild.BlockSurveillance, err = r.env.OnOff("BLOCK_SURVEILLANCE", params.Default("on"), - params.RetroKeys([]string{"BLOCK_NSA"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable BLOCK_SURVEILLANCE (or BLOCK_NSA): %w", err) - } - - settings.BlacklistBuild.BlockAds, err = r.env.OnOff("BLOCK_ADS", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable BLOCK_ADS: %w", err) - } - - if err := settings.readPrivateAddresses(r.env); err != nil { - return err - } - - return settings.readBlacklistUnblockedHostnames(r) -} - -var ( - ErrInvalidPrivateAddress = errors.New("private address is not a valid IP or CIDR range") -) - -func (settings *DNS) readPrivateAddresses(env params.Interface) (err error) { - privateAddresses, err := env.CSV("DOT_PRIVATE_ADDRESS") - if err != nil { - return fmt.Errorf("environment variable DOT_PRIVATE_ADDRESS: %w", err) - } else if len(privateAddresses) == 0 { - return nil - } - - ips := make([]netaddr.IP, 0, len(privateAddresses)) - ipPrefixes := make([]netaddr.IPPrefix, 0, len(privateAddresses)) - - for _, address := range privateAddresses { - ip, err := netaddr.ParseIP(address) - if err == nil { - ips = append(ips, ip) - continue - } - - ipPrefix, err := netaddr.ParseIPPrefix(address) - if err == nil { - ipPrefixes = append(ipPrefixes, ipPrefix) - continue - } - - return fmt.Errorf("%w: %s", ErrInvalidPrivateAddress, address) - } - - settings.BlacklistBuild.AddBlockedIPs = append(settings.BlacklistBuild.AddBlockedIPs, ips...) - settings.BlacklistBuild.AddBlockedIPPrefixes = append(settings.BlacklistBuild.AddBlockedIPPrefixes, ipPrefixes...) - - return nil -} - -func (settings *DNS) readBlacklistUnblockedHostnames(r reader) (err error) { - hostnames, err := r.env.CSV("UNBLOCK") - if err != nil { - return fmt.Errorf("environment variable UNBLOCK: %w", err) - } else if len(hostnames) == 0 { - return nil - } - for _, hostname := range hostnames { - if !r.regex.MatchHostname(hostname) { - return fmt.Errorf("%w: %s", ErrInvalidHostname, hostname) - } - } - - settings.BlacklistBuild.AllowedHosts = append(settings.BlacklistBuild.AllowedHosts, hostnames...) - return nil -} diff --git a/internal/configuration/expressvpn.go b/internal/configuration/expressvpn.go deleted file mode 100644 index 031c047e..00000000 --- a/internal/configuration/expressvpn.go +++ /dev/null @@ -1,40 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readExpressvpn(r reader) (err error) { - settings.Name = constants.Expressvpn - servers := r.servers.GetExpressvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.ExpressvpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ExpressvpnCountriesChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ExpressvpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.OpenVPN.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/fastestvpn.go b/internal/configuration/fastestvpn.go deleted file mode 100644 index ada9bdec..00000000 --- a/internal/configuration/fastestvpn.go +++ /dev/null @@ -1,30 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readFastestvpn(r reader) (err error) { - settings.Name = constants.Fastestvpn - servers := r.servers.GetFastestvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.FastestvpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.FastestvpnCountriesChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} diff --git a/internal/configuration/firewall.go b/internal/configuration/firewall.go deleted file mode 100644 index 27eb4334..00000000 --- a/internal/configuration/firewall.go +++ /dev/null @@ -1,99 +0,0 @@ -package configuration - -import ( - "fmt" - "net" - "strings" - - "github.com/qdm12/golibs/params" -) - -// Firewall contains settings to customize the firewall operation. -type Firewall struct { - VPNInputPorts []uint16 - InputPorts []uint16 - OutboundSubnets []net.IPNet - Enabled bool - Debug bool -} - -func (settings *Firewall) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *Firewall) lines() (lines []string) { - if !settings.Enabled { - lines = append(lines, lastIndent+"Firewall: disabled ⚠️") - return lines - } - - lines = append(lines, lastIndent+"Firewall:") - - if settings.Debug { - lines = append(lines, indent+lastIndent+"Debug: on") - } - - if len(settings.VPNInputPorts) > 0 { - lines = append(lines, indent+lastIndent+"VPN input ports: "+ - strings.Join(uint16sToStrings(settings.VPNInputPorts), ", ")) - } - - if len(settings.InputPorts) > 0 { - lines = append(lines, indent+lastIndent+"Input ports: "+ - strings.Join(uint16sToStrings(settings.InputPorts), ", ")) - } - - if len(settings.OutboundSubnets) > 0 { - lines = append(lines, indent+lastIndent+"Outbound subnets: "+ - strings.Join(ipNetsToStrings(settings.OutboundSubnets), ", ")) - } - - return lines -} - -func (settings *Firewall) read(r reader) (err error) { - settings.Enabled, err = r.env.OnOff("FIREWALL", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable FIREWALL: %w", err) - } - - settings.Debug, err = r.env.OnOff("FIREWALL_DEBUG", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable FIREWALL_DEBUG: %w", err) - } - - if err := settings.readVPNInputPorts(r.env); err != nil { - return err - } - - if err := settings.readInputPorts(r.env); err != nil { - return err - } - - return settings.readOutboundSubnets(r) -} - -func (settings *Firewall) readVPNInputPorts(env params.Interface) (err error) { - settings.VPNInputPorts, err = readCSVPorts(env, "FIREWALL_VPN_INPUT_PORTS") - if err != nil { - return fmt.Errorf("environment variable FIREWALL_VPN_INPUT_PORTS: %w", err) - } - return nil -} - -func (settings *Firewall) readInputPorts(env params.Interface) (err error) { - settings.InputPorts, err = readCSVPorts(env, "FIREWALL_INPUT_PORTS") - if err != nil { - return fmt.Errorf("environment variable FIREWALL_INPUT_PORTS: %w", err) - } - return nil -} - -func (settings *Firewall) readOutboundSubnets(r reader) (err error) { - retroOption := params.RetroKeys([]string{"EXTRA_SUBNETS"}, r.onRetroActive) - settings.OutboundSubnets, err = readCSVIPNets(r.env, "FIREWALL_OUTBOUND_SUBNETS", retroOption) - if err != nil { - return fmt.Errorf("environment variable FIREWALL_OUTBOUND_SUBNETS: %w", err) - } - return nil -} diff --git a/internal/configuration/health.go b/internal/configuration/health.go deleted file mode 100644 index 986e17a8..00000000 --- a/internal/configuration/health.go +++ /dev/null @@ -1,72 +0,0 @@ -package configuration - -import ( - "fmt" - "strings" - - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params" -) - -// Health contains settings for the healthcheck and health server. -type Health struct { - ServerAddress string - AddressToPing string - VPN HealthyWait -} - -func (settings *Health) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *Health) lines() (lines []string) { - lines = append(lines, lastIndent+"Health:") - - lines = append(lines, indent+lastIndent+"Server address: "+settings.ServerAddress) - - lines = append(lines, indent+lastIndent+"Address to ping: "+settings.AddressToPing) - - lines = append(lines, indent+lastIndent+"VPN:") - for _, line := range settings.VPN.lines() { - lines = append(lines, indent+indent+line) - } - - return lines -} - -// Read is to be used for the healthcheck query mode. -func (settings *Health) Read(env params.Interface, warner Warner) (err error) { - reader := newReader(env, models.AllServers{}, warner) // note: no need for servers data - return settings.read(reader) -} - -func (settings *Health) read(r reader) (err error) { - var warning string - settings.ServerAddress, warning, err = r.env.ListeningAddress( - "HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999")) - if warning != "" { - r.warner.Warn("environment variable HEALTH_SERVER_ADDRESS: " + warning) - } - if err != nil { - return fmt.Errorf("environment variable HEALTH_SERVER_ADDRESS: %w", err) - } - - settings.AddressToPing, err = r.env.Get("HEALTH_ADDRESS_TO_PING", params.Default("github.com")) - if err != nil { - return fmt.Errorf("environment variable HEALTH_ADDRESS_TO_PING: %w", err) - } - - retroKeyOption := params.RetroKeys([]string{"HEALTH_OPENVPN_DURATION_INITIAL"}, r.onRetroActive) - settings.VPN.Initial, err = r.env.Duration("HEALTH_VPN_DURATION_INITIAL", params.Default("6s"), retroKeyOption) - if err != nil { - return fmt.Errorf("environment variable HEALTH_VPN_DURATION_INITIAL: %w", err) - } - - retroKeyOption = params.RetroKeys([]string{"HEALTH_OPENVPN_DURATION_ADDITION"}, r.onRetroActive) - settings.VPN.Addition, err = r.env.Duration("HEALTH_VPN_DURATION_ADDITION", params.Default("5s"), retroKeyOption) - if err != nil { - return fmt.Errorf("environment variable HEALTH_VPN_DURATION_ADDITION: %w", err) - } - - return nil -} diff --git a/internal/configuration/health_test.go b/internal/configuration/health_test.go deleted file mode 100644 index 999f2611..00000000 --- a/internal/configuration/health_test.go +++ /dev/null @@ -1,272 +0,0 @@ -package configuration - -import ( - "errors" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Health_String(t *testing.T) { - t.Parallel() - - health := Health{ - ServerAddress: "a", - AddressToPing: "b", - } - const expected = `|--Health: - |--Server address: a - |--Address to ping: b - |--VPN: - |--Initial duration: 0s` - - s := health.String() - - assert.Equal(t, expected, s) -} - -func Test_Health_lines(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - settings Health - lines []string - }{ - "empty": { - lines: []string{ - "|--Health:", - " |--Server address: ", - " |--Address to ping: ", - " |--VPN:", - " |--Initial duration: 0s", - }, - }, - "filled settings": { - settings: Health{ - ServerAddress: "address:9999", - AddressToPing: "github.com", - VPN: HealthyWait{ - Initial: time.Second, - Addition: time.Minute, - }, - }, - lines: []string{ - "|--Health:", - " |--Server address: address:9999", - " |--Address to ping: github.com", - " |--VPN:", - " |--Initial duration: 1s", - " |--Addition duration: 1m0s", - }, - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - lines := testCase.settings.lines() - - assert.Equal(t, testCase.lines, lines) - }) - } -} - -func Test_Health_read(t *testing.T) { - t.Parallel() - - errDummy := errors.New("dummy") - - type stringCall struct { - call bool - s string - err error - } - - type stringCallWithWarning struct { - call bool - s string - warning string - err error - } - - type durationCall struct { - call bool - duration time.Duration - err error - } - - testCases := map[string]struct { - serverAddress stringCallWithWarning - addressToPing stringCall - vpnInitial durationCall - vpnAddition durationCall - expected Health - err error - }{ - "success": { - serverAddress: stringCallWithWarning{ - call: true, - s: "127.0.0.1:9999", - }, - addressToPing: stringCall{ - call: true, - s: "1.2.3.4", - }, - vpnInitial: durationCall{ - call: true, - duration: time.Second, - }, - vpnAddition: durationCall{ - call: true, - duration: time.Minute, - }, - expected: Health{ - ServerAddress: "127.0.0.1:9999", - AddressToPing: "1.2.3.4", - VPN: HealthyWait{ - Initial: time.Second, - Addition: time.Minute, - }, - }, - }, - "listening address error": { - serverAddress: stringCallWithWarning{ - call: true, - s: "127.0.0.1:9999", - warning: "warning", - err: errDummy, - }, - expected: Health{ - ServerAddress: "127.0.0.1:9999", - }, - err: errors.New("environment variable HEALTH_SERVER_ADDRESS: dummy"), - }, - "address to ping error": { - serverAddress: stringCallWithWarning{ - call: true, - }, - addressToPing: stringCall{ - call: true, - s: "address", - err: errDummy, - }, - expected: Health{ - AddressToPing: "address", - }, - err: errors.New("environment variable HEALTH_ADDRESS_TO_PING: dummy"), - }, - "initial error": { - serverAddress: stringCallWithWarning{ - call: true, - }, - addressToPing: stringCall{ - call: true, - }, - vpnInitial: durationCall{ - call: true, - duration: time.Second, - err: errDummy, - }, - expected: Health{ - VPN: HealthyWait{ - Initial: time.Second, - }, - }, - err: errors.New("environment variable HEALTH_VPN_DURATION_INITIAL: dummy"), - }, - "addition error": { - serverAddress: stringCallWithWarning{ - call: true, - }, - addressToPing: stringCall{ - call: true, - }, - vpnInitial: durationCall{ - call: true, - duration: time.Second, - }, - vpnAddition: durationCall{ - call: true, - duration: time.Minute, - err: errDummy, - }, - expected: Health{ - VPN: HealthyWait{ - Initial: time.Second, - Addition: time.Minute, - }, - }, - err: errors.New("environment variable HEALTH_VPN_DURATION_ADDITION: dummy"), - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - - env := mock_params.NewMockInterface(ctrl) - warner := NewMockWarner(ctrl) - - if testCase.serverAddress.call { - value := testCase.serverAddress.s - warning := testCase.serverAddress.warning - err := testCase.serverAddress.err - env.EXPECT().ListeningAddress("HEALTH_SERVER_ADDRESS", gomock.Any()). - Return(value, warning, err) - if warning != "" { - warner.EXPECT().Warn("environment variable HEALTH_SERVER_ADDRESS: " + warning) - } - } - - if testCase.addressToPing.call { - value := testCase.addressToPing.s - err := testCase.addressToPing.err - env.EXPECT().Get("HEALTH_ADDRESS_TO_PING", gomock.Any()). - Return(value, err) - } - - if testCase.vpnInitial.call { - value := testCase.vpnInitial.duration - err := testCase.vpnInitial.err - env.EXPECT(). - Duration("HEALTH_VPN_DURATION_INITIAL", gomock.Any()). - Return(value, err) - } - - if testCase.vpnAddition.call { - value := testCase.vpnAddition.duration - err := testCase.vpnAddition.err - env.EXPECT(). - Duration("HEALTH_VPN_DURATION_ADDITION", gomock.Any()). - Return(value, err) - } - - r := reader{ - env: env, - warner: warner, - } - - var health Health - - err := health.read(r) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.expected, health) - }) - } -} diff --git a/internal/configuration/healthwait_test.go b/internal/configuration/healthwait_test.go deleted file mode 100644 index 7abe619b..00000000 --- a/internal/configuration/healthwait_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package configuration - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func Test_HealthyWait_String(t *testing.T) { - t.Parallel() - - var healthyWait HealthyWait - const expected = "|--Initial duration: 0s" - - s := healthyWait.String() - - assert.Equal(t, expected, s) -} - -func Test_HealthyWait_lines(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - settings HealthyWait - lines []string - }{ - "empty": { - lines: []string{ - "|--Initial duration: 0s", - }, - }, - "filled settings": { - settings: HealthyWait{ - Initial: time.Second, - Addition: time.Minute, - }, - lines: []string{ - "|--Initial duration: 1s", - "|--Addition duration: 1m0s", - }, - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - lines := testCase.settings.lines() - - assert.Equal(t, testCase.lines, lines) - }) - } -} diff --git a/internal/configuration/healthywait.go b/internal/configuration/healthywait.go deleted file mode 100644 index ee85940d..00000000 --- a/internal/configuration/healthywait.go +++ /dev/null @@ -1,30 +0,0 @@ -package configuration - -import ( - "strings" - "time" -) - -type HealthyWait struct { - // Initial is the initial duration to wait for the program - // to be healthy before taking action. - Initial time.Duration - // Addition is the duration to add to the Initial duration - // after Initial has expired to wait longer for the program - // to be healthy. - Addition time.Duration -} - -func (settings *HealthyWait) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *HealthyWait) lines() (lines []string) { - lines = append(lines, lastIndent+"Initial duration: "+settings.Initial.String()) - - if settings.Addition > 0 { - lines = append(lines, lastIndent+"Addition duration: "+settings.Addition.String()) - } - - return lines -} diff --git a/internal/configuration/hidemyass.go b/internal/configuration/hidemyass.go deleted file mode 100644 index 77fdf02a..00000000 --- a/internal/configuration/hidemyass.go +++ /dev/null @@ -1,40 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readHideMyAss(r reader) (err error) { - settings.Name = constants.HideMyAss - servers := r.servers.GetHideMyAss() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.HideMyAssCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.HideMyAssRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.HideMyAssCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.HideMyAssHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolAndPort(r) -} diff --git a/internal/configuration/httpproxy.go b/internal/configuration/httpproxy.go deleted file mode 100644 index a2184121..00000000 --- a/internal/configuration/httpproxy.go +++ /dev/null @@ -1,106 +0,0 @@ -package configuration - -import ( - "fmt" - "strconv" - "strings" - - "github.com/qdm12/golibs/params" -) - -// HTTPProxy contains settings to configure the HTTP proxy. -type HTTPProxy struct { - User string - Password string - Port uint16 - Enabled bool - Stealth bool - Log bool -} - -func (settings *HTTPProxy) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *HTTPProxy) lines() (lines []string) { - if !settings.Enabled { - return nil - } - - lines = append(lines, lastIndent+"HTTP proxy:") - - lines = append(lines, indent+lastIndent+"Port: "+strconv.Itoa(int(settings.Port))) - - if settings.User != "" { - lines = append(lines, indent+lastIndent+"Authentication: enabled") - } - - if settings.Log { - lines = append(lines, indent+lastIndent+"Log: enabled") - } - - if settings.Stealth { - lines = append(lines, indent+lastIndent+"Stealth: enabled") - } - - return lines -} - -func (settings *HTTPProxy) read(r reader) (err error) { - settings.Enabled, err = r.env.OnOff("HTTPPROXY", params.Default("off"), - params.RetroKeys([]string{"TINYPROXY", "PROXY"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY (or TINYPROXY, PROXY): %w", err) - } - - settings.User, err = r.getFromEnvOrSecretFile("HTTPPROXY_USER", false, // compulsory - []string{"TINYPROXY_USER", "PROXY_USER"}) - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY_USER (or TINYPROXY_USER, PROXY_USER): %w", err) - } - - settings.Password, err = r.getFromEnvOrSecretFile("HTTPPROXY_PASSWORD", false, - []string{"TINYPROXY_PASSWORD", "PROXY_PASSWORD"}) - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY_PASSWORD (or TINYPROXY_PASSWORD, PROXY_PASSWORD): %w", err) - } - - settings.Stealth, err = r.env.OnOff("HTTPPROXY_STEALTH", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY_STEALTH: %w", err) - } - - if err := settings.readLog(r); err != nil { - return err - } - - var warning string - settings.Port, warning, err = r.env.ListeningPort("HTTPPROXY_PORT", params.Default("8888"), - params.RetroKeys([]string{"TINYPROXY_PORT", "PROXY_PORT"}, r.onRetroActive)) - if len(warning) > 0 { - r.warner.Warn(warning) - } - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY_PORT (or TINYPROXY_PORT, PROXY_PORT): %w", err) - } - - return nil -} - -func (settings *HTTPProxy) readLog(r reader) error { - s, err := r.env.Get("HTTPPROXY_LOG", - params.RetroKeys([]string{"PROXY_LOG_LEVEL", "TINYPROXY_LOG"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable HTTPPROXY_LOG (or TINYPROXY_LOG, PROXY_LOG_LEVEL): %w", err) - } - - switch strings.ToLower(s) { - case "on": - settings.Log = true - // Retro compatibility - case "info", "connect", "notice": - settings.Log = true - } - - return nil -} diff --git a/internal/configuration/ipvanish.go b/internal/configuration/ipvanish.go deleted file mode 100644 index 52e8858a..00000000 --- a/internal/configuration/ipvanish.go +++ /dev/null @@ -1,35 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readIpvanish(r reader) (err error) { - settings.Name = constants.Ipvanish - servers := r.servers.GetIpvanish() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IpvanishCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IpvanishCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.IpvanishHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} diff --git a/internal/configuration/ipvanish_test.go b/internal/configuration/ipvanish_test.go deleted file mode 100644 index fd82c4e0..00000000 --- a/internal/configuration/ipvanish_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package configuration - -import ( - "errors" - "net" - "testing" - - "github.com/golang/mock/gomock" - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Provider_readIpvanish(t *testing.T) { - t.Parallel() - - var errDummy = errors.New("dummy test error") - - type singleStringCall struct { - call bool - value string - err error - } - - type sliceStringCall struct { - call bool - values []string - err error - } - - testCases := map[string]struct { - targetIP singleStringCall - countries sliceStringCall - cities sliceStringCall - hostnames sliceStringCall - protocol singleStringCall - settings Provider - err error - }{ - "target IP error": { - targetIP: singleStringCall{call: true, value: "something", err: errDummy}, - settings: Provider{ - Name: constants.Ipvanish, - }, - err: errors.New("environment variable OPENVPN_TARGET_IP: dummy test error"), - }, - "countries error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ipvanish, - }, - err: errors.New("environment variable COUNTRY: dummy test error"), - }, - "cities error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ipvanish, - }, - err: errors.New("environment variable CITY: dummy test error"), - }, - "hostnames error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ipvanish, - }, - err: errors.New("environment variable SERVER_HOSTNAME: dummy test error"), - }, - "protocol error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ipvanish, - }, - err: errors.New("environment variable OPENVPN_PROTOCOL: dummy test error"), - }, - "default settings": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true}, - settings: Provider{ - Name: constants.Ipvanish, - }, - }, - "set settings": { - targetIP: singleStringCall{call: true, value: "1.2.3.4"}, - countries: sliceStringCall{call: true, values: []string{"A", "B"}}, - cities: sliceStringCall{call: true, values: []string{"C", "D"}}, - hostnames: sliceStringCall{call: true, values: []string{"E", "F"}}, - protocol: singleStringCall{call: true, value: constants.TCP}, - settings: Provider{ - Name: constants.Ipvanish, - ServerSelection: ServerSelection{ - OpenVPN: OpenVPNSelection{ - TCP: true, - }, - TargetIP: net.IPv4(1, 2, 3, 4), - Countries: []string{"A", "B"}, - Cities: []string{"C", "D"}, - Hostnames: []string{"E", "F"}, - }, - }, - }, - } - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - - servers := []models.IpvanishServer{{Hostname: "a"}} - allServers := models.AllServers{ - Ipvanish: models.IpvanishServers{ - Servers: servers, - }, - } - - env := mock_params.NewMockInterface(ctrl) - if testCase.targetIP.call { - env.EXPECT().Get("OPENVPN_TARGET_IP"). - Return(testCase.targetIP.value, testCase.targetIP.err) - } - if testCase.countries.call { - env.EXPECT().CSVInside("COUNTRY", constants.IpvanishCountryChoices(servers)). - Return(testCase.countries.values, testCase.countries.err) - } - if testCase.cities.call { - env.EXPECT().CSVInside("CITY", constants.IpvanishCityChoices(servers)). - Return(testCase.cities.values, testCase.cities.err) - } - if testCase.hostnames.call { - env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices(servers)). - Return(testCase.hostnames.values, testCase.hostnames.err) - } - if testCase.protocol.call { - env.EXPECT().Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()). - Return(testCase.protocol.value, testCase.protocol.err) - } - - r := reader{ - servers: allServers, - env: env, - } - - var settings Provider - err := settings.readIpvanish(r) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.settings, settings) - }) - } -} diff --git a/internal/configuration/ivpn.go b/internal/configuration/ivpn.go deleted file mode 100644 index 8d4084b9..00000000 --- a/internal/configuration/ivpn.go +++ /dev/null @@ -1,73 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readIvpn(r reader) (err error) { - settings.Name = constants.Ivpn - servers := r.servers.GetIvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IvpnCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IvpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.IvpnISPChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable ISP: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - err = settings.ServerSelection.OpenVPN.readIVPN(r) - if err != nil { - return err - } - - return settings.ServerSelection.Wireguard.readIVPN(r.env) -} - -func (settings *OpenVPNSelection) readIVPN(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{ - tcp: settings.TCP, - allowedTCP: []uint16{80, 443, 1443}, - allowedUDP: []uint16{53, 1194, 2049, 2050}, - }) - if err != nil { - return err - } - - return nil -} - -func (settings *WireguardSelection) readIVPN(env params.Interface) (err error) { - settings.EndpointPort, err = readWireguardCustomPort(env, - []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237}) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/ivpn_test.go b/internal/configuration/ivpn_test.go deleted file mode 100644 index 96574142..00000000 --- a/internal/configuration/ivpn_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package configuration - -import ( - "errors" - "net" - "testing" - - "github.com/golang/mock/gomock" - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Provider_readIvpn(t *testing.T) { //nolint:gocognit - t.Parallel() - - var errDummy = errors.New("dummy test error") - - type singleStringCall struct { - call bool - value string - err error - } - - type portCall struct { - getCall bool - getValue string // "" or "0" - getErr error - portCall bool - portValue uint16 - portErr error - } - - type sliceStringCall struct { - call bool - values []string - err error - } - - testCases := map[string]struct { - targetIP singleStringCall - countries sliceStringCall - cities sliceStringCall - isps sliceStringCall - hostnames sliceStringCall - protocol singleStringCall - ovpnPort portCall - ovpnOldPort portCall - wgPort portCall - wgOldPort portCall - settings Provider - err error - }{ - "target IP error": { - targetIP: singleStringCall{call: true, value: "something", err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable OPENVPN_TARGET_IP: dummy test error"), - }, - "countries error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable COUNTRY: dummy test error"), - }, - "cities error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable CITY: dummy test error"), - }, - "isps error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable ISP: dummy test error"), - }, - "hostnames error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable SERVER_HOSTNAME: dummy test error"), - }, - "openvpn protocol error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable OPENVPN_PROTOCOL: dummy test error"), - }, - "openvpn custom port error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true}, - ovpnPort: portCall{getCall: true, getErr: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable OPENVPN_PORT: dummy test error"), - }, - "wireguard custom port error": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true}, - ovpnPort: portCall{getCall: true, getValue: "0"}, - ovpnOldPort: portCall{getCall: true, getValue: "0"}, - wgPort: portCall{getCall: true, getErr: errDummy}, - settings: Provider{ - Name: constants.Ivpn, - }, - err: errors.New("environment variable WIREGUARD_ENDPOINT_PORT: dummy test error"), - }, - "default settings": { - targetIP: singleStringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - isps: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: singleStringCall{call: true}, - ovpnPort: portCall{getCall: true, getValue: "0"}, - ovpnOldPort: portCall{getCall: true, getValue: "0"}, - wgPort: portCall{getCall: true, getValue: "0"}, - wgOldPort: portCall{getCall: true, getValue: "0"}, - settings: Provider{ - Name: constants.Ivpn, - }, - }, - "set settings": { - targetIP: singleStringCall{call: true, value: "1.2.3.4"}, - countries: sliceStringCall{call: true, values: []string{"A", "B"}}, - cities: sliceStringCall{call: true, values: []string{"C", "D"}}, - isps: sliceStringCall{call: true, values: []string{"ISP 1"}}, - hostnames: sliceStringCall{call: true, values: []string{"E", "F"}}, - protocol: singleStringCall{call: true, value: constants.TCP}, - ovpnPort: portCall{getCall: true, portCall: true, portValue: 443}, - wgPort: portCall{getCall: true, portCall: true, portValue: 2049}, - settings: Provider{ - Name: constants.Ivpn, - ServerSelection: ServerSelection{ - OpenVPN: OpenVPNSelection{ - TCP: true, - CustomPort: 443, - }, - Wireguard: WireguardSelection{ - EndpointPort: 2049, - }, - TargetIP: net.IPv4(1, 2, 3, 4), - Countries: []string{"A", "B"}, - Cities: []string{"C", "D"}, - ISPs: []string{"ISP 1"}, - Hostnames: []string{"E", "F"}, - }, - }, - }, - } - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - - env := mock_params.NewMockInterface(ctrl) - - servers := []models.IvpnServer{{Hostname: "a"}} - allServers := models.AllServers{ - Ivpn: models.IvpnServers{ - Servers: servers, - }, - } - - if testCase.targetIP.call { - env.EXPECT().Get("OPENVPN_TARGET_IP"). - Return(testCase.targetIP.value, testCase.targetIP.err) - } - if testCase.countries.call { - env.EXPECT().CSVInside("COUNTRY", constants.IvpnCountryChoices(servers)). - Return(testCase.countries.values, testCase.countries.err) - } - if testCase.cities.call { - env.EXPECT().CSVInside("CITY", constants.IvpnCityChoices(servers)). - Return(testCase.cities.values, testCase.cities.err) - } - if testCase.isps.call { - env.EXPECT().CSVInside("ISP", constants.IvpnISPChoices(servers)). - Return(testCase.isps.values, testCase.isps.err) - } - if testCase.hostnames.call { - env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IvpnHostnameChoices(servers)). - Return(testCase.hostnames.values, testCase.hostnames.err) - } - if testCase.protocol.call { - env.EXPECT().Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()). - Return(testCase.protocol.value, testCase.protocol.err) - } - if testCase.ovpnPort.getCall { - env.EXPECT().Get("OPENVPN_PORT", gomock.Any()). - Return(testCase.ovpnPort.getValue, testCase.ovpnPort.getErr) - } - if testCase.ovpnPort.portCall { - env.EXPECT().Port("OPENVPN_PORT"). - Return(testCase.ovpnPort.portValue, testCase.ovpnPort.portErr) - } - if testCase.ovpnOldPort.getCall { - env.EXPECT().Get("PORT", gomock.Any()). - Return(testCase.ovpnOldPort.getValue, testCase.ovpnOldPort.getErr) - } - if testCase.ovpnOldPort.portCall { - env.EXPECT().Port("PORT"). - Return(testCase.ovpnOldPort.portValue, testCase.ovpnOldPort.portErr) - } - if testCase.wgPort.getCall { - env.EXPECT().Get("WIREGUARD_ENDPOINT_PORT", gomock.Any()). - Return(testCase.wgPort.getValue, testCase.wgPort.getErr) - } - if testCase.wgPort.portCall { - env.EXPECT().Port("WIREGUARD_ENDPOINT_PORT"). - Return(testCase.wgPort.portValue, testCase.wgPort.portErr) - } - if testCase.wgOldPort.getCall { - env.EXPECT().Get("WIREGUARD_PORT", gomock.Any()). - Return(testCase.wgOldPort.getValue, testCase.wgOldPort.getErr) - } - if testCase.wgOldPort.portCall { - env.EXPECT().Port("WIREGUARD_PORT"). - Return(testCase.wgOldPort.portValue, testCase.wgOldPort.portErr) - } - - r := reader{ - servers: allServers, - env: env, - } - - var settings Provider - err := settings.readIvpn(r) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.settings, settings) - }) - } -} diff --git a/internal/configuration/keys.go b/internal/configuration/keys.go deleted file mode 100644 index 6a832a11..00000000 --- a/internal/configuration/keys.go +++ /dev/null @@ -1,29 +0,0 @@ -package configuration - -import ( - "errors" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/openvpn/parse" -) - -var ( - errClientCert = errors.New("cannot read client certificate") - errClientKey = errors.New("cannot read client key") -) - -func readClientKey(r reader) (clientKey string, err error) { - b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTKEY", constants.ClientKey) - if err != nil { - return "", err - } - return parse.ExtractPrivateKey(b) -} - -func readClientCertificate(r reader) (clientCertificate string, err error) { - b, err := r.getFromFileOrSecretFile("OPENVPN_CLIENTCRT", constants.ClientCertificate) - if err != nil { - return "", err - } - return parse.ExtractCert(b) -} diff --git a/internal/configuration/lines.go b/internal/configuration/lines.go deleted file mode 100644 index 8f7c00e9..00000000 --- a/internal/configuration/lines.go +++ /dev/null @@ -1,22 +0,0 @@ -package configuration - -import ( - "net" - "strconv" -) - -func uint16sToStrings(uint16s []uint16) (strings []string) { - strings = make([]string, len(uint16s)) - for i := range uint16s { - strings[i] = strconv.Itoa(int(uint16s[i])) - } - return strings -} - -func ipNetsToStrings(ipNets []net.IPNet) (strings []string) { - strings = make([]string, len(ipNets)) - for i := range ipNets { - strings[i] = ipNets[i].String() - } - return strings -} diff --git a/internal/configuration/log.go b/internal/configuration/log.go deleted file mode 100644 index 143c3b31..00000000 --- a/internal/configuration/log.go +++ /dev/null @@ -1,30 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/golibs/logging" - "github.com/qdm12/golibs/params" -) - -type Log struct { - Level logging.Level `json:"level"` -} - -func (settings *Log) lines() (lines []string) { - lines = append(lines, lastIndent+"Log:") - - lines = append(lines, indent+lastIndent+"Level: "+settings.Level.String()) - - return lines -} - -func (settings *Log) read(env params.Interface) (err error) { - defaultLevel := logging.LevelInfo.String() - settings.Level, err = env.LogLevel("LOG_LEVEL", params.Default(defaultLevel)) - if err != nil { - return fmt.Errorf("environment variable LOG_LEVEL: %w", err) - } - - return nil -} diff --git a/internal/configuration/mullvad.go b/internal/configuration/mullvad.go deleted file mode 100644 index 1c888bc7..00000000 --- a/internal/configuration/mullvad.go +++ /dev/null @@ -1,77 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readMullvad(r reader) (err error) { - settings.Name = constants.Mullvad - servers := r.servers.GetMullvad() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.MullvadCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.MullvadCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.MullvadHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.ISPs, err = r.env.CSVInside("ISP", constants.MullvadISPChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable ISP: %w", err) - } - - settings.ServerSelection.Owned, err = r.env.YesNo("OWNED", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable OWNED: %w", err) - } - - err = settings.ServerSelection.OpenVPN.readMullvad(r) - if err != nil { - return err - } - - return settings.ServerSelection.Wireguard.readMullvad(r.env) -} - -func (settings *OpenVPNSelection) readMullvad(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{ - tcp: settings.TCP, - allowedTCP: []uint16{80, 443, 1401}, - allowedUDP: []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}, - }) - if err != nil { - return err - } - - return nil -} - -func (settings *WireguardSelection) readMullvad(env params.Interface) (err error) { - settings.EndpointPort, err = readWireguardCustomPort(env, nil) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/nordvpn.go b/internal/configuration/nordvpn.go deleted file mode 100644 index 6ea78dff..00000000 --- a/internal/configuration/nordvpn.go +++ /dev/null @@ -1,58 +0,0 @@ -package configuration - -import ( - "fmt" - "strconv" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readNordvpn(r reader) (err error) { - settings.Name = constants.Nordvpn - servers := r.servers.GetNordvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.NordvpnRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.NordvpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.Numbers, err = readNordVPNServerNumbers(r.env) - if err != nil { - return err - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} - -func readNordVPNServerNumbers(env params.Interface) (numbers []uint16, err error) { - const possiblePortsCount = 65537 - possibilities := make([]string, possiblePortsCount) - for i := range possibilities { - possibilities[i] = fmt.Sprintf("%d", i) - } - possibilities[65536] = "" - values, err := env.CSVInside("SERVER_NUMBER", possibilities) - if err != nil { - return nil, err - } - numbers = make([]uint16, len(values)) - for i := range values { - n, err := strconv.Atoi(values[i]) - if err != nil { - return nil, err - } - numbers[i] = uint16(n) - } - return numbers, nil -} diff --git a/internal/configuration/openvpn.go b/internal/configuration/openvpn.go deleted file mode 100644 index 003acfe1..00000000 --- a/internal/configuration/openvpn.go +++ /dev/null @@ -1,207 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -// OpenVPN contains settings to configure the OpenVPN client. -type OpenVPN struct { - User string `json:"user"` - Password string `json:"password"` - Verbosity int `json:"verbosity"` - Flags []string `json:"flags"` - MSSFix uint16 `json:"mssfix"` - Root bool `json:"run_as_root"` - Ciphers []string `json:"ciphers"` - Auth string `json:"auth"` - ConfFile string `json:"conf_file"` - Version string `json:"version"` - ClientCrt string `json:"-"` // Cyberghost - ClientKey string `json:"-"` // Cyberghost, VPNUnlimited - EncPreset string `json:"encryption_preset"` // PIA - IPv6 bool `json:"ipv6"` // Mullvad - ProcUser string `json:"procuser"` // Process username - Interface string `json:"interface"` -} - -func (settings *OpenVPN) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *OpenVPN) lines() (lines []string) { - lines = append(lines, lastIndent+"OpenVPN:") - - lines = append(lines, indent+lastIndent+"Version: "+settings.Version) - - lines = append(lines, indent+lastIndent+"Verbosity level: "+strconv.Itoa(settings.Verbosity)) - - lines = append(lines, indent+lastIndent+"Network interface: "+settings.Interface) - - if len(settings.Flags) > 0 { - lines = append(lines, indent+lastIndent+"Flags: "+strings.Join(settings.Flags, " ")) - } - - if settings.Root { - lines = append(lines, indent+lastIndent+"Run as root: enabled") - } - - if len(settings.Ciphers) > 0 { - lines = append(lines, indent+lastIndent+"Custom ciphers: "+commaJoin(settings.Ciphers)) - } - if len(settings.Auth) > 0 { - lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth) - } - - if settings.ConfFile != "" { - lines = append(lines, indent+lastIndent+"Configuration file: "+settings.ConfFile) - } - - if settings.ClientKey != "" { - lines = append(lines, indent+lastIndent+"Client key is set") - } - - if settings.ClientCrt != "" { - lines = append(lines, indent+lastIndent+"Client certificate is set") - } - - if settings.IPv6 { - lines = append(lines, indent+lastIndent+"IPv6: enabled") - } - - if settings.EncPreset != "" { // PIA only - lines = append(lines, indent+lastIndent+"Encryption preset: "+settings.EncPreset) - } - - return lines -} - -func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) { - credentialsRequired := false - switch serviceProvider { - case constants.Custom: - case constants.VPNUnlimited: - default: - credentialsRequired = true - } - - settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"}) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_USER: %w", err) - } - // Remove spaces in user ID to simplify user's life, thanks @JeordyR - settings.User = strings.ReplaceAll(settings.User, " ", "") - - if serviceProvider == constants.Mullvad { - settings.Password = "m" - } else { - settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", credentialsRequired, []string{"PASSWORD"}) - if err != nil { - return err - } - } - - settings.Version, err = r.env.Inside("OPENVPN_VERSION", - []string{constants.Openvpn24, constants.Openvpn25}, params.Default(constants.Openvpn25)) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_VERSION: %w", err) - } - - settings.Verbosity, err = r.env.IntRange("OPENVPN_VERBOSITY", 0, 6, params.Default("1")) //nolint:gomnd - if err != nil { - return fmt.Errorf("environment variable OPENVPN_VERBOSITY: %w", err) - } - - settings.Flags = []string{} - flagsStr, err := r.env.Get("OPENVPN_FLAGS") - if err != nil { - return fmt.Errorf("environment variable OPENVPN_FLAGS: %w", err) - } - if flagsStr != "" { - settings.Flags = strings.Fields(flagsStr) - } - - settings.Root, err = r.env.YesNo("OPENVPN_ROOT", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_ROOT: %w", err) - } - - settings.Ciphers, err = r.env.CSV("OPENVPN_CIPHER") - if err != nil { - return fmt.Errorf("environment variable OPENVPN_CIPHER: %w", err) - } - - settings.Auth, err = r.env.Get("OPENVPN_AUTH") - if err != nil { - return fmt.Errorf("environment variable OPENVPN_AUTH: %w", err) - } - - const maxMSSFix = 10000 - mssFix, err := r.env.IntRange("OPENVPN_MSSFIX", 0, maxMSSFix, params.Default("0")) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_MSSFIX: %w", err) - } - settings.MSSFix = uint16(mssFix) - - settings.IPv6, err = r.env.OnOff("OPENVPN_IPV6", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_IPV6: %w", err) - } - - settings.Interface, err = readInterface(r.env) - if err != nil { - return err - } - - switch serviceProvider { - case constants.Custom: - err = settings.readCustom(r) // read OPENVPN_CUSTOM_CONFIG - case constants.Cyberghost: - err = settings.readCyberghost(r) - case constants.PrivateInternetAccess: - settings.EncPreset, err = getPIAEncryptionPreset(r) - case constants.VPNUnlimited: - err = settings.readVPNUnlimited(r) - case constants.Wevpn: - err = settings.readWevpn(r) - } - if err != nil { - return err - } - - return nil -} - -func readOpenVPNProtocol(r reader) (tcp bool, err error) { - protocol, err := r.env.Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, - params.Default(constants.UDP), params.RetroKeys([]string{"PROTOCOL"}, r.onRetroActive)) - if err != nil { - return false, fmt.Errorf("environment variable OPENVPN_PROTOCOL: %w", err) - } - return protocol == constants.TCP, nil -} - -const openvpnIntfRegexString = `^.*[0-9]$` - -var openvpnIntfRegexp = regexp.MustCompile(openvpnIntfRegexString) -var errInterfaceNameNotValid = errors.New("interface name is not valid") - -func readInterface(env params.Interface) (intf string, err error) { - intf, err = env.Get("OPENVPN_INTERFACE", params.Default("tun0")) - if err != nil { - return "", fmt.Errorf("environment variable OPENVPN_INTERFACE: %w", err) - } - - if !openvpnIntfRegexp.MatchString(intf) { - return "", fmt.Errorf("%w: does not match regex %s: %s", - errInterfaceNameNotValid, openvpnIntfRegexString, intf) - } - - return intf, nil -} diff --git a/internal/configuration/openvpn_test.go b/internal/configuration/openvpn_test.go deleted file mode 100644 index f6f0af67..00000000 --- a/internal/configuration/openvpn_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package configuration - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_OpenVPN_JSON(t *testing.T) { - t.Parallel() - in := OpenVPN{ - Root: true, - Flags: []string{}, - Ciphers: []string{}, - } - data, err := json.MarshalIndent(in, "", " ") - require.NoError(t, err) - assert.Equal(t, `{ - "user": "", - "password": "", - "verbosity": 0, - "flags": [], - "mssfix": 0, - "run_as_root": true, - "ciphers": [], - "auth": "", - "conf_file": "", - "version": "", - "encryption_preset": "", - "ipv6": false, - "procuser": "", - "interface": "" -}`, string(data)) - var out OpenVPN - err = json.Unmarshal(data, &out) - require.NoError(t, err) - assert.Equal(t, in, out) -} diff --git a/internal/configuration/perfectprivacy.go b/internal/configuration/perfectprivacy.go deleted file mode 100644 index 51af4dcd..00000000 --- a/internal/configuration/perfectprivacy.go +++ /dev/null @@ -1,43 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readPerfectPrivacy(r reader) (err error) { - settings.Name = constants.Perfectprivacy - servers := r.servers.GetPerfectprivacy() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PerfectprivacyCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - return settings.ServerSelection.OpenVPN.readPerfectPrivacy(r) -} - -func (settings *OpenVPNSelection) readPerfectPrivacy(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - portValidation := openvpnPortValidation{ - tcp: settings.TCP, - allowedTCP: []uint16{44, 443, 4433}, - allowedUDP: []uint16{44, 443, 4433}, - } - settings.CustomPort, err = readOpenVPNCustomPort(r, portValidation) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/privado.go b/internal/configuration/privado.go deleted file mode 100644 index 52cff26e..00000000 --- a/internal/configuration/privado.go +++ /dev/null @@ -1,39 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readPrivado(r reader) (err error) { - settings.Name = constants.Privado - servers := r.servers.GetPrivado() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivadoCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PrivadoRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivadoCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PrivadoHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return nil -} diff --git a/internal/configuration/privateinternetaccess.go b/internal/configuration/privateinternetaccess.go deleted file mode 100644 index 9cc3d14c..00000000 --- a/internal/configuration/privateinternetaccess.go +++ /dev/null @@ -1,75 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readPrivateInternetAccess(r reader) (err error) { - settings.Name = constants.PrivateInternetAccess - servers := r.servers.GetPia() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PIAGeoChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PIAHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.PIANameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_NAME: %w", err) - } - - settings.PortForwarding.Enabled, err = r.env.OnOff("PORT_FORWARDING", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable PORT_FORWARDING: %w", err) - } - - if settings.PortForwarding.Enabled { - settings.PortForwarding.Filepath, err = r.env.Path("PORT_FORWARDING_STATUS_FILE", - params.Default("/tmp/gluetun/forwarded_port"), params.CaseSensitiveValue()) - if err != nil { - return fmt.Errorf("environment variable PORT_FORWARDING_STATUS_FILE: %w", err) - } - } - - return settings.ServerSelection.OpenVPN.readPrivateInternetAccess(r) -} - -func (settings *OpenVPNSelection) readPrivateInternetAccess(r reader) (err error) { - settings.EncPreset, err = getPIAEncryptionPreset(r) - if err != nil { - return err - } - - settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{allAllowed: true}) - if err != nil { - return err - } - - return nil -} - -func getPIAEncryptionPreset(r reader) (encryptionPreset string, err error) { - encryptionPreset, err = r.env.Inside("PIA_ENCRYPTION", - []string{constants.PIAEncryptionPresetNone, constants.PIAEncryptionPresetNormal, constants.PIAEncryptionPresetStrong}, - params.RetroKeys([]string{"ENCRYPTION"}, r.onRetroActive), - params.Default(constants.PIAEncryptionPresetStrong), - ) - if err != nil { - return "", fmt.Errorf("environment variable PIA_ENCRYPTION: %w", err) - } - - return encryptionPreset, nil -} diff --git a/internal/configuration/privatevpn.go b/internal/configuration/privatevpn.go deleted file mode 100644 index 3fb2adbb..00000000 --- a/internal/configuration/privatevpn.go +++ /dev/null @@ -1,35 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readPrivatevpn(r reader) (err error) { - settings.Name = constants.Privatevpn - servers := r.servers.GetPrivatevpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PrivatevpnCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PrivatevpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.PrivatevpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolAndPort(r) -} diff --git a/internal/configuration/protonvpn.go b/internal/configuration/protonvpn.go deleted file mode 100644 index 2f50ffc0..00000000 --- a/internal/configuration/protonvpn.go +++ /dev/null @@ -1,51 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readProtonvpn(r reader) (err error) { - settings.Name = constants.Protonvpn - servers := r.servers.GetProtonvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ProtonvpnCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.ProtonvpnRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ProtonvpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Names, err = r.env.CSVInside("SERVER_NAME", constants.ProtonvpnNameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_NAME: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.ProtonvpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.FreeOnly, err = r.env.YesNo("FREE_ONLY", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable FREE_ONLY: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolAndPort(r) -} diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go deleted file mode 100644 index d4bd2a4e..00000000 --- a/internal/configuration/provider.go +++ /dev/null @@ -1,248 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "net" - "strings" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -// Provider contains settings specific to a VPN provider. -type Provider struct { - Name string `json:"name"` - ServerSelection ServerSelection `json:"server_selection"` - PortForwarding PortForwarding `json:"port_forwarding"` -} - -func (settings *Provider) lines() (lines []string) { - if settings.Name == "" { // custom OpenVPN configuration - return nil - } - - lines = append(lines, lastIndent+strings.Title(settings.Name)+" settings:") - - for _, line := range settings.ServerSelection.toLines() { - lines = append(lines, indent+line) - } - - if settings.PortForwarding.Enabled { // PIA - lines = append(lines, indent+lastIndent+"Port forwarding:") - for _, line := range settings.PortForwarding.lines() { - lines = append(lines, indent+indent+line) - } - } - - return lines -} - -var ( - ErrInvalidVPNProvider = errors.New("invalid VPN provider") -) - -func (settings *Provider) read(r reader, vpnType string) error { - err := settings.readVPNServiceProvider(r, vpnType) - if err != nil { - return err - } - - switch settings.Name { - case constants.Custom: - err = settings.readCustom(r, vpnType) - case constants.Cyberghost: - err = settings.readCyberghost(r) - case constants.Expressvpn: - err = settings.readExpressvpn(r) - case constants.Fastestvpn: - err = settings.readFastestvpn(r) - case constants.HideMyAss: - err = settings.readHideMyAss(r) - case constants.Ipvanish: - err = settings.readIpvanish(r) - case constants.Ivpn: - err = settings.readIvpn(r) - case constants.Mullvad: - err = settings.readMullvad(r) - case constants.Nordvpn: - err = settings.readNordvpn(r) - case constants.Perfectprivacy: - err = settings.readPerfectPrivacy(r) - case constants.Privado: - err = settings.readPrivado(r) - case constants.PrivateInternetAccess: - err = settings.readPrivateInternetAccess(r) - case constants.Privatevpn: - err = settings.readPrivatevpn(r) - case constants.Protonvpn: - err = settings.readProtonvpn(r) - case constants.Purevpn: - err = settings.readPurevpn(r) - case constants.Surfshark: - err = settings.readSurfshark(r) - case constants.Torguard: - err = settings.readTorguard(r) - case constants.VPNUnlimited: - err = settings.readVPNUnlimited(r) - case constants.Vyprvpn: - err = settings.readVyprvpn(r) - case constants.Wevpn: - err = settings.readWevpn(r) - case constants.Windscribe: - err = settings.readWindscribe(r) - default: - return fmt.Errorf("%w: %s", ErrInvalidVPNProvider, settings.Name) - } - if err != nil { - return err - } - - settings.ServerSelection.VPN = vpnType - return nil -} - -func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err error) { - var allowedVPNServiceProviders []string - switch vpnType { - case constants.OpenVPN: - allowedVPNServiceProviders = []string{ - constants.Custom, - "cyberghost", constants.Expressvpn, "fastestvpn", "hidemyass", "ipvanish", - "ivpn", "mullvad", "nordvpn", - constants.Perfectprivacy, "privado", "pia", "private internet access", "privatevpn", "protonvpn", - "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", - constants.Wevpn, "windscribe"} - case constants.Wireguard: - allowedVPNServiceProviders = []string{ - constants.Custom, constants.Ivpn, - constants.Mullvad, constants.Windscribe, - } - } - - vpnsp, err := r.env.Inside("VPNSP", allowedVPNServiceProviders, - params.Default("private internet access")) - if err != nil { - return fmt.Errorf("environment variable VPNSP: %w", err) - } - if vpnsp == "pia" { // retro compatibility - vpnsp = "private internet access" - } - - if settings.isOpenVPNCustomConfig(r.env) { // retro compatibility - vpnsp = constants.Custom - } - - settings.Name = vpnsp - - return nil -} - -func commaJoin(slice []string) string { - return strings.Join(slice, ", ") -} - -func protoToString(tcp bool) string { - if tcp { - return constants.TCP - } - return constants.UDP -} - -func readTargetIP(env params.Interface) (targetIP net.IP, err error) { - targetIP, err = readIP(env, "OPENVPN_TARGET_IP") - if err != nil { - return nil, fmt.Errorf("environment variable OPENVPN_TARGET_IP: %w", err) - } - return targetIP, nil -} - -type openvpnPortValidation struct { - allAllowed bool - tcp bool - allowedTCP []uint16 - allowedUDP []uint16 -} - -func readOpenVPNCustomPort(r reader, validation openvpnPortValidation) ( - port uint16, err error) { - port, err = readPortOrZero(r.env, "OPENVPN_PORT") - if err != nil { - return 0, fmt.Errorf("environment variable OPENVPN_PORT: %w", err) - } else if port == 0 { - // Try using old variable name - port, err = readPortOrZero(r.env, "PORT") - if err != nil { - r.onRetroActive("PORT", "OPENVPN_PORT") - return 0, fmt.Errorf("environment variable PORT: %w", err) - } - } - - if port == 0 || validation.allAllowed { - return port, nil - } - - if validation.tcp { - for _, allowedPort := range validation.allowedTCP { - if port == allowedPort { - return port, nil - } - } - return 0, fmt.Errorf( - "environment variable PORT: %w: port %d for TCP protocol, can only be one of %s", - ErrInvalidPort, port, portsToString(validation.allowedTCP)) - } - for _, allowedPort := range validation.allowedUDP { - if port == allowedPort { - return port, nil - } - } - return 0, fmt.Errorf( - "environment variable PORT: %w: port %d for UDP protocol, can only be one of %s", - ErrInvalidPort, port, portsToString(validation.allowedUDP)) -} - -// note: set allowed to an empty slice to allow all valid ports -func readWireguardCustomPort(env params.Interface, allowed []uint16) (port uint16, err error) { - port, err = readPortOrZero(env, "WIREGUARD_ENDPOINT_PORT") - if err != nil { - return 0, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_PORT: %w", err) - } else if port == 0 { - port, _ = readPortOrZero(env, "WIREGUARD_PORT") - if err == nil { - return port, nil // 0 or WIREGUARD_PORT value - } - return 0, nil // default 0 - } - - if len(allowed) == 0 { - return port, nil - } - - for i := range allowed { - if allowed[i] == port { - return port, nil - } - } - - return 0, fmt.Errorf( - "environment variable WIREGUARD_PORT: %w: port %d, can only be one of %s", - ErrInvalidPort, port, portsToString(allowed)) -} - -func portsToString(ports []uint16) string { - slice := make([]string, len(ports)) - for i := range ports { - slice[i] = fmt.Sprint(ports[i]) - } - return strings.Join(slice, ", ") -} - -// isOpenVPNCustomConfig is for retro compatibility to set VPNSP=custom -// if OPENVPN_CUSTOM_CONFIG is set. -func (settings Provider) isOpenVPNCustomConfig(env params.Interface) (ok bool) { - s, _ := env.Get("VPN_TYPE") - isOpenVPN := s == constants.OpenVPN - s, _ = env.Get("OPENVPN_CUSTOM_CONFIG") - return isOpenVPN && s != "" -} diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go deleted file mode 100644 index 79b1782f..00000000 --- a/internal/configuration/provider_test.go +++ /dev/null @@ -1,462 +0,0 @@ -package configuration - -import ( - "errors" - "testing" - - "github.com/golang/mock/gomock" - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var errDummy = errors.New("dummy") - -func Test_Provider_lines(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - settings Provider - lines []string - }{ - "cyberghost": { - settings: Provider{ - Name: constants.Cyberghost, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "El country"}, - }, - }, - lines: []string{ - "|--Cyberghost settings:", - " |--Countries: a, El country", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "expressvpn": { - settings: Provider{ - Name: constants.Expressvpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Hostnames: []string{"a", "b"}, - Countries: []string{"c", "d"}, - Cities: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Expressvpn settings:", - " |--Countries: c, d", - " |--Cities: e, f", - " |--Hostnames: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "fastestvpn": { - settings: Provider{ - Name: constants.Fastestvpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Hostnames: []string{"a", "b"}, - Countries: []string{"c", "d"}, - }, - }, - lines: []string{ - "|--Fastestvpn settings:", - " |--Countries: c, d", - " |--Hostnames: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "hidemyass": { - settings: Provider{ - Name: constants.HideMyAss, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Hidemyass settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--Hostnames: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "ipvanish": { - settings: Provider{ - Name: constants.Ipvanish, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Ipvanish settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--Hostnames: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "ivpn": { - settings: Provider{ - Name: constants.Ivpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Ivpn settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--Hostnames: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "mullvad": { - settings: Provider{ - Name: constants.Mullvad, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - ISPs: []string{"e", "f"}, - OpenVPN: OpenVPNSelection{ - CustomPort: 1, - }, - }, - }, - lines: []string{ - "|--Mullvad settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--ISPs: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - " |--Custom port: 1", - }, - }, - "nordvpn": { - settings: Provider{ - Name: constants.Nordvpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - Numbers: []uint16{1, 2}, - }, - }, - lines: []string{ - "|--Nordvpn settings:", - " |--Regions: a, b", - " |--Numbers: 1, 2", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "perfectprivacy": { - settings: Provider{ - Name: constants.Perfectprivacy, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Cities: []string{"a", "b"}, - }, - }, - lines: []string{ - "|--Perfect Privacy settings:", - " |--Cities: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "privado": { - settings: Provider{ - Name: constants.Privado, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Hostnames: []string{"a", "b"}, - }, - }, - lines: []string{ - "|--Privado settings:", - " |--Hostnames: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "privatevpn": { - settings: Provider{ - Name: constants.Privatevpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Hostnames: []string{"a", "b"}, - Countries: []string{"c", "d"}, - Cities: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Privatevpn settings:", - " |--Countries: c, d", - " |--Cities: e, f", - " |--Hostnames: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "protonvpn": { - settings: Provider{ - Name: constants.Protonvpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Regions: []string{"c", "d"}, - Cities: []string{"e", "f"}, - Names: []string{"g", "h"}, - Hostnames: []string{"i", "j"}, - }, - }, - lines: []string{ - "|--Protonvpn settings:", - " |--Countries: a, b", - " |--Regions: c, d", - " |--Cities: e, f", - " |--Hostnames: i, j", - " |--Names: g, h", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "private internet access": { - settings: Provider{ - Name: constants.PrivateInternetAccess, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - OpenVPN: OpenVPNSelection{ - CustomPort: 1, - }, - }, - PortForwarding: PortForwarding{ - Enabled: true, - Filepath: string("/here"), - }, - }, - lines: []string{ - "|--Private Internet Access settings:", - " |--Regions: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - " |--Custom port: 1", - " |--Port forwarding:", - " |--File path: /here", - }, - }, - "purevpn": { - settings: Provider{ - Name: constants.Purevpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - Countries: []string{"c", "d"}, - Cities: []string{"e", "f"}, - }, - }, - lines: []string{ - "|--Purevpn settings:", - " |--Countries: c, d", - " |--Regions: a, b", - " |--Cities: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "surfshark": { - settings: Provider{ - Name: constants.Surfshark, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - }, - }, - lines: []string{ - "|--Surfshark settings:", - " |--Regions: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "torguard": { - settings: Provider{ - Name: constants.Torguard, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e"}, - }, - }, - lines: []string{ - "|--Torguard settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--Hostnames: e", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - constants.VPNUnlimited: { - settings: Provider{ - Name: constants.VPNUnlimited, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Countries: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e", "f"}, - FreeOnly: true, - StreamOnly: true, - }, - }, - lines: []string{ - "|--Vpn Unlimited settings:", - " |--Countries: a, b", - " |--Cities: c, d", - " |--Free servers only", - " |--Stream servers only", - " |--Hostnames: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "vyprvpn": { - settings: Provider{ - Name: constants.Vyprvpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - }, - }, - lines: []string{ - "|--Vyprvpn settings:", - " |--Regions: a, b", - " |--OpenVPN selection:", - " |--Protocol: udp", - }, - }, - "wevpn": { - settings: Provider{ - Name: constants.Wevpn, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Cities: []string{"a", "b"}, - Hostnames: []string{"c", "d"}, - OpenVPN: OpenVPNSelection{ - CustomPort: 1, - }, - }, - }, - lines: []string{ - "|--Wevpn settings:", - " |--Cities: a, b", - " |--Hostnames: c, d", - " |--OpenVPN selection:", - " |--Protocol: udp", - " |--Custom port: 1", - }, - }, - "windscribe": { - settings: Provider{ - Name: constants.Windscribe, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - Regions: []string{"a", "b"}, - Cities: []string{"c", "d"}, - Hostnames: []string{"e", "f"}, - OpenVPN: OpenVPNSelection{ - CustomPort: 1, - }, - }, - }, - lines: []string{ - "|--Windscribe settings:", - " |--Regions: a, b", - " |--Cities: c, d", - " |--Hostnames: e, f", - " |--OpenVPN selection:", - " |--Protocol: udp", - " |--Custom port: 1", - }, - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - lines := testCase.settings.lines() - - assert.Equal(t, testCase.lines, lines) - }) - } -} - -func Test_readProtocol(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - mockStr string - mockErr error - tcp bool - err error - }{ - "error": { - mockErr: errDummy, - err: errors.New("environment variable OPENVPN_PROTOCOL: dummy"), - }, - "success": { - mockStr: "tcp", - tcp: true, - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - - env := mock_params.NewMockInterface(ctrl) - env.EXPECT(). - Inside("OPENVPN_PROTOCOL", []string{"tcp", "udp"}, gomock.Any(), gomock.Any()). - Return(testCase.mockStr, testCase.mockErr) - reader := reader{ - env: env, - } - - tcp, err := readOpenVPNProtocol(reader) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.tcp, tcp) - }) - } -} diff --git a/internal/configuration/publicip.go b/internal/configuration/publicip.go deleted file mode 100644 index c934b46c..00000000 --- a/internal/configuration/publicip.go +++ /dev/null @@ -1,47 +0,0 @@ -package configuration - -import ( - "fmt" - "strings" - "time" - - "github.com/qdm12/golibs/params" -) - -type PublicIP struct { - Period time.Duration `json:"period"` - IPFilepath string `json:"ip_filepath"` -} - -func (settings *PublicIP) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *PublicIP) lines() (lines []string) { - if settings.Period == 0 { - lines = append(lines, lastIndent+"Public IP getter: disabled") - return lines - } - - lines = append(lines, lastIndent+"Public IP getter:") - lines = append(lines, indent+lastIndent+"Fetch period: "+settings.Period.String()) - lines = append(lines, indent+lastIndent+"IP file: "+settings.IPFilepath) - - return lines -} - -func (settings *PublicIP) read(r reader) (err error) { - settings.Period, err = r.env.Duration("PUBLICIP_PERIOD", params.Default("12h")) - if err != nil { - return fmt.Errorf("environment variable PUBLICIP_PERIOD: %w", err) - } - - settings.IPFilepath, err = r.env.Path("PUBLICIP_FILE", params.CaseSensitiveValue(), - params.Default("/tmp/gluetun/ip"), - params.RetroKeys([]string{"IP_STATUS_FILE"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable PUBLICIP_FILE (or IP_STATUS_FILE): %w", err) - } - - return nil -} diff --git a/internal/configuration/purevpn.go b/internal/configuration/purevpn.go deleted file mode 100644 index 49ae1af7..00000000 --- a/internal/configuration/purevpn.go +++ /dev/null @@ -1,39 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readPurevpn(r reader) (err error) { - settings.Name = constants.Purevpn - servers := r.servers.GetPurevpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.PurevpnRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.PurevpnCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PurevpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.PurevpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} diff --git a/internal/configuration/reader.go b/internal/configuration/reader.go deleted file mode 100644 index aa4fb5ab..00000000 --- a/internal/configuration/reader.go +++ /dev/null @@ -1,134 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "net" - "strconv" - "strings" - - "github.com/qdm12/gluetun/internal/models" - ovpnextract "github.com/qdm12/gluetun/internal/openvpn/extract" - "github.com/qdm12/golibs/params" - "github.com/qdm12/golibs/verification" -) - -//go:generate mockgen -destination=warner_mock_test.go -package configuration . Warner - -type reader struct { - servers models.AllServers - env params.Interface - warner Warner - regex verification.Regex - ovpnExt ovpnextract.Interface -} - -type Warner interface { - Warn(s string) -} - -func newReader(env params.Interface, - servers models.AllServers, warner Warner) reader { - return reader{ - servers: servers, - env: env, - warner: warner, - regex: verification.NewRegex(), - ovpnExt: ovpnextract.New(), - } -} - -func (r *reader) onRetroActive(oldKey, newKey string) { - r.warner.Warn( - "You are using the old environment variable " + oldKey + - ", please consider changing it to " + newKey) -} - -var ( - ErrInvalidPort = errors.New("invalid port") -) - -func readCSVPorts(env params.Interface, key string) (ports []uint16, err error) { - s, err := env.Get(key) - if err != nil { - return nil, err - } else if s == "" { - return nil, nil - } - - portsStr := strings.Split(s, ",") - ports = make([]uint16, len(portsStr)) - for i, portStr := range portsStr { - portInt, err := strconv.Atoi(portStr) - if err != nil { - return nil, fmt.Errorf("%w: %s: %s", ErrInvalidPort, portStr, err) - } else if portInt <= 0 || portInt > 65535 { - return nil, fmt.Errorf("%w: %d: must be between 1 and 65535", ErrInvalidPort, portInt) - } - ports[i] = uint16(portInt) - } - - return ports, nil -} - -var ( - ErrInvalidIPNet = errors.New("invalid IP network") -) - -func readCSVIPNets(env params.Interface, key string, options ...params.OptionSetter) ( - ipNets []net.IPNet, err error) { - s, err := env.Get(key, options...) - if err != nil { - return nil, err - } else if s == "" { - return nil, nil - } - - ipNetsStr := strings.Split(s, ",") - ipNets = make([]net.IPNet, len(ipNetsStr)) - for i, ipNetStr := range ipNetsStr { - _, ipNet, err := net.ParseCIDR(ipNetStr) - if err != nil { - return nil, fmt.Errorf("%w: %s: %s", - ErrInvalidIPNet, ipNetStr, err) - } else if ipNet == nil { - return nil, fmt.Errorf("%w: %s: subnet is nil", ErrInvalidIPNet, ipNetStr) - } - ipNets[i] = *ipNet - } - - return ipNets, nil -} - -var ( - ErrInvalidIP = errors.New("invalid IP address") -) - -func readIP(env params.Interface, key string) (ip net.IP, err error) { - s, err := env.Get(key) - if s == "" { - return nil, nil - } else if err != nil { - return nil, err - } - - ip = net.ParseIP(s) - if ip == nil { - return nil, fmt.Errorf("%w: %s", ErrInvalidIP, s) - } - - return ip, nil -} - -func readPortOrZero(env params.Interface, key string) (port uint16, err error) { - s, err := env.Get(key, params.Default("0")) - if err != nil { - return 0, err - } - - if s == "0" { - return 0, nil - } - - return env.Port(key) -} diff --git a/internal/configuration/secrets.go b/internal/configuration/secrets.go deleted file mode 100644 index 5c8118d4..00000000 --- a/internal/configuration/secrets.go +++ /dev/null @@ -1,119 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/qdm12/golibs/params" -) - -var ( - ErrGetSecretFilepath = errors.New("cannot get secret file path from env") - ErrReadSecretFile = errors.New("cannot read secret file") - ErrSecretFileIsEmpty = errors.New("secret file is empty") - ErrReadNonSecretFile = errors.New("cannot read non secret file") - ErrFilesDoNotExist = errors.New("files do not exist") -) - -func cleanSuffix(value string) string { - value = strings.TrimSuffix(value, "\n") - value = strings.TrimSuffix(value, "\r") - return value -} - -func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKeys []string) (value string, err error) { - envOptions := []params.OptionSetter{ - params.Compulsory(), // to fallback on file reading - params.CaseSensitiveValue(), - params.Unset(), - params.RetroKeys(retroKeys, r.onRetroActive), - } - value, envErr := r.env.Get(envKey, envOptions...) - if envErr == nil { - value = cleanSuffix(value) - return value, nil - } - - secretFilepathEnvKey := envKey + "_SECRETFILE" - defaultSecretFile := "/run/secrets/" + strings.ToLower(envKey) - filepath, err := r.env.Get(secretFilepathEnvKey, - params.CaseSensitiveValue(), - params.Default(defaultSecretFile), - ) - if err != nil { - return "", fmt.Errorf("%w: environment variable %s: %s", - ErrGetSecretFilepath, secretFilepathEnvKey, err) - } - - file, fileErr := os.OpenFile(filepath, os.O_RDONLY, 0) - if os.IsNotExist(fileErr) { - if compulsory { - return "", fmt.Errorf("environment variable %s: %w", envKey, envErr) - } - return "", nil - } else if fileErr != nil { - return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, fileErr) - } - - b, err := io.ReadAll(file) - if err != nil { - return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, err) - } - - value = string(b) - value = cleanSuffix(value) - if compulsory && value == "" { - return "", fmt.Errorf("%s: %w", filepath, ErrSecretFileIsEmpty) - } - - return value, nil -} - -// Tries to read from the secret file then the non secret file. -func (r *reader) getFromFileOrSecretFile(secretName, filepath string) ( - b []byte, err error) { - defaultSecretFile := "/run/secrets/" + strings.ToLower(secretName) - key := strings.ToUpper(secretName) + "_SECRETFILE" - secretFilepath, err := r.env.Get(key, - params.CaseSensitiveValue(), - params.Default(defaultSecretFile), - ) - if err != nil { - return b, fmt.Errorf("environment variable %s: %w: %s", key, ErrGetSecretFilepath, err) - } - - b, err = readFromFile(secretFilepath) - if err != nil && !os.IsNotExist(err) { - return b, fmt.Errorf("%w: %s", ErrReadSecretFile, err) - } else if err == nil { - return b, nil - } - - // Secret file does not exist, try the non secret file - b, err = readFromFile(filepath) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("%w: %s", ErrReadSecretFile, err) - } else if err == nil { - return b, nil - } - return nil, fmt.Errorf("%w: %s and %s", ErrFilesDoNotExist, secretFilepath, filepath) -} - -func readFromFile(filepath string) (b []byte, err error) { - file, err := os.Open(filepath) - if err != nil { - return nil, err - } - b, err = io.ReadAll(file) - if err != nil { - _ = file.Close() - return nil, err - } - if err := file.Close(); err != nil { - return nil, err - } - return b, nil -} diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go deleted file mode 100644 index eaaa8d38..00000000 --- a/internal/configuration/selection.go +++ /dev/null @@ -1,189 +0,0 @@ -package configuration - -import ( - "fmt" - "net" - - "github.com/qdm12/gluetun/internal/constants" -) - -type ServerSelection struct { //nolint:maligned - // Common - VPN string `json:"vpn"` // note: this is required - TargetIP net.IP `json:"target_ip,omitempty"` - // TODO comments - // Cyberghost, PIA, Protonvpn, Surfshark, Windscribe, Vyprvpn, NordVPN - Regions []string `json:"regions"` - - // Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited - Countries []string `json:"countries"` - // Expressvpn, HideMyAss, IPVanish, IVPN, Mullvad, Perfectprivacy, PrivateVPN, Protonvpn, - // PureVPN, VPNUnlimited, WeVPN, Windscribe - Cities []string `json:"cities"` - // Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited, WeVPN - Hostnames []string `json:"hostnames"` - Names []string `json:"names"` // Protonvpn - - // Mullvad - ISPs []string `json:"isps"` - Owned bool `json:"owned"` - - // NordVPN - Numbers []uint16 `json:"numbers"` - - // ProtonVPN - FreeOnly bool `json:"free_only"` - - // VPNUnlimited - StreamOnly bool `json:"stream_only"` - - // Surfshark - MultiHopOnly bool `json:"multihop_only"` - - OpenVPN OpenVPNSelection `json:"openvpn"` - Wireguard WireguardSelection `json:"wireguard"` -} - -func (selection ServerSelection) toLines() (lines []string) { - if selection.TargetIP != nil { - lines = append(lines, lastIndent+"Target IP address: "+selection.TargetIP.String()) - } - - if len(selection.Countries) > 0 { - lines = append(lines, lastIndent+"Countries: "+commaJoin(selection.Countries)) - } - - if len(selection.Regions) > 0 { - lines = append(lines, lastIndent+"Regions: "+commaJoin(selection.Regions)) - } - - if len(selection.Cities) > 0 { - lines = append(lines, lastIndent+"Cities: "+commaJoin(selection.Cities)) - } - - if len(selection.ISPs) > 0 { - lines = append(lines, lastIndent+"ISPs: "+commaJoin(selection.ISPs)) - } - - if selection.FreeOnly { - lines = append(lines, lastIndent+"Free servers only") - } - - if selection.StreamOnly { - lines = append(lines, lastIndent+"Stream servers only") - } - - if len(selection.Hostnames) > 0 { - lines = append(lines, lastIndent+"Hostnames: "+commaJoin(selection.Hostnames)) - } - - if len(selection.Names) > 0 { - lines = append(lines, lastIndent+"Names: "+commaJoin(selection.Names)) - } - - if len(selection.Numbers) > 0 { - numbersString := make([]string, len(selection.Numbers)) - for i, numberUint16 := range selection.Numbers { - numbersString[i] = fmt.Sprint(numberUint16) - } - lines = append(lines, lastIndent+"Numbers: "+commaJoin(numbersString)) - } - - if selection.VPN == constants.OpenVPN { - lines = append(lines, selection.OpenVPN.lines()...) - } else { // wireguard - lines = append(lines, selection.Wireguard.lines()...) - } - - return lines -} - -type OpenVPNSelection struct { - ConfFile string `json:"conf_file"` // Custom configuration file path - TCP bool `json:"tcp"` // UDP if TCP is false - CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe - EncPreset string `json:"encryption_preset"` // PIA - needed to get the port number -} - -func (settings *OpenVPNSelection) lines() (lines []string) { - lines = append(lines, lastIndent+"OpenVPN selection:") - - if settings.ConfFile != "" { - lines = append(lines, indent+lastIndent+"Custom configuration file: "+settings.ConfFile) - } - - lines = append(lines, indent+lastIndent+"Protocol: "+protoToString(settings.TCP)) - - if settings.CustomPort != 0 { - lines = append(lines, indent+lastIndent+"Custom port: "+fmt.Sprint(settings.CustomPort)) - } - - if settings.EncPreset != "" { - lines = append(lines, indent+lastIndent+"PIA encryption preset: "+settings.EncPreset) - } - - return lines -} - -func (settings *OpenVPNSelection) readProtocolOnly(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - return err -} - -func (settings *OpenVPNSelection) readProtocolAndPort(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{allAllowed: true}) - if err != nil { - return err - } - - return nil -} - -type WireguardSelection struct { - // EndpointPort is a the server port to use for the VPN server. - // It is optional for Wireguard VPN providers IVPN, Mullvad - // and Windscribe, and compulsory for the others - EndpointPort uint16 `json:"port,omitempty"` - // PublicKey is the server public key. - // It is only used with VPN providers generating Wireguard - // configurations specific to each server and user. - PublicKey string `json:"publickey,omitempty"` - // EndpointIP is the server endpoint IP address. - // It is only used with VPN providers generating Wireguard - // configurations specific to each server and user. - EndpointIP net.IP `json:"endpoint_ip,omitempty"` -} - -func (settings *WireguardSelection) lines() (lines []string) { - lines = append(lines, lastIndent+"Wireguard selection:") - - if settings.PublicKey != "" { - lines = append(lines, indent+lastIndent+"Public key: "+settings.PublicKey) - } - - if settings.EndpointIP != nil { - endpoint := settings.EndpointIP.String() + ":" + fmt.Sprint(settings.EndpointPort) - lines = append(lines, indent+lastIndent+"Server endpoint: "+endpoint) - } else if settings.EndpointPort != 0 { - lines = append(lines, indent+lastIndent+"Custom port: "+fmt.Sprint(settings.EndpointPort)) - } - - return lines -} - -// PortForwarding contains settings for port forwarding. -type PortForwarding struct { - Enabled bool `json:"enabled"` - Filepath string `json:"filepath"` -} - -func (p *PortForwarding) lines() (lines []string) { - return []string{ - lastIndent + "File path: " + p.Filepath, - } -} diff --git a/internal/configuration/server.go b/internal/configuration/server.go deleted file mode 100644 index 925a070d..00000000 --- a/internal/configuration/server.go +++ /dev/null @@ -1,50 +0,0 @@ -package configuration - -import ( - "fmt" - "strconv" - "strings" - - "github.com/qdm12/golibs/params" -) - -// ControlServer contains settings to customize the control server operation. -type ControlServer struct { - Port uint16 - Log bool -} - -func (settings *ControlServer) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *ControlServer) lines() (lines []string) { - lines = append(lines, lastIndent+"HTTP control server:") - - lines = append(lines, indent+lastIndent+"Listening port: "+strconv.Itoa(int(settings.Port))) - - if settings.Log { - lines = append(lines, indent+lastIndent+"Logging: enabled") - } - - return lines -} - -func (settings *ControlServer) read(r reader) (err error) { - settings.Log, err = r.env.OnOff("HTTP_CONTROL_SERVER_LOG", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable HTTP_CONTROL_SERVER_LOG: %w", err) - } - - var warning string - settings.Port, warning, err = r.env.ListeningPort( - "HTTP_CONTROL_SERVER_PORT", params.Default("8000")) - if len(warning) > 0 { - r.warner.Warn(warning) - } - if err != nil { - return fmt.Errorf("environment variable HTTP_CONTROL_SERVER_PORT: %w", err) - } - - return nil -} diff --git a/internal/configuration/settings.go b/internal/configuration/settings.go deleted file mode 100644 index f98476b1..00000000 --- a/internal/configuration/settings.go +++ /dev/null @@ -1,125 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "strings" - - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params" -) - -// Settings contains all settings for the program to run. -type Settings struct { - VPN VPN - System System - DNS DNS - Firewall Firewall - HTTPProxy HTTPProxy - ShadowSocks ShadowSocks - Updater Updater - PublicIP PublicIP - VersionInformation bool - ControlServer ControlServer - Health Health - Log Log -} - -func (settings *Settings) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *Settings) lines() (lines []string) { - lines = append(lines, "Settings summary below:") - lines = append(lines, settings.VPN.lines()...) - lines = append(lines, settings.DNS.lines()...) - lines = append(lines, settings.Firewall.lines()...) - lines = append(lines, settings.Log.lines()...) - lines = append(lines, settings.System.lines()...) - lines = append(lines, settings.HTTPProxy.lines()...) - lines = append(lines, settings.ShadowSocks.lines()...) - lines = append(lines, settings.Health.lines()...) - lines = append(lines, settings.ControlServer.lines()...) - lines = append(lines, settings.Updater.lines()...) - lines = append(lines, settings.PublicIP.lines()...) - if settings.VersionInformation { - lines = append(lines, lastIndent+"Github version information: enabled") - } - return lines -} - -var ( - ErrVPN = errors.New("cannot read VPN settings") - ErrSystem = errors.New("cannot read System settings") - ErrDNS = errors.New("cannot read DNS settings") - ErrFirewall = errors.New("cannot read firewall settings") - ErrHTTPProxy = errors.New("cannot read HTTP proxy settings") - ErrShadowsocks = errors.New("cannot read Shadowsocks settings") - ErrControlServer = errors.New("cannot read control server settings") - ErrUpdater = errors.New("cannot read Updater settings") - ErrPublicIP = errors.New("cannot read Public IP getter settings") - ErrHealth = errors.New("cannot read health settings") - ErrLog = errors.New("cannot read log settings") -) - -// Read obtains all configuration options for the program and returns an error as soon -// as an error is encountered reading them. -func (settings *Settings) Read(env params.Interface, servers models.AllServers, - warner Warner) (err error) { - r := newReader(env, servers, warner) - - settings.VersionInformation, err = r.env.OnOff("VERSION_INFORMATION", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable VERSION_INFORMATION: %w", err) - } - - if err := settings.VPN.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrVPN, err) - } - - if err := settings.System.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrSystem, err) - } - - if err := settings.DNS.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrDNS, err) - } - - if err := settings.Firewall.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrFirewall, err) - } - - if err := settings.HTTPProxy.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrHTTPProxy, err) - } - - if err := settings.ShadowSocks.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrShadowsocks, err) - } - - if err := settings.ControlServer.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrControlServer, err) - } - - if err := settings.Updater.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrUpdater, err) - } - - if ip := settings.DNS.PlaintextAddress; ip != nil { - settings.Updater.DNSAddress = ip.String() - } - - if err := settings.PublicIP.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrPublicIP, err) - } - - if err := settings.Health.read(r); err != nil { - return fmt.Errorf("%w: %s", ErrHealth, err) - } - - if err := settings.Log.read(r.env); err != nil { - return fmt.Errorf("%w: %s", ErrLog, err) - } - - return nil -} diff --git a/internal/configuration/settings/dns.go b/internal/configuration/settings/dns.go new file mode 100644 index 00000000..07a74330 --- /dev/null +++ b/internal/configuration/settings/dns.go @@ -0,0 +1,82 @@ +package settings + +import ( + "fmt" + "net" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// DNS contains settings to configure DNS. +type DNS struct { + // ServerAddress is the DNS server to use inside + // the Go program and for the system. + // It defaults to '127.0.0.1' to be used with the + // DoT server. It cannot be nil in the internal + // state. + ServerAddress net.IP + // KeepNameserver is true if the Docker DNS server + // found in /etc/resolv.conf should be kept. + // Note settings this to true will go around the + // DoT server blocking. + // It defaults to false and cannot be nil in the + // internal state. + KeepNameserver *bool + // DOT contains settings to configure the DoT + // server. + DoT DoT +} + +func (d DNS) validate() (err error) { + err = d.DoT.validate() + if err != nil { + return fmt.Errorf("failed validating DoT settings: %w", err) + } + + return nil +} + +func (d *DNS) Copy() (copied DNS) { + return DNS{ + ServerAddress: helpers.CopyIP(d.ServerAddress), + KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver), + DoT: d.DoT.copy(), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (d *DNS) mergeWith(other DNS) { + d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress) + d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver) + d.DoT.mergeWith(other.DoT) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (d *DNS) overrideWith(other DNS) { + d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress) + d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver) + d.DoT.overrideWith(other.DoT) +} + +func (d *DNS) setDefaults() { + localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd + d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost) + d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false) + d.DoT.setDefaults() +} + +func (d DNS) String() string { + return d.toLinesNode().String() +} + +func (d DNS) toLinesNode() (node *gotree.Node) { + node = gotree.New("DNS settings:") + node.Appendf("DNS server address to use: %s", d.ServerAddress) + node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver)) + node.AppendNode(d.DoT.toLinesNode()) + return node +} diff --git a/internal/configuration/settings/dnsblacklist.go b/internal/configuration/settings/dnsblacklist.go new file mode 100644 index 00000000..7a892a5e --- /dev/null +++ b/internal/configuration/settings/dnsblacklist.go @@ -0,0 +1,138 @@ +package settings + +import ( + "errors" + "fmt" + "regexp" + + "github.com/qdm12/dns/pkg/blacklist" + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" + "inet.af/netaddr" +) + +// DNSBlacklist is settings for the DNS blacklist building. +type DNSBlacklist struct { + BlockMalicious *bool + BlockAds *bool + BlockSurveillance *bool + AllowedHosts []string + AddBlockedHosts []string + AddBlockedIPs []netaddr.IP + AddBlockedIPPrefixes []netaddr.IPPrefix +} + +func (b *DNSBlacklist) setDefaults() { + b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true) + b.BlockAds = helpers.DefaultBool(b.BlockAds, false) + b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true) +} + +var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll + +var ( + ErrAllowedHostNotValid = errors.New("allowed host is not valid") + ErrBlockedHostNotValid = errors.New("blocked host is not valid") +) + +func (b DNSBlacklist) validate() (err error) { + for _, host := range b.AllowedHosts { + if !hostRegex.MatchString(host) { + return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host) + } + } + + for _, host := range b.AddBlockedHosts { + if !hostRegex.MatchString(host) { + return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host) + } + } + + return nil +} + +func (b DNSBlacklist) copy() (copied DNSBlacklist) { + return DNSBlacklist{ + BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious), + BlockAds: helpers.CopyBoolPtr(b.BlockAds), + BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance), + AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts), + AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts), + AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs), + AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes), + } +} + +func (b *DNSBlacklist) mergeWith(other DNSBlacklist) { + b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious) + b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds) + b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance) + b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts) + b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts) + b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs) + b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) +} + +func (b *DNSBlacklist) overrideWith(other DNSBlacklist) { + b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious) + b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds) + b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance) + b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts) + b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts) + b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs) + b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes) +} + +func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) { + return blacklist.BuilderSettings{ + BlockMalicious: *b.BlockMalicious, + BlockAds: *b.BlockAds, + BlockSurveillance: *b.BlockSurveillance, + AllowedHosts: b.AllowedHosts, + AddBlockedHosts: b.AddBlockedHosts, + AddBlockedIPs: b.AddBlockedIPs, + AddBlockedIPPrefixes: b.AddBlockedIPPrefixes, + }, nil +} + +func (b DNSBlacklist) String() string { + return b.toLinesNode().String() +} + +func (b DNSBlacklist) toLinesNode() (node *gotree.Node) { + node = gotree.New("DNS filtering settings:") + + node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious)) + node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds)) + node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance)) + + if len(b.AllowedHosts) > 0 { + allowedHostsNode := node.Appendf("Allowed hosts:") + for _, host := range b.AllowedHosts { + allowedHostsNode.Appendf(host) + } + } + + if len(b.AddBlockedHosts) > 0 { + blockedHostsNode := node.Appendf("Blocked hosts:") + for _, host := range b.AddBlockedHosts { + blockedHostsNode.Appendf(host) + } + } + + if len(b.AddBlockedIPs) > 0 { + blockedIPsNode := node.Appendf("Blocked IP addresses:") + for _, ip := range b.AddBlockedIPs { + blockedIPsNode.Appendf(ip.String()) + } + } + + if len(b.AddBlockedIPPrefixes) > 0 { + blockedIPPrefixesNode := node.Appendf("Blocked IP networks:") + for _, ipNetwork := range b.AddBlockedIPPrefixes { + blockedIPPrefixesNode.Appendf(ipNetwork.String()) + } + } + + return node +} diff --git a/internal/configuration/settings/dot.go b/internal/configuration/settings/dot.go new file mode 100644 index 00000000..d432b96a --- /dev/null +++ b/internal/configuration/settings/dot.go @@ -0,0 +1,113 @@ +package settings + +import ( + "errors" + "fmt" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// DoT contains settings to configure the DoT server. +type DoT struct { + // Enabled is true if the DoT server should be running + // and used. It defaults to true, and cannot be nil + // in the internal state. + Enabled *bool + // UpdatePeriod is the period to update DNS block + // lists and cryptographic files for DNSSEC validation. + // It can be set to 0 to disable the update. + // It defaults to 24h and cannot be nil in + // the internal state. + UpdatePeriod *time.Duration + // Unbound contains settings to configure Unbound. + Unbound Unbound + // Blacklist contains settings to configure the filter + // block lists. + Blacklist DNSBlacklist +} + +var ( + ErrDoTUpdatePeriodTooShort = errors.New("update period is too short") +) + +func (d DoT) validate() (err error) { + const minUpdatePeriod = 30 * time.Second + if *d.UpdatePeriod < minUpdatePeriod { + return fmt.Errorf("%w: %s must be bigger than %s", + ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) + } + + err = d.Unbound.validate() + if err != nil { + return err + } + + err = d.Blacklist.validate() + if err != nil { + return err + } + + return nil +} + +func (d *DoT) copy() (copied DoT) { + return DoT{ + Enabled: helpers.CopyBoolPtr(d.Enabled), + UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod), + Unbound: d.Unbound.copy(), + Blacklist: d.Blacklist.copy(), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (d *DoT) mergeWith(other DoT) { + d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled) + d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod) + d.Unbound.mergeWith(other.Unbound) + d.Blacklist.mergeWith(other.Blacklist) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (d *DoT) overrideWith(other DoT) { + d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled) + d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod) + d.Unbound.overrideWith(other.Unbound) + d.Blacklist.overrideWith(other.Blacklist) +} + +func (d *DoT) setDefaults() { + d.Enabled = helpers.DefaultBool(d.Enabled, true) + const defaultUpdatePeriod = 24 * time.Hour + d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod) + d.Unbound.setDefaults() + d.Blacklist.setDefaults() +} + +func (d DoT) String() string { + return d.toLinesNode().String() +} + +func (d DoT) toLinesNode() (node *gotree.Node) { + node = gotree.New("DNS over TLS settings:") + + node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled)) + if !*d.Enabled { + return node + } + + update := "disabled" + if *d.UpdatePeriod > 0 { + update = "every " + d.UpdatePeriod.String() + } + node.Appendf("Update period: %s", update) + + node.AppendNode(d.Unbound.toLinesNode()) + node.AppendNode(d.Blacklist.toLinesNode()) + + return node +} diff --git a/internal/configuration/settings/errors.go b/internal/configuration/settings/errors.go new file mode 100644 index 00000000..e93b2889 --- /dev/null +++ b/internal/configuration/settings/errors.go @@ -0,0 +1,51 @@ +package settings + +import "errors" + +var ( + ErrCityNotValid = errors.New("the city specified is not valid") + ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root") + ErrCountryNotValid = errors.New("the country specified is not valid") + ErrFirewallZeroPort = errors.New("cannot have a zero port to block") + ErrHostnameNotValid = errors.New("the hostname specified is not valid") + ErrISPNotValid = errors.New("the ISP specified is not valid") + ErrNameNotValid = errors.New("the server name specified is not valid") + ErrOpenVPNClientCertMissing = errors.New("client certificate is missing") + ErrOpenVPNClientCertNotValid = errors.New("client certificate is not valid") + ErrOpenVPNClientKeyMissing = errors.New("client key is missing") + ErrOpenVPNClientKeyNotValid = errors.New("client key is not valid") + ErrOpenVPNConfigFile = errors.New("custom configuration file error") + ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed") + ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid") + ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid") + ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high") + ErrOpenVPNPasswordIsEmpty = errors.New("password is empty") + ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported") + ErrOpenVPNUserIsEmpty = errors.New("user is empty") + ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds") + ErrOpenVPNVersionIsNotValid = errors.New("version is not valid") + ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled") + ErrPortForwardingFilepathNotValid = errors.New("port forwarding filepath given is not valid") + ErrPublicIPFilepathNotValid = errors.New("public IP address file path is not valid") + ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short") + ErrRegionNotValid = errors.New("the region specified is not valid") + ErrServerAddressNotValid = errors.New("server listening address is not valid") + ErrSystemPGIDNotValid = errors.New("process group id is not valid") + ErrSystemPUIDNotValid = errors.New("process user id is not valid") + ErrSystemTimezoneNotValid = errors.New("timezone is not valid") + ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid") + ErrVPNTypeNotValid = errors.New("VPN type is not valid") + ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set") + ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed") + ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set") + ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set") + ErrWireguardInterfaceNotValid = errors.New("interface name is not valid") + ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set") + ErrWireguardPreSharedKeyNotValid = errors.New("pre-shared key is not valid") + ErrWireguardPrivateKeyNotSet = errors.New("private key is not set") + ErrWireguardPrivateKeyNotValid = errors.New("private key is not valid") + ErrWireguardPublicKeyNotSet = errors.New("public key is not set") + ErrWireguardPublicKeyNotValid = errors.New("public key is not valid") + + ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small") +) diff --git a/internal/configuration/settings/firewall.go b/internal/configuration/settings/firewall.go new file mode 100644 index 00000000..d0edbf4d --- /dev/null +++ b/internal/configuration/settings/firewall.go @@ -0,0 +1,117 @@ +package settings + +import ( + "fmt" + "net" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// Firewall contains settings to customize the firewall operation. +type Firewall struct { + VPNInputPorts []uint16 + InputPorts []uint16 + OutboundSubnets []net.IPNet + Enabled *bool + Debug *bool +} + +func (f Firewall) validate() (err error) { + if hasZeroPort(f.VPNInputPorts) { + return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort) + } + + if hasZeroPort(f.InputPorts) { + return fmt.Errorf("input ports: %w", ErrFirewallZeroPort) + } + + return nil +} + +func hasZeroPort(ports []uint16) (has bool) { + for _, port := range ports { + if port == 0 { + return true + } + } + return false +} + +func (f *Firewall) copy() (copied Firewall) { + return Firewall{ + VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts), + InputPorts: helpers.CopyUint16Slice(f.InputPorts), + OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets), + Enabled: helpers.CopyBoolPtr(f.Enabled), + Debug: helpers.CopyBoolPtr(f.Debug), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +// It merges values of slices together, even if they +// are set in the receiver settings. +func (f *Firewall) mergeWith(other Firewall) { + f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts) + f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts) + f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets) + f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled) + f.Debug = helpers.MergeWithBool(f.Debug, other.Debug) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (f *Firewall) overrideWith(other Firewall) { + f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts) + f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts) + f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets) + f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled) + f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug) +} + +func (f *Firewall) setDefaults() { + f.Enabled = helpers.DefaultBool(f.Enabled, true) + f.Debug = helpers.DefaultBool(f.Debug, false) +} + +func (f Firewall) String() string { + return f.toLinesNode().String() +} + +func (f Firewall) toLinesNode() (node *gotree.Node) { + node = gotree.New("Firewall settings:") + + node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled)) + if !*f.Enabled { + return node + } + + if *f.Debug { + node.Appendf("Debug mode: on") + } + + if len(f.VPNInputPorts) > 0 { + vpnInputPortsNode := node.Appendf("VPN input ports:") + for _, port := range f.VPNInputPorts { + vpnInputPortsNode.Appendf("%d", port) + } + } + + if len(f.InputPorts) > 0 { + inputPortsNode := node.Appendf("Input ports:") + for _, port := range f.InputPorts { + inputPortsNode.Appendf("%d", port) + } + } + + if len(f.OutboundSubnets) > 0 { + outboundSubnets := node.Appendf("Outbound subnets:") + for _, subnet := range f.OutboundSubnets { + outboundSubnets.Appendf("%s", subnet) + } + } + + return node +} diff --git a/internal/configuration/settings/health.go b/internal/configuration/settings/health.go new file mode 100644 index 00000000..65846c9a --- /dev/null +++ b/internal/configuration/settings/health.go @@ -0,0 +1,83 @@ +package settings + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" + "github.com/qdm12/govalid/address" +) + +// Health contains settings for the healthcheck and health server. +type Health struct { + // ServerAddress is the listening address + // for the health check server. + // It cannot be the empty string in the internal state. + ServerAddress string + // AddressToPing is the IP address or domain name to + // ping periodically for the health check. + // It cannot be the empty string in the internal state. + AddressToPing string + VPN HealthyWait +} + +func (h Health) Validate() (err error) { + uid := os.Getuid() + _, err = address.Validate(h.ServerAddress, + address.OptionListening(uid)) + if err != nil { + return fmt.Errorf("%w: %s", + ErrServerAddressNotValid, err) + } + + err = h.VPN.validate() + if err != nil { + return fmt.Errorf("health VPN settings validation failed: %w", err) + } + + return nil +} + +func (h *Health) copy() (copied Health) { + return Health{ + ServerAddress: h.ServerAddress, + AddressToPing: h.AddressToPing, + VPN: h.VPN.copy(), + } +} + +// MergeWith merges the other settings into any +// unset field of the receiver settings object. +func (h *Health) MergeWith(other Health) { + h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress) + h.AddressToPing = helpers.MergeWithString(h.AddressToPing, other.AddressToPing) + h.VPN.mergeWith(other.VPN) +} + +// OverrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (h *Health) OverrideWith(other Health) { + h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress) + h.AddressToPing = helpers.OverrideWithString(h.AddressToPing, other.AddressToPing) + h.VPN.overrideWith(other.VPN) +} + +func (h *Health) SetDefaults() { + h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999") + h.AddressToPing = helpers.DefaultString(h.AddressToPing, "github.com") + h.VPN.setDefaults() +} + +func (h Health) String() string { + return h.toLinesNode().String() +} + +func (h Health) toLinesNode() (node *gotree.Node) { + node = gotree.New("Health settings:") + node.Appendf("Server listening address: %s", h.ServerAddress) + node.Appendf("Address to ping: %s", h.AddressToPing) + node.AppendNode(h.VPN.toLinesNode("VPN")) + return node +} diff --git a/internal/configuration/settings/healthywait.go b/internal/configuration/settings/healthywait.go new file mode 100644 index 00000000..0d397735 --- /dev/null +++ b/internal/configuration/settings/healthywait.go @@ -0,0 +1,66 @@ +package settings + +import ( + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +type HealthyWait struct { + // Initial is the initial duration to wait for the program + // to be healthy before taking action. + // It cannot be nil in the internal state. + Initial *time.Duration + // Addition is the duration to add to the Initial duration + // after Initial has expired to wait longer for the program + // to be healthy. + // It cannot be nil in the internal state. + Addition *time.Duration +} + +func (h HealthyWait) validate() (err error) { + return nil +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (h *HealthyWait) copy() (copied HealthyWait) { + return HealthyWait{ + Initial: helpers.CopyDurationPtr(h.Initial), + Addition: helpers.CopyDurationPtr(h.Addition), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (h *HealthyWait) mergeWith(other HealthyWait) { + h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial) + h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (h *HealthyWait) overrideWith(other HealthyWait) { + h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial) + h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition) +} + +func (h *HealthyWait) setDefaults() { + const initialDurationDefault = 6 * time.Second + const additionDurationDefault = 5 * time.Second + h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault) + h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault) +} + +func (h HealthyWait) String() string { + return h.toLinesNode("Health").String() +} + +func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) { + node = gotree.New(kind + " wait durations:") + node.Appendf("Initial duration: %s", *h.Initial) + node.Appendf("Additional duration: %s", *h.Addition) + return node +} diff --git a/internal/configuration/settings/helpers/belong.go b/internal/configuration/settings/helpers/belong.go new file mode 100644 index 00000000..d60e6dd6 --- /dev/null +++ b/internal/configuration/settings/helpers/belong.go @@ -0,0 +1,45 @@ +package helpers + +import ( + "errors" + "fmt" + "strings" +) + +func IsOneOf(value string, choices ...string) (ok bool) { + for _, choice := range choices { + if value == choice { + return true + } + } + return false +} + +var ErrValueNotOneOf = errors.New("value is not one of the possible choices") + +func AreAllOneOf(values, choices []string) (err error) { + set := make(map[string]struct{}, len(choices)) + for _, choice := range choices { + choice = strings.ToLower(choice) + set[choice] = struct{}{} + } + + for _, value := range values { + _, ok := set[value] + if !ok { + return fmt.Errorf("%w: value %q, choices available are %s", + ErrValueNotOneOf, value, strings.Join(choices, ", ")) + } + } + + return nil +} + +func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) { + for _, choice := range choices { + if port == choice { + return true + } + } + return false +} diff --git a/internal/configuration/settings/helpers/copy.go b/internal/configuration/settings/helpers/copy.go new file mode 100644 index 00000000..e2937b24 --- /dev/null +++ b/internal/configuration/settings/helpers/copy.go @@ -0,0 +1,190 @@ +package helpers + +import ( + "net" + "time" + + "github.com/qdm12/golibs/logging" + "inet.af/netaddr" +) + +func CopyStringPtr(original *string) (copied *string) { + if original == nil { + return nil + } + copied = new(string) + *copied = *original + return copied +} + +func CopyBoolPtr(original *bool) (copied *bool) { + if original == nil { + return nil + } + copied = new(bool) + *copied = *original + return copied +} + +func CopyUint8Ptr(original *uint8) (copied *uint8) { + if original == nil { + return nil + } + copied = new(uint8) + *copied = *original + return copied +} + +func CopyUint16Ptr(original *uint16) (copied *uint16) { + if original == nil { + return nil + } + copied = new(uint16) + *copied = *original + return copied +} + +func CopyIntPtr(original *int) (copied *int) { + if original == nil { + return nil + } + copied = new(int) + *copied = *original + return copied +} + +func CopyDurationPtr(original *time.Duration) (copied *time.Duration) { + if original == nil { + return nil + } + copied = new(time.Duration) + *copied = *original + return copied +} + +func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) { + if original == nil { + return nil + } + copied = new(logging.Level) + *copied = *original + return copied +} + +func CopyIP(original net.IP) (copied net.IP) { + if original == nil { + return nil + } + copied = make(net.IP, len(original)) + copy(copied, original) + return copied +} + +func CopyIPNet(original net.IPNet) (copied net.IPNet) { + if original.IP != nil { + copied.IP = make(net.IP, len(original.IP)) + copy(copied.IP, original.IP) + } + + if original.Mask != nil { + copied.Mask = make(net.IPMask, len(original.Mask)) + copy(copied.Mask, original.Mask) + } + + return copied +} + +func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) { + if original == nil { + return nil + } + + copied = new(net.IPNet) + *copied = CopyIPNet(*original) + return copied +} + +func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) { + b, err := original.MarshalBinary() + if err != nil { + panic(err) + } + + err = copied.UnmarshalBinary(b) + if err != nil { + panic(err) + } + + return copied +} + +func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) { + b, err := original.MarshalText() + if err != nil { + panic(err) + } + + err = copied.UnmarshalText(b) + if err != nil { + panic(err) + } + + return copied +} + +func CopyStringSlice(original []string) (copied []string) { + if original == nil { + return nil + } + + copied = make([]string, len(original)) + copy(copied, original) + return copied +} + +func CopyUint16Slice(original []uint16) (copied []uint16) { + if original == nil { + return nil + } + + copied = make([]uint16, len(original)) + copy(copied, original) + return copied +} + +func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) { + if original == nil { + return nil + } + + copied = make([]net.IPNet, len(original)) + for i := range original { + copied[i] = CopyIPNet(original[i]) + } + return copied +} + +func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) { + if original == nil { + return nil + } + + copied = make([]netaddr.IPPrefix, len(original)) + for i := range original { + copied[i] = CopyIPPrefix(original[i]) + } + return copied +} + +func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) { + if original == nil { + return nil + } + + copied = make([]netaddr.IP, len(original)) + for i := range original { + copied[i] = CopyNetaddrIP(original[i]) + } + + return copied +} diff --git a/internal/configuration/settings/helpers/default.go b/internal/configuration/settings/helpers/default.go new file mode 100644 index 00000000..944377a5 --- /dev/null +++ b/internal/configuration/settings/helpers/default.go @@ -0,0 +1,93 @@ +package helpers + +import ( + "net" + "time" + + "github.com/qdm12/golibs/logging" +) + +func DefaultInt(existing *int, defaultValue int) ( + result *int) { + if existing != nil { + return existing + } + result = new(int) + *result = defaultValue + return result +} + +func DefaultUint8(existing *uint8, defaultValue uint8) ( + result *uint8) { + if existing != nil { + return existing + } + result = new(uint8) + *result = defaultValue + return result +} + +func DefaultUint16(existing *uint16, defaultValue uint16) ( + result *uint16) { + if existing != nil { + return existing + } + result = new(uint16) + *result = defaultValue + return result +} + +func DefaultBool(existing *bool, defaultValue bool) ( + result *bool) { + if existing != nil { + return existing + } + result = new(bool) + *result = defaultValue + return result +} + +func DefaultString(existing string, defaultValue string) ( + result string) { + if existing != "" { + return existing + } + return defaultValue +} + +func DefaultStringPtr(existing *string, defaultValue string) (result *string) { + if existing != nil { + return existing + } + result = new(string) + *result = defaultValue + return result +} + +func DefaultDuration(existing *time.Duration, + defaultValue time.Duration) (result *time.Duration) { + if existing != nil { + return existing + } + result = new(time.Duration) + *result = defaultValue + return result +} + +func DefaultLogLevel(existing *logging.Level, + defaultValue logging.Level) (result *logging.Level) { + if existing != nil { + return existing + } + result = new(logging.Level) + *result = defaultValue + return result +} + +func DefaultIP(existing net.IP, defaultValue net.IP) ( + result net.IP) { + if existing != nil { + return existing + } + return defaultValue +} diff --git a/internal/configuration/settings/helpers/files.go b/internal/configuration/settings/helpers/files.go new file mode 100644 index 00000000..998e2ebc --- /dev/null +++ b/internal/configuration/settings/helpers/files.go @@ -0,0 +1,31 @@ +package helpers + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +var ( + ErrFileDoesNotExist = errors.New("file does not exist") + ErrFileRead = errors.New("cannot read file") + ErrFileClose = errors.New("cannot close file") +) + +func FileExists(path string) (err error) { + path = filepath.Clean(path) + + f, err := os.Open(path) + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("%w: %s", ErrFileDoesNotExist, path) + } else if err != nil { + return fmt.Errorf("%w: %s", ErrFileRead, err) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("%w: %s", ErrFileClose, err) + } + + return nil +} diff --git a/internal/configuration/settings/helpers/merge.go b/internal/configuration/settings/helpers/merge.go new file mode 100644 index 00000000..fd1f96b6 --- /dev/null +++ b/internal/configuration/settings/helpers/merge.go @@ -0,0 +1,226 @@ +package helpers + +import ( + "net" + "time" + + "github.com/qdm12/golibs/logging" + "inet.af/netaddr" +) + +func MergeWithBool(existing, other *bool) (result *bool) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(bool) + *result = *other + return result +} + +func MergeWithString(existing, other string) (result string) { + if existing != "" { + return existing + } + return other +} + +func MergeWithStringPtr(existing, other *string) (result *string) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(string) + *result = *other + return result +} + +func MergeWithInt(existing, other *int) (result *int) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(int) + *result = *other + return result +} + +func MergeWithUint8(existing, other *uint8) (result *uint8) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(uint8) + *result = *other + return result +} + +func MergeWithUint16(existing, other *uint16) (result *uint16) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(uint16) + *result = *other + return result +} + +func MergeWithIP(existing, other net.IP) (result net.IP) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = make(net.IP, len(other)) + copy(result, other) + return result +} + +func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) { + if existing != nil { + return existing + } + return other +} + +func MergeWithLogLevel(existing, other *logging.Level) (result *logging.Level) { + if existing != nil { + return existing + } else if other == nil { + return nil + } + result = new(logging.Level) + *result = *other + return result +} + +func MergeStringSlices(a, b []string) (result []string) { + if a == nil && b == nil { + return nil + } + + seen := make(map[string]struct{}, len(a)+len(b)) + result = make([]string, 0, len(a)+len(b)) + for _, s := range a { + if _, ok := seen[s]; ok { + continue // duplicate + } + result = append(result, s) + seen[s] = struct{}{} + } + for _, s := range b { + if _, ok := seen[s]; ok { + continue // duplicate + } + result = append(result, s) + seen[s] = struct{}{} + } + return result +} + +func MergeUint16Slices(a, b []uint16) (result []uint16) { + if a == nil && b == nil { + return nil + } + + seen := make(map[uint16]struct{}, len(a)+len(b)) + result = make([]uint16, 0, len(a)+len(b)) + for _, n := range a { + if _, ok := seen[n]; ok { + continue // duplicate + } + result = append(result, n) + seen[n] = struct{}{} + } + for _, n := range b { + if _, ok := seen[n]; ok { + continue // duplicate + } + result = append(result, n) + seen[n] = struct{}{} + } + return result +} + +func MergeIPNetsSlices(a, b []net.IPNet) (result []net.IPNet) { + if a == nil && b == nil { + return nil + } + + seen := make(map[string]struct{}, len(a)+len(b)) + result = make([]net.IPNet, 0, len(a)+len(b)) + for _, ipNet := range a { + key := ipNet.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ipNet) + seen[key] = struct{}{} + } + for _, ipNet := range b { + key := ipNet.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ipNet) + seen[key] = struct{}{} + } + return result +} + +func MergeNetaddrIPsSlices(a, b []netaddr.IP) (result []netaddr.IP) { + if a == nil && b == nil { + return nil + } + + seen := make(map[string]struct{}, len(a)+len(b)) + result = make([]netaddr.IP, 0, len(a)+len(b)) + for _, ip := range a { + key := ip.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ip) + seen[key] = struct{}{} + } + for _, ip := range b { + key := ip.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ip) + seen[key] = struct{}{} + } + return result +} + +func MergeIPPrefixesSlices(a, b []netaddr.IPPrefix) (result []netaddr.IPPrefix) { + if a == nil && b == nil { + return nil + } + + seen := make(map[string]struct{}, len(a)+len(b)) + result = make([]netaddr.IPPrefix, 0, len(a)+len(b)) + for _, ipPrefix := range a { + key := ipPrefix.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ipPrefix) + seen[key] = struct{}{} + } + for _, ipPrefix := range b { + key := ipPrefix.String() + if _, ok := seen[key]; ok { + continue // duplicate + } + result = append(result, ipPrefix) + seen[key] = struct{}{} + } + return result +} diff --git a/internal/configuration/settings/helpers/messages.go b/internal/configuration/settings/helpers/messages.go new file mode 100644 index 00000000..1442b979 --- /dev/null +++ b/internal/configuration/settings/helpers/messages.go @@ -0,0 +1,29 @@ +package helpers + +import ( + "fmt" + "strings" +) + +func ChoicesOrString(choices []string) string { + return strings.Join( + choices[:len(choices)-1], ", ") + + " or " + choices[len(choices)-1] +} + +func PortChoicesOrString(ports []uint16) (s string) { + switch len(ports) { + case 0: + return "there is no allowed port" + case 1: + return "allowed port is " + fmt.Sprint(ports[0]) + } + + s = "allowed ports are " + portStrings := make([]string, len(ports)) + for i := range ports { + portStrings[i] = fmt.Sprint(ports[i]) + } + s += ChoicesOrString(portStrings) + return s +} diff --git a/internal/configuration/settings/helpers/obfuscate.go b/internal/configuration/settings/helpers/obfuscate.go new file mode 100644 index 00000000..7835b618 --- /dev/null +++ b/internal/configuration/settings/helpers/obfuscate.go @@ -0,0 +1,25 @@ +package helpers + +func ObfuscateWireguardKey(fullKey string) (obfuscatedKey string) { + const minKeyLength = 10 + if len(fullKey) < minKeyLength { + return "(too short)" + } + + lastIndex := len(fullKey) - 1 + return fullKey[0:2] + "..." + fullKey[lastIndex-2:] +} + +func ObfuscatePassword(password string) (obfuscatedPassword string) { + if password != "" { + return "[set]" + } + return "[not set]" +} + +func ObfuscateData(data string) (obfuscated string) { + if data != "" { + return "[set]" + } + return "[not set]" +} diff --git a/internal/configuration/settings/helpers/override.go b/internal/configuration/settings/helpers/override.go new file mode 100644 index 00000000..29b8bbf0 --- /dev/null +++ b/internal/configuration/settings/helpers/override.go @@ -0,0 +1,133 @@ +package helpers + +import ( + "net" + "time" + + "github.com/qdm12/golibs/logging" + "inet.af/netaddr" +) + +func OverrideWithBool(existing, other *bool) (result *bool) { + if other == nil { + return existing + } + result = new(bool) + *result = *other + return result +} + +func OverrideWithString(existing, other string) (result string) { + if other == "" { + return existing + } + return other +} + +func OverrideWithStringPtr(existing, other *string) (result *string) { + if other == nil { + return existing + } + result = new(string) + *result = *other + return result +} + +func OverrideWithInt(existing, other *int) (result *int) { + if other == nil { + return existing + } + result = new(int) + *result = *other + return result +} + +func OverrideWithUint8(existing, other *uint8) (result *uint8) { + if other == nil { + return existing + } + result = new(uint8) + *result = *other + return result +} + +func OverrideWithUint16(existing, other *uint16) (result *uint16) { + if other == nil { + return existing + } + result = new(uint16) + *result = *other + return result +} + +func OverrideWithIP(existing, other net.IP) (result net.IP) { + if other == nil { + return existing + } + result = make(net.IP, len(other)) + copy(result, other) + return result +} + +func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) { + if other == nil { + return existing + } + result = new(time.Duration) + *result = *other + return result +} + +func OverrideWithLogLevel(existing, other *logging.Level) (result *logging.Level) { + if other == nil { + return existing + } + result = new(logging.Level) + *result = *other + return result +} + +func OverrideWithStringSlice(existing, other []string) (result []string) { + if other == nil { + return existing + } + result = make([]string, len(other)) + copy(result, other) + return result +} + +func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) { + if other == nil { + return existing + } + result = make([]uint16, len(other)) + copy(result, other) + return result +} + +func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) { + if other == nil { + return existing + } + result = make([]net.IPNet, len(other)) + copy(result, other) + return result +} + +func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) { + if other == nil { + return existing + } + result = make([]netaddr.IP, len(other)) + copy(result, other) + return result +} + +func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) { + if other == nil { + return existing + } + result = make([]netaddr.IPPrefix, len(other)) + copy(result, other) + return result +} diff --git a/internal/configuration/settings/helpers/pointers.go b/internal/configuration/settings/helpers/pointers.go new file mode 100644 index 00000000..cfd17488 --- /dev/null +++ b/internal/configuration/settings/helpers/pointers.go @@ -0,0 +1,11 @@ +package helpers + +import "time" + +// StringPtr returns a pointer to the string value +// passed as argument. +func StringPtr(s string) *string { return &s } + +// DurationPtr returns a pointer to the duration value +// passed as argument. +func DurationPtr(d time.Duration) *time.Duration { return &d } diff --git a/internal/configuration/settings/helpers/string.go b/internal/configuration/settings/helpers/string.go new file mode 100644 index 00000000..594036f9 --- /dev/null +++ b/internal/configuration/settings/helpers/string.go @@ -0,0 +1,15 @@ +package helpers + +func BoolPtrToYesNo(b *bool) string { + if *b { + return "yes" + } + return "no" +} + +func TCPPtrToString(tcp *bool) string { + if *tcp { + return "TCP" + } + return "UDP" +} diff --git a/internal/configuration/settings/helpers_test.go b/internal/configuration/settings/helpers_test.go new file mode 100644 index 00000000..52d2cfd9 --- /dev/null +++ b/internal/configuration/settings/helpers_test.go @@ -0,0 +1,4 @@ +package settings + +func boolPtr(b bool) *bool { return &b } +func uint8Ptr(n uint8) *uint8 { return &n } diff --git a/internal/configuration/settings/httpproxy.go b/internal/configuration/settings/httpproxy.go new file mode 100644 index 00000000..abeb8c79 --- /dev/null +++ b/internal/configuration/settings/httpproxy.go @@ -0,0 +1,112 @@ +package settings + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" + "github.com/qdm12/govalid/address" +) + +// HTTPProxy contains settings to configure the HTTP proxy. +type HTTPProxy struct { + // User is the username to use for the HTTP proxy. + // It cannot be nil in the internal state. + User *string + // Password is the password to use for the HTTP proxy. + // It cannot be nil in the internal state. + Password *string + // ListeningAddress is the listening address + // of the HTTP proxy server. + // It cannot be the empty string in the internal state. + ListeningAddress string + // Enabled is true if the HTTP proxy server should run, + // and false otherwise. It cannot be nil in the + // internal state. + Enabled *bool + // Stealth is true if the HTTP proxy server should hide + // each request has been proxied to the destination. + // It cannot be nil in the internal state. + Stealth *bool + // Log is true if the HTTP proxy server should log + // each request/response. It cannot be nil in the + // internal state. + Log *bool +} + +func (h HTTPProxy) validate() (err error) { + // Do not validate user and password + + uid := os.Getuid() + _, err = address.Validate(h.ListeningAddress, address.OptionListening(uid)) + if err != nil { + return fmt.Errorf("%w: %s", + ErrServerAddressNotValid, h.ListeningAddress) + } + + return nil +} + +func (h *HTTPProxy) copy() (copied HTTPProxy) { + return HTTPProxy{ + User: helpers.CopyStringPtr(h.User), + Password: helpers.CopyStringPtr(h.Password), + ListeningAddress: h.ListeningAddress, + Enabled: helpers.CopyBoolPtr(h.Enabled), + Stealth: helpers.CopyBoolPtr(h.Stealth), + Log: helpers.CopyBoolPtr(h.Log), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (h *HTTPProxy) mergeWith(other HTTPProxy) { + h.User = helpers.MergeWithStringPtr(h.User, other.User) + h.Password = helpers.MergeWithStringPtr(h.Password, other.Password) + h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress) + h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled) + h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth) + h.Log = helpers.MergeWithBool(h.Log, other.Log) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (h *HTTPProxy) overrideWith(other HTTPProxy) { + h.User = helpers.OverrideWithStringPtr(h.User, other.User) + h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password) + h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress) + h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled) + h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth) + h.Log = helpers.OverrideWithBool(h.Log, other.Log) +} + +func (h *HTTPProxy) setDefaults() { + h.User = helpers.DefaultStringPtr(h.User, "") + h.Password = helpers.DefaultStringPtr(h.Password, "") + h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, "") + h.Enabled = helpers.DefaultBool(h.Enabled, false) + h.Stealth = helpers.DefaultBool(h.Stealth, false) + h.Log = helpers.DefaultBool(h.Log, false) +} + +func (h HTTPProxy) String() string { + return h.toLinesNode().String() +} + +func (h HTTPProxy) toLinesNode() (node *gotree.Node) { + node = gotree.New("HTTP proxy settings:") + node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled)) + if !*h.Enabled { + return node + } + + node.Appendf("Listening address: %s", h.ListeningAddress) + node.Appendf("User: %s", *h.User) + node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password)) + node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth)) + node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log)) + + return node +} diff --git a/internal/configuration/settings/log.go b/internal/configuration/settings/log.go new file mode 100644 index 00000000..a2bf5f15 --- /dev/null +++ b/internal/configuration/settings/log.go @@ -0,0 +1,51 @@ +package settings + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/golibs/logging" + "github.com/qdm12/gotree" +) + +// Log contains settings to configure the logger. +type Log struct { + // Level is the log level of the logger. + // It cannot be nil in the internal state. + Level *logging.Level +} + +func (l Log) validate() (err error) { + return nil +} + +func (l *Log) copy() (copied Log) { + return Log{ + Level: helpers.CopyLogLevelPtr(l.Level), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (l *Log) mergeWith(other Log) { + l.Level = helpers.MergeWithLogLevel(l.Level, other.Level) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (l *Log) overrideWith(other Log) { + l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level) +} + +func (l *Log) setDefaults() { + l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo) +} + +func (l Log) String() string { + return l.toLinesNode().String() +} + +func (l Log) toLinesNode() (node *gotree.Node) { + node = gotree.New("Log settings:") + node.Appendf("Log level: %s", l.Level.String()) + return node +} diff --git a/internal/configuration/settings/openvpn.go b/internal/configuration/settings/openvpn.go new file mode 100644 index 00000000..7717ecc3 --- /dev/null +++ b/internal/configuration/settings/openvpn.go @@ -0,0 +1,318 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/openvpn/parse" + "github.com/qdm12/gotree" +) + +// OpenVPN contains settings to configure the OpenVPN client. +type OpenVPN struct { + // Version is the OpenVPN version to run. + // It can only be "2.4" or "2.5". + Version string + // User is the OpenVPN authentication username. + // It cannot be an empty string in the internal state + // if OpenVPN is used. + User string + // Password is the OpenVPN authentication password. + // It cannot be an empty string in the internal state + // if OpenVPN is used. + Password string + // ConfFile is a custom OpenVPN configuration file path. + // It can be set to the empty string for it to be ignored. + // It cannot be nil in the internal state. + ConfFile *string + // Ciphers is a list of ciphers to use for OpenVPN, + // different from the ones specified by the VPN + // service provider configuration files. + Ciphers []string + // Auth is an auth algorithm to use in OpenVPN instead + // of the one specified by the VPN service provider. + // It cannot be nil in the internal state. + // It is ignored if it is set to the empty string. + Auth *string + // ClientCrt is the OpenVPN client certificate. + // This is notably used by Cyberghost. + // It can be set to the empty string to be ignored. + // It cannot be nil in the internal state. + ClientCrt *string + // ClientKey is the OpenVPN client key. + // This is used by Cyberghost and VPN Unlimited. + // It can be set to the empty string to be ignored. + // It cannot be nil in the internal state. + ClientKey *string + // PIAEncPreset is the encryption preset for + // Private Internet Access. It can be set to an + // empty string for other providers. + PIAEncPreset *string + // IPv6 is set to true if IPv6 routing should be + // set to be tunnel in OpenVPN, and false otherwise. + // It cannot be nil in the internal state. + IPv6 *bool // TODO automate like with Wireguard + // MSSFix is the value (1 to 10000) to set for the + // mssfix option for OpenVPN. It is ignored if set to 0. + // It cannot be nil in the internal state. + MSSFix *uint16 + // Interface is the OpenVPN device interface name. + // It cannot be an empty string in the internal state. + Interface string + // Root is true if OpenVPN is to be run as root, + // and false otherwise. It cannot be nil in the + // internal state. + Root *bool + // ProcUser is the OpenVPN process OS username + // to use. It cannot be nil in the internal state. + // This is set and injected at runtime. + // TODO only use ProcUser and not Root field. + ProcUser string + // Verbosity is the OpenVPN verbosity level from 0 to 6. + // It cannot be nil in the internal state. + Verbosity *int + // Flags is a slice of additional flags to be passed + // to the OpenVPN program. + Flags []string +} + +func (o OpenVPN) validate(vpnProvider string) (err error) { + // Validate version + validVersions := []string{constants.Openvpn24, constants.Openvpn25} + if !helpers.IsOneOf(o.Version, validVersions...) { + return fmt.Errorf("%w: %q can only be one of %s", + ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", ")) + } + + if o.User == "" { + return ErrOpenVPNUserIsEmpty + } + + if o.Password == "" { + return ErrOpenVPNPasswordIsEmpty + } + + // Validate ConfFile + if vpnProvider == constants.Custom { + if *o.ConfFile == "" { + return fmt.Errorf("%w: no file path specified", ErrOpenVPNConfigFile) + } + err := helpers.FileExists(*o.ConfFile) + if err != nil { + return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err) + } + } + + // Check client certificate + switch vpnProvider { + case + constants.Cyberghost, + constants.VPNUnlimited: + if *o.ClientCrt == "" { + return ErrOpenVPNClientCertMissing + } + } + if *o.ClientCrt != "" { + _, err = parse.ExtractCert([]byte(*o.ClientCrt)) + if err != nil { + return fmt.Errorf("%w: %s", ErrOpenVPNClientCertNotValid, err) + } + } + + // Check client key + switch vpnProvider { + case + constants.Cyberghost, + constants.VPNUnlimited, + constants.Wevpn: + if *o.ClientKey == "" { + return ErrOpenVPNClientKeyMissing + } + } + if *o.ClientKey != "" { + _, err = parse.ExtractPrivateKey([]byte(*o.ClientKey)) + if err != nil { + return fmt.Errorf("%w: %s", ErrOpenVPNClientKeyNotValid, err) + } + } + + // Validate MSSFix + const maxMSSFix = 10000 + if *o.MSSFix > maxMSSFix { + return fmt.Errorf("%w: %d is over the maximum value of %d", + ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix) + } + + if !regexpInterfaceName.MatchString(o.Interface) { + return fmt.Errorf("%w: '%s' does not match regex '%s'", + ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName) + } + + // Validate Verbosity + if *o.Verbosity < 0 || *o.Verbosity > 6 { + return fmt.Errorf("%w: %d can only be between 0 and 5", + ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity) + } + + return nil +} + +func (o *OpenVPN) copy() (copied OpenVPN) { + return OpenVPN{ + Version: o.Version, + User: o.User, + Password: o.Password, + ConfFile: helpers.CopyStringPtr(o.ConfFile), + Ciphers: helpers.CopyStringSlice(o.Ciphers), + Auth: helpers.CopyStringPtr(o.Auth), + ClientCrt: helpers.CopyStringPtr(o.ClientCrt), + ClientKey: helpers.CopyStringPtr(o.ClientKey), + PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset), + IPv6: helpers.CopyBoolPtr(o.IPv6), + MSSFix: helpers.CopyUint16Ptr(o.MSSFix), + Interface: o.Interface, + Root: helpers.CopyBoolPtr(o.Root), + ProcUser: o.ProcUser, + Verbosity: helpers.CopyIntPtr(o.Verbosity), + Flags: helpers.CopyStringSlice(o.Flags), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (o *OpenVPN) mergeWith(other OpenVPN) { + o.Version = helpers.MergeWithString(o.Version, other.Version) + o.User = helpers.MergeWithString(o.User, other.User) + o.Password = helpers.MergeWithString(o.Password, other.Password) + o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile) + o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers) + o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth) + o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt) + o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey) + o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) + o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6) + o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix) + o.Interface = helpers.MergeWithString(o.Interface, other.Interface) + o.Root = helpers.MergeWithBool(o.Root, other.Root) + o.ProcUser = helpers.MergeWithString(o.ProcUser, other.ProcUser) + o.Verbosity = helpers.MergeWithInt(o.Verbosity, other.Verbosity) + o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (o *OpenVPN) overrideWith(other OpenVPN) { + o.Version = helpers.OverrideWithString(o.Version, other.Version) + o.User = helpers.OverrideWithString(o.User, other.User) + o.Password = helpers.OverrideWithString(o.Password, other.Password) + o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile) + o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers) + o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth) + o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt) + o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey) + o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) + o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6) + o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix) + o.Interface = helpers.OverrideWithString(o.Interface, other.Interface) + o.Root = helpers.OverrideWithBool(o.Root, other.Root) + o.ProcUser = helpers.OverrideWithString(o.ProcUser, other.ProcUser) + o.Verbosity = helpers.OverrideWithInt(o.Verbosity, other.Verbosity) + o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags) +} + +func (o *OpenVPN) setDefaults(vpnProvider string) { + o.Version = helpers.DefaultString(o.Version, constants.Openvpn25) + if vpnProvider == constants.Mullvad { + o.Password = "m" + } + + o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "") + o.Auth = helpers.DefaultStringPtr(o.Auth, "") + o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "") + o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "") + + var defaultEncPreset string + if vpnProvider == constants.PrivateInternetAccess { + defaultEncPreset = constants.PIAEncryptionPresetStrong + } + o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset) + + o.IPv6 = helpers.DefaultBool(o.IPv6, false) + o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0) + o.Interface = helpers.DefaultString(o.Interface, "tun0") + o.Root = helpers.DefaultBool(o.Root, true) + o.ProcUser = helpers.DefaultString(o.ProcUser, "root") + o.Verbosity = helpers.DefaultInt(o.Verbosity, 1) +} + +func (o OpenVPN) String() string { + return o.toLinesNode().String() +} + +func (o OpenVPN) toLinesNode() (node *gotree.Node) { + node = gotree.New("OpenVPN server selection settings:") + node.Appendf("OpenVPN version: %s", o.Version) + node.Appendf("User: %s", helpers.ObfuscatePassword(o.User)) + node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password)) + + if *o.ConfFile != "" { + node.Appendf("Custom configuration file: %s", *o.ConfFile) + } + + if len(o.Ciphers) > 0 { + node.Appendf("Ciphers: %s", o.Ciphers) + } + + if *o.Auth != "" { + node.Appendf("Auth: %s", *o.Auth) + } + + if *o.ClientCrt != "" { + node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt)) + } + + if *o.ClientKey != "" { + node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey)) + } + + if *o.PIAEncPreset != "" { + node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset) + } + + node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6)) + + if *o.MSSFix > 0 { + node.Appendf("MSS Fix: %d", *o.MSSFix) + } + + if o.Interface != "" { + node.Appendf("Network interface: %s", o.Interface) + } + + processUser := "root" + if !*o.Root { + processUser = "some non root user" // TODO + if o.ProcUser != "" { + processUser = o.ProcUser + } + } + node.Appendf("Run OpenVPN as: %s", processUser) + + node.Appendf("Verbosity level: %d", *o.Verbosity) + + if len(o.Flags) > 0 { + node.Appendf("Flags: %s", o.Flags) + } + + return node +} + +// WithDefaults is a shorthand using setDefaults. +// It's used in unit tests in other packages. +func (o OpenVPN) WithDefaults(provider string) OpenVPN { + o.setDefaults(provider) + return o +} diff --git a/internal/configuration/settings/openvpnselection.go b/internal/configuration/settings/openvpnselection.go new file mode 100644 index 00000000..fbc82b10 --- /dev/null +++ b/internal/configuration/settings/openvpnselection.go @@ -0,0 +1,170 @@ +package settings + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gotree" +) + +type OpenVPNSelection struct { + // ConfFile is the custom configuration file path. + // It can be set to an empty string to indicate to + // NOT use a custom configuration file. + // It cannot be nil in the internal state. + ConfFile *string + // TCP is true if the OpenVPN protocol is TCP, + // and false for UDP. + // It cannot be nil in the internal state. + TCP *bool + // CustomPort is the OpenVPN server endpoint port. + // It can be set to 0 to indicate no custom port should + // be used. It cannot be nil in the internal state. + CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe + // PIAEncPreset is the encryption preset for + // Private Internet Access. It can be set to an + // empty string for other providers. + PIAEncPreset *string +} + +func (o OpenVPNSelection) validate(vpnProvider string) (err error) { + // Validate ConfFile + if confFile := *o.ConfFile; confFile != "" { + err := helpers.FileExists(confFile) + if err != nil { + return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err) + } + } + + // Validate TCP + if *o.TCP && helpers.IsOneOf(vpnProvider, + constants.Perfectprivacy, + constants.Privado, + constants.Vyprvpn, + ) { + return fmt.Errorf("%w: for VPN service provider %s", + ErrOpenVPNTCPNotSupported, vpnProvider) + } + + // Validate CustomPort + if *o.CustomPort != 0 { + switch vpnProvider { + // no restriction on port + case constants.Cyberghost, constants.HideMyAss, + constants.PrivateInternetAccess, constants.Privatevpn, + constants.Protonvpn, constants.Torguard: + // no custom port allowed + case constants.Expressvpn, constants.Fastestvpn, + constants.Ipvanish, constants.Nordvpn, + constants.Privado, constants.Purevpn, + constants.Surfshark, constants.VPNUnlimited, + constants.Vyprvpn: + return fmt.Errorf("%w: for VPN service provider %s", + ErrOpenVPNCustomPortNotAllowed, vpnProvider) + default: + var allowedTCP, allowedUDP []uint16 + switch vpnProvider { + case constants.Ivpn: + allowedTCP = []uint16{80, 443, 1143} + allowedUDP = []uint16{53, 1194, 2049, 2050} + case constants.Mullvad: + allowedTCP = []uint16{80, 443, 1401} + allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400} + case constants.Perfectprivacy: + allowedTCP = []uint16{44, 443, 4433} + allowedUDP = []uint16{44, 443, 4433} + case constants.Wevpn: + allowedTCP = []uint16{53, 1195, 1199, 2018} + allowedUDP = []uint16{80, 1194, 1198} + case constants.Windscribe: + allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783} + allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783} + } + + if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) { + return fmt.Errorf("%w: %d for VPN service provider %s; %s", + ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider, + helpers.PortChoicesOrString(allowedTCP)) + } else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) { + return fmt.Errorf("%w: %d for VPN service provider %s; %s", + ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider, + helpers.PortChoicesOrString(allowedUDP)) + } + } + } + + // Validate EncPreset + if vpnProvider == constants.PrivateInternetAccess { + validEncryptionPresets := []string{ + constants.PIAEncryptionPresetNone, + constants.PIAEncryptionPresetNormal, + constants.PIACertificateStrong, + } + if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) { + return fmt.Errorf("%w: %s; valid presets are %s", + ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset, + helpers.ChoicesOrString(validEncryptionPresets)) + } + } + + return nil +} + +func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) { + return OpenVPNSelection{ + ConfFile: helpers.CopyStringPtr(o.ConfFile), + TCP: helpers.CopyBoolPtr(o.TCP), + CustomPort: helpers.CopyUint16Ptr(o.CustomPort), + PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset), + } +} + +func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) { + o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile) + o.TCP = helpers.MergeWithBool(o.TCP, other.TCP) + o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort) + o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) +} + +func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) { + o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile) + o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP) + o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort) + o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset) +} + +func (o *OpenVPNSelection) setDefaults(vpnProvider string) { + o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "") + o.TCP = helpers.DefaultBool(o.TCP, false) + o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0) + + var defaultEncPreset string + if vpnProvider == constants.PrivateInternetAccess { + defaultEncPreset = constants.PIAEncryptionPresetStrong + } + o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset) +} + +func (o OpenVPNSelection) String() string { + return o.toLinesNode().String() +} + +func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) { + node = gotree.New("OpenVPN server selection settings:") + node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP)) + + if *o.CustomPort != 0 { + node.Appendf("Custom port: %d", *o.CustomPort) + } + + if *o.PIAEncPreset != "" { + node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset) + } + + if *o.ConfFile != "" { + node.Appendf("Custom configuration file: %s", *o.ConfFile) + } + + return node +} diff --git a/internal/configuration/settings/portforward.go b/internal/configuration/settings/portforward.go new file mode 100644 index 00000000..c1695e25 --- /dev/null +++ b/internal/configuration/settings/portforward.go @@ -0,0 +1,89 @@ +package settings + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gotree" +) + +// PortForwarding contains settings for port forwarding. +type PortForwarding struct { + // Enabled is true if port forwarding should be activated. + // It cannot be nil for the internal state. + Enabled *bool + // Filepath is the port forwarding status file path + // to use. It can be the empty string to indicate not + // to write to a file. It cannot be nil for the + // internal state + Filepath *string +} + +func (p PortForwarding) validate(vpnProvider string) (err error) { + if !*p.Enabled { + return nil + } + + // Validate Enabled + validProviders := []string{constants.PrivateInternetAccess} + if !helpers.IsOneOf(vpnProvider, validProviders...) { + return fmt.Errorf("%w: for provider %s, it is only available for %s", + ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", ")) + } + + // Validate Filepath + if *p.Filepath != "" { // optional + _, err := filepath.Abs(*p.Filepath) + if err != nil { + return fmt.Errorf("%w: %s", ErrPortForwardingFilepathNotValid, err) + } + } + + return nil +} + +func (p *PortForwarding) copy() (copied PortForwarding) { + return PortForwarding{ + Enabled: helpers.CopyBoolPtr(p.Enabled), + Filepath: helpers.CopyStringPtr(p.Filepath), + } +} + +func (p *PortForwarding) mergeWith(other PortForwarding) { + p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled) + p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath) +} + +func (p *PortForwarding) overrideWith(other PortForwarding) { + p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled) + p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath) +} + +func (p *PortForwarding) setDefaults() { + p.Enabled = helpers.DefaultBool(p.Enabled, false) + p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port") +} + +func (p PortForwarding) String() string { + return p.toLinesNode().String() +} + +func (p PortForwarding) toLinesNode() (node *gotree.Node) { + if !*p.Enabled { + return nil + } + + node = gotree.New("Automatic port forwarding settings:") + node.Appendf("Enabled: yes") + + filepath := *p.Filepath + if filepath == "" { + filepath = "[not set]" + } + node.Appendf("Forwarded port file path: %s", filepath) + + return node +} diff --git a/internal/configuration/settings/portforward_test.go b/internal/configuration/settings/portforward_test.go new file mode 100644 index 00000000..96ab68d4 --- /dev/null +++ b/internal/configuration/settings/portforward_test.go @@ -0,0 +1,19 @@ +package settings + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_PortForwarding_String(t *testing.T) { + t.Parallel() + + settings := PortForwarding{ + Enabled: boolPtr(false), + } + + s := settings.String() + + assert.Empty(t, s) +} diff --git a/internal/configuration/settings/provider.go b/internal/configuration/settings/provider.go new file mode 100644 index 00000000..3006b6bc --- /dev/null +++ b/internal/configuration/settings/provider.go @@ -0,0 +1,92 @@ +package settings + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gotree" +) + +// Provider contains settings specific to a VPN provider. +type Provider struct { + // Name is the VPN service provider name. + // It cannot be nil in the internal state. + Name *string + // ServerSelection is the settings to + // select the VPN server. + ServerSelection ServerSelection + // PortForwarding is the settings about port forwarding. + PortForwarding PortForwarding +} + +func (p Provider) validate(vpnType string, allServers models.AllServers) (err error) { + // Validate Name + var validNames []string + if vpnType == constants.OpenVPN { + validNames = constants.AllProviders() + validNames = append(validNames, "pia") // Retro-compatibility + } else { // Wireguard + validNames = []string{ + constants.Custom, + constants.Ivpn, + constants.Mullvad, + constants.Windscribe, + } + } + if !helpers.IsOneOf(*p.Name, validNames...) { + return fmt.Errorf("%w: %q can only be one of %s", + ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames)) + } + + err = p.ServerSelection.validate(*p.Name, allServers) + if err != nil { + return fmt.Errorf("server selection settings validation failed: %w", err) + } + + err = p.PortForwarding.validate(*p.Name) + if err != nil { + return fmt.Errorf("port forwarding settings validation failed: %w", err) + } + + return nil +} + +func (p *Provider) copy() (copied Provider) { + return Provider{ + Name: helpers.CopyStringPtr(p.Name), + ServerSelection: p.ServerSelection.copy(), + PortForwarding: p.PortForwarding.copy(), + } +} + +func (p *Provider) mergeWith(other Provider) { + p.Name = helpers.MergeWithStringPtr(p.Name, other.Name) + p.ServerSelection.mergeWith(other.ServerSelection) + p.PortForwarding.mergeWith(other.PortForwarding) +} + +func (p *Provider) overrideWith(other Provider) { + p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name) + p.ServerSelection.overrideWith(other.ServerSelection) + p.PortForwarding.overrideWith(other.PortForwarding) +} + +func (p *Provider) setDefaults() { + p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess) + p.ServerSelection.setDefaults(*p.Name) + p.PortForwarding.setDefaults() +} + +func (p Provider) String() string { + return p.toLinesNode().String() +} + +func (p Provider) toLinesNode() (node *gotree.Node) { + node = gotree.New("VPN provider settings:") + node.Appendf("Name: %s", *p.Name) + node.AppendNode(p.ServerSelection.toLinesNode()) + node.AppendNode(p.PortForwarding.toLinesNode()) + return node +} diff --git a/internal/configuration/settings/publicip.go b/internal/configuration/settings/publicip.go new file mode 100644 index 00000000..0ec4af9b --- /dev/null +++ b/internal/configuration/settings/publicip.go @@ -0,0 +1,89 @@ +package settings + +import ( + "fmt" + "path/filepath" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// PublicIP contains settings for port forwarding. +type PublicIP struct { + // Period is the period to get the public IP address. + // It can be set to 0 to disable periodic checking. + // It cannot be nil for the internal state. + // TODO change to value and add enabled field + Period *time.Duration + // IPFilepath is the public IP address status file path + // to use. It can be the empty string to indicate not + // to write to a file. It cannot be nil for the + // internal state + IPFilepath *string +} + +func (p PublicIP) validate() (err error) { + const minPeriod = 5 * time.Second + if *p.Period < minPeriod { + return fmt.Errorf("%w: %s must be at least %s", + ErrPublicIPPeriodTooShort, p.Period, minPeriod) + } + + if *p.IPFilepath != "" { // optional + _, err := filepath.Abs(*p.IPFilepath) + if err != nil { + return fmt.Errorf("%w: %s", ErrPublicIPFilepathNotValid, err) + } + } + + return nil +} + +func (p *PublicIP) copy() (copied PublicIP) { + return PublicIP{ + Period: helpers.CopyDurationPtr(p.Period), + IPFilepath: helpers.CopyStringPtr(p.IPFilepath), + } +} + +func (p *PublicIP) mergeWith(other PublicIP) { + p.Period = helpers.MergeWithDuration(p.Period, other.Period) + p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath) +} + +func (p *PublicIP) overrideWith(other PublicIP) { + p.Period = helpers.OverrideWithDuration(p.Period, other.Period) + p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath) +} + +func (p *PublicIP) setDefaults() { + const defaultPeriod = 12 * time.Hour + p.Period = helpers.DefaultDuration(p.Period, defaultPeriod) + p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip") +} + +func (p PublicIP) String() string { + return p.toLinesNode().String() +} + +func (p PublicIP) toLinesNode() (node *gotree.Node) { + node = gotree.New("Public IP settings:") + + if *p.Period == 0 { + node.Appendf("Enabled: no") + return node + } + + updatePeriod := "disabled" + if *p.Period > 0 { + updatePeriod = "every " + p.Period.String() + } + node.Appendf("Fetching: %s", updatePeriod) + + if *p.IPFilepath != "" { + node.Appendf("IP file path: %s", *p.IPFilepath) + } + + return node +} diff --git a/internal/configuration/settings/server.go b/internal/configuration/settings/server.go new file mode 100644 index 00000000..9b7f488f --- /dev/null +++ b/internal/configuration/settings/server.go @@ -0,0 +1,71 @@ +package settings + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// ControlServer contains settings to customize the control server operation. +type ControlServer struct { + // Port is the listening port to use. + // It can be set to 0 to bind to a random port. + // It cannot be nil in the internal state. + // TODO change to address + Port *uint16 + // Log can be true or false to enable logging on requests. + // It cannot be nil in the internal state. + Log *bool +} + +func (c ControlServer) validate() (err error) { + uid := os.Getuid() + const maxPrivilegedPort uint16 = 1023 + if uid != 0 && *c.Port <= maxPrivilegedPort { + return fmt.Errorf("%w: %d when running with user ID %d", + ErrControlServerPrivilegedPort, *c.Port, uid) + } + + return nil +} + +func (c *ControlServer) copy() (copied ControlServer) { + return ControlServer{ + Port: helpers.CopyUint16Ptr(c.Port), + Log: helpers.CopyBoolPtr(c.Log), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (c *ControlServer) mergeWith(other ControlServer) { + c.Port = helpers.MergeWithUint16(c.Port, other.Port) + c.Log = helpers.MergeWithBool(c.Log, other.Log) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (c *ControlServer) overrideWith(other ControlServer) { + c.Port = helpers.MergeWithUint16(c.Port, other.Port) + c.Log = helpers.MergeWithBool(c.Log, other.Log) +} + +func (c *ControlServer) setDefaults() { + const defaultPort = 8000 + c.Port = helpers.DefaultUint16(c.Port, defaultPort) + c.Log = helpers.DefaultBool(c.Log, true) +} + +func (c ControlServer) String() string { + return c.toLinesNode().String() +} + +func (c ControlServer) toLinesNode() (node *gotree.Node) { + node = gotree.New("Control server settings:") + node.Appendf("Listening port: %d", *c.Port) + node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log)) + return node +} diff --git a/internal/configuration/settings/serverselection.go b/internal/configuration/settings/serverselection.go new file mode 100644 index 00000000..17348c6d --- /dev/null +++ b/internal/configuration/settings/serverselection.go @@ -0,0 +1,386 @@ +package settings + +import ( + "fmt" + "net" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gotree" +) + +type ServerSelection struct { //nolint:maligned + // VPN is the VPN type which can be 'openvpn' + // or 'wireguard'. It cannot be the empty string + // in the internal state. + VPN string + // TargetIP is the server endpoint IP address to use. + // It will override any IP address from the picked + // built-in server. It cannot be nil in the internal + // state, and can be set to an empty net.IP{} to indicate + // there is not target IP address to use. + TargetIP net.IP + // Counties is the list of countries to filter VPN servers with. + Countries []string + // Regions is the list of regions to filter VPN servers with. + Regions []string + // Cities is the list of cities to filter VPN servers with. + Cities []string + // ISPs is the list of ISP names to filter VPN servers with. + ISPs []string + // Names is the list of server names to filter VPN servers with. + Names []string + // Numbers is the list of server numbers to filter VPN servers with. + Numbers []uint16 + // Hostnames is the list of hostnames to filter VPN servers with. + Hostnames []string + // OwnedOnly is true if only VPN provider owned servers + // should be filtered. This is used with Mullvad. + OwnedOnly *bool + // FreeOnly is true if only free VPN servers + // should be filtered. This is used with ProtonVPN. + FreeOnly *bool + // FreeOnly is true if only free VPN servers + // should be filtered. This is used with ProtonVPN. + StreamOnly *bool + // MultiHopOnly is true if only multihop VPN servers + // should be filtered. This is used with Surfshark. + MultiHopOnly *bool + + // OpenVPN contains settings to select OpenVPN servers + // and the final connection. + OpenVPN OpenVPNSelection + // Wireguard contains settings to select Wireguard servers + // and the final connection. + Wireguard WireguardSelection +} + +func (ss *ServerSelection) validate(vpnServiceProvider string, + allServers models.AllServers) (err error) { + switch ss.VPN { + case constants.OpenVPN, constants.Wireguard: + default: + return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN) + } + + var countryChoices, regionChoices, cityChoices, + ispChoices, nameChoices, hostnameChoices []string + switch vpnServiceProvider { + case constants.Cyberghost: + servers := allServers.GetCyberghost() + countryChoices = constants.CyberghostCountryChoices(servers) + hostnameChoices = constants.CyberghostHostnameChoices(servers) + case constants.Expressvpn: + servers := allServers.GetExpressvpn() + countryChoices = constants.ExpressvpnCountriesChoices(servers) + cityChoices = constants.ExpressvpnCityChoices(servers) + hostnameChoices = constants.ExpressvpnHostnameChoices(servers) + case constants.Fastestvpn: + servers := allServers.GetFastestvpn() + countryChoices = constants.FastestvpnCountriesChoices(servers) + hostnameChoices = constants.FastestvpnHostnameChoices(servers) + case constants.HideMyAss: + servers := allServers.GetHideMyAss() + countryChoices = constants.HideMyAssCountryChoices(servers) + regionChoices = constants.HideMyAssRegionChoices(servers) + cityChoices = constants.HideMyAssCityChoices(servers) + hostnameChoices = constants.HideMyAssHostnameChoices(servers) + case constants.Ipvanish: + servers := allServers.GetIpvanish() + countryChoices = constants.IpvanishCountryChoices(servers) + cityChoices = constants.IpvanishCityChoices(servers) + hostnameChoices = constants.IpvanishHostnameChoices(servers) + case constants.Ivpn: + servers := allServers.GetIvpn() + countryChoices = constants.IvpnCountryChoices(servers) + cityChoices = constants.IvpnCityChoices(servers) + ispChoices = constants.IvpnISPChoices(servers) + hostnameChoices = constants.IvpnHostnameChoices(servers) + case constants.Mullvad: + servers := allServers.GetMullvad() + countryChoices = constants.MullvadCountryChoices(servers) + cityChoices = constants.MullvadCityChoices(servers) + ispChoices = constants.MullvadISPChoices(servers) + hostnameChoices = constants.MullvadHostnameChoices(servers) + case constants.Nordvpn: + servers := allServers.GetNordvpn() + regionChoices = constants.NordvpnRegionChoices(servers) + hostnameChoices = constants.NordvpnHostnameChoices(servers) + case constants.Perfectprivacy: + servers := allServers.GetPerfectprivacy() + cityChoices = constants.PerfectprivacyCityChoices(servers) + case constants.Privado: + servers := allServers.GetPrivado() + countryChoices = constants.PrivadoCountryChoices(servers) + regionChoices = constants.PrivadoRegionChoices(servers) + cityChoices = constants.PrivadoCityChoices(servers) + hostnameChoices = constants.PrivadoHostnameChoices(servers) + case constants.PrivateInternetAccess: + servers := allServers.GetPia() + regionChoices = constants.PIAGeoChoices(servers) + hostnameChoices = constants.PIAHostnameChoices(servers) + nameChoices = constants.PIANameChoices(servers) + case constants.Privatevpn: + servers := allServers.GetPrivatevpn() + countryChoices = constants.PrivatevpnCountryChoices(servers) + cityChoices = constants.PrivatevpnCityChoices(servers) + hostnameChoices = constants.PrivatevpnHostnameChoices(servers) + case constants.Protonvpn: + servers := allServers.GetProtonvpn() + countryChoices = constants.ProtonvpnCountryChoices(servers) + regionChoices = constants.ProtonvpnRegionChoices(servers) + cityChoices = constants.ProtonvpnCityChoices(servers) + nameChoices = constants.ProtonvpnNameChoices(servers) + hostnameChoices = constants.ProtonvpnHostnameChoices(servers) + case constants.Purevpn: + servers := allServers.GetPurevpn() + countryChoices = constants.PurevpnCountryChoices(servers) + regionChoices = constants.PurevpnRegionChoices(servers) + cityChoices = constants.PurevpnCityChoices(servers) + hostnameChoices = constants.PurevpnHostnameChoices(servers) + case constants.Surfshark: + servers := allServers.GetSurfshark() + countryChoices = constants.SurfsharkCountryChoices(servers) + cityChoices = constants.SurfsharkCityChoices(servers) + hostnameChoices = constants.SurfsharkHostnameChoices(servers) + regionChoices = constants.SurfsharkRegionChoices(servers) + // TODO v4 remove + regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...) + if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil { + return fmt.Errorf("%w: %s", ErrRegionNotValid, err) + } + // Retro compatibility + // TODO remove in v4 + *ss = surfsharkRetroRegion(*ss) + case constants.Torguard: + servers := allServers.GetTorguard() + countryChoices = constants.TorguardCountryChoices(servers) + cityChoices = constants.TorguardCityChoices(servers) + hostnameChoices = constants.TorguardHostnameChoices(servers) + case constants.VPNUnlimited: + servers := allServers.GetVPNUnlimited() + countryChoices = constants.VPNUnlimitedCountryChoices(servers) + cityChoices = constants.VPNUnlimitedCityChoices(servers) + hostnameChoices = constants.VPNUnlimitedHostnameChoices(servers) + case constants.Vyprvpn: + servers := allServers.GetVyprvpn() + regionChoices = constants.VyprvpnRegionChoices(servers) + case constants.Wevpn: + servers := allServers.GetWevpn() + cityChoices = constants.WevpnCityChoices(servers) + hostnameChoices = constants.WevpnHostnameChoices(servers) + case constants.Windscribe: + servers := allServers.GetWindscribe() + regionChoices = constants.WindscribeRegionChoices(servers) + cityChoices = constants.WindscribeCityChoices(servers) + hostnameChoices = constants.WindscribeHostnameChoices(servers) + default: + return fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, ss.VPN) + } + + err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices, + ispChoices, nameChoices, hostnameChoices) + if err != nil { + return err // already wrapped error + } + + err = ss.OpenVPN.validate(vpnServiceProvider) + if err != nil { + return fmt.Errorf("OpenVPN server selection settings validation failed: %w", err) + } + + err = ss.Wireguard.validate(vpnServiceProvider) + if err != nil { + return fmt.Errorf("Wireguard server selection settings validation failed: %w", err) + } + + return nil +} + +// validateServerFilters validates filters against the choices given as arguments. +// Set an argument to nil to pass the check for a particular filter. +func validateServerFilters(settings ServerSelection, + countryChoices, regionChoices, cityChoices, ispChoices, + nameChoices, hostnameChoices []string) (err error) { + if countryChoices != nil { + if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil { + return fmt.Errorf("%w: %s", ErrCountryNotValid, err) + } + } + + if regionChoices != nil { + if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil { + return fmt.Errorf("%w: %s", ErrRegionNotValid, err) + } + } + + if cityChoices != nil { + if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil { + return fmt.Errorf("%w: %s", ErrCityNotValid, err) + } + } + + if ispChoices != nil { + if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil { + return fmt.Errorf("%w: %s", ErrISPNotValid, err) + } + } + + if hostnameChoices != nil { + if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil { + return fmt.Errorf("%w: %s", ErrHostnameNotValid, err) + } + } + + if nameChoices != nil { + if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil { + return fmt.Errorf("%w: %s", ErrNameNotValid, err) + } + } + + return nil +} + +func (ss *ServerSelection) copy() (copied ServerSelection) { + return ServerSelection{ + VPN: ss.VPN, + TargetIP: helpers.CopyIP(ss.TargetIP), + Countries: helpers.CopyStringSlice(ss.Countries), + Regions: helpers.CopyStringSlice(ss.Regions), + Cities: helpers.CopyStringSlice(ss.Cities), + ISPs: helpers.CopyStringSlice(ss.ISPs), + Hostnames: helpers.CopyStringSlice(ss.Hostnames), + Names: helpers.CopyStringSlice(ss.Names), + Numbers: helpers.CopyUint16Slice(ss.Numbers), + OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly), + FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly), + StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly), + MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly), + OpenVPN: ss.OpenVPN.copy(), + Wireguard: ss.Wireguard.copy(), + } +} + +func (ss *ServerSelection) mergeWith(other ServerSelection) { + ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN) + ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP) + ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries) + ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions) + ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities) + ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs) + ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames) + ss.Names = helpers.MergeStringSlices(ss.Hostnames, other.Names) + ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers) + ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly) + ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly) + ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly) + ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly) + + ss.OpenVPN.mergeWith(other.OpenVPN) + ss.Wireguard.mergeWith(other.Wireguard) +} + +func (ss *ServerSelection) overrideWith(other ServerSelection) { + ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN) + ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP) + ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries) + ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions) + ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities) + ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs) + ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames) + ss.Names = helpers.OverrideWithStringSlice(ss.Hostnames, other.Names) + ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers) + ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly) + ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly) + ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly) + ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly) + ss.OpenVPN.overrideWith(other.OpenVPN) + ss.Wireguard.overrideWith(other.Wireguard) +} + +func (ss *ServerSelection) setDefaults(vpnProvider string) { + ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN) + ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{}) + ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false) + ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false) + ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false) + ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false) + ss.OpenVPN.setDefaults(vpnProvider) + ss.Wireguard.setDefaults() +} + +func (ss ServerSelection) String() string { + return ss.toLinesNode().String() +} + +func (ss ServerSelection) toLinesNode() (node *gotree.Node) { + node = gotree.New("Server selection settings:") + node.Appendf("VPN type: %s", ss.VPN) + if len(ss.TargetIP) > 0 { + node.Appendf("Target IP address: %s", ss.TargetIP) + } + + if len(ss.Countries) > 0 { + node.Appendf("Countries: %s", strings.Join(ss.Countries, ", ")) + } + + if len(ss.Regions) > 0 { + node.Appendf("Regions: %s", strings.Join(ss.Regions, ", ")) + } + + if len(ss.Cities) > 0 { + node.Appendf("Cities: %s", strings.Join(ss.Cities, ", ")) + } + + if len(ss.ISPs) > 0 { + node.Appendf("ISPs: %s", strings.Join(ss.ISPs, ", ")) + } + + if len(ss.Names) > 0 { + node.Appendf("Server names: %s", strings.Join(ss.Names, ", ")) + } + + if len(ss.Numbers) > 0 { + numbersNode := node.Appendf("Server numbers:") + for _, number := range ss.Numbers { + numbersNode.Appendf("%d", number) + } + } + + if len(ss.Hostnames) > 0 { + node.Appendf("Hostnames: %s", strings.Join(ss.Hostnames, ", ")) + } + + if *ss.OwnedOnly { + node.Appendf("Owned only servers: yes") + } + + if *ss.FreeOnly { + node.Appendf("Free only servers: yes") + } + + if *ss.StreamOnly { + node.Appendf("Stream only servers: yes") + } + + if *ss.MultiHopOnly { + node.Appendf("Multi-hop only servers: yes") + } + + if ss.VPN == constants.OpenVPN { + node.AppendNode(ss.OpenVPN.toLinesNode()) + } else { + node.AppendNode(ss.Wireguard.toLinesNode()) + } + + return node +} + +// WithDefaults is a shorthand using setDefaults. +// It's used in unit tests in other packages. +func (ss ServerSelection) WithDefaults(provider string) ServerSelection { + ss.setDefaults(provider) + return ss +} diff --git a/internal/configuration/settings/settings.go b/internal/configuration/settings/settings.go new file mode 100644 index 00000000..ab7d8152 --- /dev/null +++ b/internal/configuration/settings/settings.go @@ -0,0 +1,144 @@ +package settings + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gotree" +) + +type Settings struct { + ControlServer ControlServer + DNS DNS + Firewall Firewall + Health Health + HTTPProxy HTTPProxy + Log Log + PublicIP PublicIP + Shadowsocks Shadowsocks + System System + Updater Updater + Version Version + VPN VPN +} + +func (s Settings) Validate(allServers models.AllServers) (err error) { + nameToValidation := map[string]func() error{ + "control server": s.ControlServer.validate, + "dns": s.DNS.validate, + "firewall": s.Firewall.validate, + "health": s.Health.Validate, + "http proxy": s.HTTPProxy.validate, + "log": s.Log.validate, + "public ip check": s.PublicIP.validate, + "shadowsocks": s.Shadowsocks.validate, + "system": s.System.validate, + "updater": s.Updater.Validate, + "version": s.Version.validate, + "VPN": func() error { + return s.VPN.validate(allServers) + }, + } + + for name, validation := range nameToValidation { + err = validation() + if err != nil { + return fmt.Errorf("failed validating %s settings: %w", name, err) + } + } + + return nil +} + +func (s *Settings) copy() (copied Settings) { + return Settings{ + ControlServer: s.ControlServer.copy(), + DNS: s.DNS.Copy(), + Firewall: s.Firewall.copy(), + Health: s.Health.copy(), + HTTPProxy: s.HTTPProxy.copy(), + Log: s.Log.copy(), + PublicIP: s.PublicIP.copy(), + Shadowsocks: s.Shadowsocks.copy(), + System: s.System.copy(), + Updater: s.Updater.copy(), + Version: s.Version.copy(), + VPN: s.VPN.copy(), + } +} + +func (s *Settings) MergeWith(other Settings) { + s.ControlServer.mergeWith(other.ControlServer) + s.DNS.mergeWith(other.DNS) + s.Firewall.mergeWith(other.Firewall) + s.Health.MergeWith(other.Health) + s.HTTPProxy.mergeWith(other.HTTPProxy) + s.Log.mergeWith(other.Log) + s.PublicIP.mergeWith(other.PublicIP) + s.Shadowsocks.mergeWith(other.Shadowsocks) + s.System.mergeWith(other.System) + s.Updater.mergeWith(other.Updater) + s.Version.mergeWith(other.Version) + s.VPN.mergeWith(other.VPN) +} + +func (s *Settings) OverrideWith(other Settings, + allServers models.AllServers) (err error) { + patchedSettings := s.copy() + patchedSettings.ControlServer.overrideWith(other.ControlServer) + patchedSettings.DNS.overrideWith(other.DNS) + patchedSettings.Firewall.overrideWith(other.Firewall) + patchedSettings.Health.OverrideWith(other.Health) + patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy) + patchedSettings.Log.overrideWith(other.Log) + patchedSettings.PublicIP.overrideWith(other.PublicIP) + patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks) + patchedSettings.System.overrideWith(other.System) + patchedSettings.Updater.overrideWith(other.Updater) + patchedSettings.Version.overrideWith(other.Version) + patchedSettings.VPN.overrideWith(other.VPN) + err = patchedSettings.Validate(allServers) + if err != nil { + return err + } + *s = patchedSettings + return nil +} + +func (s *Settings) SetDefaults() { + s.ControlServer.setDefaults() + s.DNS.setDefaults() + s.Firewall.setDefaults() + s.Health.SetDefaults() + s.HTTPProxy.setDefaults() + s.Log.setDefaults() + s.PublicIP.setDefaults() + s.Shadowsocks.setDefaults() + s.System.setDefaults() + s.Updater.setDefaults() + s.Version.setDefaults() + s.VPN.setDefaults() +} + +func (s Settings) String() string { + return s.toLinesNode().String() +} + +func (s Settings) toLinesNode() (node *gotree.Node) { + node = gotree.New("Settings summary:") + + node.AppendNode(s.VPN.toLinesNode()) + node.AppendNode(s.DNS.toLinesNode()) + node.AppendNode(s.Firewall.toLinesNode()) + node.AppendNode(s.Log.toLinesNode()) + node.AppendNode(s.Health.toLinesNode()) + node.AppendNode(s.Shadowsocks.toLinesNode()) + node.AppendNode(s.HTTPProxy.toLinesNode()) + node.AppendNode(s.ControlServer.toLinesNode()) + node.AppendNode(s.System.toLinesNode()) + node.AppendNode(s.PublicIP.toLinesNode()) + node.AppendNode(s.Updater.toLinesNode()) + node.AppendNode(s.Version.toLinesNode()) + + return node +} diff --git a/internal/configuration/settings/settings_test.go b/internal/configuration/settings/settings_test.go new file mode 100644 index 00000000..1f10ddad --- /dev/null +++ b/internal/configuration/settings/settings_test.go @@ -0,0 +1,101 @@ +package settings + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Settings_String(t *testing.T) { + t.Parallel() + + withDefaults := func(s Settings) Settings { + s.SetDefaults() + return s + } + + testCases := map[string]struct { + settings Settings + s string + }{ + "default settings": { + settings: withDefaults(Settings{}), + s: `Settings summary: +├── VPN settings: +| ├── VPN provider settings: +| | ├── Name: private internet access +| | └── Server selection settings: +| | ├── VPN type: openvpn +| | └── OpenVPN server selection settings: +| | ├── Protocol: UDP +| | └── Private Internet Access encryption preset: strong +| └── OpenVPN server selection settings: +| ├── OpenVPN version: 2.5 +| ├── User: [not set] +| ├── Password: [not set] +| ├── Private Internet Access encryption preset: strong +| ├── Tunnel IPv6: no +| ├── Network interface: tun0 +| ├── Run OpenVPN as: root +| └── Verbosity level: 1 +├── DNS settings: +| ├── DNS server address to use: 127.0.0.1 +| ├── Keep existing nameserver(s): no +| └── DNS over TLS settings: +| ├── Enabled: yes +| ├── Update period: every 24h0m0s +| ├── Unbound settings: +| | ├── Authoritative servers: +| | | └── Cloudflare +| | ├── Caching: yes +| | ├── IPv6: no +| | ├── Verbosity level: 1 +| | ├── Verbosity details level: 0 +| | ├── Validation log level: 0 +| | ├── System user: root +| | └── Allowed networks: +| | ├── 0.0.0.0/0 +| | └── ::/0 +| └── DNS filtering settings: +| ├── Block malicious: yes +| ├── Block ads: no +| └── Block surveillance: yes +├── Firewall settings: +| └── Enabled: yes +├── Log settings: +| └── Log level: INFO +├── Health settings: +| ├── Server listening address: 127.0.0.1:9999 +| ├── Address to ping: github.com +| └── VPN wait durations: +| ├── Initial duration: 6s +| └── Additional duration: 5s +├── Shadowsocks server settings: +| └── Enabled: no +├── HTTP proxy settings: +| └── Enabled: no +├── Control server settings: +| ├── Listening port: 8000 +| └── Logging: yes +├── OS Alpine settings: +| ├── Process UID: 1000 +| └── Process GID: 1000 +├── Public IP settings: +| ├── Fetching: every 12h0m0s +| └── IP file path: /tmp/gluetun/ip +└── Version settings: + └── Enabled: yes`, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + s := testCase.settings.String() + + assert.Equal(t, testCase.s, s) + }) + } +} diff --git a/internal/configuration/settings/shadowsocks.go b/internal/configuration/settings/shadowsocks.go new file mode 100644 index 00000000..4ad3364c --- /dev/null +++ b/internal/configuration/settings/shadowsocks.go @@ -0,0 +1,68 @@ +package settings + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" + "github.com/qdm12/ss-server/pkg/tcpudp" +) + +// Shadowsocks contains settings to configure the Shadowsocks server. +type Shadowsocks struct { + // Enabled is true if the server should be running. + // It defaults to false, and cannot be nil in the internal state. + Enabled *bool + // Settings are settings for the TCP+UDP server. + tcpudp.Settings +} + +func (s Shadowsocks) validate() (err error) { + return s.Settings.Validate() +} + +func (s *Shadowsocks) copy() (copied Shadowsocks) { + return Shadowsocks{ + Enabled: helpers.CopyBoolPtr(s.Enabled), + Settings: s.Settings.Copy(), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (s *Shadowsocks) mergeWith(other Shadowsocks) { + s.Enabled = helpers.MergeWithBool(s.Enabled, other.Enabled) + s.Settings.MergeWith(other.Settings) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (s *Shadowsocks) overrideWith(other Shadowsocks) { + s.Enabled = helpers.OverrideWithBool(s.Enabled, other.Enabled) + s.Settings.OverrideWith(other.Settings) +} + +func (s *Shadowsocks) setDefaults() { + s.Enabled = helpers.DefaultBool(s.Enabled, false) + s.Settings.SetDefaults() +} + +func (s Shadowsocks) String() string { + return s.toLinesNode().String() +} + +func (s Shadowsocks) toLinesNode() (node *gotree.Node) { + node = gotree.New("Shadowsocks server settings:") + + node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(s.Enabled)) + if !*s.Enabled { + return node + } + + // TODO have ToLinesNode in qdm12/ss-server + node.Appendf("Listening address: %s", s.Address) + node.Appendf("Cipher: %s", s.CipherName) + node.Appendf("Password: %s", helpers.ObfuscatePassword(*s.Password)) + node.Appendf("Log addresses: %s", helpers.BoolPtrToYesNo(s.LogAddresses)) + + return node +} diff --git a/internal/configuration/settings/surfshark_retro.go b/internal/configuration/settings/surfshark_retro.go new file mode 100644 index 00000000..5ce8a01b --- /dev/null +++ b/internal/configuration/settings/surfshark_retro.go @@ -0,0 +1,56 @@ +package settings + +import ( + "strings" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" +) + +func surfsharkRetroRegion(selection ServerSelection) ( + updatedSelection ServerSelection) { + locationData := constants.SurfsharkLocationData() + + retroToLocation := make(map[string]models.SurfsharkLocationData, len(locationData)) + for _, data := range locationData { + if data.RetroLoc == "" { + continue + } + retroToLocation[strings.ToLower(data.RetroLoc)] = data + } + + for i, region := range selection.Regions { + location, ok := retroToLocation[region] + if !ok { + continue + } + selection.Regions[i] = strings.ToLower(location.Region) + selection.Countries = append(selection.Countries, strings.ToLower(location.Country)) + selection.Cities = append(selection.Cities, strings.ToLower(location.City)) // even empty string + selection.Hostnames = append(selection.Hostnames, location.Hostname) + } + + selection.Regions = dedupSlice(selection.Regions) + selection.Countries = dedupSlice(selection.Countries) + selection.Cities = dedupSlice(selection.Cities) + selection.Hostnames = dedupSlice(selection.Hostnames) + + return selection +} + +func dedupSlice(slice []string) (deduped []string) { + if slice == nil { + return nil + } + + deduped = make([]string, 0, len(slice)) + seen := make(map[string]struct{}, len(slice)) + for _, s := range slice { + if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + deduped = append(deduped, s) + } + } + + return deduped +} diff --git a/internal/configuration/settings/system.go b/internal/configuration/settings/system.go new file mode 100644 index 00000000..1639cff3 --- /dev/null +++ b/internal/configuration/settings/system.go @@ -0,0 +1,61 @@ +package settings + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// System contains settings to configure system related elements. +type System struct { + PUID *uint16 + PGID *uint16 + Timezone string +} + +// Validate validates System settings. +func (s System) validate() (err error) { + return nil +} + +func (s *System) copy() (copied System) { + return System{ + PUID: helpers.CopyUint16Ptr(s.PUID), + PGID: helpers.CopyUint16Ptr(s.PGID), + Timezone: s.Timezone, + } +} + +func (s *System) mergeWith(other System) { + s.PUID = helpers.MergeWithUint16(s.PUID, other.PUID) + s.PGID = helpers.MergeWithUint16(s.PGID, other.PGID) + s.Timezone = helpers.MergeWithString(s.Timezone, other.Timezone) +} + +func (s *System) overrideWith(other System) { + s.PUID = helpers.OverrideWithUint16(s.PUID, other.PUID) + s.PGID = helpers.OverrideWithUint16(s.PGID, other.PGID) + s.Timezone = helpers.OverrideWithString(s.Timezone, other.Timezone) +} + +func (s *System) setDefaults() { + const defaultID = 1000 + s.PUID = helpers.DefaultUint16(s.PUID, defaultID) + s.PGID = helpers.DefaultUint16(s.PGID, defaultID) +} + +func (s System) String() string { + return s.toLinesNode().String() +} + +func (s System) toLinesNode() (node *gotree.Node) { + node = gotree.New("OS Alpine settings:") + + node.Appendf("Process UID: %d", *s.PUID) + node.Appendf("Process GID: %d", *s.PGID) + + if s.Timezone != "" { + node.Appendf("Timezone: %s", s.Timezone) + } + + return node +} diff --git a/internal/configuration/settings/unbound.go b/internal/configuration/settings/unbound.go new file mode 100644 index 00000000..52a779f8 --- /dev/null +++ b/internal/configuration/settings/unbound.go @@ -0,0 +1,193 @@ +package settings + +import ( + "errors" + "fmt" + "net" + + "github.com/qdm12/dns/pkg/provider" + "github.com/qdm12/dns/pkg/unbound" + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" + "inet.af/netaddr" +) + +// Unbound is settings for the Unbound program. +type Unbound struct { + Providers []string + Caching *bool + IPv6 *bool + VerbosityLevel *uint8 + VerbosityDetailsLevel *uint8 + ValidationLogLevel *uint8 + Username string + Allowed []netaddr.IPPrefix +} + +func (u *Unbound) setDefaults() { + if len(u.Providers) == 0 { + u.Providers = []string{ + provider.Cloudflare().String(), + } + } + + u.Caching = helpers.DefaultBool(u.Caching, true) + u.IPv6 = helpers.DefaultBool(u.IPv6, false) + + const defaultVerbosityLevel = 1 + u.VerbosityLevel = helpers.DefaultUint8(u.VerbosityLevel, defaultVerbosityLevel) + + const defaultVerbosityDetailsLevel = 0 + u.VerbosityDetailsLevel = helpers.DefaultUint8(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel) + + const defaultValidationLogLevel = 0 + u.ValidationLogLevel = helpers.DefaultUint8(u.ValidationLogLevel, defaultValidationLogLevel) + + if u.Allowed == nil { + u.Allowed = []netaddr.IPPrefix{ + netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0), + netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0), + } + } + + u.Username = helpers.DefaultString(u.Username, "root") +} + +var ( + ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid") + ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid") + ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid") +) + +func (u Unbound) validate() (err error) { + for _, s := range u.Providers { + _, err := provider.Parse(s) + if err != nil { + return err + } + } + + const maxVerbosityLevel = 5 + if *u.VerbosityLevel > maxVerbosityLevel { + return fmt.Errorf("%w: %d must be between 0 and %d", + ErrUnboundVerbosityLevelNotValid, + *u.VerbosityLevel, + maxVerbosityLevel) + } + + const maxVerbosityDetailsLevel = 4 + if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel { + return fmt.Errorf("%w: %d must be between 0 and %d", + ErrUnboundVerbosityDetailsLevelNotValid, + *u.VerbosityDetailsLevel, + maxVerbosityDetailsLevel) + } + + const maxValidationLogLevel = 2 + if *u.ValidationLogLevel > maxValidationLogLevel { + return fmt.Errorf("%w: %d must be between 0 and %d", + ErrUnboundValidationLogLevelNotValid, + *u.ValidationLogLevel, maxValidationLogLevel) + } + + return nil +} + +func (u Unbound) copy() (copied Unbound) { + return Unbound{ + Providers: helpers.CopyStringSlice(u.Providers), + Caching: helpers.CopyBoolPtr(u.Caching), + IPv6: helpers.CopyBoolPtr(u.IPv6), + VerbosityLevel: helpers.CopyUint8Ptr(u.VerbosityLevel), + VerbosityDetailsLevel: helpers.CopyUint8Ptr(u.VerbosityDetailsLevel), + ValidationLogLevel: helpers.CopyUint8Ptr(u.ValidationLogLevel), + Username: u.Username, + Allowed: helpers.CopyIPPrefixSlice(u.Allowed), + } +} + +func (u *Unbound) mergeWith(other Unbound) { + u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers) + u.Caching = helpers.MergeWithBool(u.Caching, other.Caching) + u.IPv6 = helpers.MergeWithBool(u.IPv6, other.IPv6) + u.VerbosityLevel = helpers.MergeWithUint8(u.VerbosityLevel, other.VerbosityLevel) + u.VerbosityDetailsLevel = helpers.MergeWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel) + u.ValidationLogLevel = helpers.MergeWithUint8(u.ValidationLogLevel, other.ValidationLogLevel) + u.Username = helpers.MergeWithString(u.Username, other.Username) + u.Allowed = helpers.MergeIPPrefixesSlices(u.Allowed, other.Allowed) +} + +func (u *Unbound) overrideWith(other Unbound) { + u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers) + u.Caching = helpers.OverrideWithBool(u.Caching, other.Caching) + u.IPv6 = helpers.OverrideWithBool(u.IPv6, other.IPv6) + u.VerbosityLevel = helpers.OverrideWithUint8(u.VerbosityLevel, other.VerbosityLevel) + u.VerbosityDetailsLevel = helpers.OverrideWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel) + u.ValidationLogLevel = helpers.OverrideWithUint8(u.ValidationLogLevel, other.ValidationLogLevel) + u.Username = helpers.OverrideWithString(u.Username, other.Username) + u.Allowed = helpers.OverrideWithIPPrefixesSlice(u.Allowed, other.Allowed) +} + +func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) { + providers := make([]provider.Provider, len(u.Providers)) + for i := range providers { + providers[i], err = provider.Parse(u.Providers[i]) + if err != nil { + return settings, err + } + } + + const port = 53 + + return unbound.Settings{ + ListeningPort: port, + IPv4: true, + Providers: providers, + Caching: *u.Caching, + IPv6: *u.IPv6, + VerbosityLevel: *u.VerbosityLevel, + VerbosityDetailsLevel: *u.VerbosityDetailsLevel, + ValidationLogLevel: *u.ValidationLogLevel, + AccessControl: unbound.AccessControlSettings{ + Allowed: u.Allowed, + }, + Username: u.Username, + }, nil +} + +func (u Unbound) GetFirstPlaintextIPv4() (ipv4 net.IP, err error) { + s := u.Providers[0] + provider, err := provider.Parse(s) + if err != nil { + return nil, err + } + + return provider.DNS().IPv4[0], nil +} + +func (u Unbound) String() string { + return u.toLinesNode().String() +} + +func (u Unbound) toLinesNode() (node *gotree.Node) { + node = gotree.New("Unbound settings:") + + authServers := node.Appendf("Authoritative servers:") + for _, provider := range u.Providers { + authServers.Appendf(provider) + } + + node.Appendf("Caching: %s", helpers.BoolPtrToYesNo(u.Caching)) + node.Appendf("IPv6: %s", helpers.BoolPtrToYesNo(u.IPv6)) + node.Appendf("Verbosity level: %d", *u.VerbosityLevel) + node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel) + node.Appendf("Validation log level: %d", *u.ValidationLogLevel) + node.Appendf("System user: %s", u.Username) + + allowedNetworks := node.Appendf("Allowed networks:") + for _, network := range u.Allowed { + allowedNetworks.Appendf(network.String()) + } + + return node +} diff --git a/internal/configuration/settings/unbound_test.go b/internal/configuration/settings/unbound_test.go new file mode 100644 index 00000000..35f4db0e --- /dev/null +++ b/internal/configuration/settings/unbound_test.go @@ -0,0 +1,43 @@ +package settings + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "inet.af/netaddr" +) + +func Test_Unbound_JSON(t *testing.T) { + t.Parallel() + + settings := Unbound{ + Providers: []string{"cloudflare"}, + Caching: boolPtr(true), + IPv6: boolPtr(false), + VerbosityLevel: uint8Ptr(1), + VerbosityDetailsLevel: nil, + ValidationLogLevel: uint8Ptr(0), + Username: "user", + Allowed: []netaddr.IPPrefix{ + netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0), + netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0), + }, + } + + b, err := json.Marshal(settings) + require.NoError(t, err) + + const expected = `{"Providers":["cloudflare"],"Caching":true,"IPv6":false,` + + `"VerbosityLevel":1,"VerbosityDetailsLevel":null,"ValidationLogLevel":0,` + + `"Username":"user","Allowed":["0.0.0.0/0","::/0"]}` + + assert.Equal(t, expected, string(b)) + + var resultSettings Unbound + err = json.Unmarshal(b, &resultSettings) + require.NoError(t, err) + + assert.Equal(t, settings, resultSettings) +} diff --git a/internal/configuration/settings/updater.go b/internal/configuration/settings/updater.go new file mode 100644 index 00000000..c906357c --- /dev/null +++ b/internal/configuration/settings/updater.go @@ -0,0 +1,113 @@ +package settings + +import ( + "fmt" + "net" + "strings" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gotree" +) + +// Updater contains settings to configure the VPN +// server information updater. +type Updater struct { + // Period is the period for which the updater + // should run. It can be set to 0 to disable the + // updater. It cannot be nil in the internal state. + // TODO change to value and add Enabled field. + Period *time.Duration + // DNSAddress is the DNS server address to use + // to resolve VPN server hostnames to IP addresses. + // It cannot be nil in the internal state. + DNSAddress net.IP + // Providers is the list of VPN service providers + // to update server information for. + Providers []string + // CLI is to precise the updater is running in CLI + // mode. This is set automatically and cannot be set + // by settings sources. It cannot be nil in the + // internal state. + CLI *bool +} + +func (u Updater) Validate() (err error) { + const minPeriod = time.Minute + if *u.Period > 0 && *u.Period < minPeriod { + return fmt.Errorf("%w: %d must be larger than %s", + ErrUpdaterPeriodTooSmall, *u.Period, minPeriod) + } + + for i, provider := range u.Providers { + valid := false + for _, validProvider := range constants.AllProviders() { + if provider == validProvider { + valid = true + break + } + } + if !valid { + return fmt.Errorf("%w: %s at index %d", + ErrVPNProviderNameNotValid, provider, i) + } + } + + return nil +} + +func (u *Updater) copy() (copied Updater) { + return Updater{ + Period: helpers.CopyDurationPtr(u.Period), + DNSAddress: helpers.CopyIP(u.DNSAddress), + Providers: helpers.CopyStringSlice(u.Providers), + CLI: u.CLI, + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (u *Updater) mergeWith(other Updater) { + u.Period = helpers.MergeWithDuration(u.Period, other.Period) + u.DNSAddress = helpers.MergeWithIP(u.DNSAddress, other.DNSAddress) + u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers) + u.CLI = helpers.MergeWithBool(u.CLI, other.CLI) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (u *Updater) overrideWith(other Updater) { + u.Period = helpers.OverrideWithDuration(u.Period, other.Period) + u.DNSAddress = helpers.OverrideWithIP(u.DNSAddress, other.DNSAddress) + u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers) + u.CLI = helpers.MergeWithBool(u.CLI, other.CLI) +} + +func (u *Updater) setDefaults() { + u.Period = helpers.DefaultDuration(u.Period, 0) + u.DNSAddress = helpers.DefaultIP(u.DNSAddress, net.IPv4(1, 1, 1, 1)) + u.CLI = helpers.DefaultBool(u.CLI, false) +} + +func (u Updater) String() string { + return u.toLinesNode().String() +} + +func (u Updater) toLinesNode() (node *gotree.Node) { + if *u.Period == 0 { + return nil + } + + node = gotree.New("Server data updater settings:") + node.Appendf("Update period: %s", *u.Period) + node.Appendf("DNS address: %s", u.DNSAddress) + node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", ")) + + if *u.CLI { + node.Appendf("CLI mode: enabled") + } + + return node +} diff --git a/internal/configuration/settings/version.go b/internal/configuration/settings/version.go new file mode 100644 index 00000000..8f481f6d --- /dev/null +++ b/internal/configuration/settings/version.go @@ -0,0 +1,53 @@ +package settings + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gotree" +) + +// Version contains settings to configure the version +// information fetcher. +type Version struct { + // Enabled is true if the version information should + // be fetched from Github. + Enabled *bool +} + +func (v Version) validate() (err error) { + return nil +} + +func (v *Version) copy() (copied Version) { + return Version{ + Enabled: helpers.CopyBoolPtr(v.Enabled), + } +} + +// mergeWith merges the other settings into any +// unset field of the receiver settings object. +func (v *Version) mergeWith(other Version) { + v.Enabled = helpers.MergeWithBool(v.Enabled, other.Enabled) +} + +// overrideWith overrides fields of the receiver +// settings object with any field set in the other +// settings. +func (v *Version) overrideWith(other Version) { + v.Enabled = helpers.OverrideWithBool(v.Enabled, other.Enabled) +} + +func (v *Version) setDefaults() { + v.Enabled = helpers.DefaultBool(v.Enabled, true) +} + +func (v Version) String() string { + return v.toLinesNode().String() +} + +func (v Version) toLinesNode() (node *gotree.Node) { + node = gotree.New("Version settings:") + + node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(v.Enabled)) + + return node +} diff --git a/internal/configuration/settings/vpn.go b/internal/configuration/settings/vpn.go new file mode 100644 index 00000000..71bd62bc --- /dev/null +++ b/internal/configuration/settings/vpn.go @@ -0,0 +1,98 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gotree" +) + +type VPN struct { + // Type is the VPN type and can only be + // 'openvpn' or 'wireguard'. It cannot be the + // empty string in the internal state. + Type string + Provider Provider + OpenVPN OpenVPN + Wireguard Wireguard +} + +// Validate validates VPN settings. +func (v VPN) validate(allServers models.AllServers) (err error) { + // Validate Type + validVPNTypes := []string{constants.OpenVPN, constants.Wireguard} + if !helpers.IsOneOf(v.Type, validVPNTypes...) { + return fmt.Errorf("%w: %q and can only be one of %s", + ErrVPNTypeNotValid, v.Type, strings.Join(validVPNTypes, ", ")) + } + + err = v.Provider.validate(v.Type, allServers) + if err != nil { + return fmt.Errorf("provider settings validation failed: %w", err) + } + + if v.Type == constants.OpenVPN { + err := v.OpenVPN.validate(*v.Provider.Name) + if err != nil { + return fmt.Errorf("OpenVPN settings validation failed: %w", err) + } + } else { + err := v.Wireguard.validate(*v.Provider.Name) + if err != nil { + return fmt.Errorf("Wireguard settings validation failed: %w", err) + } + } + + return nil +} + +func (v *VPN) copy() (copied VPN) { + return VPN{ + Type: v.Type, + Provider: v.Provider.copy(), + OpenVPN: v.OpenVPN.copy(), + Wireguard: v.Wireguard.copy(), + } +} + +func (v *VPN) mergeWith(other VPN) { + v.Type = helpers.MergeWithString(v.Type, other.Type) + v.Provider.mergeWith(other.Provider) + v.OpenVPN.mergeWith(other.OpenVPN) + v.Wireguard.mergeWith(other.Wireguard) +} + +func (v *VPN) overrideWith(other VPN) { + v.Type = helpers.OverrideWithString(v.Type, other.Type) + v.Provider.overrideWith(other.Provider) + v.OpenVPN.overrideWith(other.OpenVPN) + v.Wireguard.overrideWith(other.Wireguard) +} + +func (v *VPN) setDefaults() { + v.Type = helpers.DefaultString(v.Type, constants.OpenVPN) + v.Provider.setDefaults() + v.OpenVPN.setDefaults(*v.Provider.Name) + v.Wireguard.setDefaults() +} + +func (v VPN) String() string { + return v.toLinesNode().String() +} + +func (v VPN) toLinesNode() (node *gotree.Node) { + node = gotree.New("VPN settings:") + + node.AppendNode(v.Provider.toLinesNode()) + + if v.Type == constants.OpenVPN { + node.AppendNode(v.OpenVPN.toLinesNode()) + } else { + node.AppendNode(v.Wireguard.toLinesNode()) + } + + return node +} diff --git a/internal/configuration/settings/wireguard.go b/internal/configuration/settings/wireguard.go new file mode 100644 index 00000000..9e1cc854 --- /dev/null +++ b/internal/configuration/settings/wireguard.go @@ -0,0 +1,138 @@ +package settings + +import ( + "fmt" + "net" + "regexp" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gotree" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +// Wireguard contains settings to configure the Wireguard client. +type Wireguard struct { + // PrivateKey is the Wireguard client peer private key. + // It cannot be nil in the internal state. + PrivateKey *string + // PreSharedKey is the Wireguard pre-shared key. + // It can be the empty string to indicate there + // is no pre-shared key. + // It cannot be nil in the internal state. + PreSharedKey *string + // Addresses are the Wireguard interface addresses. + Addresses []net.IPNet + // Interface is the name of the Wireguard interface + // to create. It cannot be the empty string in the + // internal state. + Interface string +} + +var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + +// Validate validates Wireguard settings. +// It should only be ran if the VPN type chosen is Wireguard. +func (w Wireguard) validate(vpnProvider string) (err error) { + if !helpers.IsOneOf(vpnProvider, + constants.Custom, + constants.Ivpn, + constants.Mullvad, + constants.Windscribe, + ) { + // do not validate for VPN provider not supporting Wireguard + return nil + } + + // Validate PrivateKey + if *w.PrivateKey == "" { + return ErrWireguardPrivateKeyNotSet + } + _, err = wgtypes.ParseKey(*w.PrivateKey) + if err != nil { + return fmt.Errorf("%w: %s", ErrWireguardPrivateKeyNotValid, err) + } + + // Validate PreSharedKey + if *w.PreSharedKey != "" { // Note: this is optional + _, err = wgtypes.ParseKey(*w.PreSharedKey) + if err != nil { + return fmt.Errorf("%w: %s", ErrWireguardPreSharedKeyNotValid, err) + } + } + + // Validate Addresses + if len(w.Addresses) == 0 { + return ErrWireguardInterfaceAddressNotSet + } + for i, ipNet := range w.Addresses { + if ipNet.IP == nil || ipNet.Mask == nil { + return fmt.Errorf("%w: for address at index %d: %s", + ErrWireguardInterfaceAddressNotSet, i, ipNet.String()) + } + } + + // Validate interface + if !regexpInterfaceName.MatchString(w.Interface) { + return fmt.Errorf("%w: '%s' does not match regex '%s'", + ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName) + } + + return nil +} + +func (w *Wireguard) copy() (copied Wireguard) { + return Wireguard{ + PrivateKey: helpers.CopyStringPtr(w.PrivateKey), + PreSharedKey: helpers.CopyStringPtr(w.PreSharedKey), + Addresses: helpers.CopyIPNetSlice(w.Addresses), + Interface: w.Interface, + } +} + +func (w *Wireguard) mergeWith(other Wireguard) { + w.PrivateKey = helpers.MergeWithStringPtr(w.PrivateKey, other.PrivateKey) + w.PreSharedKey = helpers.MergeWithStringPtr(w.PreSharedKey, other.PreSharedKey) + w.Addresses = helpers.MergeIPNetsSlices(w.Addresses, other.Addresses) + w.Interface = helpers.MergeWithString(w.Interface, other.Interface) +} + +func (w *Wireguard) overrideWith(other Wireguard) { + w.PrivateKey = helpers.OverrideWithStringPtr(w.PrivateKey, other.PrivateKey) + w.PreSharedKey = helpers.OverrideWithStringPtr(w.PreSharedKey, other.PreSharedKey) + w.Addresses = helpers.OverrideWithIPNetsSlice(w.Addresses, other.Addresses) + w.Interface = helpers.OverrideWithString(w.Interface, other.Interface) +} + +func (w *Wireguard) setDefaults() { + w.PrivateKey = helpers.DefaultStringPtr(w.PrivateKey, "") + w.PreSharedKey = helpers.DefaultStringPtr(w.PreSharedKey, "") + w.Interface = helpers.DefaultString(w.Interface, "wg0") +} + +func (w Wireguard) String() string { + return w.toLinesNode().String() +} + +func (w Wireguard) toLinesNode() (node *gotree.Node) { + node = gotree.New("Wireguard settings:") + + if *w.PrivateKey != "" { + s := helpers.ObfuscateWireguardKey(*w.PrivateKey) + node.Appendf("Private key: %s", s) + } + + if *w.PreSharedKey != "" { + s := helpers.ObfuscateWireguardKey(*w.PreSharedKey) + node.Appendf("Pre-shared key: %s", s) + } + + addressesNode := node.Appendf("Interface addresses:") + for _, address := range w.Addresses { + addressesNode.Appendf(address.String()) + } + + node.Appendf("Network interface: %s", w.Interface) + + return node +} diff --git a/internal/configuration/settings/wireguardselection.go b/internal/configuration/settings/wireguardselection.go new file mode 100644 index 00000000..9d611848 --- /dev/null +++ b/internal/configuration/settings/wireguardselection.go @@ -0,0 +1,144 @@ +package settings + +import ( + "fmt" + "net" + + "github.com/qdm12/gluetun/internal/configuration/settings/helpers" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gotree" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WireguardSelection struct { + // EndpointIP is the server endpoint IP address. + // It is only used with VPN providers generating Wireguard + // configurations specific to each server and user. + // To indicate it should not be used, it should be set + // to the empty net.IP{} slice. It can never be nil + // in the internal state. + EndpointIP net.IP + // EndpointPort is a the server port to use for the VPN server. + // It is optional for VPN providers IVPN, Mullvad + // and Windscribe, and compulsory for the others. + // When optional, it can be set to 0 to indicate not use + // a custom endpoint port. It cannot be nil in the internal + // state. + EndpointPort *uint16 + // PublicKey is the server public key. + // It is only used with VPN providers generating Wireguard + // configurations specific to each server and user. + PublicKey string +} + +// Validate validates WireguardSelection settings. +// It should only be ran if the VPN type chosen is Wireguard. +func (w WireguardSelection) validate(vpnProvider string) (err error) { + // Validate EndpointIP + switch vpnProvider { + case constants.Ivpn, constants.Mullvad, constants.Windscribe: // endpoint IP addresses are baked in + case constants.Custom: + if len(w.EndpointIP) == 0 { + return ErrWireguardEndpointIPNotSet + } + default: // Providers not supporting Wireguard + } + + // Validate EndpointPort + switch vpnProvider { + // EndpointPort is required + case constants.Custom: + if *w.EndpointPort == 0 { + return ErrWireguardEndpointPortNotSet + } + case constants.Ivpn, constants.Mullvad, constants.Windscribe: + // EndpointPort is optional and can be 0 + if *w.EndpointPort == 0 { + break // no custom endpoint port set + } + if vpnProvider == constants.Mullvad { + break // no restriction on custom endpoint port value + } + var allowed []uint16 + switch vpnProvider { + case constants.Ivpn: + allowed = []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237} + case constants.Windscribe: + allowed = []uint16{53, 80, 123, 443, 1194, 65142} + } + + if helpers.Uint16IsOneOf(*w.EndpointPort, allowed) { + break + } + return fmt.Errorf("%w: %d for VPN service provider %s; %s", + ErrWireguardEndpointPortNotAllowed, w.EndpointPort, vpnProvider, + helpers.PortChoicesOrString(allowed)) + default: // Providers not supporting Wireguard + } + + // Validate PublicKey + switch vpnProvider { + case constants.Ivpn, constants.Mullvad, constants.Windscribe: // public keys are baked in + case constants.Custom: + if w.PublicKey == "" { + return ErrWireguardPublicKeyNotSet + } + default: // Providers not supporting Wireguard + } + if w.PublicKey != "" { + _, err := wgtypes.ParseKey(w.PublicKey) + if err != nil { + return fmt.Errorf("%w: %s: %s", + ErrWireguardPublicKeyNotValid, w.PublicKey, err) + } + } + + return nil +} + +func (w *WireguardSelection) copy() (copied WireguardSelection) { + return WireguardSelection{ + EndpointIP: helpers.CopyIP(w.EndpointIP), + EndpointPort: helpers.CopyUint16Ptr(w.EndpointPort), + PublicKey: w.PublicKey, + } +} + +func (w *WireguardSelection) mergeWith(other WireguardSelection) { + w.EndpointIP = helpers.MergeWithIP(w.EndpointIP, other.EndpointIP) + w.EndpointPort = helpers.MergeWithUint16(w.EndpointPort, other.EndpointPort) + w.PublicKey = helpers.MergeWithString(w.PublicKey, other.PublicKey) +} + +func (w *WireguardSelection) overrideWith(other WireguardSelection) { + w.EndpointIP = helpers.OverrideWithIP(w.EndpointIP, other.EndpointIP) + w.EndpointPort = helpers.OverrideWithUint16(w.EndpointPort, other.EndpointPort) + w.PublicKey = helpers.OverrideWithString(w.PublicKey, other.PublicKey) +} + +func (w *WireguardSelection) setDefaults() { + w.EndpointIP = helpers.DefaultIP(w.EndpointIP, net.IP{}) + w.EndpointPort = helpers.DefaultUint16(w.EndpointPort, 0) +} + +func (w WireguardSelection) String() string { + return w.toLinesNode().String() +} + +func (w WireguardSelection) toLinesNode() (node *gotree.Node) { + node = gotree.New("Wireguard selection settings:") + + if len(w.EndpointIP) > 0 { + node.Appendf("Endpoint IP address: %s", w.EndpointIP) + } + + if *w.EndpointPort != 0 { + node.Appendf("Endpoint port: %d", *w.EndpointPort) + } + + if w.PublicKey != "" { + node.Appendf("Server public key: %s", w.PublicKey) + } + + return node +} diff --git a/internal/configuration/settings_test.go b/internal/configuration/settings_test.go deleted file mode 100644 index 7ad59490..00000000 --- a/internal/configuration/settings_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package configuration - -import ( - "testing" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/stretchr/testify/assert" -) - -func Test_Settings_lines(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - settings Settings - lines []string - }{ - "default settings": { - settings: Settings{ - VPN: VPN{ - Type: constants.OpenVPN, - Provider: Provider{ - Name: constants.Mullvad, - ServerSelection: ServerSelection{ - VPN: constants.OpenVPN, - }, - }, - OpenVPN: OpenVPN{ - Version: constants.Openvpn25, - Interface: "tun", - }, - }, - }, - lines: []string{ - "Settings summary below:", - "|--VPN:", - " |--Type: openvpn", - " |--OpenVPN:", - " |--Version: 2.5", - " |--Verbosity level: 0", - " |--Network interface: tun", - " |--Mullvad settings:", - " |--OpenVPN selection:", - " |--Protocol: udp", - "|--DNS:", - "|--Firewall: disabled ⚠️", - "|--Log:", - " |--Level: DEBUG", - "|--System:", - " |--Process user ID: 0", - " |--Process group ID: 0", - " |--Timezone: NOT SET ⚠️ - it can cause time related issues", - "|--Health:", - " |--Server address: ", - " |--Address to ping: ", - " |--VPN:", - " |--Initial duration: 0s", - "|--HTTP control server:", - " |--Listening port: 0", - "|--Public IP getter: disabled", - }, - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - lines := testCase.settings.lines() - - assert.Equal(t, testCase.lines, lines) - }) - } -} diff --git a/internal/configuration/shadowsocks.go b/internal/configuration/shadowsocks.go deleted file mode 100644 index ab68eb47..00000000 --- a/internal/configuration/shadowsocks.go +++ /dev/null @@ -1,109 +0,0 @@ -package configuration - -import ( - "fmt" - "strings" - - "github.com/qdm12/golibs/params" - "github.com/qdm12/ss-server/pkg/tcpudp" -) - -// ShadowSocks contains settings to configure the Shadowsocks server. -type ShadowSocks struct { - Enabled bool - tcpudp.Settings -} - -func (settings *ShadowSocks) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *ShadowSocks) lines() (lines []string) { - if !settings.Enabled { - return nil - } - - lines = append(lines, lastIndent+"Shadowsocks server:") - - lines = append(lines, indent+lastIndent+"Listening address: "+settings.Address) - - lines = append(lines, indent+lastIndent+"Cipher: "+settings.CipherName) - - if settings.LogAddresses { - lines = append(lines, indent+lastIndent+"Log addresses: enabled") - } - - return lines -} - -func (settings *ShadowSocks) read(r reader) (err error) { - settings.Enabled, err = r.env.OnOff("SHADOWSOCKS", params.Default("off")) - if !settings.Enabled { - return nil - } else if err != nil { - return fmt.Errorf("environment variable SHADOWSOCKS: %w", err) - } - - settings.Password, err = r.getFromEnvOrSecretFile("SHADOWSOCKS_PASSWORD", settings.Enabled, nil) - if err != nil { - return err - } - - settings.LogAddresses, err = r.env.OnOff("SHADOWSOCKS_LOG", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable SHADOWSOCKS_LOG: %w", err) - } - - settings.CipherName, err = r.env.Get("SHADOWSOCKS_CIPHER", params.Default("chacha20-ietf-poly1305"), - params.RetroKeys([]string{"SHADOWSOCKS_METHOD"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable SHADOWSOCKS_CIPHER (or SHADOWSOCKS_METHOD): %w", err) - } - - warning, err := settings.getAddress(r.env) - if warning != "" { - r.warner.Warn(warning) - } - if err != nil { - return err - } - - return nil -} - -func (settings *ShadowSocks) getAddress(env params.Interface) ( - warning string, err error) { - address, err := env.Get("SHADOWSOCKS_LISTENING_ADDRESS") - if err != nil { - return "", fmt.Errorf("environment variable SHADOWSOCKS_LISTENING_ADDRESS: %w", err) - } - - if address != "" { - address, warning, err := env.ListeningAddress("SHADOWSOCKS_LISTENING_ADDRESS") - if err != nil { - return "", fmt.Errorf("environment variable SHADOWSOCKS_LISTENING_ADDRESS: %w", err) - } - settings.Address = address - return warning, nil - } - - // Retro-compatibility - const retroWarning = "You are using the old environment variable " + - "SHADOWSOCKS_PORT, please consider using " + - "SHADOWSOCKS_LISTENING_ADDRESS instead" - portStr, err := env.Get("SHADOWSOCKS_PORT") - if err != nil { - return retroWarning, fmt.Errorf("environment variable SHADOWSOCKS_PORT: %w", err) - } else if portStr != "" { - port, _, err := env.ListeningPort("SHADOWSOCKS_PORT") - if err != nil { - return retroWarning, fmt.Errorf("environment variable SHADOWSOCKS_PORT: %w", err) - } - settings.Address = ":" + fmt.Sprint(port) - return retroWarning, nil - } - - // Default value - settings.Address = ":8388" - return "", nil -} diff --git a/internal/configuration/sources/env/dns.go b/internal/configuration/sources/env/dns.go new file mode 100644 index 00000000..df88ef87 --- /dev/null +++ b/internal/configuration/sources/env/dns.go @@ -0,0 +1,51 @@ +package env + +import ( + "fmt" + "net" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readDNS() (dns settings.DNS, err error) { + dns.ServerAddress, err = r.readDNSServerAddress() + if err != nil { + return dns, err + } + + dns.KeepNameserver, err = envToBoolPtr("DNS_KEEP_NAMESERVER") + if err != nil { + return dns, fmt.Errorf("environment variable DNS_KEEP_NAMESERVER: %w", err) + } + + dns.DoT, err = r.readDoT() + if err != nil { + return dns, fmt.Errorf("cannot read DoT settings: %w", err) + } + + return dns, nil +} + +func (r *Reader) readDNSServerAddress() (address net.IP, err error) { + s := os.Getenv("DNS_PLAINTEXT_ADDRESS") + if s == "" { + return nil, nil + } + + address = net.ParseIP(s) + if address == nil { + return nil, fmt.Errorf("environment variable DNS_PLAINTEXT_ADDRESS: %w: %s", ErrIPAddressParse, s) + } + + // TODO remove in v4 + if !address.Equal(net.IPv4(127, 0, 0, 1)) { //nolint:gomnd + r.warner.Warn("DNS_PLAINTEXT_ADDRESS is set to " + s + + " so the DNS over TLS (DoT) server will not be used." + + " The default value changed to 127.0.0.1 so it uses the internal DoT server." + + " If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" + + " corresponding to the first DoT provider chosen is used.") + } + + return address, nil +} diff --git a/internal/configuration/sources/env/dnsblacklist.go b/internal/configuration/sources/env/dnsblacklist.go new file mode 100644 index 00000000..b8571a2e --- /dev/null +++ b/internal/configuration/sources/env/dnsblacklist.go @@ -0,0 +1,90 @@ +package env + +import ( + "errors" + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "inet.af/netaddr" +) + +func (r *Reader) readDNSBlacklist() (blacklist settings.DNSBlacklist, err error) { + blacklist.BlockMalicious, err = envToBoolPtr("BLOCK_MALICIOUS") + if err != nil { + return blacklist, fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err) + } + + blacklist.BlockSurveillance, err = r.readBlockSurveillance() + if err != nil { + return blacklist, fmt.Errorf("environment variable BLOCK_MALICIOUS: %w", err) + } + + blacklist.BlockAds, err = envToBoolPtr("BLOCK_ADS") + if err != nil { + return blacklist, fmt.Errorf("environment variable BLOCK_ADS: %w", err) + } + + blacklist.AddBlockedIPs, blacklist.AddBlockedIPPrefixes, + err = readDoTPrivateAddresses() // TODO v4 split in 2 + if err != nil { + return blacklist, err + } + + blacklist.AllowedHosts = envToCSV("UNBLOCK") // TODO v4 change name + + return blacklist, nil +} + +func (r *Reader) readBlockSurveillance() (blocked *bool, err error) { + blocked, err = envToBoolPtr("BLOCK_SURVEILLANCE") + if err != nil { + return nil, fmt.Errorf("environment variable BLOCK_SURVEILLANCE: %w", err) + } else if blocked != nil { + return blocked, nil + } + + blocked, err = envToBoolPtr("BLOCK_NSA") + if err != nil { + return nil, fmt.Errorf("environment variable BLOCK_NSA: %w", err) + } else if blocked != nil { + r.onRetroActive("BLOCK_NSA", "BLOCK_SURVEILLANCE") + return blocked, nil + } + + return nil, nil //nolint:nilnil +} + +var ( + ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range") +) + +func readDoTPrivateAddresses() (ips []netaddr.IP, + ipPrefixes []netaddr.IPPrefix, err error) { + privateAddresses := envToCSV("DOT_PRIVATE_ADDRESS") + if len(privateAddresses) == 0 { + return nil, nil, nil + } + + ips = make([]netaddr.IP, 0, len(privateAddresses)) + ipPrefixes = make([]netaddr.IPPrefix, 0, len(privateAddresses)) + + for _, privateAddress := range privateAddresses { + ip, err := netaddr.ParseIP(privateAddress) + if err == nil { + ips = append(ips, ip) + continue + } + + ipPrefix, err := netaddr.ParseIPPrefix(privateAddress) + if err == nil { + ipPrefixes = append(ipPrefixes, ipPrefix) + continue + } + + return nil, nil, fmt.Errorf( + "environment variable DOT_PRIVATE_ADDRESS: %w: %s", + ErrPrivateAddressNotValid, privateAddress) + } + + return ips, ipPrefixes, nil +} diff --git a/internal/configuration/sources/env/dot.go b/internal/configuration/sources/env/dot.go new file mode 100644 index 00000000..273f9857 --- /dev/null +++ b/internal/configuration/sources/env/dot.go @@ -0,0 +1,31 @@ +package env + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readDoT() (dot settings.DoT, err error) { + dot.Enabled, err = envToBoolPtr("DOT") + if err != nil { + return dot, fmt.Errorf("environment variable DOT: %w", err) + } + + dot.UpdatePeriod, err = envToDurationPtr("DNS_UPDATE_PERIOD") + if err != nil { + return dot, fmt.Errorf("environment variable DNS_UPDATE_PERIOD: %w", err) + } + + dot.Unbound, err = readUnbound() + if err != nil { + return dot, err + } + + dot.Blacklist, err = r.readDNSBlacklist() + if err != nil { + return dot, err + } + + return dot, nil +} diff --git a/internal/configuration/sources/env/firewall.go b/internal/configuration/sources/env/firewall.go new file mode 100644 index 00000000..2ae038af --- /dev/null +++ b/internal/configuration/sources/env/firewall.go @@ -0,0 +1,96 @@ +package env + +import ( + "errors" + "fmt" + "net" + "strconv" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readFirewall() (firewall settings.Firewall, err error) { + vpnInputPortStrings := envToCSV("FIREWALL_VPN_INPUT_PORTS") + firewall.VPNInputPorts, err = stringsToPorts(vpnInputPortStrings) + if err != nil { + return firewall, fmt.Errorf("environment variable FIREWALL_VPN_INPUT_PORTS: %w", err) + } + + inputPortStrings := envToCSV("FIREWALL_INPUT_PORTS") + firewall.InputPorts, err = stringsToPorts(inputPortStrings) + if err != nil { + return firewall, fmt.Errorf("environment variable FIREWALL_INPUT_PORTS: %w", err) + } + + outboundSubnetsKey := "FIREWALL_OUTBOUND_SUBNETS" + outboundSubnetStrings := envToCSV(outboundSubnetsKey) + if len(outboundSubnetStrings) == 0 { + // Retro-compatibility + outboundSubnetStrings = envToCSV("EXTRA_SUBNETS") + if len(outboundSubnetStrings) > 0 { + outboundSubnetsKey = "EXTRA_SUBNETS" + r.onRetroActive("EXTRA_SUBNETS", "FIREWALL_OUTBOUND_SUBNETS") + } + } + firewall.OutboundSubnets, err = stringsToIPNets(outboundSubnetStrings) + if err != nil { + return firewall, fmt.Errorf("environment variable %s: %w", outboundSubnetsKey, err) + } + + firewall.Enabled, err = envToBoolPtr("FIREWALL") + if err != nil { + return firewall, fmt.Errorf("environment variable FIREWALL: %w", err) + } + + firewall.Debug, err = envToBoolPtr("FIREWALL_DEBUG") + if err != nil { + return firewall, fmt.Errorf("environment variable FIREWALL_DEBUG: %w", err) + } + + return firewall, nil +} + +var ( + ErrPortParsing = errors.New("cannot parse port") + ErrPortValue = errors.New("port value is not valid") +) + +func stringsToPorts(ss []string) (ports []uint16, err error) { + if len(ss) == 0 { + return nil, nil + } + ports = make([]uint16, len(ss)) + for i, s := range ss { + port, err := strconv.Atoi(s) + if err != nil { + return nil, fmt.Errorf("%w: %s: %s", + ErrPortParsing, s, err) + } else if port < 1 || port > 2^16 { + return nil, fmt.Errorf("%w: must be between 1 and 65535: %d", + ErrPortValue, port) + } + ports[i] = uint16(port) + } + return ports, nil +} + +var ( + ErrIPNetParsing = errors.New("cannot parse IP network") +) + +func stringsToIPNets(ss []string) (ipNets []net.IPNet, err error) { + if len(ss) == 0 { + return nil, nil + } + ipNets = make([]net.IPNet, len(ss)) + for i, s := range ss { + ip, ipNet, err := net.ParseCIDR(s) + if err != nil { + return nil, fmt.Errorf("%w: %s: %s", + ErrIPNetParsing, s, err) + } + ipNet.IP = ip + ipNets[i] = *ipNet + } + return ipNets, nil +} diff --git a/internal/configuration/sources/env/health.go b/internal/configuration/sources/env/health.go new file mode 100644 index 00000000..e5ec08a9 --- /dev/null +++ b/internal/configuration/sources/env/health.go @@ -0,0 +1,52 @@ +package env + +import ( + "fmt" + "os" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) ReadHealth() (health settings.Health, err error) { + health.ServerAddress = os.Getenv("HEALTH_SERVER_ADDRESS") + health.AddressToPing = os.Getenv("HEALTH_ADDRESS_TO_PING") + + health.VPN.Initial, err = r.readDurationWithRetro( + "HEALTH_VPN_DURATION_INITIAL", + "HEALTH_OPENVPN_DURATION_INITIAL") + if err != nil { + return health, err + } + + health.VPN.Initial, err = r.readDurationWithRetro( + "HEALTH_VPN_DURATION_ADDITION", + "HEALTH_OPENVPN_DURATION_ADDITION") + if err != nil { + return health, err + } + + return health, nil +} + +func (r *Reader) readDurationWithRetro(envKey, retroEnvKey string) (d *time.Duration, err error) { + s := os.Getenv(envKey) + if s == "" { + s = os.Getenv(retroEnvKey) + if s == "" { + return nil, nil //nolint:nilnil + } + r.onRetroActive(envKey, retroEnvKey) + envKey = retroEnvKey + } + + d = new(time.Duration) + *d, err = time.ParseDuration(s) + if err != nil { + return nil, fmt.Errorf( + "environment variable %s: %w", + envKey, err) + } + + return d, nil +} diff --git a/internal/configuration/sources/env/helpers.go b/internal/configuration/sources/env/helpers.go new file mode 100644 index 00000000..72502709 --- /dev/null +++ b/internal/configuration/sources/env/helpers.go @@ -0,0 +1,134 @@ +package env + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/qdm12/govalid/binary" + "github.com/qdm12/govalid/integer" +) + +func envToCSV(envKey string) (values []string) { + csv := os.Getenv(envKey) + if csv == "" { + return nil + } + return lowerAndSplit(csv) +} + +func envToStringPtr(envKey string) (stringPtr *string) { + s := os.Getenv(envKey) + if s == "" { + return nil + } + return &s +} + +func envToBoolPtr(envKey string) (boolPtr *bool, err error) { + s := os.Getenv(envKey) + if s == "" { + return nil, nil //nolint:nilnil + } + value, err := binary.Validate(s) + if err != nil { + return nil, err + } + return &value, nil +} + +func envToIntPtr(envKey string) (intPtr *int, err error) { + s := os.Getenv(envKey) + if s == "" { + return nil, nil //nolint:nilnil + } + value, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + return &value, nil +} + +func envToUint8Ptr(envKey string) (uint8Ptr *uint8, err error) { + s := os.Getenv(envKey) + if s == "" { + return nil, nil //nolint:nilnil + } + + const min, max = 0, 255 + value, err := integer.Validate(s, integer.OptionRange(min, max)) + if err != nil { + return nil, err + } + + uint8Ptr = new(uint8) + *uint8Ptr = uint8(value) + return uint8Ptr, nil +} + +func envToUint16Ptr(envKey string) (uint16Ptr *uint16, err error) { + s := os.Getenv(envKey) + if s == "" { + return nil, nil //nolint:nilnil + } + + const min, max = 0, 65535 + value, err := integer.Validate(s, integer.OptionRange(min, max)) + if err != nil { + return nil, err + } + + uint16Ptr = new(uint16) + *uint16Ptr = uint16(value) + return uint16Ptr, nil +} + +func envToDurationPtr(envKey string) (durationPtr *time.Duration, err error) { + s := os.Getenv(envKey) + if s == "" { + return nil, nil //nolint:nilnil + } + + durationPtr = new(time.Duration) + *durationPtr, err = time.ParseDuration(s) + if err != nil { + return nil, err + } + + return durationPtr, nil +} + +func lowerAndSplit(csv string) (values []string) { + csv = strings.ToLower(csv) + return strings.Split(csv, ",") +} + +var ErrDecodeBase64 = errors.New("cannot decode base64 string") + +func decodeBase64(b64String string) (decoded string, err error) { + b, err := base64.StdEncoding.DecodeString(b64String) + if err != nil { + return "", fmt.Errorf("%w: %s: %s", + ErrDecodeBase64, b64String, err) + } + return string(b), nil +} + +func unsetEnvKeys(envKeys []string, err error) (newErr error) { + newErr = err + for _, envKey := range envKeys { + unsetErr := os.Unsetenv(envKey) + if unsetErr != nil && newErr == nil { + newErr = fmt.Errorf("cannot unset environment variable %s: %w", envKey, unsetErr) + } + } + return newErr +} + +func stringPtr(s string) *string { return &s } +func uint16Ptr(n uint16) *uint16 { return &n } +func boolPtr(b bool) *bool { return &b } diff --git a/internal/configuration/sources/env/httproxy.go b/internal/configuration/sources/env/httproxy.go new file mode 100644 index 00000000..b7b87b41 --- /dev/null +++ b/internal/configuration/sources/env/httproxy.go @@ -0,0 +1,186 @@ +package env + +import ( + "fmt" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/govalid/binary" +) + +func (r *Reader) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) { + httpProxy.User = r.readHTTProxyUser() + httpProxy.Password = r.readHTTProxyPassword() + httpProxy.ListeningAddress = r.readHTTProxyListeningAddress() + + httpProxy.Enabled, err = r.readHTTProxyEnabled() + if err != nil { + return httpProxy, err + } + + httpProxy.Stealth, err = envToBoolPtr("HTTPPROXY_STEALTH") + if err != nil { + return httpProxy, fmt.Errorf("environment variable HTTPPROXY_STEALTH: %w", err) + } + + httpProxy.Log, err = r.readHTTProxyLog() + if err != nil { + return httpProxy, err + } + + return httpProxy, nil +} + +func (r *Reader) readHTTProxyUser() (user *string) { + s := os.Getenv("HTTPPROXY_USER") + if s != "" { + return &s + } + + // Retro-compatibility + s = os.Getenv("TINYPROXY_USER") + if s != "" { + r.onRetroActive("TINYPROXY_USER", "HTTPPROXY_USER") + return &s + } + + // Retro-compatibility + s = os.Getenv("PROXY_USER") + if s != "" { + r.onRetroActive("PROXY_USER", "HTTPPROXY_USER") + return &s + } + + return nil +} + +func (r *Reader) readHTTProxyPassword() (user *string) { + s := os.Getenv("HTTPPROXY_PASSWORD") + if s != "" { + return &s + } + + // Retro-compatibility + s = os.Getenv("TINYPROXY_PASSWORD") + if s != "" { + r.onRetroActive("TINYPROXY_PASSWORD", "HTTPPROXY_PASSWORD") + return &s + } + + // Retro-compatibility + s = os.Getenv("PROXY_PASSWORD") + if s != "" { + r.onRetroActive("PROXY_PASSWORD", "HTTPPROXY_PASSWORD") + return &s + } + + return nil +} + +func (r *Reader) readHTTProxyListeningAddress() (listeningAddress string) { + s := os.Getenv("HTTPPROXY_LISTENING_ADDRESS") + if s != "" { + return s + } + + // Retro-compatibility + s = os.Getenv("HTTPPROXY_PORT") + if s != "" { + r.onRetroActive("HTTPPROXY_PORT", "HTTPPROXY_LISTENING_ADDRESS") + return ":" + s + } + + // Retro-compatibility + s = os.Getenv("TINYPROXY_PORT") + if s != "" { + r.onRetroActive("TINYPROXY_PORT", "HTTPPROXY_LISTENING_ADDRESS") + return ":" + s + } + + // Retro-compatibility + s = os.Getenv("PROXY_PORT") + if s != "" { + r.onRetroActive("PROXY_PORT", "HTTPPROXY_LISTENING_ADDRESS") + return ":" + s + } + + return "" +} + +func (r *Reader) readHTTProxyEnabled() (enabled *bool, err error) { + s := strings.ToLower(os.Getenv("HTTPPROXY")) + if s != "" { + enabled = new(bool) + *enabled, err = binary.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable HTTPPROXY: %w", err) + } + return enabled, nil + } + + // Retro-compatibility + s = strings.ToLower(os.Getenv("TINYPROXY")) + if s != "" { + r.onRetroActive("TINYPROXY", "HTTPPROXY") + enabled = new(bool) + *enabled, err = binary.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable TINYPROXY: %w", err) + } + return enabled, nil + } + + // Retro-compatibility + s = strings.ToLower(os.Getenv("PROXY")) + if s != "" { + r.onRetroActive("PROXY", "HTTPPROXY") + enabled = new(bool) + *enabled, err = binary.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable PROXY: %w", err) + } + return enabled, nil + } + + return nil, nil //nolint:nilnil +} + +func (r *Reader) readHTTProxyLog() (enabled *bool, err error) { + s := strings.ToLower(os.Getenv("HTTPPROXY_LOG")) + if s != "" { + enabled = new(bool) + *enabled, err = binary.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable HTTPPROXY_LOG: %w", err) + } + return enabled, nil + } + + // Retro-compatibility + retroOption := binary.OptionEnabled("on", "info", "connect", "notice") + s = strings.ToLower(os.Getenv("TINYPROXY_LOG")) + if s != "" { + r.onRetroActive("TINYPROXY_LOG", "HTTPPROXY_LOG") + enabled = new(bool) + *enabled, err = binary.Validate(s, retroOption) + if err != nil { + return nil, fmt.Errorf("environment variable TINYPROXY_LOG: %w", err) + } + return enabled, nil + } + + // Retro-compatibility + s = strings.ToLower(os.Getenv("PROXY_LOG_LEVEL")) + if s != "" { + r.onRetroActive("PROXY_LOG_LEVEL", "HTTPPROXY_LOG") + enabled = new(bool) + *enabled, err = binary.Validate(s, retroOption) + if err != nil { + return nil, fmt.Errorf("environment variable PROXY_LOG_LEVEL: %w", err) + } + return enabled, nil + } + + return nil, nil //nolint:nilnil +} diff --git a/internal/configuration/sources/env/log.go b/internal/configuration/sources/env/log.go new file mode 100644 index 00000000..c84175d9 --- /dev/null +++ b/internal/configuration/sources/env/log.go @@ -0,0 +1,54 @@ +package env + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/golibs/logging" +) + +func readLog() (log settings.Log, err error) { + log.Level, err = readLogLevel() + if err != nil { + return log, err + } + + return log, nil +} + +func readLogLevel() (level *logging.Level, err error) { + s := os.Getenv("LOG_LEVEL") + if s == "" { + return nil, nil //nolint:nilnil + } + + level = new(logging.Level) + *level, err = parseLogLevel(s) + if err != nil { + return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err) + } + + return level, nil +} + +var ErrLogLevelUnknown = errors.New("log level is unknown") + +func parseLogLevel(s string) (level logging.Level, err error) { + switch strings.ToLower(s) { + case "debug": + return logging.LevelDebug, nil + case "info": + return logging.LevelInfo, nil + case "warning": + return logging.LevelWarn, nil + case "error": + return logging.LevelError, nil + default: + return level, fmt.Errorf( + "%w: %s: can be one of: debug, info, warning or error", + ErrLogLevelUnknown, s) + } +} diff --git a/internal/configuration/sources/env/openvpn.go b/internal/configuration/sources/env/openvpn.go new file mode 100644 index 00000000..db1a3c66 --- /dev/null +++ b/internal/configuration/sources/env/openvpn.go @@ -0,0 +1,125 @@ +package env + +import ( + "fmt" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readOpenVPN() ( + openVPN settings.OpenVPN, err error) { + defer func() { + err = unsetEnvKeys([]string{"OPENVPN_CLIENTKEY", "OPENVPN_CLIENTCRT"}, err) + }() + + openVPN.Version = os.Getenv("OPENVPN_VERSION") + openVPN.User = r.readOpenVPNUser() + openVPN.Password = r.readOpenVPNPassword() + confFile := os.Getenv("OPENVPN_CUSTOM_CONFIG") + if confFile != "" { + openVPN.ConfFile = &confFile + } + + openVPN.Ciphers = envToCSV("OPENVPN_CIPHER") + auth := os.Getenv("OPENVPN_AUTH") + if auth != "" { + openVPN.Auth = &auth + } + + openVPN.ClientCrt, err = readBase64OrNil("OPENVPN_CLIENTCRT") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_CLIENTCRT: %w", err) + } + + openVPN.ClientKey, err = readBase64OrNil("OPENVPN_CLIENTKEY") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_CLIENTKEY: %w", err) + } + + openVPN.PIAEncPreset = r.readPIAEncryptionPreset() + + openVPN.IPv6, err = envToBoolPtr("OPENVPN_IPV6") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_IPV6: %w", err) + } + + openVPN.MSSFix, err = envToUint16Ptr("OPENVPN_MSSFIX") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_MSSFIX: %w", err) + } + + openVPN.Interface = os.Getenv("OPENVPN_INTERFACE") + + openVPN.Root, err = envToBoolPtr("OPENVPN_ROOT") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_ROOT: %w", err) + } + + // TODO ProcUser once Root is deprecated. + + openVPN.Verbosity, err = envToIntPtr("OPENVPN_VERBOSITY") + if err != nil { + return openVPN, fmt.Errorf("environment variable OPENVPN_VERBOSITY: %w", err) + } + + return openVPN, nil +} + +func (r *Reader) readOpenVPNUser() (user string) { + user = os.Getenv("OPENVPN_USER") + if user == "" { + // Retro-compatibility + user = os.Getenv("USER") + if user != "" { + r.onRetroActive("USER", "OPENVPN_USER") + } + } + // Remove spaces in user ID to simplify user's life, thanks @JeordyR + return strings.ReplaceAll(user, " ", "") +} + +func (r *Reader) readOpenVPNPassword() (password string) { + password = os.Getenv("OPENVPN_PASSWORD") + if password != "" { + return password + } + + // Retro-compatibility + password = os.Getenv("PASSWORD") + if password != "" { + r.onRetroActive("PASSWORD", "OPENVPN_PASSWORD") + } + return password +} + +func readBase64OrNil(envKey string) (valueOrNil *string, err error) { + value := os.Getenv(envKey) + if value == "" { + return nil, nil //nolint:nilnil + } + + decoded, err := decodeBase64(value) + if err != nil { + return nil, err + } + + return &decoded, nil +} + +func (r *Reader) readPIAEncryptionPreset() (presetPtr *string) { + preset := strings.ToLower(os.Getenv("PIA_ENCRYPTION")) + if preset != "" { + return &preset + } + + // Retro-compatibility + preset = strings.ToLower(os.Getenv("ENCRYPTION")) + if preset != "" { + r.onRetroActive("ENCRYPTION", "PIA_ENCRYPTION") + return &preset + } + + return nil +} diff --git a/internal/configuration/sources/env/openvpnselection.go b/internal/configuration/sources/env/openvpnselection.go new file mode 100644 index 00000000..17280c85 --- /dev/null +++ b/internal/configuration/sources/env/openvpnselection.go @@ -0,0 +1,78 @@ +package env + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/govalid/port" +) + +func (r *Reader) readOpenVPNSelection() ( + selection settings.OpenVPNSelection, err error) { + selection.TCP, err = r.readOpenVPNProtocol() + if err != nil { + return selection, err + } + + selection.CustomPort, err = r.readOpenVPNCustomPort() + if err != nil { + return selection, err + } + + selection.PIAEncPreset = r.readPIAEncryptionPreset() + + return selection, nil +} + +var ErrOpenVPNProtocolNotValid = errors.New("OpenVPN protocol is not valid") + +func (r *Reader) readOpenVPNProtocol() (tcp *bool, err error) { + envKey := "OPENVPN_PROTOCOL" + protocol := strings.ToLower(os.Getenv("OPENVPN_PROTOCOL")) + if protocol == "" { + // Retro-compatibility + protocol = strings.ToLower(os.Getenv("PROTOCOL")) + if protocol != "" { + envKey = "PROTOCOL" + r.onRetroActive("PROTOCOL", "OPENVPN_PROTOCOL") + } + } + + switch protocol { + case "": + return nil, nil //nolint:nilnil + case constants.UDP: + return boolPtr(false), nil + case constants.TCP: + return boolPtr(true), nil + default: + return nil, fmt.Errorf("environment variable %s: %w: %s", + envKey, ErrOpenVPNProtocolNotValid, protocol) + } +} + +func (r *Reader) readOpenVPNCustomPort() (customPort *uint16, err error) { + key := "OPENVPN_PORT" + s := os.Getenv(key) + if s == "" { + // Retro-compatibility + key = "PORT" + s = os.Getenv(key) + if s == "" { + return nil, nil //nolint:nilnil + } + r.onRetroActive("PORT", "OPENVPN_PORT") + } + + customPort = new(uint16) + *customPort, err = port.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable %s: %w", key, err) + } + + return customPort, nil +} diff --git a/internal/configuration/sources/env/portforward.go b/internal/configuration/sources/env/portforward.go new file mode 100644 index 00000000..36c9fece --- /dev/null +++ b/internal/configuration/sources/env/portforward.go @@ -0,0 +1,19 @@ +package env + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readPortForward() ( + portForwarding settings.PortForwarding, err error) { + portForwarding.Enabled, err = envToBoolPtr("PORT_FORWARDING") + if err != nil { + return portForwarding, fmt.Errorf("environment variable PORT_FORWARDING: %w", err) + } + + portForwarding.Filepath = envToStringPtr("PORT_FORWARDING_STATUS_FILE") + + return portForwarding, nil +} diff --git a/internal/configuration/sources/env/provider.go b/internal/configuration/sources/env/provider.go new file mode 100644 index 00000000..e0e12348 --- /dev/null +++ b/internal/configuration/sources/env/provider.go @@ -0,0 +1,40 @@ +package env + +import ( + "fmt" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" +) + +func (r *Reader) readProvider(vpnType string) (provider settings.Provider, err error) { + provider.Name = readVPNServiceProvider(vpnType) + + provider.ServerSelection, err = r.readServerSelection(*provider.Name, vpnType) + if err != nil { + return provider, fmt.Errorf("cannot read server selection settings: %w", err) + } + + provider.PortForwarding, err = readPortForward() + if err != nil { + return provider, fmt.Errorf("cannot read port forwarding settings: %w", err) + } + + return provider, nil +} + +func readVPNServiceProvider(vpnType string) (vpnProviderPtr *string) { + s := strings.ToLower(os.Getenv("VPNSP")) + switch { + case vpnType == constants.OpenVPN && + os.Getenv("OPENVPN_CUSTOM_CONFIG") != "": // retro compatibility + return stringPtr(constants.Custom) + case s == "": + return nil + case s == "pia": // retro compatibility + return stringPtr(constants.PrivateInternetAccess) + } + return stringPtr(s) +} diff --git a/internal/configuration/sources/env/publicip.go b/internal/configuration/sources/env/publicip.go new file mode 100644 index 00000000..ac048a36 --- /dev/null +++ b/internal/configuration/sources/env/publicip.go @@ -0,0 +1,51 @@ +package env + +import ( + "fmt" + "os" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readPublicIP() (publicIP settings.PublicIP, err error) { + publicIP.Period, err = readPublicIPPeriod() + if err != nil { + return publicIP, err + } + + publicIP.IPFilepath = r.readPublicIPFilepath() + + return publicIP, nil +} + +func readPublicIPPeriod() (period *time.Duration, err error) { + s := os.Getenv("PUBLICIP_PERIOD") + if s == "" { + return nil, nil //nolint:nilnil + } + + period = new(time.Duration) + *period, err = time.ParseDuration(s) + if err != nil { + return nil, fmt.Errorf("environment variable PUBLICIP_PERIOD: %w", err) + } + + return period, nil +} + +func (r *Reader) readPublicIPFilepath() (filepath *string) { + s := os.Getenv("PUBLICIP_FILE") + if s != "" { + return &s + } + + // Retro-compatibility + s = os.Getenv("IP_STATUS_FILE") + if s != "" { + r.onRetroActive("IP_STATUS_FILE", "PUBLICIP_FILE") + return &s + } + + return nil +} diff --git a/internal/configuration/sources/env/reader.go b/internal/configuration/sources/env/reader.go new file mode 100644 index 00000000..5a6383c2 --- /dev/null +++ b/internal/configuration/sources/env/reader.go @@ -0,0 +1,87 @@ +package env + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/configuration/sources" +) + +var _ sources.Source = (*Reader)(nil) + +type Reader struct { + warner Warner +} + +type Warner interface { + Warn(s string) +} + +func New(warner Warner) *Reader { + return &Reader{ + warner: warner, + } +} + +func (r *Reader) Read() (settings settings.Settings, err error) { + settings.VPN, err = r.readVPN() + if err != nil { + return settings, err + } + + settings.Firewall, err = r.readFirewall() + if err != nil { + return settings, err + } + + settings.System, err = r.readSystem() + if err != nil { + return settings, err + } + + settings.Health, err = r.ReadHealth() + if err != nil { + return settings, err + } + + settings.HTTPProxy, err = r.readHTTPProxy() + if err != nil { + return settings, err + } + + settings.Log, err = readLog() + if err != nil { + return settings, err + } + + settings.PublicIP, err = r.readPublicIP() + if err != nil { + return settings, err + } + + settings.Updater, err = readUpdater() + if err != nil { + return settings, err + } + + settings.Version, err = readVersion() + if err != nil { + return settings, err + } + + settings.Shadowsocks, err = r.readShadowsocks() + if err != nil { + return settings, err + } + + settings.DNS, err = r.readDNS() + if err != nil { + return settings, err + } + + return settings, nil +} + +func (r *Reader) onRetroActive(oldKey, newKey string) { + r.warner.Warn( + "You are using the old environment variable " + oldKey + + ", please consider changing it to " + newKey) +} diff --git a/internal/configuration/sources/env/serverselection.go b/internal/configuration/sources/env/serverselection.go new file mode 100644 index 00000000..cfdd8537 --- /dev/null +++ b/internal/configuration/sources/env/serverselection.go @@ -0,0 +1,115 @@ +package env + +import ( + "errors" + "fmt" + "net" + "os" + "strconv" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" +) + +var ( + ErrServerNumberNotValid = errors.New("server number is not valid") +) + +func (r *Reader) readServerSelection(vpnProvider, vpnType string) ( + ss settings.ServerSelection, err error) { + ss.VPN = vpnType + + ss.TargetIP, err = readOpenVPNTargetIP() + if err != nil { + return ss, err + } + + countriesCSV := os.Getenv("COUNTRY") + if vpnProvider == constants.Cyberghost && countriesCSV == "" { + // Retro-compatibility + r.onRetroActive("REGION", "COUNTRY") + countriesCSV = os.Getenv("REGION") + } + if countriesCSV != "" { + ss.Countries = lowerAndSplit(countriesCSV) + } + + ss.Regions = envToCSV("REGION") + ss.Cities = envToCSV("CITY") + ss.ISPs = envToCSV("ISP") + ss.Hostnames = envToCSV("SERVER_HOSTNAME") + ss.Names = envToCSV("SERVER_NAME") + + if csv := os.Getenv("SERVER_NUMBER"); csv != "" { + numbersStrings := strings.Split(csv, ",") + numbers := make([]uint16, len(numbersStrings)) + for i, numberString := range numbersStrings { + number, err := strconv.Atoi(numberString) + if err != nil { + return ss, fmt.Errorf("%w: %s", + ErrServerNumberNotValid, numberString) + } else if number < 0 || number > 2^16 { + return ss, fmt.Errorf("%w: %d must be between 0 and 2^16", + ErrServerNumberNotValid, number) + } + numbers[i] = uint16(number) + } + ss.Numbers = numbers + } + + // Mullvad only + ss.OwnedOnly, err = envToBoolPtr("OWNED") + if err != nil { + return ss, fmt.Errorf("environment variable OWNED: %w", err) + } + + // VPNUnlimited only + ss.FreeOnly, err = envToBoolPtr("FREE_ONLY") + if err != nil { + return ss, fmt.Errorf("environment variable FREE_ONLY: %w", err) + } + + // VPNUnlimited only + ss.MultiHopOnly, err = envToBoolPtr("MULTIHOP_ONLY") + if err != nil { + return ss, fmt.Errorf("environment variable MULTIHOP_ONLY: %w", err) + } + + // VPNUnlimited only + ss.MultiHopOnly, err = envToBoolPtr("STREAM_ONLY") + if err != nil { + return ss, fmt.Errorf("environment variable STREAM_ONLY: %w", err) + } + + ss.OpenVPN, err = r.readOpenVPNSelection() + if err != nil { + return ss, err + } + + ss.Wireguard, err = r.readWireguardSelection() + if err != nil { + return ss, err + } + + return ss, nil +} + +var ( + ErrInvalidIP = errors.New("invalid IP address") +) + +func readOpenVPNTargetIP() (ip net.IP, err error) { + s := os.Getenv("OPENVPN_TARGET_IP") + if s == "" { + return nil, nil + } + + ip = net.ParseIP(s) + if ip == nil { + return nil, fmt.Errorf("environment variable OPENVPN_TARGET_IP: %w: %s", + ErrInvalidIP, s) + } + + return ip, nil +} diff --git a/internal/configuration/sources/env/shadowsocks.go b/internal/configuration/sources/env/shadowsocks.go new file mode 100644 index 00000000..e470269b --- /dev/null +++ b/internal/configuration/sources/env/shadowsocks.go @@ -0,0 +1,54 @@ +package env + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readShadowsocks() (shadowsocks settings.Shadowsocks, err error) { + shadowsocks.Enabled, err = envToBoolPtr("SHADOWSOCKS") + if err != nil { + return shadowsocks, fmt.Errorf("environment variable SHADOWSOCKS: %w", err) + } + + shadowsocks.Address = r.readShadowsocksAddress() + shadowsocks.LogAddresses, err = envToBoolPtr("SHADOWSOCKS_LOG") + if err != nil { + return shadowsocks, fmt.Errorf("environment variable SHADOWSOCKS_LOG: %w", err) + } + shadowsocks.CipherName = r.readShadowsocksCipher() + shadowsocks.Password = envToStringPtr("SHADOWSOCKS_PASSWORD") + + return shadowsocks, nil +} + +func (r *Reader) readShadowsocksAddress() (address string) { + address = os.Getenv("SHADOWSOCKS_LISTENING_ADDRESS") + if address != "" { + return address + } + + // Retro-compatibility + portString := os.Getenv("SHADOWSOCKS_PORT") + if portString != "" { + r.onRetroActive("SHADOWSOCKS_PORT", "SHADOWSOCKS_LISTENING_ADDRESS") + return ":" + portString + } + + return "" +} + +func (r *Reader) readShadowsocksCipher() (cipher string) { + cipher = os.Getenv("SHADOWSOCKS_CIPHER") + if cipher != "" { + return cipher + } + // Retro-compatibility + cipher = os.Getenv("SHADOWSOCKS_METHOD") + if cipher != "" { + r.onRetroActive("SHADOWSOCKS_METHOD", "SHADOWSOCKS_CIPHER") + } + return cipher +} diff --git a/internal/configuration/sources/env/system.go b/internal/configuration/sources/env/system.go new file mode 100644 index 00000000..d8139cc5 --- /dev/null +++ b/internal/configuration/sources/env/system.go @@ -0,0 +1,63 @@ +package env + +import ( + "errors" + "fmt" + "os" + "strconv" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +var ( + ErrSystemPUIDNotValid = errors.New("PUID is not valid") + ErrSystemPGIDNotValid = errors.New("PGID is not valid") + ErrSystemTimezoneNotValid = errors.New("timezone is not valid") +) + +func (r *Reader) readSystem() (system settings.System, err error) { + system.PUID, err = r.readID("PUID", "UID") + if err != nil { + return system, err + } + + system.PGID, err = r.readID("PGID", "GID") + if err != nil { + return system, err + } + + system.Timezone = os.Getenv("TZ") + + return system, nil +} + +var ErrSystemIDNotValid = errors.New("system ID is not valid") + +func (r *Reader) readID(key, retroKey string) ( + id *uint16, err error) { + idEnvKey := key + idString := os.Getenv(key) + if idString == "" { + // retro-compatibility + idString = os.Getenv(retroKey) + if idString != "" { + idEnvKey = retroKey + r.onRetroActive(retroKey, key) + } + } + + if idString == "" { + return nil, nil //nolint:nilnil + } + + idInt, err := strconv.Atoi(idString) + if err != nil { + return nil, fmt.Errorf("environment variable %s: %w: %s: %s", + idEnvKey, ErrSystemIDNotValid, idString, err) + } else if idInt < 0 || idInt > 2^16 { + return nil, fmt.Errorf("environment variable %s: %w: %d: must be between 0 and 65535", + idEnvKey, ErrSystemIDNotValid, idInt) + } + + return uint16Ptr(uint16(idInt)), nil +} diff --git a/internal/configuration/sources/env/unbound.go b/internal/configuration/sources/env/unbound.go new file mode 100644 index 00000000..38056bb6 --- /dev/null +++ b/internal/configuration/sources/env/unbound.go @@ -0,0 +1,38 @@ +package env + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readUnbound() (unbound settings.Unbound, err error) { + unbound.Providers = envToCSV("DOT_PROVIDERS") + + unbound.Caching, err = envToBoolPtr("DOT_CACHING") + if err != nil { + return unbound, fmt.Errorf("environment variable DOT_CACHING: %w", err) + } + + unbound.IPv6, err = envToBoolPtr("DOT_IPV6") + if err != nil { + return unbound, fmt.Errorf("environment variable DOT_IPV6: %w", err) + } + + unbound.VerbosityLevel, err = envToUint8Ptr("DOT_VERBOSITY") + if err != nil { + return unbound, fmt.Errorf("environment variable DOT_VERBOSITY: %w", err) + } + + unbound.VerbosityDetailsLevel, err = envToUint8Ptr("DOT_VERBOSITY_DETAILS") + if err != nil { + return unbound, fmt.Errorf("environment variable DOT_VERBOSITY_DETAILS: %w", err) + } + + unbound.ValidationLogLevel, err = envToUint8Ptr("DOT_VALIDATION_LOGLEVEL") + if err != nil { + return unbound, fmt.Errorf("environment variable DOT_VALIDATION_LOGLEVEL: %w", err) + } + + return unbound, nil +} diff --git a/internal/configuration/sources/env/updater.go b/internal/configuration/sources/env/updater.go new file mode 100644 index 00000000..1a9fdfa7 --- /dev/null +++ b/internal/configuration/sources/env/updater.go @@ -0,0 +1,50 @@ +package env + +import ( + "fmt" + "net" + "os" + "time" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" +) + +func readUpdater() (updater settings.Updater, err error) { + updater.Period, err = readUpdaterPeriod() + if err != nil { + return updater, err + } + + updater.DNSAddress, err = readUpdaterDNSAddress() + if err != nil { + return updater, err + } + + // TODO use current provider being used + updater.Providers = constants.AllProviders() + + return updater, nil +} + +func readUpdaterPeriod() (period *time.Duration, err error) { + s := os.Getenv("UPDATER_PERIOD") + if s == "" { + return nil, nil //nolint:nilnil + } + period = new(time.Duration) + *period, err = time.ParseDuration(s) + if err != nil { + return nil, fmt.Errorf("environment variable UPDATER_PERIOD: %w", err) + } + return period, nil +} + +func readUpdaterDNSAddress() (ip net.IP, err error) { + // TODO this is currently using Cloudflare in + // plaintext to not be blocked by DNS over TLS by default. + // If a plaintext address is set in the DNS settings, this one will be used. + // use custom future encrypted DNS written in Go without blocking + // as it's too much trouble to start another parallel unbound instance for now. + return nil, nil +} diff --git a/internal/configuration/sources/env/version.go b/internal/configuration/sources/env/version.go new file mode 100644 index 00000000..3e1e688c --- /dev/null +++ b/internal/configuration/sources/env/version.go @@ -0,0 +1,33 @@ +package env + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/govalid/binary" +) + +func readVersion() (version settings.Version, err error) { + version.Enabled, err = readVersionEnabled() + if err != nil { + return version, err + } + + return version, nil +} + +func readVersionEnabled() (enabled *bool, err error) { + s := os.Getenv("VERSION_INFORMATION") + if s == "" { + return nil, nil //nolint:nilnil + } + + enabled = new(bool) + *enabled, err = binary.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable VERSION_INFORMATION: %w", err) + } + + return enabled, nil +} diff --git a/internal/configuration/sources/env/vpn.go b/internal/configuration/sources/env/vpn.go new file mode 100644 index 00000000..82eddee4 --- /dev/null +++ b/internal/configuration/sources/env/vpn.go @@ -0,0 +1,29 @@ +package env + +import ( + "fmt" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readVPN() (vpn settings.VPN, err error) { + vpn.Type = os.Getenv("VPN_TYPE") + + vpn.Provider, err = r.readProvider(vpn.Type) + if err != nil { + return vpn, fmt.Errorf("cannot read provider settings: %w", err) + } + + vpn.OpenVPN, err = r.readOpenVPN() + if err != nil { + return vpn, fmt.Errorf("cannot read OpenVPN settings: %w", err) + } + + vpn.Wireguard, err = readWireguard() + if err != nil { + return vpn, fmt.Errorf("cannot read Wireguard settings: %w", err) + } + + return vpn, nil +} diff --git a/internal/configuration/sources/env/wireguard.go b/internal/configuration/sources/env/wireguard.go new file mode 100644 index 00000000..fccc3539 --- /dev/null +++ b/internal/configuration/sources/env/wireguard.go @@ -0,0 +1,44 @@ +package env + +import ( + "fmt" + "net" + "os" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readWireguard() (wireguard settings.Wireguard, err error) { + defer func() { + err = unsetEnvKeys([]string{"WIREGUARD_PRIVATE_KEY", "WIREGUARD_PRESHARED_KEY"}, err) + }() + wireguard.PrivateKey = envToStringPtr("WIREGUARD_PRIVATE_KEY") + wireguard.PreSharedKey = envToStringPtr("WIREGUARD_PRESHARED_KEY") + wireguard.Interface = os.Getenv("WIREGUARD_INTERFACE") + wireguard.Addresses, err = readWireguardAddresses() + if err != nil { + return wireguard, err // already wrapped + } + return wireguard, nil +} + +func readWireguardAddresses() (addresses []net.IPNet, err error) { + addressesCSV := os.Getenv("WIREGUARD_ADDRESS") + if addressesCSV == "" { + return nil, nil + } + + addressStrings := strings.Split(addressesCSV, ",") + addresses = make([]net.IPNet, len(addressStrings)) + for i, addressString := range addressStrings { + ip, ipNet, err := net.ParseCIDR(addressString) + if err != nil { + return nil, fmt.Errorf("environment variable WIREGUARD_ADDRESS: %w", err) + } + ipNet.IP = ip + addresses[i] = *ipNet + } + + return addresses, nil +} diff --git a/internal/configuration/sources/env/wireguardselection.go b/internal/configuration/sources/env/wireguardselection.go new file mode 100644 index 00000000..044d928c --- /dev/null +++ b/internal/configuration/sources/env/wireguardselection.go @@ -0,0 +1,65 @@ +package env + +import ( + "errors" + "fmt" + "net" + "os" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/govalid/port" +) + +func (r *Reader) readWireguardSelection() ( + selection settings.WireguardSelection, err error) { + selection.EndpointIP, err = readWireguardEndpointIP() + if err != nil { + return selection, err + } + + selection.EndpointPort, err = r.readWireguardCustomPort() + if err != nil { + return selection, err + } + + selection.PublicKey = os.Getenv("WIREGUARD_PUBLIC_KEY") + + return selection, nil +} + +var ErrIPAddressParse = errors.New("cannot parse IP address") + +func readWireguardEndpointIP() (endpointIP net.IP, err error) { + s := os.Getenv("WIREGUARD_ENDPOINT_IP") + if s == "" { + return nil, nil + } + endpointIP = net.ParseIP(s) + if endpointIP == nil { + return nil, fmt.Errorf("environment variable WIREGUARD_ENDPOINT_IP: %w: %s", + ErrIPAddressParse, s) + } + return endpointIP, nil +} + +func (r *Reader) readWireguardCustomPort() (customPort *uint16, err error) { + key := "WIREGUARD_ENDPOINT_PORT" + s := os.Getenv(key) + if s == "" { + // Retro-compatibility + key = "WIREGUARD_PORT" + s = os.Getenv(key) + if s == "" { + return nil, nil //nolint:nilnil + } + r.onRetroActive("WIREGUARD_PORT", "WIREGUARD_ENDPOINT_PORT") + } + + customPort = new(uint16) + *customPort, err = port.Validate(s) + if err != nil { + return nil, fmt.Errorf("environment variable %s: %w", key, err) + } + + return customPort, nil +} diff --git a/internal/configuration/sources/files/health.go b/internal/configuration/sources/files/health.go new file mode 100644 index 00000000..aaf57b11 --- /dev/null +++ b/internal/configuration/sources/files/health.go @@ -0,0 +1,5 @@ +package files + +import "github.com/qdm12/gluetun/internal/configuration/settings" + +func (r *Reader) ReadHealth() (settings settings.Health, err error) { return settings, nil } diff --git a/internal/configuration/sources/files/helpers.go b/internal/configuration/sources/files/helpers.go new file mode 100644 index 00000000..a2aea24b --- /dev/null +++ b/internal/configuration/sources/files/helpers.go @@ -0,0 +1,31 @@ +package files + +import ( + "io" + "os" +) + +// ReadFromFile reads the content of the file as a string. +// It returns a nil string pointer if the file does not exist. +func ReadFromFile(filepath string) (s *string, err error) { + file, err := os.Open(filepath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil //nolint:nilnil + } + return nil, err + } + + b, err := io.ReadAll(file) + if err != nil { + _ = file.Close() + return nil, err + } + + if err := file.Close(); err != nil { + return nil, err + } + + content := string(b) + return &content, nil +} diff --git a/internal/configuration/sources/files/openvpn.go b/internal/configuration/sources/files/openvpn.go new file mode 100644 index 00000000..151564e9 --- /dev/null +++ b/internal/configuration/sources/files/openvpn.go @@ -0,0 +1,22 @@ +package files + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants" +) + +func (r *Reader) readOpenVPN() (settings settings.OpenVPN, err error) { + settings.ClientKey, err = ReadFromFile(constants.ClientKey) + if err != nil { + return settings, fmt.Errorf("cannot read client key: %w", err) + } + + settings.ClientCrt, err = ReadFromFile(constants.ClientCertificate) + if err != nil { + return settings, fmt.Errorf("cannot read client certificate: %w", err) + } + + return settings, nil +} diff --git a/internal/configuration/sources/files/reader.go b/internal/configuration/sources/files/reader.go new file mode 100644 index 00000000..78789166 --- /dev/null +++ b/internal/configuration/sources/files/reader.go @@ -0,0 +1,28 @@ +package files + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/configuration/sources" +) + +var _ sources.Source = (*Reader)(nil) + +type Reader struct{} + +func New() *Reader { + return &Reader{} +} + +func (r *Reader) Read() (settings settings.Settings, err error) { + settings.VPN, err = r.readVPN() + if err != nil { + return settings, err + } + + settings.System, err = r.readSystem() + if err != nil { + return settings, err + } + + return settings, nil +} diff --git a/internal/configuration/sources/files/system.go b/internal/configuration/sources/files/system.go new file mode 100644 index 00000000..e5be9bd9 --- /dev/null +++ b/internal/configuration/sources/files/system.go @@ -0,0 +1,10 @@ +package files + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readSystem() (system settings.System, err error) { + // TODO timezone from /etc/localtime + return system, nil +} diff --git a/internal/configuration/sources/files/vpn.go b/internal/configuration/sources/files/vpn.go new file mode 100644 index 00000000..4af013c8 --- /dev/null +++ b/internal/configuration/sources/files/vpn.go @@ -0,0 +1,16 @@ +package files + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (r *Reader) readVPN() (vpn settings.VPN, err error) { + vpn.OpenVPN, err = r.readOpenVPN() + if err != nil { + return vpn, fmt.Errorf("cannot read OpenVPN settings: %w", err) + } + + return vpn, nil +} diff --git a/internal/configuration/sources/mux/reader.go b/internal/configuration/sources/mux/reader.go new file mode 100644 index 00000000..856464fd --- /dev/null +++ b/internal/configuration/sources/mux/reader.go @@ -0,0 +1,57 @@ +package mux + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/configuration/sources" +) + +var _ sources.Source = (*Reader)(nil) + +type Reader struct { + sources []sources.Source +} + +func New(sources ...sources.Source) *Reader { + return &Reader{ + sources: sources, + } +} + +// Read reads the settings for each source, merging unset fields +// with field set by the next source. +// It then set defaults to remaining unset fields. +func (r *Reader) Read() (settings settings.Settings, err error) { + for _, source := range r.sources { + settingsFromSource, err := source.Read() + if err != nil { + return settings, fmt.Errorf("cannot read from source %T: %w", source, err) + } + settings.MergeWith(settingsFromSource) + } + settings.SetDefaults() + return settings, nil +} + +// ReadHealth reads the health settings for each source, merging unset fields +// with field set by the next source. +// It then set defaults to remaining unset fields, and validate +// all the fields. +func (r *Reader) ReadHealth() (settings settings.Health, err error) { + for _, source := range r.sources { + settingsFromSource, err := source.ReadHealth() + if err != nil { + return settings, fmt.Errorf("cannot read from source %T: %w", source, err) + } + settings.MergeWith(settingsFromSource) + } + settings.SetDefaults() + + err = settings.Validate() + if err != nil { + return settings, err + } + + return settings, nil +} diff --git a/internal/configuration/sources/secrets/health.go b/internal/configuration/sources/secrets/health.go new file mode 100644 index 00000000..ffc0aa5a --- /dev/null +++ b/internal/configuration/sources/secrets/health.go @@ -0,0 +1,5 @@ +package secrets + +import "github.com/qdm12/gluetun/internal/configuration/settings" + +func (r *Reader) ReadHealth() (settings settings.Health, err error) { return settings, nil } diff --git a/internal/configuration/sources/secrets/helpers.go b/internal/configuration/sources/secrets/helpers.go new file mode 100644 index 00000000..043d899f --- /dev/null +++ b/internal/configuration/sources/secrets/helpers.go @@ -0,0 +1,31 @@ +package secrets + +import ( + "os" + + "github.com/qdm12/gluetun/internal/configuration/sources/files" +) + +func readSecretFileAsStringPtr(secretPathEnvKey, defaultSecretPath string) ( + stringPtr *string, err error) { + path := os.Getenv(secretPathEnvKey) + if path == "" { + path = defaultSecretPath + } + return files.ReadFromFile(path) +} + +func readSecretFileAsString(secretPathEnvKey, defaultSecretPath string) ( + s string, err error) { + path := os.Getenv(secretPathEnvKey) + if path == "" { + path = defaultSecretPath + } + stringPtr, err := files.ReadFromFile(path) + if err != nil { + return "", err + } else if stringPtr == nil { + return "", nil + } + return *stringPtr, nil +} diff --git a/internal/configuration/sources/secrets/httpproxy.go b/internal/configuration/sources/secrets/httpproxy.go new file mode 100644 index 00000000..537aa9bb --- /dev/null +++ b/internal/configuration/sources/secrets/httpproxy.go @@ -0,0 +1,27 @@ +package secrets + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readHTTPProxy() (settings settings.HTTPProxy, err error) { + settings.User, err = readSecretFileAsStringPtr( + "HTTPPROXY_USER_SECRETFILE", + "/run/secrets/httpproxy_user", + ) + if err != nil { + return settings, fmt.Errorf("cannot read HTTP proxy user secret file: %w", err) + } + + settings.Password, err = readSecretFileAsStringPtr( + "HTTPPROXY_PASSWORD_SECRETFILE", + "/run/secrets/httpproxy_password", + ) + if err != nil { + return settings, fmt.Errorf("cannot read OpenVPN password secret file: %w", err) + } + + return settings, nil +} diff --git a/internal/configuration/sources/secrets/openvpn.go b/internal/configuration/sources/secrets/openvpn.go new file mode 100644 index 00000000..f10dbe92 --- /dev/null +++ b/internal/configuration/sources/secrets/openvpn.go @@ -0,0 +1,44 @@ +package secrets + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readOpenVPN() ( + settings settings.OpenVPN, err error) { + settings.User, err = readSecretFileAsString( + "OPENVPN_USER_SECRETFILE", + "/run/secrets/openvpn_user", + ) + if err != nil { + return settings, fmt.Errorf("cannot read user file: %w", err) + } + + settings.Password, err = readSecretFileAsString( + "OPENVPN_PASSWORD_SECRETFILE", + "/run/secrets/openvpn_password", + ) + if err != nil { + return settings, fmt.Errorf("cannot read password file: %w", err) + } + + settings.ClientKey, err = readSecretFileAsStringPtr( + "OPENVPN_CLIENTKEY_SECRETFILE", + "/run/secrets/openvpn_clientkey", + ) + if err != nil { + return settings, fmt.Errorf("cannot read client key file: %w", err) + } + + settings.ClientCrt, err = readSecretFileAsStringPtr( + "OPENVPN_CLIENTCRT_SECRETFILE", + "/run/secrets/openvpn_clientcrt", + ) + if err != nil { + return settings, fmt.Errorf("cannot read client certificate file: %w", err) + } + + return settings, nil +} diff --git a/internal/configuration/sources/secrets/reader.go b/internal/configuration/sources/secrets/reader.go new file mode 100644 index 00000000..8ae754de --- /dev/null +++ b/internal/configuration/sources/secrets/reader.go @@ -0,0 +1,34 @@ +package secrets + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/configuration/sources" +) + +var _ sources.Source = (*Reader)(nil) + +type Reader struct { +} + +func New() *Reader { + return &Reader{} +} + +func (r *Reader) Read() (settings settings.Settings, err error) { + settings.VPN, err = readVPN() + if err != nil { + return settings, err + } + + settings.HTTPProxy, err = readHTTPProxy() + if err != nil { + return settings, err + } + + settings.Shadowsocks, err = readShadowsocks() + if err != nil { + return settings, err + } + + return settings, nil +} diff --git a/internal/configuration/sources/secrets/shadowsocks.go b/internal/configuration/sources/secrets/shadowsocks.go new file mode 100644 index 00000000..bda606b0 --- /dev/null +++ b/internal/configuration/sources/secrets/shadowsocks.go @@ -0,0 +1,19 @@ +package secrets + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readShadowsocks() (settings settings.Shadowsocks, err error) { + settings.Password, err = readSecretFileAsStringPtr( + "SHADOWSOCKS_PASSWORD_SECRETFILE", + "/run/secrets/shadowsocks_password", + ) + if err != nil { + return settings, fmt.Errorf("cannot read Shadowsocks password secret file: %w", err) + } + + return settings, nil +} diff --git a/internal/configuration/sources/secrets/vpn.go b/internal/configuration/sources/secrets/vpn.go new file mode 100644 index 00000000..a8518193 --- /dev/null +++ b/internal/configuration/sources/secrets/vpn.go @@ -0,0 +1,16 @@ +package secrets + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func readVPN() (vpn settings.VPN, err error) { + vpn.OpenVPN, err = readOpenVPN() + if err != nil { + return vpn, fmt.Errorf("cannot read OpenVPN settings: %w", err) + } + + return vpn, nil +} diff --git a/internal/configuration/sources/source.go b/internal/configuration/sources/source.go new file mode 100644 index 00000000..bd805fb9 --- /dev/null +++ b/internal/configuration/sources/source.go @@ -0,0 +1,8 @@ +package sources + +import "github.com/qdm12/gluetun/internal/configuration/settings" + +type Source interface { + Read() (settings settings.Settings, err error) + ReadHealth() (settings settings.Health, err error) +} diff --git a/internal/configuration/surfshark.go b/internal/configuration/surfshark.go deleted file mode 100644 index 6f928baf..00000000 --- a/internal/configuration/surfshark.go +++ /dev/null @@ -1,102 +0,0 @@ -package configuration - -import ( - "fmt" - "strings" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readSurfshark(r reader) (err error) { - settings.Name = constants.Surfshark - servers := r.servers.GetSurfshark() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.SurfsharkCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.SurfsharkCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.SurfsharkHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - regionChoices := constants.SurfsharkRegionChoices(servers) - regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...) - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", regionChoices) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - // Retro compatibility - // TODO remove in v4 - settings.ServerSelection = surfsharkRetroRegion(settings.ServerSelection) - - settings.ServerSelection.MultiHopOnly, err = r.env.YesNo("MULTIHOP_ONLY", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable MULTIHOP_ONLY: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} - -func surfsharkRetroRegion(selection ServerSelection) ( - updatedSelection ServerSelection) { - locationData := constants.SurfsharkLocationData() - - retroToLocation := make(map[string]models.SurfsharkLocationData, len(locationData)) - for _, data := range locationData { - if data.RetroLoc == "" { - continue - } - retroToLocation[strings.ToLower(data.RetroLoc)] = data - } - - for i, region := range selection.Regions { - location, ok := retroToLocation[region] - if !ok { - continue - } - selection.Regions[i] = strings.ToLower(location.Region) - selection.Countries = append(selection.Countries, strings.ToLower(location.Country)) - selection.Cities = append(selection.Cities, strings.ToLower(location.City)) // even empty string - selection.Hostnames = append(selection.Hostnames, location.Hostname) - } - - selection.Regions = dedupSlice(selection.Regions) - selection.Countries = dedupSlice(selection.Countries) - selection.Cities = dedupSlice(selection.Cities) - selection.Hostnames = dedupSlice(selection.Hostnames) - - return selection -} - -func dedupSlice(slice []string) (deduped []string) { - if slice == nil { - return nil - } - - deduped = make([]string, 0, len(slice)) - seen := make(map[string]struct{}, len(slice)) - for _, s := range slice { - if _, ok := seen[s]; !ok { - seen[s] = struct{}{} - deduped = append(deduped, s) - } - } - - return deduped -} diff --git a/internal/configuration/surfshark_test.go b/internal/configuration/surfshark_test.go deleted file mode 100644 index ba5f377c..00000000 --- a/internal/configuration/surfshark_test.go +++ /dev/null @@ -1,305 +0,0 @@ -package configuration - -import ( - "errors" - "net" - "testing" - - "github.com/golang/mock/gomock" - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Provider_readSurfshark(t *testing.T) { - t.Parallel() - - var errDummy = errors.New("dummy test error") - - type stringCall struct { - call bool - value string - err error - } - - type boolCall struct { - call bool - value bool - err error - } - - type sliceStringCall struct { - call bool - values []string - err error - } - - testCases := map[string]struct { - targetIP stringCall - countries sliceStringCall - cities sliceStringCall - hostnames sliceStringCall - regions sliceStringCall - multiHop boolCall - protocol stringCall - settings Provider - err error - }{ - "target IP error": { - targetIP: stringCall{call: true, value: "something", err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable OPENVPN_TARGET_IP: dummy test error"), - }, - "countries error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable COUNTRY: dummy test error"), - }, - "cities error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable CITY: dummy test error"), - }, - "hostnames error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable SERVER_HOSTNAME: dummy test error"), - }, - "regions error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - regions: sliceStringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable REGION: dummy test error"), - }, - "multi hop error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - regions: sliceStringCall{call: true}, - multiHop: boolCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable MULTIHOP_ONLY: dummy test error"), - }, - "openvpn protocol error": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - regions: sliceStringCall{call: true}, - multiHop: boolCall{call: true}, - protocol: stringCall{call: true, err: errDummy}, - settings: Provider{ - Name: constants.Surfshark, - }, - err: errors.New("environment variable OPENVPN_PROTOCOL: dummy test error"), - }, - "default settings": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - hostnames: sliceStringCall{call: true}, - regions: sliceStringCall{call: true}, - multiHop: boolCall{call: true}, - protocol: stringCall{call: true}, - settings: Provider{ - Name: constants.Surfshark, - }, - }, - "set settings": { - targetIP: stringCall{call: true, value: "1.2.3.4"}, - countries: sliceStringCall{call: true, values: []string{"A", "B"}}, - cities: sliceStringCall{call: true, values: []string{"C", "D"}}, - regions: sliceStringCall{call: true, values: []string{ - "E", "F", "netherlands amsterdam", - }}, // Netherlands Amsterdam is for retro compatibility test - multiHop: boolCall{call: true}, - hostnames: sliceStringCall{call: true, values: []string{"E", "F"}}, - protocol: stringCall{call: true, value: constants.TCP}, - settings: Provider{ - Name: constants.Surfshark, - ServerSelection: ServerSelection{ - OpenVPN: OpenVPNSelection{ - TCP: true, - }, - TargetIP: net.IPv4(1, 2, 3, 4), - Regions: []string{"E", "F", "europe"}, - Countries: []string{"A", "B", "netherlands"}, - Cities: []string{"C", "D", "amsterdam"}, - Hostnames: []string{"E", "F", "nl-ams.prod.surfshark.com"}, - }, - }, - }, - "Netherlands Amsterdam": { - targetIP: stringCall{call: true}, - countries: sliceStringCall{call: true}, - cities: sliceStringCall{call: true}, - regions: sliceStringCall{call: true, values: []string{"netherlands amsterdam"}}, - multiHop: boolCall{call: true}, - hostnames: sliceStringCall{call: true}, - protocol: stringCall{call: true}, - settings: Provider{ - Name: constants.Surfshark, - ServerSelection: ServerSelection{ - Regions: []string{"europe"}, - Countries: []string{"netherlands"}, - Cities: []string{"amsterdam"}, - Hostnames: []string{"nl-ams.prod.surfshark.com"}, - }, - }, - }, - } - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - - env := mock_params.NewMockInterface(ctrl) - - servers := []models.SurfsharkServer{{Hostname: "a"}} - allServers := models.AllServers{ - Surfshark: models.SurfsharkServers{ - Servers: servers, - }, - } - - if testCase.targetIP.call { - env.EXPECT().Get("OPENVPN_TARGET_IP"). - Return(testCase.targetIP.value, testCase.targetIP.err) - } - if testCase.countries.call { - env.EXPECT().CSVInside("COUNTRY", constants.SurfsharkCountryChoices(servers)). - Return(testCase.countries.values, testCase.countries.err) - } - if testCase.cities.call { - env.EXPECT().CSVInside("CITY", constants.SurfsharkCityChoices(servers)). - Return(testCase.cities.values, testCase.cities.err) - } - if testCase.hostnames.call { - env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.SurfsharkHostnameChoices(servers)). - Return(testCase.hostnames.values, testCase.hostnames.err) - } - if testCase.regions.call { - regionChoices := constants.SurfsharkRegionChoices(servers) - regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...) - env.EXPECT().CSVInside("REGION", regionChoices). - Return(testCase.regions.values, testCase.regions.err) - } - if testCase.multiHop.call { - env.EXPECT().YesNo("MULTIHOP_ONLY", gomock.Any()). - Return(testCase.multiHop.value, testCase.multiHop.err) - } - if testCase.protocol.call { - env.EXPECT().Inside("OPENVPN_PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()). - Return(testCase.protocol.value, testCase.protocol.err) - } - - r := reader{ - servers: allServers, - env: env, - } - - var settings Provider - err := settings.readSurfshark(r) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.settings, settings) - }) - } -} - -func Test_surfsharkRetroRegion(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - original ServerSelection - modified ServerSelection - }{ - "empty": {}, - "1 retro region": { - original: ServerSelection{ - Regions: []string{"australia adelaide"}, - }, - modified: ServerSelection{ - Regions: []string{"asia pacific"}, - Countries: []string{"australia"}, - Cities: []string{"adelaide"}, - Hostnames: []string{"au-adl.prod.surfshark.com"}, - }, - }, - "2 overlapping retro regions": { - original: ServerSelection{ - Regions: []string{"australia adelaide", "australia melbourne"}, - }, - modified: ServerSelection{ - Regions: []string{"asia pacific"}, - Countries: []string{"australia"}, - Cities: []string{"adelaide", "melbourne"}, - Hostnames: []string{"au-adl.prod.surfshark.com", "au-mel.prod.surfshark.com"}, - }, - }, - "2 distinct retro regions": { - original: ServerSelection{ - Regions: []string{"australia adelaide", "netherlands amsterdam"}, - }, - modified: ServerSelection{ - Regions: []string{"asia pacific", "europe"}, - Countries: []string{"australia", "netherlands"}, - Cities: []string{"adelaide", "amsterdam"}, - Hostnames: []string{"au-adl.prod.surfshark.com", "nl-ams.prod.surfshark.com"}, - }, - }, - "retro region with existing region": { - // note TestRegion will be ignored in the filters downstream - original: ServerSelection{ - Regions: []string{"TestRegion", "australia adelaide"}, - }, - modified: ServerSelection{ - Regions: []string{"TestRegion", "asia pacific"}, - Countries: []string{"australia"}, - Cities: []string{"adelaide"}, - Hostnames: []string{"au-adl.prod.surfshark.com"}, - }, - }, - } - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - selection := surfsharkRetroRegion(testCase.original) - - assert.Equal(t, testCase.modified, selection) - }) - } -} diff --git a/internal/configuration/system.go b/internal/configuration/system.go deleted file mode 100644 index fff97d9b..00000000 --- a/internal/configuration/system.go +++ /dev/null @@ -1,55 +0,0 @@ -package configuration - -import ( - "fmt" - "strconv" - "strings" - - "github.com/qdm12/golibs/params" -) - -// System contains settings to configure system related elements. -type System struct { - PUID int - PGID int - Timezone string -} - -func (settings *System) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *System) lines() (lines []string) { - lines = append(lines, lastIndent+"System:") - lines = append(lines, indent+lastIndent+"Process user ID: "+strconv.Itoa(settings.PUID)) - lines = append(lines, indent+lastIndent+"Process group ID: "+strconv.Itoa(settings.PGID)) - - if len(settings.Timezone) > 0 { - lines = append(lines, indent+lastIndent+"Timezone: "+settings.Timezone) - } else { - lines = append(lines, indent+lastIndent+"Timezone: NOT SET ⚠️ - it can cause time related issues") - } - return lines -} - -func (settings *System) read(r reader) (err error) { - const maxID = 65535 - settings.PUID, err = r.env.IntRange("PUID", 0, maxID, params.Default("1000"), - params.RetroKeys([]string{"UID"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable PUID (or UID): %w", err) - } - - settings.PGID, err = r.env.IntRange("PGID", 0, maxID, params.Default("1000"), - params.RetroKeys([]string{"GID"}, r.onRetroActive)) - if err != nil { - return fmt.Errorf("environment variable PGID (or GID): %w", err) - } - - settings.Timezone, err = r.env.Get("TZ") - if err != nil { - return fmt.Errorf("environment variable TZ: %w", err) - } - - return nil -} diff --git a/internal/configuration/torguard.go b/internal/configuration/torguard.go deleted file mode 100644 index 0ee76502..00000000 --- a/internal/configuration/torguard.go +++ /dev/null @@ -1,35 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readTorguard(r reader) (err error) { - settings.Name = constants.Torguard - servers := r.servers.GetTorguard() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.TorguardCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.TorguardCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.TorguardHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolAndPort(r) -} diff --git a/internal/configuration/unbound.go b/internal/configuration/unbound.go deleted file mode 100644 index 15995dc5..00000000 --- a/internal/configuration/unbound.go +++ /dev/null @@ -1,79 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "strings" - - "github.com/qdm12/dns/pkg/provider" - "github.com/qdm12/golibs/params" - "inet.af/netaddr" -) - -func (settings *DNS) readUnbound(r reader) (err error) { - if err := settings.readUnboundProviders(r.env); err != nil { - return err - } - - settings.Unbound.ListeningPort = 53 - - settings.Unbound.Caching, err = r.env.OnOff("DOT_CACHING", params.Default("on")) - if err != nil { - return fmt.Errorf("environment variable DOT_CACHING: %w", err) - } - - settings.Unbound.IPv4 = true - - settings.Unbound.IPv6, err = r.env.OnOff("DOT_IPV6", params.Default("off")) - if err != nil { - return fmt.Errorf("environment variable DOT_IPV6: %w", err) - } - - verbosityLevel, err := r.env.IntRange("DOT_VERBOSITY", 0, 5, params.Default("1")) //nolint:gomnd - if err != nil { - return fmt.Errorf("environment variable DOT_VERBOSITY: %w", err) - } - settings.Unbound.VerbosityLevel = uint8(verbosityLevel) - - verbosityDetailsLevel, err := r.env.IntRange("DOT_VERBOSITY_DETAILS", 0, 4, params.Default("0")) //nolint:gomnd - if err != nil { - return fmt.Errorf("environment variable DOT_VERBOSITY_DETAILS: %w", err) - } - settings.Unbound.VerbosityDetailsLevel = uint8(verbosityDetailsLevel) - - validationLogLevel, err := r.env.IntRange("DOT_VALIDATION_LOGLEVEL", 0, 2, params.Default("0")) //nolint:gomnd - if err != nil { - return fmt.Errorf("environment variable DOT_VALIDATION_LOGLEVEL: %w", err) - } - settings.Unbound.ValidationLogLevel = uint8(validationLogLevel) - - settings.Unbound.AccessControl.Allowed = []netaddr.IPPrefix{ - netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0), - netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0), - } - - return nil -} - -var ( - ErrInvalidDNSOverTLSProvider = errors.New("invalid DNS over TLS provider") -) - -func (settings *DNS) readUnboundProviders(env params.Interface) (err error) { - s, err := env.Get("DOT_PROVIDERS", params.Default("cloudflare")) - if err != nil { - return fmt.Errorf("environment variable DOT_PROVIDERS: %w", err) - } - for _, field := range strings.Split(s, ",") { - dnsProvider, err := provider.Parse(field) - if err != nil { - return fmt.Errorf("%w: %s", ErrInvalidDNSOverTLSProvider, err) - } - settings.Unbound.Providers = append(settings.Unbound.Providers, dnsProvider) - } - return nil -} - -var ( - ErrInvalidHostname = errors.New("invalid hostname") -) diff --git a/internal/configuration/unbound_test.go b/internal/configuration/unbound_test.go deleted file mode 100644 index fbece04e..00000000 --- a/internal/configuration/unbound_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package configuration - -import ( - "errors" - "testing" - - "github.com/golang/mock/gomock" - "github.com/qdm12/dns/pkg/provider" - "github.com/qdm12/dns/pkg/unbound" - "github.com/qdm12/golibs/params/mock_params" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_DNS_readUnboundProviders(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - envValue string - envErr error - expected DNS - err error - }{ - "bad value": { - envValue: "invalid", - err: errors.New(`invalid DNS over TLS provider: cannot parse provider: "invalid"`), - }, - "env error": { - envErr: errors.New("env error"), - err: errors.New("environment variable DOT_PROVIDERS: env error"), - }, - "multiple valid values": { - envValue: "cloudflare,google", - expected: DNS{ - Unbound: unbound.Settings{ - Providers: []provider.Provider{ - provider.Cloudflare(), - provider.Google(), - }, - }, - }, - }, - "one invalid value in two": { - envValue: "cloudflare,invalid", - expected: DNS{ - Unbound: unbound.Settings{ - Providers: []provider.Provider{ - provider.Cloudflare(), - }, - }, - }, - err: errors.New(`invalid DNS over TLS provider: cannot parse provider: "invalid"`), - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - - env := mock_params.NewMockInterface(ctrl) - env.EXPECT().Get("DOT_PROVIDERS", gomock.Any()). - Return(testCase.envValue, testCase.envErr) - - var settings DNS - err := settings.readUnboundProviders(env) - - if testCase.err != nil { - require.Error(t, err) - assert.Equal(t, testCase.err.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - assert.Equal(t, testCase.expected, settings) - }) - } -} diff --git a/internal/configuration/updater.go b/internal/configuration/updater.go deleted file mode 100644 index 6ded2a17..00000000 --- a/internal/configuration/updater.go +++ /dev/null @@ -1,90 +0,0 @@ -package configuration - -import ( - "fmt" - "strings" - "time" - - "github.com/qdm12/golibs/params" -) - -type Updater struct { - Period time.Duration `json:"period"` - DNSAddress string `json:"dns_address"` - Cyberghost bool `json:"cyberghost"` - Expressvpn bool `json:"expressvpn"` - Fastestvpn bool `json:"fastestvpn"` - HideMyAss bool `json:"hidemyass"` - Ipvanish bool `json:"ipvanish"` - Ivpn bool `json:"ivpn"` - Mullvad bool `json:"mullvad"` - Nordvpn bool `json:"nordvpn"` - Perfectprivacy bool `json:"perfectprivacy"` - PIA bool `json:"pia"` - Privado bool `json:"privado"` - Privatevpn bool `json:"privatevpn"` - Protonvpn bool `json:"protonvpn"` - Purevpn bool `json:"purevpn"` - Surfshark bool `json:"surfshark"` - Torguard bool `json:"torguard"` - VPNUnlimited bool `json:"vpnunlimited"` - Vyprvpn bool `json:"vyprvpn"` - Wevpn bool `json:"wevpn"` - Windscribe bool `json:"windscribe"` - // The two below should be used in CLI mode only - CLI bool `json:"-"` -} - -func (settings *Updater) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *Updater) lines() (lines []string) { - if settings.Period == 0 { - return nil - } - - lines = append(lines, lastIndent+"Updater:") - - lines = append(lines, indent+lastIndent+"Period: every "+settings.Period.String()) - - return lines -} - -func (settings *Updater) EnableAll() { - settings.Cyberghost = true - settings.HideMyAss = true - settings.Ipvanish = true - settings.Ivpn = true - settings.Mullvad = true - settings.Nordvpn = true - settings.Perfectprivacy = true - settings.Privado = true - settings.PIA = true - settings.Privado = true - settings.Privatevpn = true - settings.Protonvpn = true - settings.Purevpn = true - settings.Surfshark = true - settings.Torguard = true - settings.VPNUnlimited = true - settings.Vyprvpn = true - settings.Wevpn = true - settings.Windscribe = true -} - -func (settings *Updater) read(r reader) (err error) { - settings.EnableAll() - // use cloudflare in plaintext to not be blocked by DNS over TLS by default. - // If a plaintext address is set in the DNS settings, this one will be used. - // TODO use custom future encrypted DNS written in Go without blocking - // as it's too much trouble to start another parallel unbound instance for now. - settings.DNSAddress = "1.1.1.1" - - settings.Period, err = r.env.Duration("UPDATER_PERIOD", params.Default("0")) - if err != nil { - return fmt.Errorf("environment variable UPDATER_PERIOD: %w", err) - } - - return nil -} diff --git a/internal/configuration/vpn.go b/internal/configuration/vpn.go deleted file mode 100644 index d09b182e..00000000 --- a/internal/configuration/vpn.go +++ /dev/null @@ -1,79 +0,0 @@ -package configuration - -import ( - "errors" - "fmt" - "strings" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -type VPN struct { - Type string `json:"type"` - OpenVPN OpenVPN `json:"openvpn"` - Wireguard Wireguard `json:"wireguard"` - Provider Provider `json:"provider"` -} - -func (settings *VPN) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *VPN) lines() (lines []string) { - lines = append(lines, lastIndent+"VPN:") - - lines = append(lines, indent+lastIndent+"Type: "+settings.Type) - - var vpnLines []string - switch settings.Type { - case constants.OpenVPN: - vpnLines = settings.OpenVPN.lines() - case constants.Wireguard: - vpnLines = settings.Wireguard.lines() - } - for _, line := range vpnLines { - lines = append(lines, indent+line) - } - - for _, line := range settings.Provider.lines() { - lines = append(lines, indent+line) - } - - return lines -} - -var ( - errReadProviderSettings = errors.New("cannot read provider settings") - errReadOpenVPNSettings = errors.New("cannot read OpenVPN settings") - errReadWireguardSettings = errors.New("cannot read Wireguard settings") -) - -func (settings *VPN) read(r reader) (err error) { - vpnType, err := r.env.Inside("VPN_TYPE", - []string{constants.OpenVPN, constants.Wireguard}, - params.Default(constants.OpenVPN)) - if err != nil { - return fmt.Errorf("environment variable VPN_TYPE: %w", err) - } - settings.Type = vpnType - - if err := settings.Provider.read(r, vpnType); err != nil { - return fmt.Errorf("%w: %s", errReadProviderSettings, err) - } - - switch settings.Type { - case constants.OpenVPN: - err = settings.OpenVPN.read(r, settings.Provider.Name) - if err != nil { - return fmt.Errorf("%w: %s", errReadOpenVPNSettings, err) - } - case constants.Wireguard: - err = settings.Wireguard.read(r) - if err != nil { - return fmt.Errorf("%w: %s", errReadWireguardSettings, err) - } - } - - return nil -} diff --git a/internal/configuration/vpnunlimited.go b/internal/configuration/vpnunlimited.go deleted file mode 100644 index 9a64d968..00000000 --- a/internal/configuration/vpnunlimited.go +++ /dev/null @@ -1,60 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readVPNUnlimited(r reader) (err error) { - settings.Name = constants.VPNUnlimited - servers := r.servers.GetVPNUnlimited() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.VPNUnlimitedCountryChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable COUNTRY: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.VPNUnlimitedCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.VPNUnlimitedHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - settings.ServerSelection.FreeOnly, err = r.env.YesNo("FREE_ONLY", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable FREE_ONLY: %w", err) - } - - settings.ServerSelection.StreamOnly, err = r.env.YesNo("STREAM_ONLY", params.Default("no")) - if err != nil { - return fmt.Errorf("environment variable STREAM_ONLY: %w", err) - } - - return settings.ServerSelection.OpenVPN.readProtocolOnly(r) -} - -func (settings *OpenVPN) readVPNUnlimited(r reader) (err error) { - settings.ClientKey, err = readClientKey(r) - if err != nil { - return fmt.Errorf("%w: %s", errClientKey, err) - } - - settings.ClientCrt, err = readClientCertificate(r) - if err != nil { - return fmt.Errorf("%w: %s", errClientCert, err) - } - - return nil -} diff --git a/internal/configuration/vyprvpn.go b/internal/configuration/vyprvpn.go deleted file mode 100644 index 8d626328..00000000 --- a/internal/configuration/vyprvpn.go +++ /dev/null @@ -1,24 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readVyprvpn(r reader) (err error) { - settings.Name = constants.Vyprvpn - servers := r.servers.GetVyprvpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.VyprvpnRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - return nil -} diff --git a/internal/configuration/warner_mock_test.go b/internal/configuration/warner_mock_test.go deleted file mode 100644 index f9b4a340..00000000 --- a/internal/configuration/warner_mock_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/qdm12/gluetun/internal/configuration (interfaces: Warner) - -// Package configuration is a generated GoMock package. -package configuration - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockWarner is a mock of Warner interface. -type MockWarner struct { - ctrl *gomock.Controller - recorder *MockWarnerMockRecorder -} - -// MockWarnerMockRecorder is the mock recorder for MockWarner. -type MockWarnerMockRecorder struct { - mock *MockWarner -} - -// NewMockWarner creates a new mock instance. -func NewMockWarner(ctrl *gomock.Controller) *MockWarner { - mock := &MockWarner{ctrl: ctrl} - mock.recorder = &MockWarnerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockWarner) EXPECT() *MockWarnerMockRecorder { - return m.recorder -} - -// Warn mocks base method. -func (m *MockWarner) Warn(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Warn", arg0) -} - -// Warn indicates an expected call of Warn. -func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0) -} diff --git a/internal/configuration/wevpn.go b/internal/configuration/wevpn.go deleted file mode 100644 index fd6ccb25..00000000 --- a/internal/configuration/wevpn.go +++ /dev/null @@ -1,57 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" -) - -func (settings *Provider) readWevpn(r reader) (err error) { - settings.Name = constants.Wevpn - servers := r.servers.GetWevpn() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.WevpnCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.WevpnHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - return settings.ServerSelection.OpenVPN.readWevpn(r) -} - -func (settings *OpenVPNSelection) readWevpn(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - validation := openvpnPortValidation{ - tcp: settings.TCP, - allowedTCP: []uint16{53, 1195, 1199, 2018}, - allowedUDP: []uint16{80, 1194, 1198}, - } - settings.CustomPort, err = readOpenVPNCustomPort(r, validation) - if err != nil { - return err - } - - return nil -} - -func (settings *OpenVPN) readWevpn(r reader) (err error) { - settings.ClientKey, err = readClientKey(r) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/windscribe.go b/internal/configuration/windscribe.go deleted file mode 100644 index eb3f696d..00000000 --- a/internal/configuration/windscribe.go +++ /dev/null @@ -1,69 +0,0 @@ -package configuration - -import ( - "fmt" - - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/golibs/params" -) - -func (settings *Provider) readWindscribe(r reader) (err error) { - settings.Name = constants.Windscribe - servers := r.servers.GetWindscribe() - - settings.ServerSelection.TargetIP, err = readTargetIP(r.env) - if err != nil { - return err - } - - settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.WindscribeRegionChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable REGION: %w", err) - } - - settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.WindscribeCityChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable CITY: %w", err) - } - - settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", - constants.WindscribeHostnameChoices(servers)) - if err != nil { - return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) - } - - err = settings.ServerSelection.OpenVPN.readWindscribe(r) - if err != nil { - return err - } - - return settings.ServerSelection.Wireguard.readWindscribe(r.env) -} - -func (settings *OpenVPNSelection) readWindscribe(r reader) (err error) { - settings.TCP, err = readOpenVPNProtocol(r) - if err != nil { - return err - } - - settings.CustomPort, err = readOpenVPNCustomPort(r, openvpnPortValidation{ - tcp: settings.TCP, - allowedTCP: []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}, - allowedUDP: []uint16{53, 80, 123, 443, 1194, 54783}, - }) - if err != nil { - return err - } - - return nil -} - -func (settings *WireguardSelection) readWindscribe(env params.Interface) (err error) { - settings.EndpointPort, err = readWireguardCustomPort(env, - []uint16{53, 80, 123, 443, 1194, 65142}) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/wireguard.go b/internal/configuration/wireguard.go deleted file mode 100644 index 4cc9209d..00000000 --- a/internal/configuration/wireguard.go +++ /dev/null @@ -1,88 +0,0 @@ -package configuration - -import ( - "fmt" - "net" - "strings" - - "github.com/qdm12/golibs/params" -) - -// Wireguard contains settings to configure the Wireguard client. -type Wireguard struct { - PrivateKey string `json:"privatekey"` - PreSharedKey string `json:"presharedkey"` - Addresses []*net.IPNet `json:"addresses"` - Interface string `json:"interface"` -} - -func (settings *Wireguard) String() string { - return strings.Join(settings.lines(), "\n") -} - -func (settings *Wireguard) lines() (lines []string) { - lines = append(lines, lastIndent+"Wireguard:") - - lines = append(lines, indent+lastIndent+"Network interface: "+settings.Interface) - - if settings.PrivateKey != "" { - lines = append(lines, indent+lastIndent+"Private key is set") - } - - if settings.PreSharedKey != "" { - lines = append(lines, indent+lastIndent+"Pre-shared key is set") - } - - if len(settings.Addresses) > 0 { - lines = append(lines, indent+lastIndent+"Addresses: ") - for _, address := range settings.Addresses { - lines = append(lines, indent+indent+lastIndent+address.String()) - } - } - - return lines -} - -func (settings *Wireguard) read(r reader) (err error) { - settings.PrivateKey, err = r.env.Get("WIREGUARD_PRIVATE_KEY", - params.CaseSensitiveValue(), params.Unset(), params.Compulsory()) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_PRIVATE_KEY: %w", err) - } - - settings.PreSharedKey, err = r.env.Get("WIREGUARD_PRESHARED_KEY", - params.CaseSensitiveValue(), params.Unset()) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_PRESHARED_KEY: %w", err) - } - - err = settings.readAddresses(r.env) - if err != nil { - return err - } - - settings.Interface, err = r.env.Get("WIREGUARD_INTERFACE", params.Default("wg0")) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_INTERFACE: %w", err) - } - - return nil -} - -func (settings *Wireguard) readAddresses(env params.Interface) (err error) { - addressStrings, err := env.CSV("WIREGUARD_ADDRESS", params.Compulsory()) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_ADDRESS: %w", err) - } - - for _, addressString := range addressStrings { - ip, ipNet, err := net.ParseCIDR(addressString) - if err != nil { - return fmt.Errorf("environment variable WIREGUARD_ADDRESS: address %s: %w", addressString, err) - } - ipNet.IP = ip - settings.Addresses = append(settings.Addresses, ipNet) - } - - return nil -} diff --git a/internal/constants/providers.go b/internal/constants/providers.go new file mode 100644 index 00000000..be76a36e --- /dev/null +++ b/internal/constants/providers.go @@ -0,0 +1,27 @@ +package constants + +func AllProviders() []string { + return []string{ + Custom, + Cyberghost, + Expressvpn, + Fastestvpn, + HideMyAss, + Ipvanish, + Ivpn, + Mullvad, + Nordvpn, + Perfectprivacy, + Privado, + PrivateInternetAccess, + Privatevpn, + Protonvpn, + Purevpn, + Surfshark, + Torguard, + VPNUnlimited, + Vyprvpn, + Wevpn, + Windscribe, + } +} diff --git a/internal/dns/loop.go b/internal/dns/loop.go index 113a4753..8206ed75 100644 --- a/internal/dns/loop.go +++ b/internal/dns/loop.go @@ -8,7 +8,7 @@ import ( "github.com/qdm12/dns/pkg/blacklist" "github.com/qdm12/dns/pkg/unbound" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/dns/state" "github.com/qdm12/gluetun/internal/loopstate" @@ -46,8 +46,8 @@ type Loop struct { const defaultBackoffTime = 10 * time.Second -func NewLoop(conf unbound.Configurator, settings configuration.DNS, client *http.Client, - logger Logger) *Loop { +func NewLoop(conf unbound.Configurator, settings settings.DNS, + client *http.Client, logger Logger) *Loop { start := make(chan struct{}) running := make(chan models.LoopStatus) stop := make(chan struct{}) diff --git a/internal/dns/plaintext.go b/internal/dns/plaintext.go index 212d459b..c0703d87 100644 --- a/internal/dns/plaintext.go +++ b/internal/dns/plaintext.go @@ -1,35 +1,45 @@ package dns -import "github.com/qdm12/dns/pkg/nameserver" +import ( + "net" + + "github.com/qdm12/dns/pkg/nameserver" +) func (l *Loop) useUnencryptedDNS(fallback bool) { settings := l.GetSettings() // Try with user provided plaintext ip address - targetIP := settings.PlaintextAddress - if targetIP != nil { + // if it's not 127.0.0.1 (default for DoT) + targetIP := settings.ServerAddress + if targetIP != nil && !targetIP.Equal(net.IPv4(127, 0, 0, 1)) { //nolint:gomnd if fallback { l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) } else { l.logger.Info("using plaintext DNS at address " + targetIP.String()) } nameserver.UseDNSInternally(targetIP) - err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) + err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, *settings.KeepNameserver) if err != nil { l.logger.Error(err.Error()) } return } - provider := settings.Unbound.Providers[0] - targetIP = provider.DoT().IPv4[0] + // Use first plaintext DNS IPv4 address + targetIP, err := settings.DoT.Unbound.GetFirstPlaintextIPv4() + if err != nil { + // Unbound should always have a default provider + panic(err) + } + if fallback { l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) } else { l.logger.Info("using plaintext DNS at address " + targetIP.String()) } nameserver.UseDNSInternally(targetIP) - err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) + err = nameserver.UseDNSSystemWide(l.resolvConf, targetIP, *settings.KeepNameserver) if err != nil { l.logger.Error(err.Error()) } diff --git a/internal/dns/run.go b/internal/dns/run.go index 0838faa2..22795b20 100644 --- a/internal/dns/run.go +++ b/internal/dns/run.go @@ -31,7 +31,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { unboundCancel := func() { waitError <- nil } closeStreams := func() {} - for l.GetSettings().Enabled { + for *l.GetSettings().DoT.Enabled { var err error unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx) if err == nil { @@ -54,7 +54,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { l.logAndWait(ctx, err) } - if !l.GetSettings().Enabled { + if !*l.GetSettings().DoT.Enabled { const fallback = false l.useUnencryptedDNS(fallback) } diff --git a/internal/dns/settings.go b/internal/dns/settings.go index 56eeb0f3..ba975ac7 100644 --- a/internal/dns/settings.go +++ b/internal/dns/settings.go @@ -3,7 +3,7 @@ package dns import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" ) type SettingsGetSetter interface { @@ -12,17 +12,17 @@ type SettingsGetSetter interface { } type SettingsGetter interface { - GetSettings() (settings configuration.DNS) + GetSettings() (settings settings.DNS) } -func (l *Loop) GetSettings() (settings configuration.DNS) { return l.state.GetSettings() } +func (l *Loop) GetSettings() (settings settings.DNS) { return l.state.GetSettings() } type SettingsSetter interface { - SetSettings(ctx context.Context, settings configuration.DNS) ( + SetSettings(ctx context.Context, settings settings.DNS) ( outcome string) } -func (l *Loop) SetSettings(ctx context.Context, settings configuration.DNS) ( +func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) ( outcome string) { return l.state.SetSettings(ctx, settings) } diff --git a/internal/dns/setup.go b/internal/dns/setup.go index a1bd70c0..3200cf4e 100644 --- a/internal/dns/setup.go +++ b/internal/dns/setup.go @@ -26,7 +26,8 @@ func (l *Loop) setupUnbound(ctx context.Context) ( settings := l.GetSettings() unboundCtx, cancel := context.WithCancel(context.Background()) - stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx, settings.Unbound.VerbosityDetailsLevel) + stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx, + *settings.DoT.Unbound.VerbosityDetailsLevel) if err != nil { cancel() return nil, nil, nil, err @@ -42,9 +43,9 @@ func (l *Loop) setupUnbound(ctx context.Context) ( } // use Unbound - nameserver.UseDNSInternally(net.IP{127, 0, 0, 1}) - err = nameserver.UseDNSSystemWide(l.resolvConf, net.IP{127, 0, 0, 1}, - settings.KeepNameserver) + nameserver.UseDNSInternally(settings.ServerAddress) + err = nameserver.UseDNSSystemWide(l.resolvConf, settings.ServerAddress, + *settings.KeepNameserver) if err != nil { l.logger.Error(err.Error()) } diff --git a/internal/dns/state/settings.go b/internal/dns/state/settings.go index 5f3a6387..d90f5984 100644 --- a/internal/dns/state/settings.go +++ b/internal/dns/state/settings.go @@ -4,23 +4,23 @@ import ( "context" "reflect" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) type SettingsGetSetter interface { - GetSettings() (settings configuration.DNS) + GetSettings() (settings settings.DNS) SetSettings(ctx context.Context, - settings configuration.DNS) (outcome string) + settings settings.DNS) (outcome string) } -func (s *State) GetSettings() (settings configuration.DNS) { +func (s *State) GetSettings() (settings settings.DNS) { s.settingsMu.RLock() defer s.settingsMu.RUnlock() return s.settings } -func (s *State) SetSettings(ctx context.Context, settings configuration.DNS) ( +func (s *State) SetSettings(ctx context.Context, settings settings.DNS) ( outcome string) { s.settingsMu.Lock() @@ -31,8 +31,8 @@ func (s *State) SetSettings(ctx context.Context, settings configuration.DNS) ( } // Check for only update period change - tempSettings := s.settings - tempSettings.UpdatePeriod = settings.UpdatePeriod + tempSettings := s.settings.Copy() + *tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings) s.settings = settings @@ -45,7 +45,7 @@ func (s *State) SetSettings(ctx context.Context, settings configuration.DNS) ( // Restart _, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped) - if settings.Enabled { + if *settings.DoT.Enabled { outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running) } return outcome diff --git a/internal/dns/state/state.go b/internal/dns/state/state.go index 59d339ab..a0edda22 100644 --- a/internal/dns/state/state.go +++ b/internal/dns/state/state.go @@ -3,7 +3,7 @@ package state import ( "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/loopstate" ) @@ -14,7 +14,7 @@ type Manager interface { } func New(statusApplier loopstate.Applier, - settings configuration.DNS, + settings settings.DNS, updateTicker chan<- struct{}) *State { return &State{ statusApplier: statusApplier, @@ -26,7 +26,7 @@ func New(statusApplier loopstate.Applier, type State struct { statusApplier loopstate.Applier - settings configuration.DNS + settings settings.DNS settingsMu sync.RWMutex updateTicker chan<- struct{} diff --git a/internal/dns/ticker.go b/internal/dns/ticker.go index 4eac320f..e5c1d551 100644 --- a/internal/dns/ticker.go +++ b/internal/dns/ticker.go @@ -18,8 +18,8 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { timer.Stop() timerIsStopped := true settings := l.GetSettings() - if settings.UpdatePeriod > 0 { - timer.Reset(settings.UpdatePeriod) + if period := *settings.DoT.UpdatePeriod; period > 0 { + timer.Reset(period) timerIsStopped = false } lastTick := time.Unix(0, 0) @@ -47,14 +47,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { _, _ = l.statusManager.ApplyStatus(ctx, constants.Running) settings := l.GetSettings() - timer.Reset(settings.UpdatePeriod) + timer.Reset(*settings.DoT.UpdatePeriod) case <-l.updateTicker: if !timer.Stop() { <-timer.C } timerIsStopped = true settings := l.GetSettings() - newUpdatePeriod := settings.UpdatePeriod + newUpdatePeriod := *settings.DoT.UpdatePeriod if newUpdatePeriod == 0 { continue } diff --git a/internal/dns/update.go b/internal/dns/update.go index 60b10d9d..6701dd02 100644 --- a/internal/dns/update.go +++ b/internal/dns/update.go @@ -9,17 +9,27 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) { } settings := l.GetSettings() + unboundSettings, err := settings.DoT.Unbound.ToUnboundFormat() + if err != nil { + return err + } + l.logger.Info("downloading hostnames and IP block lists") - blockedHostnames, blockedIPs, blockedIPPrefixes, errs := l.blockBuilder.All( - ctx, settings.BlacklistBuild) + blacklistSettings, err := settings.DoT.Blacklist.ToBlacklistFormat() + if err != nil { + return err + } + + blockedHostnames, blockedIPs, blockedIPPrefixes, errs := + l.blockBuilder.All(ctx, blacklistSettings) for _, err := range errs { l.logger.Warn(err.Error()) } // TODO change to BlockHostnames() when migrating to qdm12/dns v2 - settings.Unbound.Blacklist.FqdnHostnames = blockedHostnames - settings.Unbound.Blacklist.IPs = blockedIPs - settings.Unbound.Blacklist.IPPrefixes = blockedIPPrefixes + unboundSettings.Blacklist.FqdnHostnames = blockedHostnames + unboundSettings.Blacklist.IPs = blockedIPs + unboundSettings.Blacklist.IPPrefixes = blockedIPPrefixes - return l.conf.MakeUnboundConf(settings.Unbound) + return l.conf.MakeUnboundConf(unboundSettings) } diff --git a/internal/healthcheck/health.go b/internal/healthcheck/health.go index 11c440c1..084c25a9 100644 --- a/internal/healthcheck/health.go +++ b/internal/healthcheck/health.go @@ -20,7 +20,7 @@ func (s *Server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) { if previousErr != nil && err == nil { s.logger.Info("healthy!") s.vpn.healthyTimer.Stop() - s.vpn.healthyWait = s.config.VPN.Initial + s.vpn.healthyWait = *s.config.VPN.Initial } else if previousErr == nil && err != nil { s.logger.Info("unhealthy: " + err.Error()) s.vpn.healthyTimer.Stop() diff --git a/internal/healthcheck/openvpn.go b/internal/healthcheck/openvpn.go index 8acf95e9..e3c8d7b2 100644 --- a/internal/healthcheck/openvpn.go +++ b/internal/healthcheck/openvpn.go @@ -19,6 +19,6 @@ func (s *Server) onUnhealthyVPN(ctx context.Context) { s.vpn.healthyWait.String() + ": restarting VPN") _, _ = s.vpn.looper.ApplyStatus(ctx, constants.Stopped) _, _ = s.vpn.looper.ApplyStatus(ctx, constants.Running) - s.vpn.healthyWait += s.config.VPN.Addition + s.vpn.healthyWait += *s.config.VPN.Addition s.vpn.healthyTimer = time.NewTimer(s.vpn.healthyWait) } diff --git a/internal/healthcheck/server.go b/internal/healthcheck/server.go index 1531ceb1..27bdc651 100644 --- a/internal/healthcheck/server.go +++ b/internal/healthcheck/server.go @@ -3,7 +3,7 @@ package healthcheck import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/vpn" ) @@ -17,11 +17,11 @@ type Server struct { logger Logger handler *handler pinger Pinger - config configuration.Health + config settings.Health vpn vpnHealth } -func NewServer(config configuration.Health, +func NewServer(config settings.Health, logger Logger, vpnLooper vpn.Looper) *Server { return &Server{ logger: logger, @@ -30,7 +30,7 @@ func NewServer(config configuration.Health, config: config, vpn: vpnHealth{ looper: vpnLooper, - healthyWait: config.VPN.Initial, + healthyWait: *config.VPN.Initial, }, } } diff --git a/internal/httpproxy/loop.go b/internal/httpproxy/loop.go index 6bfc357f..0c357272 100644 --- a/internal/httpproxy/loop.go +++ b/internal/httpproxy/loop.go @@ -5,7 +5,7 @@ import ( "context" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/httpproxy/state" "github.com/qdm12/gluetun/internal/loopstate" @@ -36,7 +36,7 @@ type Loop struct { const defaultBackoffTime = 10 * time.Second -func NewLoop(logger Logger, settings configuration.HTTPProxy) *Loop { +func NewLoop(logger Logger, settings settings.HTTPProxy) *Loop { start := make(chan struct{}) running := make(chan models.LoopStatus) stop := make(chan struct{}) diff --git a/internal/httpproxy/run.go b/internal/httpproxy/run.go index 73ce6cb8..66f37f6d 100644 --- a/internal/httpproxy/run.go +++ b/internal/httpproxy/run.go @@ -2,7 +2,6 @@ package httpproxy import ( "context" - "strconv" "github.com/qdm12/gluetun/internal/constants" ) @@ -14,7 +13,7 @@ type Runner interface { func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { defer close(done) - if !l.state.GetSettings().Enabled { + if !*l.state.GetSettings().Enabled { select { case <-l.start: case <-ctx.Done(): @@ -26,8 +25,9 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { runCtx, runCancel := context.WithCancel(ctx) settings := l.state.GetSettings() - address := ":" + strconv.Itoa(int(settings.Port)) - server := New(runCtx, address, l.logger, settings.Stealth, settings.Log, settings.User, settings.Password) + server := New(runCtx, settings.ListeningAddress, l.logger, + *settings.Stealth, *settings.Log, *settings.User, + *settings.Password) errorCh := make(chan error) go server.Run(runCtx, errorCh) diff --git a/internal/httpproxy/settings.go b/internal/httpproxy/settings.go index 1b266376..3fc5f904 100644 --- a/internal/httpproxy/settings.go +++ b/internal/httpproxy/settings.go @@ -3,17 +3,17 @@ package httpproxy import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/httpproxy/state" ) type SettingsGetSetter = state.SettingsGetSetter -func (l *Loop) GetSettings() (settings configuration.HTTPProxy) { +func (l *Loop) GetSettings() (settings settings.HTTPProxy) { return l.state.GetSettings() } -func (l *Loop) SetSettings(ctx context.Context, settings configuration.HTTPProxy) ( +func (l *Loop) SetSettings(ctx context.Context, settings settings.HTTPProxy) ( outcome string) { return l.state.SetSettings(ctx, settings) } diff --git a/internal/httpproxy/state/settings.go b/internal/httpproxy/state/settings.go index 455a66f4..58846c4f 100644 --- a/internal/httpproxy/state/settings.go +++ b/internal/httpproxy/state/settings.go @@ -4,7 +4,7 @@ import ( "context" "reflect" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) @@ -14,10 +14,10 @@ type SettingsGetSetter interface { } type SettingsGetter interface { - GetSettings() (settings configuration.HTTPProxy) + GetSettings() (settings settings.HTTPProxy) } -func (s *State) GetSettings() (settings configuration.HTTPProxy) { +func (s *State) GetSettings() (settings settings.HTTPProxy) { s.settingsMu.RLock() defer s.settingsMu.RUnlock() return s.settings @@ -25,19 +25,19 @@ func (s *State) GetSettings() (settings configuration.HTTPProxy) { type SettingsSetter interface { SetSettings(ctx context.Context, - settings configuration.HTTPProxy) (outcome string) + settings settings.HTTPProxy) (outcome string) } func (s *State) SetSettings(ctx context.Context, - settings configuration.HTTPProxy) (outcome string) { + settings settings.HTTPProxy) (outcome string) { s.settingsMu.Lock() settingsUnchanged := reflect.DeepEqual(settings, s.settings) if settingsUnchanged { s.settingsMu.Unlock() return "settings left unchanged" } - newEnabled := settings.Enabled - previousEnabled := s.settings.Enabled + newEnabled := *settings.Enabled + previousEnabled := *s.settings.Enabled s.settings = settings s.settingsMu.Unlock() // Either restart or set changed status diff --git a/internal/httpproxy/state/state.go b/internal/httpproxy/state/state.go index 185f0b6f..930cfd8a 100644 --- a/internal/httpproxy/state/state.go +++ b/internal/httpproxy/state/state.go @@ -3,7 +3,7 @@ package state import ( "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/loopstate" ) @@ -14,7 +14,7 @@ type Manager interface { } func New(statusApplier loopstate.Applier, - settings configuration.HTTPProxy) *State { + settings settings.HTTPProxy) *State { return &State{ statusApplier: statusApplier, settings: settings, @@ -23,6 +23,6 @@ func New(statusApplier loopstate.Applier, type State struct { statusApplier loopstate.Applier - settings configuration.HTTPProxy + settings settings.HTTPProxy settingsMu sync.RWMutex } diff --git a/internal/openvpn/run.go b/internal/openvpn/run.go index d3333911..59ae5ea9 100644 --- a/internal/openvpn/run.go +++ b/internal/openvpn/run.go @@ -3,17 +3,17 @@ package openvpn import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/golibs/command" ) type Runner struct { - settings configuration.OpenVPN + settings settings.OpenVPN starter command.Starter logger Logger } -func NewRunner(settings configuration.OpenVPN, starter command.Starter, +func NewRunner(settings settings.OpenVPN, starter command.Starter, logger Logger) *Runner { return &Runner{ starter: starter, diff --git a/internal/portforward/fs.go b/internal/portforward/fs.go index 9cd529f5..fde9696d 100644 --- a/internal/portforward/fs.go +++ b/internal/portforward/fs.go @@ -6,7 +6,7 @@ import ( ) func (l *Loop) removePortForwardedFile() { - filepath := l.state.GetSettings().Filepath + filepath := *l.state.GetSettings().Filepath l.logger.Info("removing port file " + filepath) if err := os.Remove(filepath); err != nil { l.logger.Error(err.Error()) @@ -14,7 +14,7 @@ func (l *Loop) removePortForwardedFile() { } func (l *Loop) writePortForwardedFile(port uint16) { - filepath := l.state.GetSettings().Filepath + filepath := *l.state.GetSettings().Filepath l.logger.Info("writing port file " + filepath) if err := writePortForwardedToFile(filepath, port); err != nil { l.logger.Error(err.Error()) diff --git a/internal/portforward/loop.go b/internal/portforward/loop.go index 6a27204f..84a118af 100644 --- a/internal/portforward/loop.go +++ b/internal/portforward/loop.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/loopstate" @@ -42,7 +42,7 @@ type Loop struct { const defaultBackoffTime = 5 * time.Second -func NewLoop(settings configuration.PortForwarding, +func NewLoop(settings settings.PortForwarding, client *http.Client, portAllower firewall.PortAllower, logger Logger) *Loop { start := make(chan struct{}) diff --git a/internal/portforward/settings.go b/internal/portforward/settings.go index 9736e4ab..db77b23f 100644 --- a/internal/portforward/settings.go +++ b/internal/portforward/settings.go @@ -3,17 +3,17 @@ package portforward import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/portforward/state" ) type SettingsGetSetter = state.SettingsGetSetter -func (l *Loop) GetSettings() (settings configuration.PortForwarding) { +func (l *Loop) GetSettings() (settings settings.PortForwarding) { return l.state.GetSettings() } -func (l *Loop) SetSettings(ctx context.Context, settings configuration.PortForwarding) ( +func (l *Loop) SetSettings(ctx context.Context, settings settings.PortForwarding) ( outcome string) { return l.state.SetSettings(ctx, settings) } diff --git a/internal/portforward/state/settings.go b/internal/portforward/state/settings.go index d85a0c75..06e624c6 100644 --- a/internal/portforward/state/settings.go +++ b/internal/portforward/state/settings.go @@ -5,23 +5,23 @@ import ( "os" "reflect" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) type SettingsGetSetter interface { - GetSettings() (settings configuration.PortForwarding) + GetSettings() (settings settings.PortForwarding) SetSettings(ctx context.Context, - settings configuration.PortForwarding) (outcome string) + settings settings.PortForwarding) (outcome string) } -func (s *State) GetSettings() (settings configuration.PortForwarding) { +func (s *State) GetSettings() (settings settings.PortForwarding) { s.settingsMu.RLock() defer s.settingsMu.RUnlock() return s.settings } -func (s *State) SetSettings(ctx context.Context, settings configuration.PortForwarding) ( +func (s *State) SetSettings(ctx context.Context, settings settings.PortForwarding) ( outcome string) { s.settingsMu.Lock() @@ -32,11 +32,11 @@ func (s *State) SetSettings(ctx context.Context, settings configuration.PortForw } if s.settings.Filepath != settings.Filepath { - _ = os.Rename(s.settings.Filepath, settings.Filepath) + _ = os.Rename(*s.settings.Filepath, *settings.Filepath) } - newEnabled := settings.Enabled - previousEnabled := s.settings.Enabled + newEnabled := *settings.Enabled + previousEnabled := *s.settings.Enabled s.settings = settings s.settingsMu.Unlock() diff --git a/internal/portforward/state/state.go b/internal/portforward/state/state.go index 79d1d5a5..55ce95a8 100644 --- a/internal/portforward/state/state.go +++ b/internal/portforward/state/state.go @@ -3,7 +3,7 @@ package state import ( "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/loopstate" ) @@ -16,7 +16,7 @@ type Manager interface { } func New(statusApplier loopstate.Applier, - settings configuration.PortForwarding) *State { + settings settings.PortForwarding) *State { return &State{ statusApplier: statusApplier, settings: settings, @@ -26,7 +26,7 @@ func New(statusApplier loopstate.Applier, type State struct { statusApplier loopstate.Applier - settings configuration.PortForwarding + settings settings.PortForwarding settingsMu sync.RWMutex portForwarded uint16 diff --git a/internal/provider/custom/connection.go b/internal/provider/custom/connection.go index 362216a0..9327c894 100644 --- a/internal/provider/custom/connection.go +++ b/internal/provider/custom/connection.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/openvpn/extract" @@ -17,7 +17,7 @@ var ( ) // GetConnection gets the connection from the OpenVPN configuration file. -func (p *Provider) GetConnection(selection configuration.ServerSelection) ( +func (p *Provider) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { switch selection.VPN { case constants.OpenVPN: @@ -30,9 +30,9 @@ func (p *Provider) GetConnection(selection configuration.ServerSelection) ( } func getOpenVPNConnection(extractor extract.Interface, - selection configuration.ServerSelection) ( + selection settings.ServerSelection) ( connection models.Connection, err error) { - _, connection, err = extractor.Data(selection.OpenVPN.ConfFile) + _, connection, err = extractor.Data(*selection.OpenVPN.ConfFile) if err != nil { return connection, fmt.Errorf("%w: %s", ErrExtractConnection, err) } @@ -41,9 +41,9 @@ func getOpenVPNConnection(extractor extract.Interface, return connection, nil } -func getWireguardConnection(selection configuration.ServerSelection) ( +func getWireguardConnection(selection settings.ServerSelection) ( connection models.Connection) { - port := getPort(selection.Wireguard.EndpointPort, selection) + port := getPort(*selection.Wireguard.EndpointPort, selection) return models.Connection{ Type: constants.Wireguard, IP: selection.Wireguard.EndpointIP, @@ -54,6 +54,6 @@ func getWireguardConnection(selection configuration.ServerSelection) ( } // Port found is overridden by custom port set with `PORT` or `WIREGUARD_ENDPOINT_PORT`. -func getPort(foundPort uint16, selection configuration.ServerSelection) (port uint16) { +func getPort(foundPort uint16, selection settings.ServerSelection) (port uint16) { return utils.GetPort(selection, foundPort, foundPort, foundPort) } diff --git a/internal/provider/custom/openvpnconf.go b/internal/provider/custom/openvpnconf.go index 0a458cb9..d835eaa7 100644 --- a/internal/provider/custom/openvpnconf.go +++ b/internal/provider/custom/openvpnconf.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" @@ -15,8 +15,8 @@ import ( var ErrExtractData = errors.New("failed extracting information from custom configuration file") func (p *Provider) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { - lines, _, err = p.extractor.Data(settings.ConfFile) + settings settings.OpenVPN) (lines []string, err error) { + lines, _, err = p.extractor.Data(*settings.ConfFile) if err != nil { return nil, fmt.Errorf("%w: %s", ErrExtractData, err) } @@ -27,7 +27,7 @@ func (p *Provider) BuildConf(connection models.Connection, } func modifyConfig(lines []string, connection models.Connection, - settings configuration.OpenVPN) (modified []string) { + settings settings.OpenVPN) (modified []string) { // Remove some lines for _, line := range lines { switch { @@ -52,9 +52,9 @@ func modifyConfig(lines []string, connection models.Connection, // Remove values eventually modified len(settings.Ciphers) > 0 && hasPrefixOneOf(line, "cipher ", "ncp-ciphers ", "data-ciphers ", "data-ciphers-fallback "), - settings.Auth != "" && strings.HasPrefix(line, "auth "), - settings.MSSFix > 0 && strings.HasPrefix(line, "mssfix "), - !settings.IPv6 && hasPrefixOneOf(line, "tun-ipv6", + *settings.Auth != "" && strings.HasPrefix(line, "auth "), + *settings.MSSFix > 0 && strings.HasPrefix(line, "mssfix "), + !*settings.IPv6 && hasPrefixOneOf(line, "tun-ipv6", `pull-filter ignore "route-ipv6"`, `pull-filter ignore "ifconfig-ipv6"`): default: @@ -74,21 +74,21 @@ func modifyConfig(lines []string, connection models.Connection, if settings.User != "" { modified = append(modified, "auth-user-pass "+constants.OpenVPNAuthConf) } - modified = append(modified, "verb "+strconv.Itoa(settings.Verbosity)) + modified = append(modified, "verb "+strconv.Itoa(*settings.Verbosity)) if len(settings.Ciphers) > 0 { modified = append(modified, utils.CipherLines(settings.Ciphers, settings.Version)...) } - if settings.Auth != "" { - modified = append(modified, "auth "+settings.Auth) + if *settings.Auth != "" { + modified = append(modified, "auth "+*settings.Auth) } - if settings.MSSFix > 0 { - modified = append(modified, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + modified = append(modified, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { modified = append(modified, `pull-filter ignore "route-ipv6"`) modified = append(modified, `pull-filter ignore "ifconfig-ipv6"`) } - if !settings.Root { + if !*settings.Root { modified = append(modified, "user "+settings.ProcUser) modified = append(modified, "persist-tun") modified = append(modified, "persist-key") diff --git a/internal/provider/custom/openvpnconf_test.go b/internal/provider/custom/openvpnconf_test.go index ef089056..86034b91 100644 --- a/internal/provider/custom/openvpnconf_test.go +++ b/internal/provider/custom/openvpnconf_test.go @@ -4,18 +4,23 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" ) +func boolPtr(b bool) *bool { return &b } +func intPtr(n int) *int { return &n } +func uint16Ptr(n uint16) *uint16 { return &n } +func stringPtr(s string) *string { return &s } + func Test_modifyConfig(t *testing.T) { t.Parallel() testCases := map[string]struct { lines []string - settings configuration.OpenVPN + settings settings.OpenVPN connection models.Connection modified []string }{ @@ -30,14 +35,16 @@ func Test_modifyConfig(t *testing.T) { "keep me here", "auth bla", }, - settings: configuration.OpenVPN{ + settings: settings.OpenVPN{ User: "user", Ciphers: []string{"cipher"}, - Auth: "auth", - MSSFix: 1000, + Auth: stringPtr("auth"), + MSSFix: uint16Ptr(1000), + Root: boolPtr(false), ProcUser: "procuser", Interface: "tun3", - }, + Verbosity: intPtr(0), + }.WithDefaults(constants.Custom), connection: models.Connection{ IP: net.IPv4(1, 2, 3, 4), Port: 1194, diff --git a/internal/provider/cyberghost/connection.go b/internal/provider/cyberghost/connection.go index 19fbb3ca..c6467e5f 100644 --- a/internal/provider/cyberghost/connection.go +++ b/internal/provider/cyberghost/connection.go @@ -1,17 +1,17 @@ package cyberghost import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (c *Cyberghost) GetConnection(selection configuration.ServerSelection) ( +func (c *Cyberghost) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 443 protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } diff --git a/internal/provider/cyberghost/filter.go b/internal/provider/cyberghost/filter.go index 12cfef93..c3e0dc9d 100644 --- a/internal/provider/cyberghost/filter.go +++ b/internal/provider/cyberghost/filter.go @@ -3,14 +3,14 @@ package cyberghost import ( "errors" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) var ErrGroupMismatchesProtocol = errors.New("server group does not match protocol") -func (c *Cyberghost) filterServers(selection configuration.ServerSelection) ( +func (c *Cyberghost) filterServers(selection settings.ServerSelection) ( servers []models.CyberghostServer, err error) { for _, server := range c.servers { switch { diff --git a/internal/provider/cyberghost/filter_test.go b/internal/provider/cyberghost/filter_test.go index ed1bb0ac..bc1d1648 100644 --- a/internal/provider/cyberghost/filter_test.go +++ b/internal/provider/cyberghost/filter_test.go @@ -4,23 +4,25 @@ import ( "errors" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Cyberghost_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.CyberghostServer - selection configuration.ServerSelection + selection settings.ServerSelection filteredServers []models.CyberghostServer err error }{ "no server": { - selection: configuration.ServerSelection{VPN: constants.OpenVPN}, + selection: settings.ServerSelection{}.WithDefaults(constants.Cyberghost), err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "servers without filter defaults to UDP": { @@ -30,6 +32,7 @@ func Test_Cyberghost_filterServers(t *testing.T) { {Country: "c", UDP: true}, {Country: "d", UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Cyberghost), filteredServers: []models.CyberghostServer{ {Country: "c", UDP: true}, {Country: "d", UDP: true}, @@ -42,11 +45,11 @@ func Test_Cyberghost_filterServers(t *testing.T) { {Country: "c", UDP: true}, {Country: "d", UDP: true}, }, - selection: configuration.ServerSelection{ - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, - }, + }.WithDefaults(constants.Cyberghost), filteredServers: []models.CyberghostServer{ {Country: "a", TCP: true}, {Country: "b", TCP: true}, @@ -59,9 +62,9 @@ func Test_Cyberghost_filterServers(t *testing.T) { {Country: "c", UDP: true}, {Country: "d", UDP: true}, }, - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Countries: []string{"a", "c"}, - }, + }.WithDefaults(constants.Cyberghost), filteredServers: []models.CyberghostServer{ {Country: "a", UDP: true}, {Country: "c", UDP: true}, @@ -73,9 +76,9 @@ func Test_Cyberghost_filterServers(t *testing.T) { {Hostname: "b", UDP: true}, {Hostname: "c", UDP: true}, }, - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"a", "c"}, - }, + }.WithDefaults(constants.Cyberghost), filteredServers: []models.CyberghostServer{ {Hostname: "a", UDP: true}, {Hostname: "c", UDP: true}, diff --git a/internal/provider/cyberghost/openvpnconf.go b/internal/provider/cyberghost/openvpnconf.go index 5aa6c4b4..eaca7676 100644 --- a/internal/provider/cyberghost/openvpnconf.go +++ b/internal/provider/cyberghost/openvpnconf.go @@ -1,16 +1,18 @@ package cyberghost import ( + "fmt" "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/openvpn/parse" "github.com/qdm12/gluetun/internal/provider/utils" ) func (c *Cyberghost) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{ constants.AES256gcm, @@ -19,8 +21,9 @@ func (c *Cyberghost) BuildConf(connection models.Connection, } } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } lines = []string{ @@ -28,13 +31,13 @@ func (c *Cyberghost) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Cyberghost specific "ping 10", "remote-cert-tls server", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -54,27 +57,35 @@ func (c *Cyberghost) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } lines = append(lines, utils.WrapOpenvpnCA( constants.CyberghostCertificate)...) - lines = append(lines, utils.WrapOpenvpnCert( - settings.ClientCrt)...) - lines = append(lines, utils.WrapOpenvpnKey( - settings.ClientKey)...) + + certData, err := parse.ExtractCert([]byte(*settings.ClientCrt)) + if err != nil { + return nil, fmt.Errorf("client cert is not valid: %w", err) + } + lines = append(lines, utils.WrapOpenvpnCert(certData)...) + + keyData, err := parse.ExtractPrivateKey([]byte(*settings.ClientKey)) + if err != nil { + return nil, fmt.Errorf("client key is not valid: %w", err) + } + lines = append(lines, utils.WrapOpenvpnKey(keyData)...) lines = append(lines, "") diff --git a/internal/provider/expressvpn/connection.go b/internal/provider/expressvpn/connection.go index 6cb24802..f9a13514 100644 --- a/internal/provider/expressvpn/connection.go +++ b/internal/provider/expressvpn/connection.go @@ -1,12 +1,12 @@ package expressvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Provider) GetConnection(selection configuration.ServerSelection) ( +func (p *Provider) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { port := getPort(selection) protocol := utils.GetProtocol(selection) @@ -33,7 +33,7 @@ func (p *Provider) GetConnection(selection configuration.ServerSelection) ( return utils.PickConnection(connections, selection, p.randSource) } -func getPort(selection configuration.ServerSelection) (port uint16) { +func getPort(selection settings.ServerSelection) (port uint16) { const ( defaultOpenVPNTCP = 0 defaultOpenVPNUDP = 1195 diff --git a/internal/provider/expressvpn/connection_test.go b/internal/provider/expressvpn/connection_test.go index 3f1d97b4..a49d8b66 100644 --- a/internal/provider/expressvpn/connection_test.go +++ b/internal/provider/expressvpn/connection_test.go @@ -6,7 +6,7 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -18,15 +18,13 @@ func Test_Provider_GetConnection(t *testing.T) { testCases := map[string]struct { servers []models.ExpressvpnServer - selection configuration.ServerSelection + selection settings.ServerSelection connection models.Connection err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Expressvpn), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.ExpressvpnServer{ @@ -34,37 +32,41 @@ func Test_Provider_GetConnection(t *testing.T) { {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Expressvpn), connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(1, 1, 1, 1), Port: 1195, Protocol: constants.UDP, }, }, "target IP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ TargetIP: net.IPv4(2, 2, 2, 2), - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1195, Protocol: constants.UDP, }, }, "with filter": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, {Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, {Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1195, Protocol: constants.UDP, diff --git a/internal/provider/expressvpn/filter.go b/internal/provider/expressvpn/filter.go index 7d382e91..0b5895b1 100644 --- a/internal/provider/expressvpn/filter.go +++ b/internal/provider/expressvpn/filter.go @@ -1,12 +1,12 @@ package expressvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Provider) filterServers(selection configuration.ServerSelection) ( +func (p *Provider) filterServers(selection settings.ServerSelection) ( servers []models.ExpressvpnServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/expressvpn/filter_test.go b/internal/provider/expressvpn/filter_test.go index 13c2afa3..eb599f00 100644 --- a/internal/provider/expressvpn/filter_test.go +++ b/internal/provider/expressvpn/filter_test.go @@ -5,27 +5,27 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Expressvpn_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.ExpressvpnServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.ExpressvpnServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Expressvpn), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.ExpressvpnServer{ @@ -33,6 +33,7 @@ func Test_Expressvpn_filterServers(t *testing.T) { {Hostname: "b", UDP: true}, {Hostname: "c", UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Expressvpn), filtered: []models.ExpressvpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, @@ -40,9 +41,9 @@ func Test_Expressvpn_filterServers(t *testing.T) { }, }, "filter by country": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Countries: []string{"b"}, - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {Country: "a", UDP: true}, {Country: "b", UDP: true}, @@ -53,9 +54,9 @@ func Test_Expressvpn_filterServers(t *testing.T) { }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {City: "a", UDP: true}, {City: "b", UDP: true}, @@ -66,9 +67,9 @@ func Test_Expressvpn_filterServers(t *testing.T) { }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, @@ -79,11 +80,11 @@ func Test_Expressvpn_filterServers(t *testing.T) { }, }, "filter by protocol": { - selection: configuration.ServerSelection{ - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, - }, + }.WithDefaults(constants.Expressvpn), servers: []models.ExpressvpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true, TCP: true}, diff --git a/internal/provider/expressvpn/openvpnconf.go b/internal/provider/expressvpn/openvpnconf.go index 79ef8d37..328218e9 100644 --- a/internal/provider/expressvpn/openvpnconf.go +++ b/internal/provider/expressvpn/openvpnconf.go @@ -3,23 +3,26 @@ package expressvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Provider) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } - if settings.MSSFix == 0 { - settings.MSSFix = 1200 + mssFix := *settings.MSSFix + if mssFix == 0 { + const defaultMSSFix = 1200 + mssFix = defaultMSSFix } lines = []string{ @@ -27,19 +30,19 @@ func (p *Provider) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Expressvpn specific "fast-io", "fragment 1300", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "sndbuf 524288", "rcvbuf 524288", "verify-x509-name Server name-prefix", // security hole I guess? "remote-cert-tls server", // updated name of ns-cert-type "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "mute-replay-warnings", @@ -59,13 +62,13 @@ func (p *Provider) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/fastestvpn/connection.go b/internal/provider/fastestvpn/connection.go index 6dc1c0a0..bcd240e8 100644 --- a/internal/provider/fastestvpn/connection.go +++ b/internal/provider/fastestvpn/connection.go @@ -1,17 +1,17 @@ package fastestvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (f *Fastestvpn) GetConnection(selection configuration.ServerSelection) ( +func (f *Fastestvpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 4443 protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } diff --git a/internal/provider/fastestvpn/filter.go b/internal/provider/fastestvpn/filter.go index 67d37542..f1909314 100644 --- a/internal/provider/fastestvpn/filter.go +++ b/internal/provider/fastestvpn/filter.go @@ -1,12 +1,12 @@ package fastestvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (f *Fastestvpn) filterServers(selection configuration.ServerSelection) ( +func (f *Fastestvpn) filterServers(selection settings.ServerSelection) ( servers []models.FastestvpnServer, err error) { for _, server := range f.servers { switch { diff --git a/internal/provider/fastestvpn/openvpnconf.go b/internal/provider/fastestvpn/openvpnconf.go index 3975fce8..c589d21b 100644 --- a/internal/provider/fastestvpn/openvpnconf.go +++ b/internal/provider/fastestvpn/openvpnconf.go @@ -3,22 +3,25 @@ package fastestvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (f *Fastestvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } - if settings.MSSFix == 0 { - settings.MSSFix = 1450 + + mssFix := *settings.MSSFix + if mssFix == 0 { + mssFix = 1450 } lines = []string{ @@ -26,14 +29,14 @@ func (f *Fastestvpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Fastestvpn specific - "mssfix " + strconv.Itoa(int(settings.MSSFix)), // defaults to 1450 + "mssfix " + strconv.Itoa(int(mssFix)), // defaults to 1450 "tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA", //nolint:lll "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, "comp-lzo", "reneg-sec 0", @@ -58,13 +61,13 @@ func (f *Fastestvpn) BuildConf(connection models.Connection, lines = append(lines, "ping 15") // FastestVPN specific } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/hidemyass/connection.go b/internal/provider/hidemyass/connection.go index 8ef2018e..6844f4c0 100644 --- a/internal/provider/hidemyass/connection.go +++ b/internal/provider/hidemyass/connection.go @@ -1,23 +1,23 @@ package hidemyass import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (h *HideMyAss) GetConnection(selection configuration.ServerSelection) ( +func (h *HideMyAss) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { var port uint16 = 553 protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP port = 8080 } - if selection.OpenVPN.CustomPort > 0 { - port = selection.OpenVPN.CustomPort + if *selection.OpenVPN.CustomPort > 0 { + port = *selection.OpenVPN.CustomPort } servers, err := h.filterServers(selection) diff --git a/internal/provider/hidemyass/filter.go b/internal/provider/hidemyass/filter.go index fa1c3e79..c3b66173 100644 --- a/internal/provider/hidemyass/filter.go +++ b/internal/provider/hidemyass/filter.go @@ -1,12 +1,12 @@ package hidemyass import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (h *HideMyAss) filterServers(selection configuration.ServerSelection) ( +func (h *HideMyAss) filterServers(selection settings.ServerSelection) ( servers []models.HideMyAssServer, err error) { for _, server := range h.servers { switch { diff --git a/internal/provider/hidemyass/openvpnconf.go b/internal/provider/hidemyass/openvpnconf.go index 37dc3384..27e28c51 100644 --- a/internal/provider/hidemyass/openvpnconf.go +++ b/internal/provider/hidemyass/openvpnconf.go @@ -3,14 +3,14 @@ package hidemyass import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (h *HideMyAss) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } @@ -20,7 +20,7 @@ func (h *HideMyAss) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // HideMyAss specific "ping 5", @@ -41,25 +41,25 @@ func (h *HideMyAss) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) + if *settings.Auth != "" { + lines = append(lines, "auth "+*settings.Auth) } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/ipvanish/connection.go b/internal/provider/ipvanish/connection.go index 270dab07..cf1a211d 100644 --- a/internal/provider/ipvanish/connection.go +++ b/internal/provider/ipvanish/connection.go @@ -3,7 +3,7 @@ package ipvanish import ( "errors" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" @@ -11,11 +11,11 @@ import ( var ErrProtocolUnsupported = errors.New("network protocol is not supported") -func (i *Ipvanish) GetConnection(selection configuration.ServerSelection) ( +func (i *Ipvanish) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 443 const protocol = constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { return connection, ErrProtocolUnsupported } diff --git a/internal/provider/ipvanish/filter.go b/internal/provider/ipvanish/filter.go index 02e3c9a8..261c9e38 100644 --- a/internal/provider/ipvanish/filter.go +++ b/internal/provider/ipvanish/filter.go @@ -1,12 +1,12 @@ package ipvanish import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (i *Ipvanish) filterServers(selection configuration.ServerSelection) ( +func (i *Ipvanish) filterServers(selection settings.ServerSelection) ( servers []models.IpvanishServer, err error) { for _, server := range i.servers { switch { diff --git a/internal/provider/ipvanish/openvpnconf.go b/internal/provider/ipvanish/openvpnconf.go index 6e66871e..4dff7eb6 100644 --- a/internal/provider/ipvanish/openvpnconf.go +++ b/internal/provider/ipvanish/openvpnconf.go @@ -3,19 +3,20 @@ package ipvanish import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (i *Ipvanish) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } lines = []string{ @@ -23,13 +24,13 @@ func (i *Ipvanish) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Ipvanish specific "verify-x509-name " + connection.Hostname + " name", "tls-cipher TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "mute-replay-warnings", @@ -45,21 +46,21 @@ func (i *Ipvanish) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/ivpn/connection.go b/internal/provider/ivpn/connection.go index 703a203d..d50fd2ee 100644 --- a/internal/provider/ivpn/connection.go +++ b/internal/provider/ivpn/connection.go @@ -1,12 +1,12 @@ package ivpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (i *Ivpn) GetConnection(selection configuration.ServerSelection) ( +func (i *Ivpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { port := getPort(selection) protocol := utils.GetProtocol(selection) @@ -34,7 +34,7 @@ func (i *Ivpn) GetConnection(selection configuration.ServerSelection) ( return utils.PickConnection(connections, selection, i.randSource) } -func getPort(selection configuration.ServerSelection) (port uint16) { +func getPort(selection settings.ServerSelection) (port uint16) { const ( defaultOpenVPNTCP = 443 defaultOpenVPNUDP = 1194 diff --git a/internal/provider/ivpn/connection_test.go b/internal/provider/ivpn/connection_test.go index 18e78233..9601e3a6 100644 --- a/internal/provider/ivpn/connection_test.go +++ b/internal/provider/ivpn/connection_test.go @@ -6,7 +6,7 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -18,53 +18,55 @@ func Test_Ivpn_GetConnection(t *testing.T) { testCases := map[string]struct { servers []models.IvpnServer - selection configuration.ServerSelection + selection settings.ServerSelection connection models.Connection err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Ivpn), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.IvpnServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Ivpn), connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(1, 1, 1, 1), Port: 1194, Protocol: constants.UDP, }, }, "target IP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ TargetIP: net.IPv4(2, 2, 2, 2), - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, }, }, "with filter": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, - {Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, - {Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}, UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}, UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}, UDP: true}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, diff --git a/internal/provider/ivpn/filter.go b/internal/provider/ivpn/filter.go index a757970b..f9b1f023 100644 --- a/internal/provider/ivpn/filter.go +++ b/internal/provider/ivpn/filter.go @@ -1,12 +1,12 @@ package ivpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (i *Ivpn) filterServers(selection configuration.ServerSelection) ( +func (i *Ivpn) filterServers(selection settings.ServerSelection) ( servers []models.IvpnServer, err error) { for _, server := range i.servers { switch { diff --git a/internal/provider/ivpn/filter_test.go b/internal/provider/ivpn/filter_test.go index 4aa3a2bb..7de99c4f 100644 --- a/internal/provider/ivpn/filter_test.go +++ b/internal/provider/ivpn/filter_test.go @@ -5,105 +5,106 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Ivpn_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.IvpnServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.IvpnServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Ivpn), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.IvpnServer{ - {Hostname: "a", UDP: true}, - {Hostname: "b", UDP: true}, - {Hostname: "c", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "c", UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Ivpn), filtered: []models.IvpnServer{ - {Hostname: "a", UDP: true}, - {Hostname: "b", UDP: true}, - {Hostname: "c", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "c", UDP: true}, }, }, "filter by country": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Countries: []string{"b"}, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {Country: "a", UDP: true}, - {Country: "b", UDP: true}, - {Country: "c", UDP: true}, + {VPN: constants.OpenVPN, Country: "a", UDP: true}, + {VPN: constants.OpenVPN, Country: "b", UDP: true}, + {VPN: constants.OpenVPN, Country: "c", UDP: true}, }, filtered: []models.IvpnServer{ - {Country: "b", UDP: true}, + {VPN: constants.OpenVPN, Country: "b", UDP: true}, }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {City: "a", UDP: true}, - {City: "b", UDP: true}, - {City: "c", UDP: true}, + {VPN: constants.OpenVPN, City: "a", UDP: true}, + {VPN: constants.OpenVPN, City: "b", UDP: true}, + {VPN: constants.OpenVPN, City: "c", UDP: true}, }, filtered: []models.IvpnServer{ - {City: "b", UDP: true}, + {VPN: constants.OpenVPN, City: "b", UDP: true}, }, }, "filter by ISP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ ISPs: []string{"b"}, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {ISP: "a", UDP: true}, - {ISP: "b", UDP: true}, - {ISP: "c", UDP: true}, + {VPN: constants.OpenVPN, ISP: "a", UDP: true}, + {VPN: constants.OpenVPN, ISP: "b", UDP: true}, + {VPN: constants.OpenVPN, ISP: "c", UDP: true}, }, filtered: []models.IvpnServer{ - {ISP: "b", UDP: true}, + {VPN: constants.OpenVPN, ISP: "b", UDP: true}, }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {Hostname: "a", UDP: true}, - {Hostname: "b", UDP: true}, - {Hostname: "c", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "c", UDP: true}, }, filtered: []models.IvpnServer{ - {Hostname: "b", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true}, }, }, "filter by protocol": { - selection: configuration.ServerSelection{ - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, - }, + }.WithDefaults(constants.Ivpn), servers: []models.IvpnServer{ - {Hostname: "a", UDP: true}, - {Hostname: "b", UDP: true, TCP: true}, - {Hostname: "c", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "a", UDP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true, TCP: true}, + {VPN: constants.OpenVPN, Hostname: "c", UDP: true}, }, filtered: []models.IvpnServer{ - {Hostname: "b", UDP: true, TCP: true}, + {VPN: constants.OpenVPN, Hostname: "b", UDP: true, TCP: true}, }, }, } diff --git a/internal/provider/ivpn/openvpnconf.go b/internal/provider/ivpn/openvpnconf.go index 347f0876..b51e5e15 100644 --- a/internal/provider/ivpn/openvpnconf.go +++ b/internal/provider/ivpn/openvpnconf.go @@ -4,14 +4,14 @@ import ( "strconv" "strings" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (i *Ivpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } @@ -23,7 +23,7 @@ func (i *Ivpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // IVPN specific "ping 5", @@ -47,25 +47,25 @@ func (i *Ivpn) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) + if *settings.Auth != "" { + lines = append(lines, "auth "+*settings.Auth) } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/mullvad/connection.go b/internal/provider/mullvad/connection.go index de7937cd..6ad251bd 100644 --- a/internal/provider/mullvad/connection.go +++ b/internal/provider/mullvad/connection.go @@ -1,12 +1,12 @@ package mullvad import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (m *Mullvad) GetConnection(selection configuration.ServerSelection) ( +func (m *Mullvad) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { port := getPort(selection) protocol := utils.GetProtocol(selection) @@ -33,7 +33,7 @@ func (m *Mullvad) GetConnection(selection configuration.ServerSelection) ( return utils.PickConnection(connections, selection, m.randSource) } -func getPort(selection configuration.ServerSelection) (port uint16) { +func getPort(selection settings.ServerSelection) (port uint16) { const ( defaultOpenVPNTCP = 443 defaultOpenVPNUDP = 1194 diff --git a/internal/provider/mullvad/connection_test.go b/internal/provider/mullvad/connection_test.go index 5556a4da..95f528ff 100644 --- a/internal/provider/mullvad/connection_test.go +++ b/internal/provider/mullvad/connection_test.go @@ -6,7 +6,7 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -18,53 +18,55 @@ func Test_Mullvad_GetConnection(t *testing.T) { testCases := map[string]struct { servers []models.MullvadServer - selection configuration.ServerSelection + selection settings.ServerSelection connection models.Connection err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Mullvad), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.MullvadServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Mullvad), connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(1, 1, 1, 1), Port: 1194, Protocol: constants.UDP, }, }, "target IP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ TargetIP: net.IPv4(2, 2, 2, 2), - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, }, }, "with filter": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, diff --git a/internal/provider/mullvad/filter.go b/internal/provider/mullvad/filter.go index d096e344..811dee1a 100644 --- a/internal/provider/mullvad/filter.go +++ b/internal/provider/mullvad/filter.go @@ -1,12 +1,12 @@ package mullvad import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (m *Mullvad) filterServers(selection configuration.ServerSelection) ( +func (m *Mullvad) filterServers(selection settings.ServerSelection) ( servers []models.MullvadServer, err error) { for _, server := range m.servers { switch { @@ -16,7 +16,7 @@ func (m *Mullvad) filterServers(selection configuration.ServerSelection) ( utils.FilterByPossibilities(server.City, selection.Cities), utils.FilterByPossibilities(server.ISP, selection.ISPs), utils.FilterByPossibilities(server.Hostname, selection.Hostnames), - selection.Owned && !server.Owned: + *selection.OwnedOnly && !server.Owned: default: servers = append(servers, server) } diff --git a/internal/provider/mullvad/filter_test.go b/internal/provider/mullvad/filter_test.go index dfdd62c8..7e5fd0a0 100644 --- a/internal/provider/mullvad/filter_test.go +++ b/internal/provider/mullvad/filter_test.go @@ -5,44 +5,45 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Mullvad_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.MullvadServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.MullvadServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Mullvad), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.MullvadServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Mullvad), filtered: []models.MullvadServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, }, "filter OpenVPN out": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ {VPN: constants.OpenVPN, Hostname: "a"}, {VPN: constants.Wireguard, Hostname: "b"}, @@ -53,68 +54,68 @@ func Test_Mullvad_filterServers(t *testing.T) { }, }, "filter by country": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Countries: []string{"b"}, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {Country: "a"}, - {Country: "b"}, - {Country: "c"}, + {VPN: constants.OpenVPN, Country: "a"}, + {VPN: constants.OpenVPN, Country: "b"}, + {VPN: constants.OpenVPN, Country: "c"}, }, filtered: []models.MullvadServer{ - {Country: "b"}, + {VPN: constants.OpenVPN, Country: "b"}, }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {City: "a"}, - {City: "b"}, - {City: "c"}, + {VPN: constants.OpenVPN, City: "a"}, + {VPN: constants.OpenVPN, City: "b"}, + {VPN: constants.OpenVPN, City: "c"}, }, filtered: []models.MullvadServer{ - {City: "b"}, + {VPN: constants.OpenVPN, City: "b"}, }, }, "filter by ISP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ ISPs: []string{"b"}, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {ISP: "a"}, - {ISP: "b"}, - {ISP: "c"}, + {VPN: constants.OpenVPN, ISP: "a"}, + {VPN: constants.OpenVPN, ISP: "b"}, + {VPN: constants.OpenVPN, ISP: "c"}, }, filtered: []models.MullvadServer{ - {ISP: "b"}, + {VPN: constants.OpenVPN, ISP: "b"}, }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, filtered: []models.MullvadServer{ - {Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "b"}, }, }, "filter by owned": { - selection: configuration.ServerSelection{ - Owned: true, - }, + selection: settings.ServerSelection{ + OwnedOnly: boolPtr(true), + }.WithDefaults(constants.Mullvad), servers: []models.MullvadServer{ - {Hostname: "a"}, - {Hostname: "b", Owned: true}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b", Owned: true}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, filtered: []models.MullvadServer{ - {Hostname: "b", Owned: true}, + {VPN: constants.OpenVPN, Hostname: "b", Owned: true}, }, }, } diff --git a/internal/provider/mullvad/openvpnconf.go b/internal/provider/mullvad/openvpnconf.go index e4b72ed9..1047ad9f 100644 --- a/internal/provider/mullvad/openvpnconf.go +++ b/internal/provider/mullvad/openvpnconf.go @@ -3,14 +3,14 @@ package mullvad import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (m *Mullvad) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc, constants.AES128gcm} } @@ -20,7 +20,7 @@ func (m *Mullvad) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), "auth-user-pass " + constants.OpenVPNAuthConf, // Mullvad specific @@ -44,8 +44,8 @@ func (m *Mullvad) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) + if *settings.Auth != "" { + lines = append(lines, "auth "+*settings.Auth) } if connection.Protocol == constants.UDP { @@ -53,22 +53,22 @@ func (m *Mullvad) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/nordvpn/connection.go b/internal/provider/nordvpn/connection.go index 15abc61d..e9a27fff 100644 --- a/internal/provider/nordvpn/connection.go +++ b/internal/provider/nordvpn/connection.go @@ -1,17 +1,17 @@ package nordvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (n *Nordvpn) GetConnection(selection configuration.ServerSelection) ( +func (n *Nordvpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { var port uint16 = 1194 protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { port = 443 protocol = constants.TCP } diff --git a/internal/provider/nordvpn/filter.go b/internal/provider/nordvpn/filter.go index 5c45326e..ddb16e6e 100644 --- a/internal/provider/nordvpn/filter.go +++ b/internal/provider/nordvpn/filter.go @@ -3,12 +3,12 @@ package nordvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (n *Nordvpn) filterServers(selection configuration.ServerSelection) ( +func (n *Nordvpn) filterServers(selection settings.ServerSelection) ( servers []models.NordvpnServer, err error) { selectedNumbers := make([]string, len(selection.Numbers)) for i := range selection.Numbers { diff --git a/internal/provider/nordvpn/openvpnconf.go b/internal/provider/nordvpn/openvpnconf.go index ad91990f..3be3d561 100644 --- a/internal/provider/nordvpn/openvpnconf.go +++ b/internal/provider/nordvpn/openvpnconf.go @@ -3,24 +3,26 @@ package nordvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (n *Nordvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } - if settings.MSSFix == 0 { - settings.MSSFix = 1450 + mssFix := *settings.MSSFix + if mssFix == 0 { + mssFix = 1450 } lines = []string{ @@ -28,17 +30,17 @@ func (n *Nordvpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Nordvpn specific "tun-mtu-extra 32", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "ping 15", "remote-cert-tls server", "reneg-sec 0", "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, "comp-lzo", // Required, NordVPN does not work without it // Added constant values @@ -60,13 +62,13 @@ func (n *Nordvpn) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/perfectprivacy/connection.go b/internal/provider/perfectprivacy/connection.go index fb464b90..b2a59aea 100644 --- a/internal/provider/perfectprivacy/connection.go +++ b/internal/provider/perfectprivacy/connection.go @@ -1,17 +1,17 @@ package perfectprivacy import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Perfectprivacy) GetConnection(selection configuration.ServerSelection) ( +func (p *Perfectprivacy) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const defaultPort uint16 = 443 port := defaultPort - if selection.OpenVPN.CustomPort > 0 { - port = selection.OpenVPN.CustomPort + if *selection.OpenVPN.CustomPort > 0 { + port = *selection.OpenVPN.CustomPort } protocol := utils.GetProtocol(selection) diff --git a/internal/provider/perfectprivacy/filter.go b/internal/provider/perfectprivacy/filter.go index b95afa52..db39bbf6 100644 --- a/internal/provider/perfectprivacy/filter.go +++ b/internal/provider/perfectprivacy/filter.go @@ -1,12 +1,12 @@ package perfectprivacy import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Perfectprivacy) filterServers(selection configuration.ServerSelection) ( +func (p *Perfectprivacy) filterServers(selection settings.ServerSelection) ( servers []models.PerfectprivacyServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/perfectprivacy/openvpnconf.go b/internal/provider/perfectprivacy/openvpnconf.go index 94a81102..42e7caa8 100644 --- a/internal/provider/perfectprivacy/openvpnconf.go +++ b/internal/provider/perfectprivacy/openvpnconf.go @@ -3,24 +3,26 @@ package perfectprivacy import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Perfectprivacy) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc, constants.AES256gcm} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } - if settings.MSSFix == 0 { - settings.MSSFix = 1450 + mssFix := *settings.MSSFix + if mssFix == 0 { + mssFix = 1450 } lines = []string{ @@ -28,18 +30,18 @@ func (p *Perfectprivacy) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Perfect Privacy specific "ping 5", "tun-mtu 1500", "tun-mtu-extra 32", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "reneg-sec 3600", "key-direction 1", "tls-cipher TLS_CHACHA20_POLY1305_SHA256:TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-128-GCM-SHA256:TLS-DHE-RSA-WITH-AES-128-CBC-SHA:TLS_AES_256_GCM_SHA384:TLS-RSA-WITH-AES-256-CBC-SHA", //nolint:lll "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -59,13 +61,13 @@ func (p *Perfectprivacy) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) // Perfect Privacy specific IPv6 diff --git a/internal/provider/privado/connection.go b/internal/provider/privado/connection.go index 41b621af..a7a39519 100644 --- a/internal/provider/privado/connection.go +++ b/internal/provider/privado/connection.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" @@ -12,11 +12,11 @@ import ( var ErrProtocolUnsupported = errors.New("network protocol is not supported") -func (p *Privado) GetConnection(selection configuration.ServerSelection) ( +func (p *Privado) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 1194 const protocol = constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { return connection, fmt.Errorf("%w: TCP for provider Privado", ErrProtocolUnsupported) } diff --git a/internal/provider/privado/filter.go b/internal/provider/privado/filter.go index 3cdf79c9..30f4a617 100644 --- a/internal/provider/privado/filter.go +++ b/internal/provider/privado/filter.go @@ -1,12 +1,12 @@ package privado import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Privado) filterServers(selection configuration.ServerSelection) ( +func (p *Privado) filterServers(selection settings.ServerSelection) ( servers []models.PrivadoServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/privado/openvpnconf.go b/internal/provider/privado/openvpnconf.go index 108e99eb..02c0e5a1 100644 --- a/internal/provider/privado/openvpnconf.go +++ b/internal/provider/privado/openvpnconf.go @@ -3,20 +3,21 @@ package privado import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Privado) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } lines = []string{ @@ -24,14 +25,14 @@ func (p *Privado) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Privado specific "ping 10", "tls-cipher TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA", "verify-x509-name " + connection.Hostname + " name", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -47,21 +48,21 @@ func (p *Privado) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/privateinternetaccess/connection.go b/internal/provider/privateinternetaccess/connection.go index e7fcbde7..fc465080 100644 --- a/internal/provider/privateinternetaccess/connection.go +++ b/internal/provider/privateinternetaccess/connection.go @@ -1,16 +1,16 @@ package privateinternetaccess import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *PIA) GetConnection(selection configuration.ServerSelection) ( +func (p *PIA) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } diff --git a/internal/provider/privateinternetaccess/filter.go b/internal/provider/privateinternetaccess/filter.go index 5efb55e8..53e54f30 100644 --- a/internal/provider/privateinternetaccess/filter.go +++ b/internal/provider/privateinternetaccess/filter.go @@ -1,12 +1,12 @@ package privateinternetaccess import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *PIA) filterServers(selection configuration.ServerSelection) ( +func (p *PIA) filterServers(selection settings.ServerSelection) ( servers []models.PIAServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/privateinternetaccess/openvpnconf.go b/internal/provider/privateinternetaccess/openvpnconf.go index 4d202fb8..3af246cb 100644 --- a/internal/provider/privateinternetaccess/openvpnconf.go +++ b/internal/provider/privateinternetaccess/openvpnconf.go @@ -3,16 +3,16 @@ package privateinternetaccess import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *PIA) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { var defaultCipher, defaultAuth, X509CRL, certificate string - switch settings.EncPreset { + switch *settings.PIAEncPreset { case constants.PIAEncryptionPresetNormal: defaultCipher = constants.AES128cbc defaultAuth = constants.SHA1 @@ -34,8 +34,9 @@ func (p *PIA) BuildConf(connection models.Connection, settings.Ciphers = []string{defaultCipher} } - if settings.Auth == "" { - settings.Auth = defaultAuth + auth := *settings.Auth + if auth == "" { + auth = defaultAuth } lines = []string{ @@ -43,12 +44,13 @@ func (p *PIA) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // PIA specific "remote-cert-tls server", "reneg-sec 0", "auth-user-pass " + constants.OpenVPNAuthConf, + "auth " + auth, // Added constant values "auth-nocache", @@ -66,25 +68,21 @@ func (p *PIA) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) } - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) - } - if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/privateinternetaccess/port.go b/internal/provider/privateinternetaccess/port.go index 1f7a77a1..a81279a1 100644 --- a/internal/provider/privateinternetaccess/port.go +++ b/internal/provider/privateinternetaccess/port.go @@ -4,21 +4,23 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) -func getPort(openvpnSelection configuration.OpenVPNSelection) ( +func getPort(openvpnSelection settings.OpenVPNSelection) ( port uint16, err error) { - if openvpnSelection.CustomPort == 0 { - return getDefaultPort(openvpnSelection.TCP, openvpnSelection.EncPreset), nil + customPort := *openvpnSelection.CustomPort + tcp := *openvpnSelection.TCP + if customPort == 0 { + return getDefaultPort(tcp, *openvpnSelection.PIAEncPreset), nil } - if err := checkPort(openvpnSelection.CustomPort, openvpnSelection.TCP); err != nil { + if err := checkPort(customPort, tcp); err != nil { return 0, err } - return openvpnSelection.CustomPort, nil + return customPort, nil } func getDefaultPort(tcp bool, encryptionPreset string) (port uint16) { diff --git a/internal/provider/privatevpn/connection.go b/internal/provider/privatevpn/connection.go index 7c0b6a3e..05857eba 100644 --- a/internal/provider/privatevpn/connection.go +++ b/internal/provider/privatevpn/connection.go @@ -1,22 +1,22 @@ package privatevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Privatevpn) GetConnection(selection configuration.ServerSelection) ( +func (p *Privatevpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP var port uint16 = 1194 - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP port = 443 } - if selection.OpenVPN.CustomPort > 0 { - port = selection.OpenVPN.CustomPort + if *selection.OpenVPN.CustomPort > 0 { + port = *selection.OpenVPN.CustomPort } servers, err := p.filterServers(selection) diff --git a/internal/provider/privatevpn/filter.go b/internal/provider/privatevpn/filter.go index af2b9c5b..fffae08b 100644 --- a/internal/provider/privatevpn/filter.go +++ b/internal/provider/privatevpn/filter.go @@ -1,12 +1,12 @@ package privatevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Privatevpn) filterServers(selection configuration.ServerSelection) ( +func (p *Privatevpn) filterServers(selection settings.ServerSelection) ( servers []models.PrivatevpnServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/privatevpn/openvpnconf.go b/internal/provider/privatevpn/openvpnconf.go index 5fd498e1..a36c3b34 100644 --- a/internal/provider/privatevpn/openvpnconf.go +++ b/internal/provider/privatevpn/openvpnconf.go @@ -3,20 +3,21 @@ package privatevpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Privatevpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES128gcm} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } lines = []string{ @@ -24,12 +25,12 @@ func (p *Privatevpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Privatevpn specific "remote-cert-tls server", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -50,17 +51,17 @@ func (p *Privatevpn) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/protonvpn/connection.go b/internal/provider/protonvpn/connection.go index 48f63367..2a2ecfdf 100644 --- a/internal/provider/protonvpn/connection.go +++ b/internal/provider/protonvpn/connection.go @@ -1,20 +1,20 @@ package protonvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Protonvpn) GetConnection(selection configuration.ServerSelection) ( +func (p *Protonvpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } - port, err := getPort(selection.OpenVPN.TCP, selection.OpenVPN.CustomPort) + port, err := getPort(*selection.OpenVPN.TCP, *selection.OpenVPN.CustomPort) if err != nil { return connection, err } diff --git a/internal/provider/protonvpn/filter.go b/internal/provider/protonvpn/filter.go index def15efa..efc72a9a 100644 --- a/internal/provider/protonvpn/filter.go +++ b/internal/provider/protonvpn/filter.go @@ -3,12 +3,12 @@ package protonvpn import ( "strings" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Protonvpn) filterServers(selection configuration.ServerSelection) ( +func (p *Protonvpn) filterServers(selection settings.ServerSelection) ( servers []models.ProtonvpnServer, err error) { for _, server := range p.servers { switch { @@ -18,7 +18,7 @@ func (p *Protonvpn) filterServers(selection configuration.ServerSelection) ( utils.FilterByPossibilities(server.City, selection.Cities), utils.FilterByPossibilities(server.Hostname, selection.Hostnames), utils.FilterByPossibilities(server.Name, selection.Names), - selection.FreeOnly && !strings.Contains(strings.ToLower(server.Name), "free"): + *selection.FreeOnly && !strings.Contains(strings.ToLower(server.Name), "free"): default: servers = append(servers, server) } diff --git a/internal/provider/protonvpn/openvpnconf.go b/internal/provider/protonvpn/openvpnconf.go index 91b2a96d..2518d40d 100644 --- a/internal/provider/protonvpn/openvpnconf.go +++ b/internal/provider/protonvpn/openvpnconf.go @@ -3,25 +3,27 @@ package protonvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Protonvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } - const defaultMSSFix = 1450 - if settings.MSSFix == 0 { - settings.MSSFix = defaultMSSFix + mssFix := *settings.MSSFix + if mssFix == 0 { + const defaultMSSFix = 1450 + mssFix = defaultMSSFix } lines = []string{ @@ -29,16 +31,16 @@ func (p *Protonvpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Protonvpn specific "remote-cert-tls server", "tun-mtu-extra 32", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "reneg-sec 0", "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -59,13 +61,13 @@ func (p *Protonvpn) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b630ea59..f81cabfc 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/custom" @@ -37,8 +37,8 @@ import ( // Provider contains methods to read and modify the openvpn configuration to connect as a client. type Provider interface { - GetConnection(selection configuration.ServerSelection) (connection models.Connection, err error) - BuildConf(connection models.Connection, settings configuration.OpenVPN) (lines []string, err error) + GetConnection(selection settings.ServerSelection) (connection models.Connection, err error) + BuildConf(connection models.Connection, settings settings.OpenVPN) (lines []string, err error) PortForwarder } @@ -96,6 +96,6 @@ func New(provider string, allServers models.AllServers, timeNow func() time.Time case constants.Windscribe: return windscribe.New(allServers.Windscribe.Servers, randSource) default: - return nil // should never occur + panic("provider " + provider + " is unknown") // should never occur } } diff --git a/internal/provider/purevpn/connection.go b/internal/provider/purevpn/connection.go index b0ea9119..f1350f0a 100644 --- a/internal/provider/purevpn/connection.go +++ b/internal/provider/purevpn/connection.go @@ -1,17 +1,17 @@ package purevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Purevpn) GetConnection(selection configuration.ServerSelection) ( +func (p *Purevpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP var port uint16 = 53 - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP port = 80 } diff --git a/internal/provider/purevpn/filter.go b/internal/provider/purevpn/filter.go index 9f2d0f0e..f853e58e 100644 --- a/internal/provider/purevpn/filter.go +++ b/internal/provider/purevpn/filter.go @@ -1,12 +1,12 @@ package purevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Purevpn) filterServers(selection configuration.ServerSelection) ( +func (p *Purevpn) filterServers(selection settings.ServerSelection) ( servers []models.PurevpnServer, err error) { for _, server := range p.servers { switch { diff --git a/internal/provider/purevpn/openvpnconf.go b/internal/provider/purevpn/openvpnconf.go index 95240f01..89a35d06 100644 --- a/internal/provider/purevpn/openvpnconf.go +++ b/internal/provider/purevpn/openvpnconf.go @@ -3,14 +3,14 @@ package purevpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Purevpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256gcm} } @@ -20,7 +20,7 @@ func (p *Purevpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Purevpn specific "ping 10", @@ -46,21 +46,21 @@ func (p *Purevpn) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) + if *settings.Auth != "" { + lines = append(lines, "auth "+*settings.Auth) } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/surfshark/connection.go b/internal/provider/surfshark/connection.go index 2eeba1e5..2947d6f6 100644 --- a/internal/provider/surfshark/connection.go +++ b/internal/provider/surfshark/connection.go @@ -1,17 +1,17 @@ package surfshark import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (s *Surfshark) GetConnection(selection configuration.ServerSelection) ( +func (s *Surfshark) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP var port uint16 = 1194 - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP port = 1443 } diff --git a/internal/provider/surfshark/filter.go b/internal/provider/surfshark/filter.go index 8d71fa77..22b4be34 100644 --- a/internal/provider/surfshark/filter.go +++ b/internal/provider/surfshark/filter.go @@ -1,12 +1,12 @@ package surfshark import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (s *Surfshark) filterServers(selection configuration.ServerSelection) ( +func (s *Surfshark) filterServers(selection settings.ServerSelection) ( servers []models.SurfsharkServer, err error) { for _, server := range s.servers { switch { @@ -16,7 +16,7 @@ func (s *Surfshark) filterServers(selection configuration.ServerSelection) ( utils.FilterByPossibilities(server.City, selection.Cities), utils.FilterByPossibilities(server.Hostname, selection.Hostnames), utils.FilterByProtocol(selection, server.TCP, server.UDP), - selection.MultiHopOnly && !server.MultiHop: + *selection.MultiHopOnly && !server.MultiHop: default: servers = append(servers, server) } diff --git a/internal/provider/surfshark/filter_test.go b/internal/provider/surfshark/filter_test.go index 11be62fc..4b25a817 100644 --- a/internal/provider/surfshark/filter_test.go +++ b/internal/provider/surfshark/filter_test.go @@ -5,27 +5,27 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Surfshark_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.SurfsharkServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.SurfsharkServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Surfshark), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.SurfsharkServer{ @@ -33,6 +33,7 @@ func Test_Surfshark_filterServers(t *testing.T) { {Hostname: "b", UDP: true}, {Hostname: "c", UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Surfshark), filtered: []models.SurfsharkServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, @@ -40,9 +41,9 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by region": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Regions: []string{"b"}, - }, + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {Region: "a", UDP: true}, {Region: "b", UDP: true}, @@ -53,9 +54,9 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by country": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Countries: []string{"b"}, - }, + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {Country: "a", UDP: true}, {Country: "b", UDP: true}, @@ -66,9 +67,9 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {City: "a", UDP: true}, {City: "b", UDP: true}, @@ -79,9 +80,9 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, @@ -92,11 +93,11 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by protocol": { - selection: configuration.ServerSelection{ - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, - }, + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true, TCP: true}, @@ -107,9 +108,9 @@ func Test_Surfshark_filterServers(t *testing.T) { }, }, "filter by multihop only": { - selection: configuration.ServerSelection{ - MultiHopOnly: true, - }, + selection: settings.ServerSelection{ + MultiHopOnly: boolPtr(true), + }.WithDefaults(constants.Surfshark), servers: []models.SurfsharkServer{ {Hostname: "a", UDP: true}, {Hostname: "b", MultiHop: true, UDP: true}, diff --git a/internal/provider/surfshark/openvpnconf.go b/internal/provider/surfshark/openvpnconf.go index 57f82b9a..14db847e 100644 --- a/internal/provider/surfshark/openvpnconf.go +++ b/internal/provider/surfshark/openvpnconf.go @@ -3,25 +3,27 @@ package surfshark import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (s *Surfshark) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256gcm} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } - const defaultMSSFix = 1450 - if settings.MSSFix == 0 { - settings.MSSFix = defaultMSSFix + mssFix := *settings.MSSFix + if mssFix == 0 { + const defaultMSSFix = 1450 + mssFix = defaultMSSFix } lines = []string{ @@ -29,17 +31,17 @@ func (s *Surfshark) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Surfshark specific "tun-mtu-extra 32", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "ping 15", "remote-cert-tls server", "reneg-sec 0", "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -59,13 +61,13 @@ func (s *Surfshark) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/torguard/connection.go b/internal/provider/torguard/connection.go index 0abc4511..01365d62 100644 --- a/internal/provider/torguard/connection.go +++ b/internal/provider/torguard/connection.go @@ -1,22 +1,22 @@ package torguard import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (t *Torguard) GetConnection(selection configuration.ServerSelection) ( +func (t *Torguard) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } var port uint16 = 1912 - if selection.OpenVPN.CustomPort > 0 { - port = selection.OpenVPN.CustomPort + if *selection.OpenVPN.CustomPort > 0 { + port = *selection.OpenVPN.CustomPort } servers, err := t.filterServers(selection) diff --git a/internal/provider/torguard/filter.go b/internal/provider/torguard/filter.go index c8a8ebb2..0456cf5c 100644 --- a/internal/provider/torguard/filter.go +++ b/internal/provider/torguard/filter.go @@ -1,12 +1,12 @@ package torguard import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (t *Torguard) filterServers(selection configuration.ServerSelection) ( +func (t *Torguard) filterServers(selection settings.ServerSelection) ( servers []models.TorguardServer, err error) { for _, server := range t.servers { switch { diff --git a/internal/provider/torguard/openvpnconf.go b/internal/provider/torguard/openvpnconf.go index c73bcc94..a64bf7e5 100644 --- a/internal/provider/torguard/openvpnconf.go +++ b/internal/provider/torguard/openvpnconf.go @@ -3,25 +3,27 @@ package torguard import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (t *Torguard) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256gcm} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } - const defaultMSSFix = 1450 - if settings.MSSFix == 0 { - settings.MSSFix = defaultMSSFix + mssFix := *settings.MSSFix + if mssFix == 0 { + const defaultMSSFix = 1450 + mssFix = defaultMSSFix } lines = []string{ @@ -29,11 +31,11 @@ func (t *Torguard) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Torguard specific "tun-mtu-extra 32", - "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "mssfix " + strconv.Itoa(int(mssFix)), "sndbuf 393216", "rcvbuf 393216", "ping 5", @@ -41,7 +43,7 @@ func (t *Torguard) BuildConf(connection models.Connection, "reneg-sec 0", "key-direction 1", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -57,7 +59,7 @@ func (t *Torguard) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") @@ -68,7 +70,7 @@ func (t *Torguard) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/provider/utils/formatting.go b/internal/provider/utils/formatting.go index 54b5f306..b9890e68 100644 --- a/internal/provider/utils/formatting.go +++ b/internal/provider/utils/formatting.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) @@ -16,13 +16,13 @@ func commaJoin(slice []string) string { var ErrNoServerFound = errors.New("no server found") -func NoServerFoundError(selection configuration.ServerSelection) (err error) { +func NoServerFoundError(selection settings.ServerSelection) (err error) { var messageParts []string messageParts = append(messageParts, "VPN "+selection.VPN) protocol := constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { protocol = constants.TCP } messageParts = append(messageParts, "protocol "+protocol) @@ -57,7 +57,7 @@ func NoServerFoundError(selection configuration.ServerSelection) (err error) { messageParts = append(messageParts, part) } - if selection.Owned { + if *selection.OwnedOnly { messageParts = append(messageParts, "owned servers only") } @@ -105,12 +105,12 @@ func NoServerFoundError(selection configuration.ServerSelection) (err error) { messageParts = append(messageParts, part) } - if selection.OpenVPN.EncPreset != "" { - part := "encryption preset " + selection.OpenVPN.EncPreset + if *selection.OpenVPN.PIAEncPreset != "" { + part := "encryption preset " + *selection.OpenVPN.PIAEncPreset messageParts = append(messageParts, part) } - if selection.FreeOnly { + if *selection.FreeOnly { messageParts = append(messageParts, "free tier only") } diff --git a/internal/provider/utils/pick.go b/internal/provider/utils/pick.go index 423261db..c201ea5d 100644 --- a/internal/provider/utils/pick.go +++ b/internal/provider/utils/pick.go @@ -6,7 +6,7 @@ import ( "math/rand" "net" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" ) @@ -17,15 +17,15 @@ import ( // Otherwise, it picks a random connection from the pool of connections // and sets the target IP address as the IP if this one is set. func PickConnection(connections []models.Connection, - selection configuration.ServerSelection, randSource rand.Source) ( + selection settings.ServerSelection, randSource rand.Source) ( connection models.Connection, err error) { - if selection.TargetIP != nil && selection.VPN == constants.Wireguard { + if len(selection.TargetIP) > 0 && selection.VPN == constants.Wireguard { // we need the right public key return getTargetIPConnection(connections, selection.TargetIP) } connection = pickRandomConnection(connections, randSource) - if selection.TargetIP != nil { + if len(selection.TargetIP) > 0 { connection.IP = selection.TargetIP } diff --git a/internal/provider/utils/port.go b/internal/provider/utils/port.go index 66f14ca0..6644adea 100644 --- a/internal/provider/utils/port.go +++ b/internal/provider/utils/port.go @@ -1,25 +1,25 @@ package utils import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) -func GetPort(selection configuration.ServerSelection, +func GetPort(selection settings.ServerSelection, defaultOpenVPNTCP, defaultOpenVPNUDP, defaultWireguard uint16) (port uint16) { switch selection.VPN { case constants.Wireguard: - customPort := selection.Wireguard.EndpointPort + customPort := *selection.Wireguard.EndpointPort if customPort > 0 { return customPort } return defaultWireguard default: // OpenVPN - customPort := selection.OpenVPN.CustomPort + customPort := *selection.OpenVPN.CustomPort if customPort > 0 { return customPort } - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { return defaultOpenVPNTCP } return defaultOpenVPNUDP diff --git a/internal/provider/utils/port_test.go b/internal/provider/utils/port_test.go index eb87d5e6..e2719451 100644 --- a/internal/provider/utils/port_test.go +++ b/internal/provider/utils/port_test.go @@ -3,11 +3,14 @@ package utils import ( "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/stretchr/testify/assert" ) +func boolPtr(b bool) *bool { return &b } +func uint16Ptr(n uint16) *uint16 { return &n } + func Test_GetPort(t *testing.T) { t.Parallel() @@ -18,47 +21,53 @@ func Test_GetPort(t *testing.T) { ) testCases := map[string]struct { - selection configuration.ServerSelection + selection settings.ServerSelection port uint16 }{ "default": { - port: defaultOpenVPNUDP, + selection: settings.ServerSelection{}.WithDefaults(""), + port: defaultOpenVPNUDP, }, "OpenVPN UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, + OpenVPN: settings.OpenVPNSelection{ + CustomPort: uint16Ptr(0), + TCP: boolPtr(false), + }, }, port: defaultOpenVPNUDP, }, "OpenVPN TCP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + OpenVPN: settings.OpenVPNSelection{ + CustomPort: uint16Ptr(0), + TCP: boolPtr(true), }, }, port: defaultOpenVPNTCP, }, "OpenVPN custom port": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - CustomPort: 1234, + OpenVPN: settings.OpenVPNSelection{ + CustomPort: uint16Ptr(1234), }, }, port: 1234, }, "Wireguard": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, - }, + }.WithDefaults(""), port: defaultWireguard, }, "Wireguard custom port": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, - Wireguard: configuration.WireguardSelection{ - EndpointPort: 1234, + Wireguard: settings.WireguardSelection{ + EndpointPort: uint16Ptr(1234), }, }, port: 1234, diff --git a/internal/provider/utils/protocol.go b/internal/provider/utils/protocol.go index 48fbb606..fe0a997f 100644 --- a/internal/provider/utils/protocol.go +++ b/internal/provider/utils/protocol.go @@ -1,24 +1,24 @@ package utils import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) -func GetProtocol(selection configuration.ServerSelection) (protocol string) { - if selection.VPN == constants.OpenVPN && selection.OpenVPN.TCP { +func GetProtocol(selection settings.ServerSelection) (protocol string) { + if selection.VPN == constants.OpenVPN && *selection.OpenVPN.TCP { return constants.TCP } return constants.UDP } -func FilterByProtocol(selection configuration.ServerSelection, +func FilterByProtocol(selection settings.ServerSelection, serverTCP, serverUDP bool) (filtered bool) { switch selection.VPN { case constants.Wireguard: return !serverUDP default: // OpenVPN - wantTCP := selection.OpenVPN.TCP + wantTCP := *selection.OpenVPN.TCP wantUDP := !wantTCP return (wantTCP && !serverTCP) || (wantUDP && !serverUDP) } diff --git a/internal/provider/utils/protocol_test.go b/internal/provider/utils/protocol_test.go index c31a6ff3..6d6ecfa1 100644 --- a/internal/provider/utils/protocol_test.go +++ b/internal/provider/utils/protocol_test.go @@ -3,7 +3,7 @@ package utils import ( "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/stretchr/testify/assert" ) @@ -12,29 +12,32 @@ func Test_GetProtocol(t *testing.T) { t.Parallel() testCases := map[string]struct { - selection configuration.ServerSelection + selection settings.ServerSelection protocol string }{ "default": { protocol: constants.UDP, }, "OpenVPN UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(false), + }, }, protocol: constants.UDP, }, "OpenVPN TCP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, }, protocol: constants.TCP, }, "Wireguard": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, }, protocol: constants.UDP, @@ -57,60 +60,60 @@ func Test_FilterByProtocol(t *testing.T) { t.Parallel() testCases := map[string]struct { - selection configuration.ServerSelection + selection settings.ServerSelection serverTCP bool serverUDP bool filtered bool }{ "Wireguard and server has UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, }, serverUDP: true, filtered: false, }, "Wireguard and server has not UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, }, serverUDP: false, filtered: true, }, "OpenVPN UDP and server has UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: false, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(false), }, }, serverUDP: true, filtered: false, }, "OpenVPN UDP and server has not UDP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: false, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(false), }, }, serverUDP: false, filtered: true, }, "OpenVPN TCP and server has TCP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, }, serverTCP: true, filtered: false, }, "OpenVPN TCP and server has not TCP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - OpenVPN: configuration.OpenVPNSelection{ - TCP: true, + OpenVPN: settings.OpenVPNSelection{ + TCP: boolPtr(true), }, }, serverTCP: false, diff --git a/internal/provider/utils/wireguard.go b/internal/provider/utils/wireguard.go index 4cc063d1..5da1a554 100644 --- a/internal/provider/utils/wireguard.go +++ b/internal/provider/utils/wireguard.go @@ -3,16 +3,16 @@ package utils import ( "net" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/wireguard" ) func BuildWireguardSettings(connection models.Connection, - userSettings configuration.Wireguard) (settings wireguard.Settings) { - settings.PrivateKey = userSettings.PrivateKey + userSettings settings.Wireguard) (settings wireguard.Settings) { + settings.PrivateKey = *userSettings.PrivateKey settings.PublicKey = connection.PubKey - settings.PreSharedKey = userSettings.PreSharedKey + settings.PreSharedKey = *userSettings.PreSharedKey settings.InterfaceName = userSettings.Interface const rulePriority = 101 // 100 is to receive external connections diff --git a/internal/provider/utils/wireguard_test.go b/internal/provider/utils/wireguard_test.go index 55e1e422..eb2b46ab 100644 --- a/internal/provider/utils/wireguard_test.go +++ b/internal/provider/utils/wireguard_test.go @@ -4,18 +4,20 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/wireguard" "github.com/stretchr/testify/assert" ) +func stringPtr(s string) *string { return &s } + func Test_BuildWireguardSettings(t *testing.T) { t.Parallel() testCases := map[string]struct { connection models.Connection - userSettings configuration.Wireguard + userSettings settings.Wireguard settings wireguard.Settings }{ "some settings": { @@ -24,10 +26,10 @@ func Test_BuildWireguardSettings(t *testing.T) { Port: 51821, PubKey: "public", }, - userSettings: configuration.Wireguard{ - PrivateKey: "private", - PreSharedKey: "pre-shared", - Addresses: []*net.IPNet{ + userSettings: settings.Wireguard{ + PrivateKey: stringPtr("private"), + PreSharedKey: stringPtr("pre-shared"), + Addresses: []net.IPNet{ {IP: net.IPv4(1, 1, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}, {IP: net.IPv4(2, 2, 2, 2), Mask: net.IPv4Mask(255, 255, 255, 255)}, }, diff --git a/internal/provider/vpnunlimited/connection.go b/internal/provider/vpnunlimited/connection.go index 8afcca49..b88258ec 100644 --- a/internal/provider/vpnunlimited/connection.go +++ b/internal/provider/vpnunlimited/connection.go @@ -3,7 +3,7 @@ package vpnunlimited import ( "errors" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" @@ -11,11 +11,11 @@ import ( var ErrProtocolUnsupported = errors.New("network protocol is not supported") -func (p *Provider) GetConnection(selection configuration.ServerSelection) ( +func (p *Provider) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 1194 const protocol = constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { return connection, ErrProtocolUnsupported } diff --git a/internal/provider/vpnunlimited/filter.go b/internal/provider/vpnunlimited/filter.go index a368d367..8f030546 100644 --- a/internal/provider/vpnunlimited/filter.go +++ b/internal/provider/vpnunlimited/filter.go @@ -1,12 +1,12 @@ package vpnunlimited import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (p *Provider) filterServers(selection configuration.ServerSelection) ( +func (p *Provider) filterServers(selection settings.ServerSelection) ( servers []models.VPNUnlimitedServer, err error) { for _, server := range p.servers { switch { @@ -14,8 +14,8 @@ func (p *Provider) filterServers(selection configuration.ServerSelection) ( utils.FilterByPossibilities(server.Country, selection.Countries), utils.FilterByPossibilities(server.City, selection.Cities), utils.FilterByPossibilities(server.Hostname, selection.Hostnames), - selection.FreeOnly && !server.Free, - selection.StreamOnly && !server.Stream, + *selection.FreeOnly && !server.Free, + *selection.StreamOnly && !server.Stream, utils.FilterByProtocol(selection, server.TCP, server.UDP): default: servers = append(servers, server) diff --git a/internal/provider/vpnunlimited/openvpnconf.go b/internal/provider/vpnunlimited/openvpnconf.go index 8c6690be..5658cc82 100644 --- a/internal/provider/vpnunlimited/openvpnconf.go +++ b/internal/provider/vpnunlimited/openvpnconf.go @@ -1,22 +1,24 @@ package vpnunlimited import ( + "fmt" "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/openvpn/parse" "github.com/qdm12/gluetun/internal/provider/utils" ) func (p *Provider) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { lines = []string{ "client", "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // VPNUnlimited specific "ping 5", @@ -40,35 +42,43 @@ func (p *Provider) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) } - if settings.Auth != "" { - lines = append(lines, "auth "+settings.Auth) + if *settings.Auth != "" { + lines = append(lines, "auth "+*settings.Auth) } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } if connection.Protocol == constants.UDP { lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } lines = append(lines, utils.WrapOpenvpnCA( constants.VPNUnlimitedCertificateAuthority)...) - lines = append(lines, utils.WrapOpenvpnCert( - settings.ClientCrt)...) - lines = append(lines, utils.WrapOpenvpnKey( - settings.ClientKey)...) + + certData, err := parse.ExtractCert([]byte(*settings.ClientCrt)) + if err != nil { + return nil, fmt.Errorf("client cert is not valid: %w", err) + } + lines = append(lines, utils.WrapOpenvpnCert(certData)...) + + keyData, err := parse.ExtractPrivateKey([]byte(*settings.ClientKey)) + if err != nil { + return nil, fmt.Errorf("client key is not valid: %w", err) + } + lines = append(lines, utils.WrapOpenvpnKey(keyData)...) lines = append(lines, "") diff --git a/internal/provider/vyprvpn/connection.go b/internal/provider/vyprvpn/connection.go index 774f1da2..30d5361f 100644 --- a/internal/provider/vyprvpn/connection.go +++ b/internal/provider/vyprvpn/connection.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" @@ -12,11 +12,11 @@ import ( var ErrProtocolUnsupported = errors.New("network protocol is not supported") -func (v *Vyprvpn) GetConnection(selection configuration.ServerSelection) ( +func (v *Vyprvpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { const port = 443 const protocol = constants.UDP - if selection.OpenVPN.TCP { + if *selection.OpenVPN.TCP { return connection, fmt.Errorf("%w: TCP for provider VyprVPN", ErrProtocolUnsupported) } diff --git a/internal/provider/vyprvpn/filter.go b/internal/provider/vyprvpn/filter.go index ae9dac80..49625f13 100644 --- a/internal/provider/vyprvpn/filter.go +++ b/internal/provider/vyprvpn/filter.go @@ -1,12 +1,12 @@ package vyprvpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (v *Vyprvpn) filterServers(selection configuration.ServerSelection) ( +func (v *Vyprvpn) filterServers(selection settings.ServerSelection) ( servers []models.VyprvpnServer, err error) { for _, server := range v.servers { switch { diff --git a/internal/provider/vyprvpn/openvpnconf.go b/internal/provider/vyprvpn/openvpnconf.go index 19ec4216..dbf86bbf 100644 --- a/internal/provider/vyprvpn/openvpnconf.go +++ b/internal/provider/vyprvpn/openvpnconf.go @@ -3,20 +3,21 @@ package vyprvpn import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (v *Vyprvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256cbc} } - if settings.Auth == "" { - settings.Auth = constants.SHA256 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA256 } lines = []string{ @@ -24,7 +25,7 @@ func (v *Vyprvpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Vyprvpn specific "ping 10", @@ -32,7 +33,7 @@ func (v *Vyprvpn) BuildConf(connection models.Connection, // "verify-x509-name lu1.vyprvpn.com name", "tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-AES-256-CBC-SHA", //nolint:lll "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, "comp-lzo", // Added constant values @@ -53,14 +54,14 @@ func (v *Vyprvpn) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } lines = append(lines, utils.WrapOpenvpnCA( diff --git a/internal/provider/wevpn/connection.go b/internal/provider/wevpn/connection.go index 5b83cf27..b3445df2 100644 --- a/internal/provider/wevpn/connection.go +++ b/internal/provider/wevpn/connection.go @@ -1,12 +1,12 @@ package wevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (w *Wevpn) GetConnection(selection configuration.ServerSelection) ( +func (w *Wevpn) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { port := getPort(selection) protocol := utils.GetProtocol(selection) @@ -32,7 +32,7 @@ func (w *Wevpn) GetConnection(selection configuration.ServerSelection) ( return utils.PickConnection(connections, selection, w.randSource) } -func getPort(selection configuration.ServerSelection) (port uint16) { +func getPort(selection settings.ServerSelection) (port uint16) { const ( defaultOpenVPNTCP = 1195 defaultOpenVPNUDP = 1194 diff --git a/internal/provider/wevpn/connection_test.go b/internal/provider/wevpn/connection_test.go index 1e4a7cf3..c671e166 100644 --- a/internal/provider/wevpn/connection_test.go +++ b/internal/provider/wevpn/connection_test.go @@ -6,7 +6,7 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -18,14 +18,14 @@ func Test_Wevpn_GetConnection(t *testing.T) { testCases := map[string]struct { servers []models.WevpnServer - selection configuration.ServerSelection + selection settings.ServerSelection connection models.Connection err error }{ "no server available": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.OpenVPN, - }, + }.WithDefaults(constants.Wevpn), err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { @@ -34,37 +34,41 @@ func Test_Wevpn_GetConnection(t *testing.T) { {UDP: true, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, {UDP: true, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Wevpn), connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(1, 1, 1, 1), Port: 1194, Protocol: constants.UDP, }, }, "target IP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ TargetIP: net.IPv4(2, 2, 2, 2), - }, + }.WithDefaults(constants.Wevpn), servers: []models.WevpnServer{ {UDP: true, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, {UDP: true, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, {UDP: true, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, }, }, "with filter": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Wevpn), servers: []models.WevpnServer{ {UDP: true, Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, {UDP: true, Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, {UDP: true, Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, diff --git a/internal/provider/wevpn/filter.go b/internal/provider/wevpn/filter.go index 827cee92..8f65add1 100644 --- a/internal/provider/wevpn/filter.go +++ b/internal/provider/wevpn/filter.go @@ -1,12 +1,12 @@ package wevpn import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (w *Wevpn) filterServers(selection configuration.ServerSelection) ( +func (w *Wevpn) filterServers(selection settings.ServerSelection) ( servers []models.WevpnServer, err error) { for _, server := range w.servers { switch { diff --git a/internal/provider/wevpn/filter_test.go b/internal/provider/wevpn/filter_test.go index 3e3c458e..c486a36b 100644 --- a/internal/provider/wevpn/filter_test.go +++ b/internal/provider/wevpn/filter_test.go @@ -5,27 +5,27 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func boolPtr(b bool) *bool { return &b } + func Test_Wevpn_filterServers(t *testing.T) { t.Parallel() testCases := map[string]struct { servers []models.WevpnServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.WevpnServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Wevpn), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.WevpnServer{ @@ -33,6 +33,7 @@ func Test_Wevpn_filterServers(t *testing.T) { {Hostname: "b", UDP: true}, {Hostname: "c", UDP: true}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Wevpn), filtered: []models.WevpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, @@ -40,9 +41,9 @@ func Test_Wevpn_filterServers(t *testing.T) { }, }, "filter by protocol": { - selection: configuration.ServerSelection{ - OpenVPN: configuration.OpenVPNSelection{TCP: true}, - }, + selection: settings.ServerSelection{ + OpenVPN: settings.OpenVPNSelection{TCP: boolPtr(true)}, + }.WithDefaults(constants.Wevpn), servers: []models.WevpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", TCP: true}, @@ -53,9 +54,9 @@ func Test_Wevpn_filterServers(t *testing.T) { }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Wevpn), servers: []models.WevpnServer{ {City: "a", UDP: true}, {City: "b", UDP: true}, @@ -66,9 +67,9 @@ func Test_Wevpn_filterServers(t *testing.T) { }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Wevpn), servers: []models.WevpnServer{ {Hostname: "a", UDP: true}, {Hostname: "b", UDP: true}, diff --git a/internal/provider/wevpn/openvpnconf.go b/internal/provider/wevpn/openvpnconf.go index f778bc33..52ea8ac5 100644 --- a/internal/provider/wevpn/openvpnconf.go +++ b/internal/provider/wevpn/openvpnconf.go @@ -1,22 +1,25 @@ package wevpn import ( + "fmt" "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/openvpn/parse" "github.com/qdm12/gluetun/internal/provider/utils" ) func (w *Wevpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{constants.AES256gcm} } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } lines = []string{ @@ -24,7 +27,7 @@ func (w *Wevpn) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Wevpn specific "ping 30", @@ -32,7 +35,7 @@ func (w *Wevpn) BuildConf(connection models.Connection, "redirect-gateway def1 bypass-dhcp", "reneg-sec 0", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -52,25 +55,29 @@ func (w *Wevpn) BuildConf(connection models.Connection, lines = append(lines, utils.CipherLines(settings.Ciphers, settings.Version)...) - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if settings.IPv6 { + if *settings.IPv6 { lines = append(lines, "tun-ipv6") } else { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } - lines = append(lines, utils.WrapOpenvpnKey( - settings.ClientKey)...) + keyData, err := parse.ExtractPrivateKey([]byte(*settings.ClientKey)) + if err != nil { + return nil, fmt.Errorf("client key is not valid: %w", err) + } + lines = append(lines, utils.WrapOpenvpnKey(keyData)...) + lines = append(lines, utils.WrapOpenvpnCA( constants.WevpnCA)...) lines = append(lines, utils.WrapOpenvpnCert( diff --git a/internal/provider/windscribe/connection.go b/internal/provider/windscribe/connection.go index d853984c..2ef1fe11 100644 --- a/internal/provider/windscribe/connection.go +++ b/internal/provider/windscribe/connection.go @@ -1,12 +1,12 @@ package windscribe import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (w *Windscribe) GetConnection(selection configuration.ServerSelection) ( +func (w *Windscribe) GetConnection(selection settings.ServerSelection) ( connection models.Connection, err error) { port := getPort(selection) protocol := utils.GetProtocol(selection) @@ -34,7 +34,7 @@ func (w *Windscribe) GetConnection(selection configuration.ServerSelection) ( return utils.PickConnection(connections, selection, w.randSource) } -func getPort(selection configuration.ServerSelection) (port uint16) { +func getPort(selection settings.ServerSelection) (port uint16) { const ( defaultOpenVPNTCP = 443 defaultOpenVPNUDP = 1194 diff --git a/internal/provider/windscribe/connection_test.go b/internal/provider/windscribe/connection_test.go index 4763d9f5..b738692f 100644 --- a/internal/provider/windscribe/connection_test.go +++ b/internal/provider/windscribe/connection_test.go @@ -6,7 +6,7 @@ import ( "net" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -18,53 +18,55 @@ func Test_Windscribe_GetConnection(t *testing.T) { testCases := map[string]struct { servers []models.WindscribeServer - selection configuration.ServerSelection + selection settings.ServerSelection connection models.Connection err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Windscribe), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.WindscribeServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Windscribe), connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(1, 1, 1, 1), Port: 1194, Protocol: constants.UDP, }, }, "target IP": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ TargetIP: net.IPv4(2, 2, 2, 2), - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ - {IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, }, }, "with filter": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ - {Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, - {Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, - {Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {VPN: constants.OpenVPN, Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {VPN: constants.OpenVPN, Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, }, connection: models.Connection{ + Type: constants.OpenVPN, IP: net.IPv4(2, 2, 2, 2), Port: 1194, Protocol: constants.UDP, diff --git a/internal/provider/windscribe/filter.go b/internal/provider/windscribe/filter.go index 84a87a91..ca4e09e9 100644 --- a/internal/provider/windscribe/filter.go +++ b/internal/provider/windscribe/filter.go @@ -1,12 +1,12 @@ package windscribe import ( - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) -func (w *Windscribe) filterServers(selection configuration.ServerSelection) ( +func (w *Windscribe) filterServers(selection settings.ServerSelection) ( servers []models.WindscribeServer, err error) { for _, server := range w.servers { switch { diff --git a/internal/provider/windscribe/filter_test.go b/internal/provider/windscribe/filter_test.go index dbbf9f66..87f98687 100644 --- a/internal/provider/windscribe/filter_test.go +++ b/internal/provider/windscribe/filter_test.go @@ -5,7 +5,7 @@ import ( "math/rand" "testing" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/stretchr/testify/assert" @@ -17,32 +17,31 @@ func Test_Windscribe_filterServers(t *testing.T) { testCases := map[string]struct { servers []models.WindscribeServer - selection configuration.ServerSelection + selection settings.ServerSelection filtered []models.WindscribeServer err error }{ "no server available": { - selection: configuration.ServerSelection{ - VPN: constants.OpenVPN, - }, - err: errors.New("no server found: for VPN openvpn; protocol udp"), + selection: settings.ServerSelection{}.WithDefaults(constants.Windscribe), + err: errors.New("no server found: for VPN openvpn; protocol udp"), }, "no filter": { servers: []models.WindscribeServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, + selection: settings.ServerSelection{}.WithDefaults(constants.Windscribe), filtered: []models.WindscribeServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, }, "filter OpenVPN out": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ VPN: constants.Wireguard, - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ {VPN: constants.OpenVPN, Hostname: "a"}, {VPN: constants.Wireguard, Hostname: "b"}, @@ -53,42 +52,42 @@ func Test_Windscribe_filterServers(t *testing.T) { }, }, "filter by region": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Regions: []string{"b"}, - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ - {Region: "a"}, - {Region: "b"}, - {Region: "c"}, + {VPN: constants.OpenVPN, Region: "a"}, + {VPN: constants.OpenVPN, Region: "b"}, + {VPN: constants.OpenVPN, Region: "c"}, }, filtered: []models.WindscribeServer{ - {Region: "b"}, + {VPN: constants.OpenVPN, Region: "b"}, }, }, "filter by city": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Cities: []string{"b"}, - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ - {City: "a"}, - {City: "b"}, - {City: "c"}, + {VPN: constants.OpenVPN, City: "a"}, + {VPN: constants.OpenVPN, City: "b"}, + {VPN: constants.OpenVPN, City: "c"}, }, filtered: []models.WindscribeServer{ - {City: "b"}, + {VPN: constants.OpenVPN, City: "b"}, }, }, "filter by hostname": { - selection: configuration.ServerSelection{ + selection: settings.ServerSelection{ Hostnames: []string{"b"}, - }, + }.WithDefaults(constants.Windscribe), servers: []models.WindscribeServer{ - {Hostname: "a"}, - {Hostname: "b"}, - {Hostname: "c"}, + {VPN: constants.OpenVPN, Hostname: "a"}, + {VPN: constants.OpenVPN, Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "c"}, }, filtered: []models.WindscribeServer{ - {Hostname: "b"}, + {VPN: constants.OpenVPN, Hostname: "b"}, }, }, } diff --git a/internal/provider/windscribe/openvpnconf.go b/internal/provider/windscribe/openvpnconf.go index 866161a6..608a7e65 100644 --- a/internal/provider/windscribe/openvpnconf.go +++ b/internal/provider/windscribe/openvpnconf.go @@ -3,14 +3,14 @@ package windscribe import ( "strconv" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/utils" ) func (w *Windscribe) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string, err error) { + settings settings.OpenVPN) (lines []string, err error) { if len(settings.Ciphers) == 0 { settings.Ciphers = []string{ constants.AES256gcm, @@ -19,8 +19,9 @@ func (w *Windscribe) BuildConf(connection models.Connection, } } - if settings.Auth == "" { - settings.Auth = constants.SHA512 + auth := *settings.Auth + if auth == "" { + auth = constants.SHA512 } lines = []string{ @@ -28,7 +29,7 @@ func (w *Windscribe) BuildConf(connection models.Connection, "nobind", "tls-exit", "dev " + settings.Interface, - "verb " + strconv.Itoa(settings.Verbosity), + "verb " + strconv.Itoa(*settings.Verbosity), // Windscribe specific "ping 10", @@ -37,7 +38,7 @@ func (w *Windscribe) BuildConf(connection models.Connection, "key-direction 1", "reneg-sec 0", "auth-user-pass " + constants.OpenVPNAuthConf, - "auth " + settings.Auth, + "auth " + auth, // Added constant values "auth-nocache", @@ -57,17 +58,17 @@ func (w *Windscribe) BuildConf(connection models.Connection, lines = append(lines, "explicit-exit-notify") } - if !settings.Root { + if !*settings.Root { lines = append(lines, "user "+settings.ProcUser) lines = append(lines, "persist-tun") lines = append(lines, "persist-key") } - if settings.MSSFix > 0 { - lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + if *settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(*settings.MSSFix))) } - if !settings.IPv6 { + if !*settings.IPv6 { lines = append(lines, `pull-filter ignore "route-ipv6"`) lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) } diff --git a/internal/publicip/loop.go b/internal/publicip/loop.go index 25a6c017..5fa61c4a 100644 --- a/internal/publicip/loop.go +++ b/internal/publicip/loop.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/loopstate" "github.com/qdm12/gluetun/internal/models" @@ -47,7 +47,7 @@ type Loop struct { const defaultBackoffTime = 5 * time.Second func NewLoop(client *http.Client, logger Logger, - settings configuration.PublicIP, puid, pgid int) *Loop { + settings settings.PublicIP, puid, pgid int) *Loop { start := make(chan struct{}) running := make(chan models.LoopStatus) stop := make(chan struct{}) diff --git a/internal/publicip/runner.go b/internal/publicip/runner.go index 855560d8..1fbab3f2 100644 --- a/internal/publicip/runner.go +++ b/internal/publicip/runner.go @@ -52,7 +52,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { case <-ctx.Done(): getCancel() close(errorCh) - filepath := l.state.GetSettings().IPFilepath + filepath := *l.state.GetSettings().IPFilepath l.logger.Info("Removing ip file " + filepath) if err := os.Remove(filepath); err != nil { l.logger.Error(err.Error()) @@ -83,7 +83,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { result.SetIP(ip) l.state.SetData(result) - filepath := l.state.GetSettings().IPFilepath + filepath := *l.state.GetSettings().IPFilepath err = persistPublicIP(filepath, ip.String(), l.puid, l.pgid) if err != nil { l.logger.Error(err.Error()) diff --git a/internal/publicip/settings.go b/internal/publicip/settings.go index e69fd228..295cdde6 100644 --- a/internal/publicip/settings.go +++ b/internal/publicip/settings.go @@ -3,14 +3,14 @@ package publicip import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" ) -func (l *Loop) GetSettings() (settings configuration.PublicIP) { +func (l *Loop) GetSettings() (settings settings.PublicIP) { return l.state.GetSettings() } -func (l *Loop) SetSettings(ctx context.Context, settings configuration.PublicIP) ( +func (l *Loop) SetSettings(ctx context.Context, settings settings.PublicIP) ( outcome string) { return l.state.SetSettings(ctx, settings) } diff --git a/internal/publicip/state/settings.go b/internal/publicip/state/settings.go index 5df81ccb..d57ef338 100644 --- a/internal/publicip/state/settings.go +++ b/internal/publicip/state/settings.go @@ -4,22 +4,22 @@ import ( "context" "reflect" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" ) type SettingsGetSetter interface { - GetSettings() (settings configuration.PublicIP) + GetSettings() (settings settings.PublicIP) SetSettings(ctx context.Context, - settings configuration.PublicIP) (outcome string) + settings settings.PublicIP) (outcome string) } -func (s *State) GetSettings() (settings configuration.PublicIP) { +func (s *State) GetSettings() (settings settings.PublicIP) { s.settingsMu.RLock() defer s.settingsMu.RUnlock() return s.settings } -func (s *State) SetSettings(ctx context.Context, settings configuration.PublicIP) ( +func (s *State) SetSettings(ctx context.Context, settings settings.PublicIP) ( outcome string) { s.settingsMu.Lock() diff --git a/internal/publicip/state/state.go b/internal/publicip/state/state.go index a8ea95b0..39e0e0a7 100644 --- a/internal/publicip/state/state.go +++ b/internal/publicip/state/state.go @@ -3,7 +3,7 @@ package state import ( "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/loopstate" "github.com/qdm12/gluetun/internal/publicip/models" ) @@ -16,7 +16,7 @@ type Manager interface { } func New(statusApplier loopstate.Applier, - settings configuration.PublicIP, + settings settings.PublicIP, updateTicker chan<- struct{}) *State { return &State{ statusApplier: statusApplier, @@ -28,7 +28,7 @@ func New(statusApplier loopstate.Applier, type State struct { statusApplier loopstate.Applier - settings configuration.PublicIP + settings settings.PublicIP settingsMu sync.RWMutex ipData models.IPInfoData diff --git a/internal/publicip/ticker.go b/internal/publicip/ticker.go index 8406c2dc..3c14be0d 100644 --- a/internal/publicip/ticker.go +++ b/internal/publicip/ticker.go @@ -16,7 +16,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { timer := time.NewTimer(time.Hour) timer.Stop() // 1 hour, cannot be a race condition timerIsStopped := true - if period := l.state.GetSettings().Period; period > 0 { + if period := *l.state.GetSettings().Period; period > 0 { timerIsStopped = false timer.Reset(period) } @@ -31,13 +31,13 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { case <-timer.C: lastTick = l.timeNow() _, _ = l.ApplyStatus(ctx, constants.Running) - timer.Reset(l.state.GetSettings().Period) + timer.Reset(*l.state.GetSettings().Period) case <-l.updateTicker: if !timerIsStopped && !timer.Stop() { <-timer.C } timerIsStopped = true - period := l.state.GetSettings().Period + period := *l.state.GetSettings().Period if period == 0 { continue } diff --git a/internal/shadowsocks/loop.go b/internal/shadowsocks/loop.go index 72cf4ae5..a9698d50 100644 --- a/internal/shadowsocks/loop.go +++ b/internal/shadowsocks/loop.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" shadowsockslib "github.com/qdm12/ss-server/pkg/tcpudp" @@ -17,8 +17,8 @@ type Looper interface { SetStatus(ctx context.Context, status models.LoopStatus) ( outcome string, err error) GetStatus() (status models.LoopStatus) - GetSettings() (settings configuration.ShadowSocks) - SetSettings(ctx context.Context, settings configuration.ShadowSocks) ( + GetSettings() (settings settings.Shadowsocks) + SetSettings(ctx context.Context, settings settings.Shadowsocks) ( outcome string) } @@ -52,7 +52,7 @@ func (l *looper) logAndWait(ctx context.Context, err error) { const defaultBackoffTime = 10 * time.Second -func NewLooper(settings configuration.ShadowSocks, logger Logger) Looper { +func NewLooper(settings settings.Shadowsocks, logger Logger) Looper { return &looper{ state: state{ status: constants.Stopped, @@ -72,7 +72,7 @@ func (l *looper) Run(ctx context.Context, done chan<- struct{}) { crashed := false - if l.GetSettings().Enabled { + if *l.GetSettings().Enabled { go func() { _, _ = l.SetStatus(ctx, constants.Running) }() diff --git a/internal/shadowsocks/state.go b/internal/shadowsocks/state.go index afa26f95..145573b3 100644 --- a/internal/shadowsocks/state.go +++ b/internal/shadowsocks/state.go @@ -7,14 +7,14 @@ import ( "reflect" "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" ) type state struct { status models.LoopStatus - settings configuration.ShadowSocks + settings settings.Shadowsocks statusMu sync.RWMutex settingsMu sync.RWMutex } @@ -84,13 +84,13 @@ func (l *looper) SetStatus(ctx context.Context, status models.LoopStatus) ( } } -func (l *looper) GetSettings() (settings configuration.ShadowSocks) { +func (l *looper) GetSettings() (settings settings.Shadowsocks) { l.state.settingsMu.RLock() defer l.state.settingsMu.RUnlock() return l.state.settings } -func (l *looper) SetSettings(ctx context.Context, settings configuration.ShadowSocks) ( +func (l *looper) SetSettings(ctx context.Context, settings settings.Shadowsocks) ( outcome string) { l.state.settingsMu.Lock() settingsUnchanged := reflect.DeepEqual(settings, l.state.settings) @@ -98,8 +98,8 @@ func (l *looper) SetSettings(ctx context.Context, settings configuration.ShadowS l.state.settingsMu.Unlock() return "settings left unchanged" } - newEnabled := settings.Enabled - previousEnabled := l.state.settings.Enabled + newEnabled := *settings.Enabled + previousEnabled := *l.state.settings.Enabled l.state.settings = settings l.state.settingsMu.Unlock() // Either restart or set changed status diff --git a/internal/updater/loop.go b/internal/updater/loop.go index 265194cc..2113231d 100644 --- a/internal/updater/loop.go +++ b/internal/updater/loop.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/storage" @@ -18,8 +18,8 @@ type Looper interface { GetStatus() (status models.LoopStatus) SetStatus(ctx context.Context, status models.LoopStatus) ( outcome string, err error) - GetSettings() (settings configuration.Updater) - SetSettings(settings configuration.Updater) (outcome string) + GetSettings() (settings settings.Updater) + SetSettings(settings settings.Updater) (outcome string) } type looper struct { @@ -44,7 +44,7 @@ type looper struct { const defaultBackoffTime = 5 * time.Second -func NewLooper(settings configuration.Updater, currentServers models.AllServers, +func NewLooper(settings settings.Updater, currentServers models.AllServers, flusher storage.Flusher, setAllServers func(allServers models.AllServers), client *http.Client, logger Logger) Looper { return &looper{ @@ -164,7 +164,7 @@ func (l *looper) RunRestartTicker(ctx context.Context, done chan<- struct{}) { timer := time.NewTimer(time.Hour) timer.Stop() timerIsStopped := true - if period := l.GetSettings().Period; period > 0 { + if period := *l.GetSettings().Period; period > 0 { timerIsStopped = false timer.Reset(period) } @@ -179,13 +179,13 @@ func (l *looper) RunRestartTicker(ctx context.Context, done chan<- struct{}) { case <-timer.C: lastTick = l.timeNow() l.start <- struct{}{} - timer.Reset(l.GetSettings().Period) + timer.Reset(*l.GetSettings().Period) case <-l.updateTicker: if !timerIsStopped && !timer.Stop() { <-timer.C } timerIsStopped = true - period := l.GetSettings().Period + period := *l.GetSettings().Period if period == 0 { continue } diff --git a/internal/updater/providers.go b/internal/updater/providers.go index 261539ac..54dfe216 100644 --- a/internal/updater/providers.go +++ b/internal/updater/providers.go @@ -48,7 +48,7 @@ func (u *updater) updateExpressvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Expressvpn.Servers)) servers, warnings, err := expressvpn.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("ExpressVPN: " + warning) } @@ -70,7 +70,7 @@ func (u *updater) updateFastestvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Fastestvpn.Servers)) servers, warnings, err := fastestvpn.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("FastestVPN: " + warning) } @@ -92,7 +92,7 @@ func (u *updater) updateHideMyAss(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.HideMyAss.Servers)) servers, warnings, err := hidemyass.GetServers( ctx, u.client, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("HideMyAss: " + warning) } @@ -114,7 +114,7 @@ func (u *updater) updateIpvanish(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Ipvanish.Servers)) servers, warnings, err := ipvanish.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("Ipvanish: " + warning) } @@ -136,7 +136,7 @@ func (u *updater) updateIvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Ivpn.Servers)) servers, warnings, err := ivpn.GetServers( ctx, u.client, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("Ivpn: " + warning) } @@ -173,7 +173,7 @@ func (u *updater) updateMullvad(ctx context.Context) (err error) { func (u *updater) updateNordvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Nordvpn.Servers)) servers, warnings, err := nordvpn.GetServers(ctx, u.client, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("NordVPN: " + warning) } @@ -194,7 +194,7 @@ func (u *updater) updateNordvpn(ctx context.Context) (err error) { func (u *updater) updatePerfectprivacy(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Perfectprivacy.Servers)) servers, warnings, err := perfectprivacy.GetServers(ctx, u.unzipper, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn(constants.Perfectprivacy + ": " + warning) } @@ -232,7 +232,7 @@ func (u *updater) updatePrivado(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Privado.Servers)) servers, warnings, err := privado.GetServers( ctx, u.unzipper, u.client, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("Privado: " + warning) } @@ -254,7 +254,7 @@ func (u *updater) updatePrivatevpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Privatevpn.Servers)) servers, warnings, err := privatevpn.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("PrivateVPN: " + warning) } @@ -275,7 +275,7 @@ func (u *updater) updatePrivatevpn(ctx context.Context) (err error) { func (u *updater) updateProtonvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Privatevpn.Servers)) servers, warnings, err := protonvpn.GetServers(ctx, u.client, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("ProtonVPN: " + warning) } @@ -297,7 +297,7 @@ func (u *updater) updatePurevpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Purevpn.Servers)) servers, warnings, err := purevpn.GetServers( ctx, u.client, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("PureVPN: " + warning) } @@ -319,7 +319,7 @@ func (u *updater) updateSurfshark(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Surfshark.Servers)) servers, warnings, err := surfshark.GetServers( ctx, u.unzipper, u.client, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("Surfshark: " + warning) } @@ -341,7 +341,7 @@ func (u *updater) updateTorguard(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Torguard.Servers)) servers, warnings, err := torguard.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("Torguard: " + warning) } @@ -363,7 +363,7 @@ func (u *updater) updateVPNUnlimited(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.VPNUnlimited.Servers)) servers, warnings, err := vpnunlimited.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn(constants.VPNUnlimited + ": " + warning) } @@ -385,7 +385,7 @@ func (u *updater) updateVyprvpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Vyprvpn.Servers)) servers, warnings, err := vyprvpn.GetServers( ctx, u.unzipper, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("VyprVPN: " + warning) } @@ -406,7 +406,7 @@ func (u *updater) updateVyprvpn(ctx context.Context) (err error) { func (u *updater) updateWevpn(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Wevpn.Servers)) servers, warnings, err := wevpn.GetServers(ctx, u.presolver, minServers) - if u.options.CLI { + if *u.options.CLI { for _, warning := range warnings { u.logger.Warn("WeVPN: " + warning) } diff --git a/internal/updater/state.go b/internal/updater/state.go index 411ceb82..42bf3637 100644 --- a/internal/updater/state.go +++ b/internal/updater/state.go @@ -7,14 +7,14 @@ import ( "reflect" "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" ) type state struct { status models.LoopStatus - settings configuration.Updater + settings settings.Updater statusMu sync.RWMutex periodMu sync.RWMutex } @@ -84,13 +84,13 @@ func (l *looper) SetStatus(ctx context.Context, status models.LoopStatus) (outco } } -func (l *looper) GetSettings() (settings configuration.Updater) { +func (l *looper) GetSettings() (settings settings.Updater) { l.state.periodMu.RLock() defer l.state.periodMu.RUnlock() return l.state.settings } -func (l *looper) SetSettings(settings configuration.Updater) (outcome string) { +func (l *looper) SetSettings(settings settings.Updater) (outcome string) { l.state.periodMu.Lock() defer l.state.periodMu.Unlock() settingsUnchanged := reflect.DeepEqual(settings, l.state.settings) diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 8664bc79..488de584 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -4,9 +4,10 @@ package updater import ( "context" "net/http" + "strings" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/updater/resolver" @@ -19,7 +20,7 @@ type Updater interface { type updater struct { // configuration - options configuration.Updater + options settings.Updater // state servers models.AllServers @@ -32,16 +33,13 @@ type updater struct { unzipper unzip.Unzipper } -func New(settings configuration.Updater, httpClient *http.Client, +func New(settings settings.Updater, httpClient *http.Client, currentServers models.AllServers, logger Logger) Updater { - if settings.DNSAddress == "" { - settings.DNSAddress = "1.1.1.1" - } unzipper := unzip.New(httpClient) return &updater{ logger: logger, timeNow: time.Now, - presolver: resolver.NewParallelResolver(settings.DNSAddress), + presolver: resolver.NewParallelResolver(settings.DNSAddress.String()), client: httpClient, unzipper: unzipper, options: settings, @@ -49,203 +47,17 @@ func New(settings configuration.Updater, httpClient *http.Client, } } -//nolint:gocognit,gocyclo +type updateFunc func(ctx context.Context) (err error) + func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServers, err error) { - if u.options.Cyberghost { - u.logger.Info("updating Cyberghost servers...") - if err := u.updateCyberghost(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } + for _, provider := range u.options.Providers { + u.logger.Info("updating " + strings.Title(provider) + " servers...") + updateProvider := u.getUpdateFunction(provider) - if u.options.Expressvpn { - u.logger.Info("updating Expressvpn servers...") - if err := u.updateExpressvpn(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Fastestvpn { - u.logger.Info("updating Fastestvpn servers...") - if err := u.updateFastestvpn(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.HideMyAss { - u.logger.Info("updating HideMyAss servers...") - if err := u.updateHideMyAss(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Ipvanish { - u.logger.Info("updating Ipvanish servers...") - if err := u.updateIpvanish(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Ivpn { - u.logger.Info("updating Ivpn servers...") - if err := u.updateIvpn(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Mullvad { - u.logger.Info("updating Mullvad servers...") - if err := u.updateMullvad(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Nordvpn { // TODO support servers offering only TCP or only UDP - u.logger.Info("updating NordVPN servers...") - if err := u.updateNordvpn(ctx); err != nil { - u.logger.Error(err.Error()) - } - if err := ctx.Err(); err != nil { - return allServers, err - } - } - - if u.options.Perfectprivacy { - u.logger.Info("updating " + constants.Perfectprivacy + " servers...") - if err := u.updatePerfectprivacy(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Privado { - u.logger.Info("updating Privado servers...") - if err := u.updatePrivado(ctx); err != nil { - u.logger.Error(err.Error()) - } - if ctx.Err() != nil { - return allServers, ctx.Err() - } - } - - if u.options.PIA { - u.logger.Info("updating Private Internet Access servers...") - if err := u.updatePIA(ctx); err != nil { - u.logger.Error(err.Error()) - } - if ctx.Err() != nil { - return allServers, ctx.Err() - } - } - - if u.options.Privatevpn { - u.logger.Info("updating Privatevpn servers...") - if err := u.updatePrivatevpn(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Protonvpn { - u.logger.Info("updating Protonvpn servers...") - if err := u.updateProtonvpn(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Purevpn { - u.logger.Info("updating PureVPN servers...") - // TODO support servers offering only TCP or only UDP - if err := u.updatePurevpn(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Surfshark { - u.logger.Info("updating Surfshark servers...") - if err := u.updateSurfshark(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Torguard { - u.logger.Info("updating Torguard servers...") - if err := u.updateTorguard(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.VPNUnlimited { - u.logger.Info("updating " + constants.VPNUnlimited + " servers...") - if err := u.updateVPNUnlimited(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Vyprvpn { - u.logger.Info("updating Vyprvpn servers...") - if err := u.updateVyprvpn(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Wevpn { - u.logger.Info("updating WeVPN servers...") - if err := u.updateWevpn(ctx); err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - return allServers, ctxErr - } - u.logger.Error(err.Error()) - } - } - - if u.options.Windscribe { - u.logger.Info("updating Windscribe servers...") - if err := u.updateWindscribe(ctx); err != nil { + // for NordVPN and PureVPN + err = updateProvider(ctx) + if err != nil { if ctxErr := ctx.Err(); ctxErr != nil { return allServers, ctxErr } @@ -255,3 +67,52 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe return u.servers, nil } + +func (u *updater) getUpdateFunction(provider string) (updateFunction updateFunc) { + switch provider { + case constants.Custom: + panic("cannot update custom provider") + case constants.Cyberghost: + return func(ctx context.Context) (err error) { return u.updateCyberghost(ctx) } + case constants.Expressvpn: + return func(ctx context.Context) (err error) { return u.updateExpressvpn(ctx) } + case constants.Fastestvpn: + return func(ctx context.Context) (err error) { return u.updateFastestvpn(ctx) } + case constants.HideMyAss: + return func(ctx context.Context) (err error) { return u.updateHideMyAss(ctx) } + case constants.Ipvanish: + return func(ctx context.Context) (err error) { return u.updateIpvanish(ctx) } + case constants.Ivpn: + return func(ctx context.Context) (err error) { return u.updateIvpn(ctx) } + case constants.Mullvad: + return func(ctx context.Context) (err error) { return u.updateMullvad(ctx) } + case constants.Nordvpn: + return func(ctx context.Context) (err error) { return u.updateNordvpn(ctx) } + case constants.Perfectprivacy: + return func(ctx context.Context) (err error) { return u.updatePerfectprivacy(ctx) } + case constants.Privado: + return func(ctx context.Context) (err error) { return u.updatePrivado(ctx) } + case constants.PrivateInternetAccess: + return func(ctx context.Context) (err error) { return u.updatePIA(ctx) } + case constants.Privatevpn: + return func(ctx context.Context) (err error) { return u.updatePrivatevpn(ctx) } + case constants.Protonvpn: + return func(ctx context.Context) (err error) { return u.updateProtonvpn(ctx) } + case constants.Purevpn: + return func(ctx context.Context) (err error) { return u.updatePurevpn(ctx) } + case constants.Surfshark: + return func(ctx context.Context) (err error) { return u.updateSurfshark(ctx) } + case constants.Torguard: + return func(ctx context.Context) (err error) { return u.updateTorguard(ctx) } + case constants.VPNUnlimited: + return func(ctx context.Context) (err error) { return u.updateVPNUnlimited(ctx) } + case constants.Vyprvpn: + return func(ctx context.Context) (err error) { return u.updateVyprvpn(ctx) } + case constants.Wevpn: + return func(ctx context.Context) (err error) { return u.updateWevpn(ctx) } + case constants.Windscribe: + return func(ctx context.Context) (err error) { return u.updateWindscribe(ctx) } + default: + panic("provider " + provider + " is unknown") + } +} diff --git a/internal/vpn/loop.go b/internal/vpn/loop.go index 48c374a6..fd9276b6 100644 --- a/internal/vpn/loop.go +++ b/internal/vpn/loop.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/dns" "github.com/qdm12/gluetun/internal/firewall" @@ -68,7 +68,7 @@ const ( defaultBackoffTime = 15 * time.Second ) -func NewLoop(vpnSettings configuration.VPN, vpnInputPorts []uint16, +func NewLoop(vpnSettings settings.VPN, vpnInputPorts []uint16, allServers models.AllServers, openvpnConf openvpn.Interface, netLinker netlink.NetLinker, fw firewallConfigurer, routing routing.VPNGetter, portForward portforward.StartStopper, starter command.Starter, diff --git a/internal/vpn/openvpn.go b/internal/vpn/openvpn.go index ae856976..e8db25e7 100644 --- a/internal/vpn/openvpn.go +++ b/internal/vpn/openvpn.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/provider" @@ -24,7 +24,7 @@ var ( // It returns a serverName for port forwarding (PIA) and an error if it fails. func setupOpenVPN(ctx context.Context, fw firewall.VPNConnectionSetter, openvpnConf openvpn.Interface, providerConf provider.Provider, - settings configuration.VPN, starter command.Starter, logger openvpn.Logger) ( + settings settings.VPN, starter command.Starter, logger openvpn.Logger) ( runner vpnRunner, serverName string, err error) { connection, err := providerConf.GetConnection(settings.Provider.ServerSelection) if err != nil { diff --git a/internal/vpn/run.go b/internal/vpn/run.go index c1d210eb..161d4855 100644 --- a/internal/vpn/run.go +++ b/internal/vpn/run.go @@ -29,9 +29,9 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { for ctx.Err() == nil { settings, allServers := l.state.GetSettingsAndServers() - providerConf := provider.New(settings.Provider.Name, allServers, time.Now) + providerConf := provider.New(*settings.Provider.Name, allServers, time.Now) - portForwarding := settings.Provider.PortForwarding.Enabled + portForwarding := *settings.Provider.PortForwarding.Enabled var vpnRunner vpnRunner var serverName, vpnInterface string var err error diff --git a/internal/vpn/settings.go b/internal/vpn/settings.go index 2539b5f0..7256c4a2 100644 --- a/internal/vpn/settings.go +++ b/internal/vpn/settings.go @@ -3,18 +3,18 @@ package vpn import ( "context" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/vpn/state" ) type SettingsGetSetter = state.SettingsGetSetter -func (l *Loop) GetSettings() (settings configuration.VPN) { +func (l *Loop) GetSettings() (settings settings.VPN) { return l.state.GetSettings() } func (l *Loop) SetSettings(ctx context.Context, - vpn configuration.VPN) ( + vpn settings.VPN) ( outcome string) { return l.state.SetSettings(ctx, vpn) } diff --git a/internal/vpn/state/state.go b/internal/vpn/state/state.go index 16adc0d7..dfb99418 100644 --- a/internal/vpn/state/state.go +++ b/internal/vpn/state/state.go @@ -3,7 +3,7 @@ package state import ( "sync" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/loopstate" "github.com/qdm12/gluetun/internal/models" ) @@ -13,11 +13,11 @@ var _ Manager = (*State)(nil) type Manager interface { SettingsGetSetter ServersGetterSetter - GetSettingsAndServers() (vpn configuration.VPN, allServers models.AllServers) + GetSettingsAndServers() (vpn settings.VPN, allServers models.AllServers) } func New(statusApplier loopstate.Applier, - vpn configuration.VPN, allServers models.AllServers) *State { + vpn settings.VPN, allServers models.AllServers) *State { return &State{ statusApplier: statusApplier, vpn: vpn, @@ -28,14 +28,14 @@ func New(statusApplier loopstate.Applier, type State struct { statusApplier loopstate.Applier - vpn configuration.VPN + vpn settings.VPN settingsMu sync.RWMutex allServers models.AllServers allServersMu sync.RWMutex } -func (s *State) GetSettingsAndServers() (vpn configuration.VPN, +func (s *State) GetSettingsAndServers() (vpn settings.VPN, allServers models.AllServers) { s.settingsMu.RLock() s.allServersMu.RLock() diff --git a/internal/vpn/state/vpn.go b/internal/vpn/state/vpn.go index 2548850a..7af2b992 100644 --- a/internal/vpn/state/vpn.go +++ b/internal/vpn/state/vpn.go @@ -4,23 +4,23 @@ import ( "context" "reflect" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/constants" ) type SettingsGetSetter interface { - GetSettings() (vpn configuration.VPN) - SetSettings(ctx context.Context, vpn configuration.VPN) (outcome string) + GetSettings() (vpn settings.VPN) + SetSettings(ctx context.Context, vpn settings.VPN) (outcome string) } -func (s *State) GetSettings() (vpn configuration.VPN) { +func (s *State) GetSettings() (vpn settings.VPN) { s.settingsMu.RLock() vpn = s.vpn s.settingsMu.RUnlock() return vpn } -func (s *State) SetSettings(ctx context.Context, vpn configuration.VPN) ( +func (s *State) SetSettings(ctx context.Context, vpn settings.VPN) ( outcome string) { s.settingsMu.Lock() settingsUnchanged := reflect.DeepEqual(s.vpn, vpn) diff --git a/internal/vpn/tunnelup.go b/internal/vpn/tunnelup.go index 1cf38086..366d5a28 100644 --- a/internal/vpn/tunnelup.go +++ b/internal/vpn/tunnelup.go @@ -26,7 +26,7 @@ func (l *Loop) onTunnelUp(ctx context.Context, data tunnelUpData) { } } - if l.dnsLooper.GetSettings().Enabled { + if *l.dnsLooper.GetSettings().DoT.Enabled { _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) } diff --git a/internal/vpn/wireguard.go b/internal/vpn/wireguard.go index 6640d3b5..b230b3ab 100644 --- a/internal/vpn/wireguard.go +++ b/internal/vpn/wireguard.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/provider" @@ -22,7 +22,7 @@ var ( // It returns a serverName for port forwarding (PIA) and an error if it fails. func setupWireguard(ctx context.Context, netlinker netlink.NetLinker, fw firewall.VPNConnectionSetter, providerConf provider.Provider, - settings configuration.VPN, logger wireguard.Logger) ( + settings settings.VPN, logger wireguard.Logger) ( wireguarder wireguard.Wireguarder, serverName string, err error) { connection, err := providerConf.GetConnection(settings.Provider.ServerSelection) if err != nil { diff --git a/internal/wireguard/settings.go b/internal/wireguard/settings.go index cfe18b23..782be886 100644 --- a/internal/wireguard/settings.go +++ b/internal/wireguard/settings.go @@ -94,7 +94,7 @@ func (s *Settings) Check() (err error) { switch { case s.Endpoint == nil: return ErrEndpointMissing - case s.Endpoint.IP == nil: + case len(s.Endpoint.IP) == 0: return ErrEndpointIPMissing case s.Endpoint.Port == 0: return ErrEndpointPortMissing