diff --git a/.github/labels.yml b/.github/labels.yml index 95568036..7f677f70 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -36,6 +36,9 @@ - name: ":cloud: Surfshark" color: "cfe8d4" description: "" +- name: ":cloud: Torguard" + color: "cfe8d4" + description: "" - name: ":cloud: Vyprvpn" color: "cfe8d4" description: "" diff --git a/.golangci.yml b/.golangci.yml index 19e55428..a6409d35 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,6 +13,9 @@ issues: - path: internal/server/ linters: - dupl + - path: internal/configuration/ + linters: + - dupl linters: disable-all: true enable: diff --git a/README.md b/README.md index ca87e8c0..c935faca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Gluetun VPN client *Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access, -Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* +Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN, Privado and TorGuard VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* **ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`* @@ -37,7 +37,7 @@ Mullvad, Windscribe, Surfshark Cyberghost, VyprVPN, NordVPN, PureVPN and Privado ## Features - Based on Alpine 3.12 for a small Docker image of 52MB -- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn**, **NordVPN**, **PureVPN** and **Privado** servers +- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark**, **Cyberghost**, **Vyprvpn**, **NordVPN**, **PureVPN**, **Privado** and **TorGuard** servers - Supports Openvpn only for now - DNS over TLS baked in with service provider(s) of your choice - DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours diff --git a/internal/cli/update.go b/internal/cli/update.go index d9c49ea9..7ba42f99 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -29,6 +29,7 @@ func (c *cli) Update(ctx context.Context, args []string, os os.OS) error { flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado 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.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers") flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers") if err := flagSet.Parse(args); err != nil { diff --git a/internal/configuration/openvpn.go b/internal/configuration/openvpn.go index a76ffeaf..e362982e 100644 --- a/internal/configuration/openvpn.go +++ b/internal/configuration/openvpn.go @@ -56,7 +56,7 @@ var ( func (settings *OpenVPN) read(r reader) (err error) { vpnsp, err := r.env.Inside("VPNSP", []string{ - "pia", "private internet access", "mullvad", "windscribe", "surfshark", + "pia", "private internet access", "mullvad", "windscribe", "surfshark", "torguard", "cyberghost", "vyprvpn", "nordvpn", "purevpn", "privado"}, params.Default("private internet access")) if err != nil { @@ -130,6 +130,8 @@ func (settings *OpenVPN) read(r reader) (err error) { readProvider = settings.Provider.readPurevpn case constants.Privado: readProvider = settings.Provider.readPrivado + case constants.Torguard: + readProvider = settings.Provider.readTorguard default: return fmt.Errorf("%w: %s", ErrInvalidVPNProvider, settings.Provider.Name) } diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index 20595941..582d5c6d 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -43,6 +43,8 @@ func (settings *Provider) lines() (lines []string) { providerLines = settings.purevpnLines() case "surfshark": providerLines = settings.surfsharkLines() + case "torguard": + providerLines = settings.torguardLines() case "vyprvpn": providerLines = settings.vyprvpnLines() case "windscribe": diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 35e0928c..8622e989 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -152,6 +152,24 @@ func Test_Provider_lines(t *testing.T) { " |--Regions: a, b", }, }, + "torguard": { + settings: Provider{ + Name: constants.Torguard, + ServerSelection: ServerSelection{ + Protocol: constants.UDP, + Countries: []string{"a", "b"}, + Cities: []string{"c", "d"}, + Hostnames: []string{"e"}, + }, + }, + lines: []string{ + "|--Torguard settings:", + " |--Network protocol: udp", + " |--Countries: a, b", + " |--Cities: c, d", + " |--Hostnames: e", + }, + }, "vyprvpn": { settings: Provider{ Name: constants.Vyprvpn, diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go index 83a87717..92e15976 100644 --- a/internal/configuration/selection.go +++ b/internal/configuration/selection.go @@ -8,7 +8,7 @@ type ServerSelection struct { // Common Protocol string `json:"network_protocol"` TargetIP net.IP `json:"target_ip,omitempty"` - + // TODO comments // Cyberghost, PIA, Surfshark, Windscribe, Vyprvpn, NordVPN Regions []string `json:"regions"` diff --git a/internal/configuration/torguard.go b/internal/configuration/torguard.go new file mode 100644 index 00000000..f34a6648 --- /dev/null +++ b/internal/configuration/torguard.go @@ -0,0 +1,52 @@ +package configuration + +import ( + "github.com/qdm12/gluetun/internal/constants" +) + +func (settings *Provider) torguardLines() (lines []string) { + if len(settings.ServerSelection.Countries) > 0 { + lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries)) + } + + if len(settings.ServerSelection.Cities) > 0 { + lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities)) + } + + if len(settings.ServerSelection.Hostnames) > 0 { + lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames)) + } + + return lines +} + +func (settings *Provider) readTorguard(r reader) (err error) { + settings.Name = constants.Torguard + + settings.ServerSelection.Protocol, err = readProtocol(r.env) + if err != nil { + return err + } + + settings.ServerSelection.TargetIP, err = readTargetIP(r.env) + if err != nil { + return err + } + + settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.TorguardCountryChoices()) + if err != nil { + return err + } + + settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.TorguardCityChoices()) + if err != nil { + return err + } + + settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.TorguardHostnamesChoices()) + if err != nil { + return err + } + + return nil +} diff --git a/internal/configuration/updater.go b/internal/configuration/updater.go index 1f554859..f10ef653 100644 --- a/internal/configuration/updater.go +++ b/internal/configuration/updater.go @@ -17,6 +17,7 @@ type Updater struct { Privado bool `json:"privado"` Purevpn bool `json:"purevpn"` Surfshark bool `json:"surfshark"` + Torguard bool `json:"torguard"` Vyprvpn bool `json:"vyprvpn"` Windscribe bool `json:"windscribe"` // The two below should be used in CLI mode only @@ -47,6 +48,7 @@ func (settings *Updater) read(r reader) (err error) { settings.PIA = true settings.Purevpn = true settings.Surfshark = true + settings.Torguard = true settings.Vyprvpn = true settings.Windscribe = true settings.Stdout = false diff --git a/internal/constants/servers.go b/internal/constants/servers.go index 216e93b4..0a56e125 100644 --- a/internal/constants/servers.go +++ b/internal/constants/servers.go @@ -41,6 +41,11 @@ func GetAllServers() (allServers models.AllServers) { Timestamp: 1612031135, Servers: SurfsharkServers(), }, + Torguard: models.TorguardServers{ + Version: 1, + Timestamp: 1613357861, + Servers: TorguardServers(), + }, Vyprvpn: models.VyprvpnServers{ Version: 1, Timestamp: 1612031135, diff --git a/internal/constants/servers_test.go b/internal/constants/servers_test.go index c32454f3..8de9cb9d 100644 --- a/internal/constants/servers_test.go +++ b/internal/constants/servers_test.go @@ -69,6 +69,11 @@ func Test_versions(t *testing.T) { version: allServers.Surfshark.Version, digest: "042bef64", }, + "Torguard": { + model: models.TorguardServer{}, + version: allServers.Torguard.Version, + digest: "752702f3", + }, "Vyprvpn": { model: models.VyprvpnServer{}, version: allServers.Vyprvpn.Version, @@ -150,6 +155,11 @@ func Test_timestamps(t *testing.T) { timestamp: allServers.Surfshark.Timestamp, digest: "1a7f38bb", }, + "Torguard": { + servers: allServers.Torguard.Servers, + timestamp: allServers.Torguard.Timestamp, + digest: "dffab93e", + }, "Vyprvpn": { servers: allServers.Vyprvpn.Servers, timestamp: allServers.Vyprvpn.Timestamp, diff --git a/internal/constants/torguard.go b/internal/constants/torguard.go new file mode 100644 index 00000000..698aebfe --- /dev/null +++ b/internal/constants/torguard.go @@ -0,0 +1,101 @@ +package constants + +import ( + "net" + + "github.com/qdm12/gluetun/internal/models" +) + +//nolint:lll +const ( + TorguardCertificate = "MIIDMTCCAhmgAwIBAgIJAKnGGJK6qLqSMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCVRHLVZQTi1DQTAgFw0xOTA1MjExNDIzMTFaGA8yMDU5MDUxMTE0MjMxMVowFDESMBAGA1UEAwwJVEctVlBOLUNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlv0UgPD3xVAvhhP6q1HCmeAWbH+9HPkyQ2P6qM5oHY5dntjmq8YT48FZGHWv7+s9O47v6Bv7rEc4UwQx15cc2LByivX2JwmE8JACvNfwEnZXYAPq9WU3ZgRrAGvA09ItuLqK2fQ4A7h8bFhmyxCbSzP1sSIT/zJY6ebuh5rDQSMJRMaoI0t1zorEZ7PlEmh+o0w5GPs0D0vY50UcnEzB4GOdWC9pJREwEqppWYLN7RRdG8JyIqmA59mhARCnQFUo38HWic4trxFe71jtD7YInNV7ShQtg0S0sXo36Rqfz72Jo08qqI70dNs5DN1aGNkQ/tRK9DhL5DLmTkaCw7mEFQIDAQABo4GDMIGAMB0GA1UdDgQWBBR7DcymXBp6u/jAaZOPUjUhEyhXfjBEBgNVHSMEPTA7gBR7DcymXBp6u/jAaZOPUjUhEyhXfqEYpBYwFDESMBAGA1UEAwwJVEctVlBOLUNBggkAqcYYkrqoupIwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAE79ngbdSlP7IBbfnJ+2Ju7vqt9/GyhcsYtjibp6gsMUxKlD8HuvlSGj5kNO5wiwN7XXqsjYtJfdhmzzVbXksi8Fnbnfa8GhFl4IAjLJ5cxaWOxjr6wx2AhIs+BVVARjaU7iTK91RXJnl6u7UDHTkQylBTl7wgpMeG6GjhaHfcOL1t7D2w8x23cTO+p+n53P3cBq+9TiAUORdzXJvbCxlPMDSDArsgBjC57W7dtdnZo7gTfQG77JTDFBeSwPwLF7PjBB4S6rzU/4fcYwy83XKP6zDn9tgUJDnpFb/7jJ/PbNkK4BWYJp3XytOtt66v9SEKw+v/fJ+VkjU16vE/9Q3h4=" + TorguardOpenvpnStaticKeyV1 = "770e8de5fc56e0248cc7b5aab56be80d0e19cbf003c1b3ed68efbaf08613c3a1a019dac6a4b84f13a6198f73229ffc21fa512394e288f82aa2cf0180f01fb3eb1a71e00a077a20f6d7a83633f5b4f47f27e30617eaf8485dd8c722a8606d56b3c183f65da5d3c9001a8cbdb96c793d936251098b24fe52a6dd2472e98cfccbc466e63520d63ade7a0eacc36208c3142a1068236a52142fbb7b3ed83d785e12a28261bccfb3bcb62a8d2f6d18f5df5f3652e59c5627d8d9c8f7877c4d7b08e19a5c363556ba68d392be78b75152dd55ba0f74d45089e84f77f4492d886524ea6c82b9f4dd83d46528d4f5c3b51cfeaf2838d938bd0597c426b0e440434f2c451f" +) + +func TorguardCountryChoices() (choices []string) { + servers := TorguardServers() + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].Country + } + return choices +} + +func TorguardCityChoices() (choices []string) { + servers := TorguardServers() + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].City + } + return choices +} + +func TorguardHostnamesChoices() (choices []string) { + servers := TorguardServers() + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].Hostname + } + return choices +} + +//nolint:lll +// TorguardServers returns a slice of all the server information for Torguard. +func TorguardServers() []models.TorguardServer { + return []models.TorguardServer{ + {Country: "Australia", City: "Sydney", Hostname: "au.torguardvpnaccess.com", IP: net.IP{168, 1, 210, 188}}, + {Country: "Austria", City: "", Hostname: "aus.torguardvpnaccess.com", IP: net.IP{37, 120, 155, 18}}, + {Country: "Belarus", City: "", Hostname: "bl.torguardvpnaccess.com", IP: net.IP{95, 47, 99, 12}}, + {Country: "Belgium", City: "", Hostname: "bg.torguardvpnaccess.com", IP: net.IP{89, 249, 73, 250}}, + {Country: "Brazil", City: "", Hostname: "br.torguardvpnaccess.com", IP: net.IP{169, 57, 191, 3}}, + {Country: "Bulgaria", City: "", Hostname: "bul.torguardvpnaccess.com", IP: net.IP{82, 102, 23, 186}}, + {Country: "Canada", City: "Toronto", Hostname: "ca.torguardvpnaccess.com", IP: net.IP{184, 75, 209, 250}}, + {Country: "Canada", City: "Vancouver", Hostname: "vanc.ca.west.torguardvpnaccess.com", IP: net.IP{107, 181, 189, 48}}, + {Country: "Chile", City: "", Hostname: "chil.torguardvpnaccess.com", IP: net.IP{37, 235, 52, 19}}, + {Country: "Cyprus", City: "", Hostname: "cp.torguardvpnaccess.com", IP: net.IP{185, 173, 226, 49}}, + {Country: "Czech", City: "", Hostname: "czech.torguardvpnaccess.com", IP: net.IP{185, 189, 115, 103}}, + {Country: "Denmark", City: "", Hostname: "den.torguardvpnaccess.com", IP: net.IP{37, 120, 131, 29}}, + {Country: "Finland", City: "", Hostname: "fin.torguardvpnaccess.com", IP: net.IP{91, 233, 116, 228}}, + {Country: "France", City: "", Hostname: "fr.torguardvpnaccess.com", IP: net.IP{93, 177, 75, 90}}, + {Country: "Germany", City: "", Hostname: "gr.torguardvpnaccess.com", IP: net.IP{93, 177, 73, 90}}, + {Country: "Greece", City: "", Hostname: "gre.torguardvpnaccess.com", IP: net.IP{45, 92, 33, 10}}, + {Country: "Hong", City: "Kong", Hostname: "hk.torguardvpnaccess.com", IP: net.IP{45, 133, 181, 158}}, + {Country: "Hungary", City: "", Hostname: "hg.torguardvpnaccess.com", IP: net.IP{37, 120, 144, 106}}, + {Country: "Iceland", City: "", Hostname: "ice.torguardvpnaccess.com", IP: net.IP{82, 221, 111, 11}}, + {Country: "India", City: "Bangalore", Hostname: "in.torguardvpnaccess.com", IP: net.IP{139, 59, 62, 109}}, + {Country: "Ireland", City: "", Hostname: "ire.torguardvpnaccess.com", IP: net.IP{81, 17, 246, 108}}, + {Country: "Israel", City: "", Hostname: "isr.torguardvpnaccess.com", IP: net.IP{91, 223, 106, 201}}, + {Country: "Italy", City: "", Hostname: "it.torguardvpnaccess.com", IP: net.IP{192, 145, 127, 190}}, + {Country: "Japan", City: "", Hostname: "jp.torguardvpnaccess.com", IP: net.IP{91, 207, 174, 50}}, + {Country: "Latvia", City: "", Hostname: "lv.torguardvpnaccess.com", IP: net.IP{109, 248, 149, 167}}, + {Country: "Luxembourg", City: "", Hostname: "lux.torguardvpnaccess.com", IP: net.IP{94, 242, 238, 66}}, + {Country: "Mexico", City: "", Hostname: "mx.torguardvpnaccess.com", IP: net.IP{45, 133, 180, 34}}, + {Country: "Moldova", City: "", Hostname: "md.torguardvpnaccess.com", IP: net.IP{178, 175, 128, 74}}, + {Country: "Netherlands", City: "", Hostname: "nl.torguardvpnaccess.com", IP: net.IP{88, 202, 177, 181}}, + {Country: "New", City: "Zealand", Hostname: "nz.torguardvpnaccess.com", IP: net.IP{103, 108, 94, 58}}, + {Country: "Norway", City: "", Hostname: "no.torguardvpnaccess.com", IP: net.IP{185, 125, 169, 31}}, + {Country: "Poland", City: "", Hostname: "pl.torguardvpnaccess.com", IP: net.IP{37, 120, 156, 194}}, + {Country: "Portugal", City: "", Hostname: "por.torguardvpnaccess.com", IP: net.IP{94, 46, 179, 75}}, + {Country: "Romania", City: "", Hostname: "ro.torguardvpnaccess.com", IP: net.IP{93, 120, 27, 162}}, + {Country: "Singapore", City: "", Hostname: "singp.torguardvpnaccess.com", IP: net.IP{206, 189, 43, 152}}, + {Country: "Slovakia", City: "", Hostname: "slk.torguardvpnaccess.com", IP: net.IP{46, 29, 2, 113}}, + {Country: "South", City: "Korea", Hostname: "sk.torguardvpnaccess.com", IP: net.IP{169, 56, 83, 216}}, + {Country: "Spain", City: "", Hostname: "sp.torguardvpnaccess.com", IP: net.IP{192, 145, 124, 242}}, + {Country: "Sweden", City: "", Hostname: "swe.torguardvpnaccess.com", IP: net.IP{37, 120, 153, 72}}, + {Country: "Switzerland", City: "", Hostname: "swiss.torguardvpnaccess.com", IP: net.IP{195, 206, 105, 37}}, + {Country: "Taiwan", City: "", Hostname: "tw.torguardvpnaccess.com", IP: net.IP{61, 216, 159, 176}}, + {Country: "Thailand", City: "", Hostname: "thai.torguardvpnaccess.com", IP: net.IP{202, 129, 16, 104}}, + {Country: "UAE", City: "", Hostname: "uae.secureconnect.me", IP: net.IP{45, 9, 250, 10}}, + {Country: "UK", City: "London", Hostname: "uk.torguardvpnaccess.com", IP: net.IP{109, 123, 118, 13}}, + {Country: "USA", City: "Atlanta", Hostname: "atl.east.usa.torguardvpnaccess.com", IP: net.IP{104, 223, 95, 50}}, + {Country: "USA", City: "Chicago", Hostname: "chi.central.usa.torguardvpnaccess.com", IP: net.IP{167, 160, 172, 106}}, + {Country: "USA", City: "Dallas", Hostname: "dal.central.usa.torguardvpnaccess.com", IP: net.IP{96, 44, 145, 26}}, + {Country: "USA", City: "Las Vegas", Hostname: "lv.west.usa.torguardvpnaccess.com", IP: net.IP{76, 164, 203, 130}}, + {Country: "USA", City: "Los Angeles", Hostname: "la.west.usa.torguardvpnaccess.com", IP: net.IP{67, 215, 236, 58}}, + {Country: "USA", City: "Miami", Hostname: "fl.east.usa.torguardvpnaccess.com", IP: net.IP{96, 47, 226, 42}}, + {Country: "USA", City: "New Jersey", Hostname: "nj.east.usa.torguardvpnaccess.com", IP: net.IP{23, 226, 128, 146}}, + {Country: "USA", City: "New York", Hostname: "ny.east.usa.torguardvpnaccess.com", IP: net.IP{209, 95, 50, 116}}, + {Country: "USA", City: "San Francisco", Hostname: "sf.west.usa.torguardvpnaccess.com", IP: net.IP{206, 189, 218, 238}}, + {Country: "USA", City: "Seattle", Hostname: "sa.west.usa.torguardvpnaccess.com", IP: net.IP{199, 229, 250, 38}}, + } +} diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go index 5bf937d2..916321f1 100644 --- a/internal/constants/vpn.go +++ b/internal/constants/vpn.go @@ -19,6 +19,8 @@ const ( Purevpn = "purevpn" // Privado is a VPN provider. Privado = "privado" + // Torguard is a VPN provider. + Torguard = "torguard" ) const ( diff --git a/internal/models/server.go b/internal/models/server.go index bdc3d765..21cafd78 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -56,6 +56,18 @@ func (s *SurfsharkServer) String() string { return fmt.Sprintf("{Region: %q, IPs: %s}", s.Region, goStringifyIPs(s.IPs)) } +type TorguardServer struct { + Country string `json:"country"` + City string `json:"city"` + Hostname string `json:"hostname"` + IP net.IP `json:"ip"` +} + +func (s *TorguardServer) String() string { + return fmt.Sprintf("{Country: %q, City: %q, Hostname: %q, IP: %s}", + s.Country, s.City, s.Hostname, goStringifyIP(s.IP)) +} + type CyberghostServer struct { Region string `json:"region"` Group string `json:"group"` diff --git a/internal/models/servers.go b/internal/models/servers.go index d72f41cf..76b18dbd 100644 --- a/internal/models/servers.go +++ b/internal/models/servers.go @@ -9,6 +9,7 @@ type AllServers struct { Privado PrivadoServers `json:"privado"` Purevpn PurevpnServers `json:"purevpn"` Surfshark SurfsharkServers `json:"surfshark"` + Torguard TorguardServers `json:"torguard"` Vyprvpn VyprvpnServers `json:"vyprvpn"` Windscribe WindscribeServers `json:"windscribe"` } @@ -21,6 +22,7 @@ func (a *AllServers) Count() int { len(a.Privado.Servers) + len(a.Purevpn.Servers) + len(a.Surfshark.Servers) + + len(a.Torguard.Servers) + len(a.Vyprvpn.Servers) + len(a.Windscribe.Servers) } @@ -60,6 +62,11 @@ type SurfsharkServers struct { Timestamp int64 `json:"timestamp"` Servers []SurfsharkServer `json:"servers"` } +type TorguardServers struct { + Version uint16 `json:"version"` + Timestamp int64 `json:"timestamp"` + Servers []TorguardServer `json:"servers"` +} type VyprvpnServers struct { Version uint16 `json:"version"` Timestamp int64 `json:"timestamp"` diff --git a/internal/provider/constants.go b/internal/provider/constants.go index a4a683a2..29d350d0 100644 --- a/internal/provider/constants.go +++ b/internal/provider/constants.go @@ -2,5 +2,6 @@ package provider const ( aes256cbc = "aes-256-cbc" + aes256gcm = "aes-256-gcm" sha256 = "sha256" ) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 655b6d27..98505c18 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -43,6 +43,8 @@ func New(provider string, allServers models.AllServers, timeNow timeNowFunc) Pro return newPurevpn(allServers.Purevpn.Servers, timeNow) case constants.Privado: return newPrivado(allServers.Privado.Servers, timeNow) + case constants.Torguard: + return newTorguard(allServers.Torguard.Servers, timeNow) default: return nil // should never occur } diff --git a/internal/provider/torguard.go b/internal/provider/torguard.go new file mode 100644 index 00000000..1a36dbd1 --- /dev/null +++ b/internal/provider/torguard.go @@ -0,0 +1,173 @@ +package provider + +import ( + "context" + "fmt" + "math/rand" + "net" + "net/http" + "strconv" + + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/firewall" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/golibs/logging" + "github.com/qdm12/golibs/os" +) + +type torguard struct { + servers []models.TorguardServer + randSource rand.Source +} + +func newTorguard(servers []models.TorguardServer, timeNow timeNowFunc) *torguard { + return &torguard{ + servers: servers, + randSource: rand.NewSource(timeNow().UnixNano()), + } +} + +func (t *torguard) filterServers(countries, cities, hostnames []string) (servers []models.TorguardServer) { + for _, server := range t.servers { + switch { + case filterByPossibilities(server.Country, countries): + case filterByPossibilities(server.City, cities): + case filterByPossibilities(server.Hostname, hostnames): + default: + servers = append(servers, server) + } + } + return servers +} + +func (t *torguard) notFoundErr(selection configuration.ServerSelection) error { + message := "no server found for protocol " + selection.Protocol + + if len(selection.Countries) > 0 { + message += " + countries " + commaJoin(selection.Countries) + } + + if len(selection.Cities) > 0 { + message += " + cities " + commaJoin(selection.Cities) + } + + if len(selection.Hostnames) > 0 { + message += " + hostnames " + commaJoin(selection.Hostnames) + } + + return fmt.Errorf(message) +} + +func (t *torguard) GetOpenVPNConnection(selection configuration.ServerSelection) ( + connection models.OpenVPNConnection, err error) { + var port uint16 = 1912 + if selection.CustomPort > 0 { + port = selection.CustomPort + } + + if selection.TargetIP != nil { + return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: selection.Protocol}, nil + } + + servers := t.filterServers(selection.Countries, selection.Cities, selection.Hostnames) + if len(servers) == 0 { + return connection, t.notFoundErr(selection) + } + + connections := make([]models.OpenVPNConnection, len(servers)) + for i := range servers { + connections[i] = models.OpenVPNConnection{ + IP: servers[i].IP, + Port: port, + Protocol: selection.Protocol, + } + } + + return pickRandomConnection(connections, t.randSource), nil +} + +func (t *torguard) BuildConf(connection models.OpenVPNConnection, + username string, settings configuration.OpenVPN) (lines []string) { + if len(settings.Cipher) == 0 { + settings.Cipher = aes256gcm + } + if len(settings.Auth) == 0 { + settings.Auth = sha256 + } + + const defaultMSSFix = 1450 + if settings.MSSFix == 0 { + settings.MSSFix = defaultMSSFix + } + + lines = []string{ + "client", + "dev tun", + "nobind", + "persist-key", + "remote-cert-tls server", + "tls-exit", + + // Torguard specific + "tun-mtu 1500", + "tun-mtu-extra 32", + "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "reneg-sec 0", + "fast-io", + "key-direction 1", + "script-security 2", + "ncp-disable", + "compress", + "keepalive 5 30", + "sndbuf 393216", + "rcvbuf 393216", + // "up /etc/openvpn/update-resolv-conf", + // "down /etc/openvpn/update-resolv-conf", + + // Added constant values + "auth-nocache", + "mute-replay-warnings", + "pull-filter ignore \"auth-token\"", // prevent auth failed loops + "pull-filter ignore \"block-outside-dns\"", + "auth-retry nointeract", + "suppress-timestamps", + + // Modified variables + "verb " + strconv.Itoa(settings.Verbosity), + "auth-user-pass " + constants.OpenVPNAuthConf, + "proto " + connection.Protocol, + "remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port)), + "cipher " + settings.Cipher, + "auth " + settings.Auth, + } + + if !settings.Root { + lines = append(lines, "user "+username) + } + + lines = append(lines, []string{ + "", + "-----BEGIN CERTIFICATE-----", + constants.TorguardCertificate, + "-----END CERTIFICATE-----", + "", + }...) + + lines = append(lines, []string{ + "", + "-----BEGIN OpenVPN Static key V1-----", + constants.TorguardOpenvpnStaticKeyV1, + "-----END OpenVPN Static key V1-----", + "", + "", + }...) + + return lines +} + +func (t *torguard) PortForward(ctx context.Context, client *http.Client, + openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator, + syncState func(port uint16) (pfFilepath string)) { + panic("port forwarding is not supported for torguard") +} diff --git a/internal/storage/merge.go b/internal/storage/merge.go index 8d84d2fc..39089b96 100644 --- a/internal/storage/merge.go +++ b/internal/storage/merge.go @@ -24,6 +24,7 @@ func (s *storage) mergeServers(hardcoded, persisted models.AllServers) models.Al Privado: s.mergePrivado(hardcoded.Privado, persisted.Privado), Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn), Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark), + Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard), Vyprvpn: s.mergeVyprvpn(hardcoded.Vyprvpn, persisted.Vyprvpn), Windscribe: s.mergeWindscribe(hardcoded.Windscribe, persisted.Windscribe), } @@ -106,6 +107,22 @@ func (s *storage) mergeSurfshark(hardcoded, persisted models.SurfsharkServers) m return persisted } +func (s *storage) mergeTorguard(hardcoded, persisted models.TorguardServers) models.TorguardServers { + if persisted.Timestamp <= hardcoded.Timestamp { + return hardcoded + } + versionDiff := hardcoded.Version - persisted.Version + if versionDiff > 0 { + s.logger.Info( + "Torguard servers from file discarded because they are %d versions behind", + versionDiff) + return hardcoded + } + s.logger.Info("Using Torguard servers from file (%s more recent)", + getUnixTimeDifference(persisted.Timestamp, hardcoded.Timestamp)) + return persisted +} + func (s *storage) mergeVyprvpn(hardcoded, persisted models.VyprvpnServers) models.VyprvpnServers { if persisted.Timestamp <= hardcoded.Timestamp { return hardcoded diff --git a/internal/storage/sync.go b/internal/storage/sync.go index 8da97fe6..d2a40448 100644 --- a/internal/storage/sync.go +++ b/internal/storage/sync.go @@ -24,6 +24,7 @@ func countServers(allServers models.AllServers) int { len(allServers.Privado.Servers) + len(allServers.Purevpn.Servers) + len(allServers.Surfshark.Servers) + + len(allServers.Torguard.Servers) + len(allServers.Vyprvpn.Servers) + len(allServers.Windscribe.Servers) } diff --git a/internal/updater/torguard.go b/internal/updater/torguard.go new file mode 100644 index 00000000..dac8b023 --- /dev/null +++ b/internal/updater/torguard.go @@ -0,0 +1,114 @@ +package updater + +import ( + "context" + "fmt" + "net" + "sort" + "strconv" + "strings" + + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/golibs/network" +) + +func (u *updater) updateTorguard(ctx context.Context) (err error) { + servers, warnings, err := findTorguardServersFromZip(ctx, u.client) + if u.options.CLI { + for _, warning := range warnings { + u.logger.Warn("Torguard: %s", warning) + } + } + if err != nil { + return fmt.Errorf("cannot update Torguard servers: %w", err) + } + if u.options.Stdout { + u.println(stringifyTorguardServers(servers)) + } + u.servers.Torguard.Timestamp = u.timeNow().Unix() + u.servers.Torguard.Servers = servers + return nil +} + +func findTorguardServersFromZip(ctx context.Context, client network.Client) ( + servers []models.TorguardServer, warnings []string, err error) { + // Note: all servers do both TCP and UDP + const zipURL = "https://torguard.net/downloads/OpenVPN-TCP-Linux.zip" + + contents, err := fetchAndExtractFiles(ctx, client, zipURL) + if err != nil { + return nil, nil, err + } + + for fileName, content := range contents { + var server models.TorguardServer + + const prefix = "TorGuard." + const suffix = ".ovpn" + s := strings.TrimPrefix(fileName, prefix) + s = strings.TrimSuffix(s, suffix) + + switch { + case strings.Count(s, ".") == 1 && !strings.HasPrefix(s, "USA"): + parts := strings.Split(s, ".") + server.Country = parts[0] + server.City = parts[1] + case strings.HasPrefix(s, "USA"): + server.Country = "USA" + s = strings.TrimPrefix(s, "USA-") + s = strings.ReplaceAll(s, "-", " ") + s = strings.ReplaceAll(s, ".", " ") + s = strings.ToLower(s) + s = strings.Title(s) + server.City = s + default: + server.Country = s + } + + hostnames := extractRemoteHostsFromOpenvpn(content, true, false) + if len(hostnames) != 1 { + warning := "found " + strconv.Itoa(len(hostnames)) + + " hostname(s) instead of 1 in " + fileName + warnings = append(warnings, warning) + continue + } + server.Hostname = hostnames[0] + + IPs := extractRemoteHostsFromOpenvpn(content, false, true) + if len(IPs) != 1 { + warning := "found " + strconv.Itoa(len(IPs)) + + " IP(s) instead of 1 in " + fileName + warnings = append(warnings, warning) + continue + } + server.IP = net.ParseIP(IPs[0]) + if server.IP == nil { + warnings = append(warnings, "IP address "+IPs[0]+" is not valid in file "+fileName) + } + + servers = append(servers, server) + } + + sort.Slice(servers, func(i, j int) bool { + if servers[i].Country == servers[j].Country { + if servers[i].City == servers[j].City { + return servers[i].Hostname < servers[j].Hostname + } + return servers[i].City < servers[j].City + } + return servers[i].Country < servers[j].Country + }) + + return servers, warnings, nil +} + +func stringifyTorguardServers(servers []models.TorguardServer) (s string) { + s = "func TorguardServers() []models.TorguardServer {\n" + s += " return []models.TorguardServer{\n" + for _, server := range servers { + s += " " + server.String() + ",\n" + } + s += " }\n" + s += "}" + return s +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 9226af2f..394bc3c2 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -50,8 +50,8 @@ func New(settings configuration.Updater, httpClient *http.Client, } } -// TODO parallelize DNS resolution. -func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServers, err error) { //nolint:gocognit +//nolint:gocognit,gocyclo +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 { @@ -124,6 +124,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe } } + 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) + } + } + if u.options.Vyprvpn { u.logger.Info("updating Vyprvpn servers...") if err := u.updateVyprvpn(ctx); err != nil { diff --git a/internal/updater/vyprvpn.go b/internal/updater/vyprvpn.go index aabd8c10..d72832a8 100644 --- a/internal/updater/vyprvpn.go +++ b/internal/updater/vyprvpn.go @@ -14,7 +14,7 @@ func (u *updater) updateVyprvpn(ctx context.Context) (err error) { servers, warnings, err := findVyprvpnServers(ctx, u.client, u.lookupIP) if u.options.CLI { for _, warning := range warnings { - u.logger.Warn("Privado: %s", warning) + u.logger.Warn("Vyprvpn: %s", warning) } } if err != nil {