diff --git a/Dockerfile b/Dockerfile index be4a0c15..0cd61478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ ENV VPNSP=pia \ TZ= \ # PIA only PASSWORD= \ - REGION="CA Montreal" \ + REGION="Austria" \ PIA_ENCRYPTION=strong \ OPENVPN_CIPHER= \ OPENVPN_AUTH= \ @@ -48,6 +48,7 @@ ENV VPNSP=pia \ COUNTRY=Sweden \ CITY= \ ISP= \ + # Mullvad and Windscribe only PORT= \ # DNS over TLS DOT=on \ diff --git a/README.md b/README.md index 1a1cdb6d..085c7021 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Private Internet Access Client +# Gluetun VPN client -*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access or Mullvad VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and Tinyproxy* +*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access, Mullvad and Windscribe VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and Tinyproxy* -**ANNOUNCEMENT**: *Support for [Mullvad](http://mullvad.net)* +**ANNOUNCEMENT**: *Support for [Windscribe](https://windscribe.com/)* @@ -33,7 +33,7 @@ ## Features - Based on Alpine 3.11 for a small Docker image below 50MB -- Supports **Private Internet Access** and **Mullvad** servers +- Supports **Private Internet Access**, **Mullvad** and **Windscribe** servers - DNS over TLS baked in with service provider(s) of your choice - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses - Choose the vpn network protocol, `udp` or `tcp` @@ -42,7 +42,7 @@ - Built in HTTP proxy (Tinyproxy, tunnels TCP) - [Connect other containers to it](https://github.com/qdm12/private-internet-access-docker#connect-to-it) - [Connect LAN devices to it](https://github.com/qdm12/private-internet-access-docker#connect-to-it) -- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, ppc64le and even that s390x 🎆 +- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7 🎆 ### Private Internet Access @@ -55,6 +55,10 @@ - Pick the [country, city and ISP](https://mullvad.net/en/servers/#openvpn) - Pick the port to use (i.e. `53` (udp) or `80` (tcp)) +### Windscribe + +- Pick the [region](https://windscribe.com/status) + ### Extra niche features - Possibility of split horizon DNS by selecting multiple DNS over TLS providers @@ -81,6 +85,8 @@ - If `VPNSP=mullvad` and `PORT=53`, allow outbound UDP 53 to the corresponding VPN server IPs, which you can fine in [the mapping of Mullvad servers](https://github.com/qdm12/private-internet-access-docker/blob/master/internal/constants/mullvad.go#L64-L667) - If `VPNSP=mullvad` and `PORT=80`, allow outbound TCP 80 to the corresponding VPN server IPs, which you can fine in [the mapping of Mullvad servers](https://github.com/qdm12/private-internet-access-docker/blob/master/internal/constants/mullvad.go#L64-L667) - If `VPNSP=mullvad` and `PORT=443`, allow outbound TCP 443 to the corresponding VPN server IPs, which you can fine in [the mapping of Mullvad servers](https://github.com/qdm12/private-internet-access-docker/blob/master/internal/constants/mullvad.go#L64-L667) + - If `VPNSP=windscribe` and `PROTOCOL=udp`: allow outbound UDP 443 to the corresponding VPN server IPs + - If `VPNSP=windscribe` and `PROTOCOL=tcp`: allow outbound TCP 1194 to the corresponding VPN server IPs - If `SHADOWSOCKS=on`, allow inbound TCP 8388 and UDP 8388 from your LAN - If `TINYPROXY=on`, allow inbound TCP 8888 from your LAN @@ -124,16 +130,16 @@ docker run --rm --network=container:pia alpine:3.11 wget -qO- https://ipinfo.io | Environment variable | Default | Description | | --- | --- | --- | -| `VPNSP` | `pia` | VPN Service Provider, one of `pia`, `mullvad` | -| `REGION` | `CA Montreal` | (PIA only) one of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) | +| `VPNSP` | `pia` | VPN Service Provider, one of `pia`, `mullvad` or `windscribe` | +| `REGION` | `Austria` | (PIA & Windscribe only) one of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) or one of the [Windscribe regions](https://windscribe.com/status) | | `COUNTRY` | `Sweden` | (Mullvad only) one of the [Mullvad countries](https://mullvad.net/en/servers/#openvpn) | | `CITY` | | (Mullvad only, *optional*) one of the [Mullvad cities](https://mullvad.net/en/servers/#openvpn) | | `ISP` | | (Mullvad only, *optional*) one of the [Mullvad ISP](https://mullvad.net/en/servers/#openvpn) | -| `PORT` | | (Mullvad only, *optional*) For TCP, `80` or `443`, or `53` for UDP. Leave blank for default Mullvad server port | +| `PORT` | | (Mullvad and Windscribe only, *optional*) **Mullvad**: For TCP, `80` or `443`, or `53` for UDP. Leave blank for default Mullvad server port; **Windscribe** see [this list of ports](https://windscribe.com/getconfig/openvpn) | | `PROTOCOL` | `udp` | `tcp` or `udp` | -| `PIA_ENCRYPTION` | `strong` | (PIA only) `normal` or `strong` or `custom` | -| `USER` | | PIA username **or** Mullvad user ID | -| `PASSWORD` | | Your PIA password | +| `ENCRYPTION` | `strong` | (PIA only) `normal` or `strong` | +| `USER` | | PIA username **or** Mullvad user ID **or** Windscribe username | +| `PASSWORD` | | Your PIA password **or** Windscribe password | | `DOT` | `on` | `on` or `off`, to activate DNS over TLS to 1.1.1.1 | | `DOT_PROVIDERS` | `cloudflare` | Comma delimited list of DNS over TLS providers from `cloudflare`, `google`, `quad9`, `quadrant`, `cleanbrowsing`, `securedns`, `libredns` | | `DOT_CACHING` | `on` | Unbound caching feature, `on` or `off` | @@ -161,7 +167,7 @@ docker run --rm --network=container:pia alpine:3.11 wget -qO- https://ipinfo.io | `TZ` | | Specify a timezone to use i.e. `Europe/London` | | `OPENVPN_VERBOSITY` | `1` | Openvpn verbosity level from 0 to 6 | | `OPENVPN_ROOT` | `no` | Run OpenVPN as root, `yes` or `no` | -| `OPENVPN_TARGET_IP` | | Specify a target VPN server IP address to use, valid for Mullvad and Private Internet Access | +| `OPENVPN_TARGET_IP` | | (Optional) Specify a target VPN server IP address to use, valid for Mullvad and Private Internet Access | | `OPENVPN_CIPHER` | | Specify a custom cipher to use, use at your own risk. It will also set `ncp-disable` if using AES GCM for PIA | | `OPENVPN_AUTH` | | Specify a custom auth algorithm to use (i.e. `sha256`) *for pia only* | @@ -456,7 +462,6 @@ Thanks for all the contributions, whether small or not so small!
Expand me

-- Support Windscribe - Gotify support for notificactions - Periodic update of malicious block lists with Unbound restart - Improve healthcheck diff --git a/cmd/main.go b/cmd/main.go index d8bcc504..7cc718d6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,6 +27,7 @@ import ( "github.com/qdm12/private-internet-access-docker/internal/shadowsocks" "github.com/qdm12/private-internet-access-docker/internal/splash" "github.com/qdm12/private-internet-access-docker/internal/tinyproxy" + "github.com/qdm12/private-internet-access-docker/internal/windscribe" ) const ( @@ -56,6 +57,7 @@ func main() { firewallConf := firewall.NewConfigurator(logger, fileManager) piaConf := pia.NewConfigurator(client, fileManager, firewallConf, logger) mullvadConf := mullvad.NewConfigurator(fileManager, logger) + windscribeConf := windscribe.NewConfigurator(fileManager) tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger) shadowsocksConf := shadowsocks.NewConfigurator(fileManager, logger) ctx, cancel := context.WithCancel(context.Background()) @@ -86,6 +88,9 @@ func main() { case "mullvad": openVPNUser = allSettings.Mullvad.User openVPNPassword = "m" + case "windscribe": + openVPNUser = allSettings.Windscribe.User + openVPNPassword = allSettings.Windscribe.Password } err = ovpnConf.WriteAuthFile(openVPNUser, openVPNPassword, uid, gid) e.FatalOnError(err) @@ -139,6 +144,11 @@ func main() { e.FatalOnError(err) err = mullvadConf.BuildConf(connections, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root, allSettings.OpenVPN.Cipher) e.FatalOnError(err) + case "windscribe": + connections, err = windscribeConf.GetOpenVPNConnections(allSettings.Windscribe.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.Windscribe.Port, allSettings.OpenVPN.TargetIP) + e.FatalOnError(err) + err = windscribeConf.BuildConf(connections, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root, allSettings.OpenVPN.Cipher, allSettings.OpenVPN.Auth) + e.FatalOnError(err) } defaultInterface, defaultGateway, defaultSubnet, err := firewallConf.GetDefaultRoute() diff --git a/docker-compose.yml b/docker-compose.yml index fa89efdd..225ecf32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,57 +1,58 @@ -version: "3.7" -services: - pia: - image: qmcgaw/private-internet-access - container_name: pia - cap_add: - - NET_ADMIN - network_mode: bridge - init: true - ports: - - 8888:8888/tcp - - 8388:8388/tcp - - 8388:8388/udp - # command: - environment: - # More variables are available, see the readme table - - VPNSP=pia - - USER=js89ds7 - - PROTOCOL=udp - - OPENVPN_VERBOSITY=1 - - OPENVPN_ROOT=no - - OPENVPN_TARGET_IP= - - TZ= - - # PIA only - - REGION=CA Montreal - - PASSWORD=8fd9s239G - - PIA_ENCRYPTION=strong - - PORT_FORWARDING=off - - OPENVPN_CIPHER= - - OPENVPN_AUTH= - - # Mullvad only - - COUNTRY=Sweden - - CITY= - - ISP= - - PORT= - - # DNS over TLS - - DOT=on - - DOT_PROVIDERS=cloudflare - - DOT_IPV6=on - - DOT_VERBOSITY=1 - - BLOCK_MALICIOUS=on - - BLOCK_SURVEILLANCE=off - - BLOCK_ADS=off - - UNBLOCK= - # Firewall - - EXTRA_SUBNETS= - # Shadowsocks - - SHADOWSOCKS=off - - SHADOWSOCKS_PASSWORD= - # Tinyproxy - - TINYPROXY=off - - TINYPROXY_USER= - - TINYPROXY_PASSWORD= - restart: always +version: "3.7" +services: + pia: + image: qmcgaw/private-internet-access + container_name: pia + cap_add: + - NET_ADMIN + network_mode: bridge + init: true + ports: + - 8888:8888/tcp + - 8388:8388/tcp + - 8388:8388/udp + # command: + environment: + # More variables are available, see the readme table + - VPNSP=pia + - USER=js89ds7 + - PROTOCOL=udp + - OPENVPN_VERBOSITY=1 + - OPENVPN_ROOT=no + - OPENVPN_TARGET_IP= + - TZ= + + # PIA and Windscribe only + - REGION=Austria + - PASSWORD=8fd9s239G + + # PIA only + - PASSWORD=8fd9s239G + - PIA_ENCRYPTION=strong + - PORT_FORWARDING=off + + # Mullvad only + - COUNTRY=Sweden + - CITY= + - ISP= + - PORT= + + # DNS over TLS + - DOT=on + - DOT_PROVIDERS=cloudflare + - DOT_IPV6=on + - DOT_VERBOSITY=1 + - BLOCK_MALICIOUS=on + - BLOCK_SURVEILLANCE=off + - BLOCK_ADS=off + - UNBLOCK= + # Firewall + - EXTRA_SUBNETS= + # Shadowsocks + - SHADOWSOCKS=off + - SHADOWSOCKS_PASSWORD= + # Tinyproxy + - TINYPROXY=off + - TINYPROXY_USER= + - TINYPROXY_PASSWORD= + restart: always diff --git a/internal/constants/splash.go b/internal/constants/splash.go index 44b799fb..ea95515c 100644 --- a/internal/constants/splash.go +++ b/internal/constants/splash.go @@ -2,9 +2,9 @@ package constants const ( // Announcement is a message announcement - Announcement = "Support for Mullvad (a bit unstable)" + Announcement = "Support for Windscribe" // AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd - AnnouncementExpiration = "2020-03-15" + AnnouncementExpiration = "2020-04-15" ) const ( diff --git a/internal/constants/windscribe.go b/internal/constants/windscribe.go new file mode 100644 index 00000000..e6f17f9c --- /dev/null +++ b/internal/constants/windscribe.go @@ -0,0 +1,310 @@ +package constants + +import ( + "github.com/qdm12/private-internet-access-docker/internal/models" +) + +const ( + WindscribeCertificate = "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" + WindscribeOpenvpnStaticKeyV1 = "5801926a57ac2ce27e3dfd1dd6ef82042d82bd4f3f0021296f57734f6f1ea714a6623845541c4b0c3dea0a050fe6746cb66dfab14cda27e5ae09d7c155aa554f399fa4a863f0e8c1af787e5c602a801d3a2ec41e395a978d56729457fe6102d7d9e9119aa83643210b33c678f9d4109e3154ac9c759e490cb309b319cf708cae83ddadc3060a7a26564d1a24411cd552fe6620ea16b755697a4fc5e6e9d0cfc0c5c4a1874685429046a424c026db672e4c2c492898052ba59128d46200b40f880027a8b6610a4d559bdc9346d33a0a6b08e75c7fd43192b162bfd0aef0c716b31584827693f676f9a5047123466f0654eade34972586b31c6ce7e395f4b478cb" +) + +func WindscribeRegionChoices() (choices []string) { + uniqueChoices := map[string]struct{}{} + for _, server := range WindscribeServers() { + uniqueChoices[string(server.Region)] = struct{}{} + } + for choice := range uniqueChoices { + choices = append(choices, choice) + } + return choices +} + +func WindscribeServers() []models.WindscribeServer { + return []models.WindscribeServer{ + { + Region: models.WindscribeRegion("albania"), + Subdomain: "al", + }, + { + Region: models.WindscribeRegion("argentina"), + Subdomain: "ar", + }, + { + Region: models.WindscribeRegion("argentina"), + Subdomain: "ar", + }, + { + Region: models.WindscribeRegion("australia"), + Subdomain: "au", + }, + { + Region: models.WindscribeRegion("austria"), + Subdomain: "at", + }, + { + Region: models.WindscribeRegion("azerbaijan"), + Subdomain: "az", + }, + { + Region: models.WindscribeRegion("belgium"), + Subdomain: "be", + }, + { + Region: models.WindscribeRegion("bosnia"), + Subdomain: "ba", + }, + { + Region: models.WindscribeRegion("brazil"), + Subdomain: "br", + }, + { + Region: models.WindscribeRegion("bulgaria"), + Subdomain: "bg", + }, + { + Region: models.WindscribeRegion("canada east"), + Subdomain: "ca", + }, + { + Region: models.WindscribeRegion("canada west"), + Subdomain: "ca-west", + }, + { + Region: models.WindscribeRegion("colombia"), + Subdomain: "co", + }, + { + Region: models.WindscribeRegion("croatia"), + Subdomain: "hr", + }, + { + Region: models.WindscribeRegion("cyprus"), + Subdomain: "cy", + }, + { + Region: models.WindscribeRegion("czech republic"), + Subdomain: "cz", + }, + { + Region: models.WindscribeRegion("denmark"), + Subdomain: "dk", + }, + { + Region: models.WindscribeRegion("estonia"), + Subdomain: "ee", + }, + { + Region: models.WindscribeRegion("egypt"), + Subdomain: "eg", + }, + { + Region: models.WindscribeRegion("fake antarctica"), + Subdomain: "aq", + }, + { + Region: models.WindscribeRegion("finland"), + Subdomain: "fi", + }, + { + Region: models.WindscribeRegion("france"), + Subdomain: "fr", + }, + { + Region: models.WindscribeRegion("georgia"), + Subdomain: "ge", + }, + { + Region: models.WindscribeRegion("germany"), + Subdomain: "de", + }, + { + Region: models.WindscribeRegion("greece"), + Subdomain: "gr", + }, + { + Region: models.WindscribeRegion("hong kong"), + Subdomain: "hk", + }, + { + Region: models.WindscribeRegion("hungary"), + Subdomain: "hu", + }, + { + Region: models.WindscribeRegion("iceland"), + Subdomain: "is", + }, + { + Region: models.WindscribeRegion("india"), + Subdomain: "in", + }, + { + Region: models.WindscribeRegion("indonesia"), + Subdomain: "id", + }, + { + Region: models.WindscribeRegion("ireland"), + Subdomain: "ie", + }, + { + Region: models.WindscribeRegion("israel"), + Subdomain: "il", + }, + { + Region: models.WindscribeRegion("italy"), + Subdomain: "it", + }, + { + Region: models.WindscribeRegion("japan"), + Subdomain: "jp", + }, + { + Region: models.WindscribeRegion("latvia"), + Subdomain: "lv", + }, + { + Region: models.WindscribeRegion("lithuania"), + Subdomain: "lt", + }, + { + Region: models.WindscribeRegion("macedonia"), + Subdomain: "mk", + }, + { + Region: models.WindscribeRegion("malaysia"), + Subdomain: "my", + }, + { + Region: models.WindscribeRegion("mexico"), + Subdomain: "mx", + }, + { + Region: models.WindscribeRegion("moldova"), + Subdomain: "md", + }, + { + Region: models.WindscribeRegion("netherlands"), + Subdomain: "nl", + }, + { + Region: models.WindscribeRegion("new zealand"), + Subdomain: "nz", + }, + { + Region: models.WindscribeRegion("norway"), + Subdomain: "no", + }, + { + Region: models.WindscribeRegion("philippines"), + Subdomain: "ph", + }, + { + Region: models.WindscribeRegion("poland"), + Subdomain: "pl", + }, + { + Region: models.WindscribeRegion("portugal"), + Subdomain: "pt", + }, + { + Region: models.WindscribeRegion("romania"), + Subdomain: "ro", + }, + { + Region: models.WindscribeRegion("russia"), + Subdomain: "ru", + }, + { + Region: models.WindscribeRegion("serbia"), + Subdomain: "rs", + }, + { + Region: models.WindscribeRegion("singapore"), + Subdomain: "sg", + }, + { + Region: models.WindscribeRegion("slovakia"), + Subdomain: "sk", + }, + { + Region: models.WindscribeRegion("slovenia"), + Subdomain: "si", + }, + { + Region: models.WindscribeRegion("south africa"), + Subdomain: "za", + }, + { + Region: models.WindscribeRegion("south korea"), + Subdomain: "kr", + }, + { + Region: models.WindscribeRegion("spain"), + Subdomain: "es", + }, + { + Region: models.WindscribeRegion("sweden"), + Subdomain: "se", + }, + { + Region: models.WindscribeRegion("switzerland"), + Subdomain: "ch", + }, + { + Region: models.WindscribeRegion("thailand"), + Subdomain: "th", + }, + { + Region: models.WindscribeRegion("tunisia"), + Subdomain: "tn", + }, + { + Region: models.WindscribeRegion("turkey"), + Subdomain: "tr", + }, + { + Region: models.WindscribeRegion("ukraine"), + Subdomain: "ua", + }, + { + Region: models.WindscribeRegion("united arab emirates"), + Subdomain: "ae", + }, + { + Region: models.WindscribeRegion("united kingdom"), + Subdomain: "uk", + }, + { + Region: models.WindscribeRegion("us central"), + Subdomain: "us-central", + }, + { + Region: models.WindscribeRegion("us east"), + Subdomain: "us-east", + }, + { + Region: models.WindscribeRegion("us west"), + Subdomain: "us-west", + }, + { + Region: models.WindscribeRegion("vietnam"), + Subdomain: "vn", + }, + { + Region: models.WindscribeRegion("windflix ca"), + Subdomain: "wf-ca", + }, + { + Region: models.WindscribeRegion("windflix jp"), + Subdomain: "wf-jp", + }, + { + Region: models.WindscribeRegion("windflix uk"), + Subdomain: "wf-uk", + }, + { + Region: models.WindscribeRegion("windflix us"), + Subdomain: "wf-us", + }, + } +} diff --git a/internal/models/alias.go b/internal/models/alias.go index 08d87d8d..d7b46e9e 100644 --- a/internal/models/alias.go +++ b/internal/models/alias.go @@ -17,6 +17,8 @@ type ( MullvadCity string // MullvadProvider is used as the Internet service provider for a Mullvad server MullvadProvider string + // WindscribeCity is used as the region for a Windscribe server + WindscribeRegion string // URL is an HTTP(s) URL address URL string // Filepath is a local filesytem file path diff --git a/internal/models/windscribe.go b/internal/models/windscribe.go new file mode 100644 index 00000000..2247ab7d --- /dev/null +++ b/internal/models/windscribe.go @@ -0,0 +1,6 @@ +package models + +type WindscribeServer struct { + Region WindscribeRegion + Subdomain string +} diff --git a/internal/params/params.go b/internal/params/params.go index 350e3859..4250ef11 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -53,6 +53,10 @@ type ParamsReader interface { GetMullvadISP() (country models.MullvadProvider, err error) GetMullvadPort() (port uint16, err error) + // Windscribe getters + GetWindscribeRegion() (country models.WindscribeRegion, err error) + GetWindscribePort(protocol models.NetworkProtocol) (port uint16, err error) + // Shadowsocks getters GetShadowSocks() (activated bool, err error) GetShadowSocksLog() (activated bool, err error) @@ -92,6 +96,6 @@ func NewParamsReader(logger logging.Logger) ParamsReader { // GetVPNSP obtains the VPN service provider to use from the environment variable VPNSP func (p *paramsReader) GetVPNSP() (vpnServiceProvider string, err error) { - s, err := p.envParams.GetValueIfInside("VPNSP", []string{"pia", "mullvad"}) + s, err := p.envParams.GetValueIfInside("VPNSP", []string{"pia", "mullvad", "windscribe"}) return s, err } diff --git a/internal/params/windscribe.go b/internal/params/windscribe.go new file mode 100644 index 00000000..76fa1e41 --- /dev/null +++ b/internal/params/windscribe.go @@ -0,0 +1,45 @@ +package params + +import ( + "fmt" + "strings" + + libparams "github.com/qdm12/golibs/params" + "github.com/qdm12/private-internet-access-docker/internal/constants" + "github.com/qdm12/private-internet-access-docker/internal/models" +) + +// GetWindscribeRegion obtains the region for the Windscribe server from the +// environment variable REGION +func (p *paramsReader) GetWindscribeRegion() (country models.WindscribeRegion, err error) { + choices := append(constants.WindscribeRegionChoices()) + s, err := p.envParams.GetValueIfInside("REGION", choices) + return models.WindscribeRegion(strings.ToLower(s)), err +} + +// GetMullvadPort obtains the port to reach the Mullvad server on from the +// environment variable PORT +func (p *paramsReader) GetWindscribePort(protocol models.NetworkProtocol) (port uint16, err error) { + n, err := p.envParams.GetEnvIntRange("PORT", 0, 65535, libparams.Default("0")) + if err != nil { + return 0, err + } + if n == 0 { + return 0, nil + } + switch protocol { + case constants.TCP: + switch n { + case 21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783: + default: + return 0, fmt.Errorf("port %d is not valid for protocol %s", n, protocol) + } + case constants.UDP: + switch n { + case 53, 80, 123, 443, 1194, 54783: + default: + return 0, fmt.Errorf("port %d is not valid for protocol %s", n, protocol) + } + } + return uint16(n), nil +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 00b4a079..aeca8c38 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -13,6 +13,7 @@ type Settings struct { OpenVPN OpenVPN PIA PIA Mullvad Mullvad + Windscribe Windscribe DNS DNS Firewall Firewall TinyProxy TinyProxy @@ -26,6 +27,8 @@ func (s *Settings) String() string { vpnServiceProvider = s.PIA.String() case "mullvad": vpnServiceProvider = s.Mullvad.String() + case "windscribe": + vpnServiceProvider = s.Windscribe.String() } return strings.Join([]string{ "Settings summary below:", @@ -75,8 +78,23 @@ func GetAllSettings(params params.ParamsReader) (settings Settings, err error) { return settings, fmt.Errorf("auth algorithm %q is not supported by Mullvad (not using auth at all)", settings.OpenVPN.Auth) } settings.Mullvad, err = GetMullvadSettings(params) + case "windscribe": + switch settings.OpenVPN.Cipher { + case "", "aes-256-cbc", "aes-256-gcm": // TODO check inside params getters + default: + return settings, fmt.Errorf("cipher %q is not supported by Windscribe", settings.OpenVPN.Cipher) + } + switch settings.OpenVPN.Auth { + case "", "sha512": + default: + return settings, fmt.Errorf("auth algorithm %q is not supported by Windscribe", settings.OpenVPN.Auth) + } + settings.Windscribe, err = GetWindscribeSettings(params, settings.OpenVPN.NetworkProtocol) default: - return settings, fmt.Errorf("VPN service provider %q is not valid", settings.VPNSP) + err = fmt.Errorf("VPN service provider %q is not valid", settings.VPNSP) + } + if err != nil { + return settings, err } if err != nil { return settings, err diff --git a/internal/settings/windscribe.go b/internal/settings/windscribe.go new file mode 100644 index 00000000..a5c58c2b --- /dev/null +++ b/internal/settings/windscribe.go @@ -0,0 +1,49 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/qdm12/private-internet-access-docker/internal/models" + "github.com/qdm12/private-internet-access-docker/internal/params" +) + +// Windscribe contains the settings to connect to a Windscribe server +type Windscribe struct { + User string + Password string + Region models.WindscribeRegion + Port uint16 +} + +func (w *Windscribe) String() string { + settingsList := []string{ + "Windscribe settings:", + "User: [redacted]", + "Password: [redacted]", + "Region: " + string(w.Region), + "Custom port: " + fmt.Sprintf("%d", w.Port), + } + return strings.Join(settingsList, "\n |--") +} + +// GetWindscribeSettings obtains Windscribe settings from environment variables using the params package. +func GetWindscribeSettings(params params.ParamsReader, protocol models.NetworkProtocol) (settings Windscribe, err error) { + settings.User, err = params.GetUser() + if err != nil { + return settings, err + } + settings.Password, err = params.GetPassword() + if err != nil { + return settings, err + } + settings.Region, err = params.GetWindscribeRegion() + if err != nil { + return settings, err + } + settings.Port, err = params.GetWindscribePort(protocol) + if err != nil { + return settings, err + } + return settings, nil +} diff --git a/internal/windscribe/conf.go b/internal/windscribe/conf.go new file mode 100644 index 00000000..306ed93e --- /dev/null +++ b/internal/windscribe/conf.go @@ -0,0 +1,118 @@ +package windscribe + +import ( + "fmt" + "net" + "strings" + + "github.com/qdm12/golibs/files" + "github.com/qdm12/private-internet-access-docker/internal/constants" + "github.com/qdm12/private-internet-access-docker/internal/models" +) + +func (c *configurator) GetOpenVPNConnections(region models.WindscribeRegion, protocol models.NetworkProtocol, customPort uint16, targetIP net.IP) (connections []models.OpenVPNConnection, err error) { + var subdomain string + for _, server := range constants.WindscribeServers() { + if server.Region == region { + subdomain = server.Subdomain + break + } + } + if len(subdomain) == 0 { + return nil, fmt.Errorf("no server found for region %q", region) + } + hostname := subdomain + ".windscribe.com" + IPs, err := c.lookupIP(hostname) + if err != nil { + return nil, err + } + if targetIP != nil { + found := false + for i := range IPs { + if IPs[i].Equal(targetIP) { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("target IP address %q not found from IP addresses resolved from %s", targetIP, hostname) + } + IPs = []net.IP{targetIP} + } + var port uint16 + switch { + case customPort > 0: + port = customPort + case protocol == constants.TCP: + port = 1194 + case protocol == constants.UDP: + port = 443 + default: + return nil, fmt.Errorf("protocol %q is unknown", protocol) + } + for _, IP := range IPs { + connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol}) + } + return connections, nil +} + +func (c *configurator) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string) (err error) { + if len(cipher) == 0 { + cipher = "AES-256-CBC" + } + if len(auth) == 0 { + auth = "sha512" + } + lines := []string{ + "client", + "dev tun", + "nobind", + "persist-key", + "persist-tun", + + // Windscribe specific + "resolv-retry infinite", + "comp-lzo", + "remote-cert-tls server", + "key-direction 1", + + // Added constant values + "auth-nocache", + "mute-replay-warnings", + "pull-filter ignore \"auth-token\"", // prevent auth failed loops + "auth-retry nointeract", + "remote-random", + + // Modified variables + fmt.Sprintf("verb %d", verbosity), + fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf), + fmt.Sprintf("proto %s", string(connections[0].Protocol)), + fmt.Sprintf("cipher %s", cipher), + fmt.Sprintf("auth %s", auth), + } + if strings.HasSuffix(cipher, "-gcm") { + lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM") + } + if !root { + lines = append(lines, "user nonrootuser") + } + for _, connection := range connections { + lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port)) + } + lines = append(lines, []string{ + "", + "-----BEGIN CERTIFICATE-----", + constants.WindscribeCertificate, + "-----END CERTIFICATE-----", + "", + }...) + lines = append(lines, []string{ + "", + "-----BEGIN OpenVPN Static key V1-----", + constants.WindscribeOpenvpnStaticKeyV1, + "-----END OpenVPN Static key V1-----", + "", + "", + }...) + return c.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400)) +} diff --git a/internal/windscribe/windscribe.go b/internal/windscribe/windscribe.go new file mode 100644 index 00000000..2f515ce6 --- /dev/null +++ b/internal/windscribe/windscribe.go @@ -0,0 +1,24 @@ +package windscribe + +import ( + "net" + + "github.com/qdm12/golibs/files" + "github.com/qdm12/private-internet-access-docker/internal/models" +) + +// Configurator contains methods to download, read and modify the openvpn configuration to connect as a client +type Configurator interface { + GetOpenVPNConnections(region models.WindscribeRegion, protocol models.NetworkProtocol, customPort uint16, targetIP net.IP) (connections []models.OpenVPNConnection, err error) + BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string) (err error) +} + +type configurator struct { + fileManager files.FileManager + lookupIP func(host string) ([]net.IP, error) +} + +// NewConfigurator returns a new Configurator object +func NewConfigurator(fileManager files.FileManager) Configurator { + return &configurator{fileManager, net.LookupIP} +}