diff --git a/internal/constants/providers/providers.go b/internal/constants/providers/providers.go index ca683051..3855fdc2 100644 --- a/internal/constants/providers/providers.go +++ b/internal/constants/providers/providers.go @@ -5,6 +5,7 @@ const ( // VPN configurations. Custom = "custom" Cyberghost = "cyberghost" + Example = "example" Expressvpn = "expressvpn" Fastestvpn = "fastestvpn" HideMyAss = "hidemyass" diff --git a/internal/provider/example/connection.go b/internal/provider/example/connection.go new file mode 100644 index 00000000..a057b2f2 --- /dev/null +++ b/internal/provider/example/connection.go @@ -0,0 +1,16 @@ +package example + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Provider) GetConnection(selection settings.ServerSelection) ( + connection models.Connection, err error) { + // TODO: Set the default ports for each VPN protocol+network protocol + // combination. If one combination is not supported, set it to `0`. + defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:gomnd + return utils.GetConnection(p.Name(), + p.storage, selection, defaults, p.randSource) +} diff --git a/internal/provider/example/openvpnconf.go b/internal/provider/example/openvpnconf.go new file mode 100644 index 00000000..1ed37bf1 --- /dev/null +++ b/internal/provider/example/openvpnconf.go @@ -0,0 +1,26 @@ +package example + +import ( + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/gluetun/internal/constants/openvpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Provider) OpenVPNConfig(connection models.Connection, + settings settings.OpenVPN) (lines []string) { + // TODO: Set the necessary fields in `providerSettings` to + // generate the right OpenVPN configuration file. + //nolint:gomnd + providerSettings := utils.OpenVPNProviderSettings{ + AuthUserPass: true, + Ciphers: []string{ + openvpn.AES256gcm, + }, + Ping: 5, + RemoteCertTLS: true, + CA: "MIIDZzCCAk+gAwIBAgIUVwHEFE6geihigDSNkBppm2Zamx0wDQYJKoZIhvcNAQELBQAwQzELMAkGA1UEBhMCQ0ExDzANBgNVBAgMBlF1ZWJlYzERMA8GA1UEBwwITW9udHJlYWwxEDAOBgNVBAoMB0dsdWV0dW4wHhcNMjIwNzAxMTY1MzE5WhcNMjcwNjMwMTY1MzE5WjBDMQswCQYDVQQGEwJDQTEPMA0GA1UECAwGUXVlYmVjMREwDwYDVQQHDAhNb250cmVhbDEQMA4GA1UECgwHR2x1ZXR1bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALmJRhTUr+87NFkHL2PWjIz7efHqQgrWuDQt8oOBHvl0Hm72N+ckO+5Q0zG4XtqlpBjFjGUSjfNUWSrRztbXlMmzDcjHKjYHUPepJpoF100fK2q3XPiFRl6sEXzYeOdFgpaTdmGHS6DL9aWeCoYA/k6NV8YqHXujr14gOYOAWG6cRimpTJf8DtEDcxtp1w6fOEoN0b5PvV7dcpLiva8LYyZKPvFYBzlc5BZxOLvq6bvhQm54R6zoHFpaKOf7FeqhxI6KOQu4IPwU12YBlOP5CbkMAQ1cWWVQ4pnh0Hwh71Sjm848jS/OcammNzsp4xWaKt/pzcix3fpJt/MDP/9fxA8CAwEAAaNTMFEwHQYDVR0OBBYEFCIQ9l28Yy1/3qJvFarXjhKdG9tVMB8GA1UdIwQYMBaAFCIQ9l28Yy1/3qJvFarXjhKdG9tVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKLPmLTppXYTTOOxHhTMyHI0oTl7ID2PQfJsref+jDshB3hib98BC17b9ESpLnwx7ugg17NRl7RYutxjuVw/CK/gwAnTMg3D3mdAnKkMRr3UxnD89KprLIpf7WQCmyJaxalsD5jjgl3kuGM7jf2FJNxQz5RrXBGlQHa465ouov+Rp5v/K5Umyt6wsCZXEbOF0SdUhEGU3nxVbFsoPimNYSHHwc29USnQmyW1O/drFDoTcOK4GdHFEVkrHQgqwU8ay1fYGYfIUDhsV/5AAWgQC41r9FWr+VQgyJC94qmDg0c46RE123dL/YifVUl2DKuJ0aOY+OkSgwknKZ+FQd+8d6k=", //nolint:lll + TLSAuth: "bc470c93ff9f5602a8abb27dee84a52814d10f20490ad23c47d5d82120c1bf859e93d0696b455d4a1b8d55d40c2685c41ca1d0aef29a3efd27274c4ef09020a3978fe45784b335da6df2d12db97bbb838416515f2a96f04715fd28949c6fe296a925cfada3f8b8928ed7fc963c1563272f5cf46e5e1d9c845d7703ca881497b7e6564a9d1dea9358adffd435295479f47d5298fabf5359613ff5992cb57ff081a04dfb81a26513a6b44a9b5490ad265f8a02384832a59cc3e075ad545461060b7bcab49bac815163cb80983dd51d5b1fd76170ffd904d8291071e96efc3fb777856c717b148d08a510f5687b8a8285dcffe737b98916dd15ef6235dee4266d3b", //nolint:lll + } + return utils.OpenVPNConfig(providerSettings, connection, settings) +} diff --git a/internal/provider/example/provider.go b/internal/provider/example/provider.go new file mode 100644 index 00000000..ea5c2f94 --- /dev/null +++ b/internal/provider/example/provider.go @@ -0,0 +1,35 @@ +package example + +import ( + "math/rand" + "net/http" + + "github.com/qdm12/gluetun/internal/constants/providers" + "github.com/qdm12/gluetun/internal/provider/common" + "github.com/qdm12/gluetun/internal/provider/example/updater" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +type Provider struct { + storage common.Storage + randSource rand.Source + utils.NoPortForwarder + common.Fetcher +} + +// TODO: remove unneeded arguments once the updater is implemented. +func New(storage common.Storage, randSource rand.Source, + updaterWarner common.Warner, client *http.Client, + unzipper common.Unzipper, parallelResolver common.ParallelResolver) *Provider { + return &Provider{ + storage: storage, + randSource: randSource, + NoPortForwarder: utils.NewNoPortForwarding(providers.Example), + Fetcher: updater.New(updaterWarner, unzipper, client, parallelResolver), + } +} + +func (p *Provider) Name() string { + // TODO: update the constant to be the right provider name. + return providers.Example +} diff --git a/internal/provider/example/updater/api.go b/internal/provider/example/updater/api.go new file mode 100644 index 00000000..0f14a309 --- /dev/null +++ b/internal/provider/example/updater/api.go @@ -0,0 +1,61 @@ +package updater + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +var ( + errHTTPStatusCodeNotOK = errors.New("HTTP status code not OK") +) + +type apiData struct { + Servers []apiServer `json:"servers"` +} + +type apiServer struct { + OpenVPNHostname string `json:"openvpn_hostname"` + WireguardHostname string `json:"wireguard_hostname"` + Country string `json:"country"` + Region string `json:"region"` + City string `json:"city"` + WgPubKey string `json:"wg_public_key"` +} + +func fetchAPI(ctx context.Context, client *http.Client) ( + data apiData, err error) { + // TODO: adapt this URL and the structures above to match the real + // API models you have. + const url = "https://example.com/servers" + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return data, err + } + + response, err := client.Do(request) + if err != nil { + return data, err + } + + if response.StatusCode != http.StatusOK { + _ = response.Body.Close() + return data, fmt.Errorf("%w: %d %s", + errHTTPStatusCodeNotOK, response.StatusCode, response.Status) + } + + decoder := json.NewDecoder(response.Body) + if err := decoder.Decode(&data); err != nil { + _ = response.Body.Close() + return data, fmt.Errorf("failed unmarshaling response body: %w", err) + } + + if err := response.Body.Close(); err != nil { + return data, fmt.Errorf("cannot close response body: %w", err) + } + + return data, nil +} diff --git a/internal/provider/example/updater/resolve.go b/internal/provider/example/updater/resolve.go new file mode 100644 index 00000000..4f976861 --- /dev/null +++ b/internal/provider/example/updater/resolve.go @@ -0,0 +1,32 @@ +package updater + +import ( + "time" + + "github.com/qdm12/gluetun/internal/updater/resolver" +) + +// TODO: remove this file if the parallel resolver is not used +// by the updater. +func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) { + // TODO: adapt these constant values below to make the resolution + // as fast and as reliable as possible. + const ( + maxFailRatio = 0.1 + maxDuration = 20 * time.Second + betweenDuration = time.Second + maxNoNew = 2 + maxFails = 2 + ) + return resolver.ParallelSettings{ + Hosts: hosts, + MaxFailRatio: maxFailRatio, + Repeat: resolver.RepeatSettings{ + MaxDuration: maxDuration, + BetweenDuration: betweenDuration, + MaxNoNew: maxNoNew, + MaxFails: maxFails, + SortIPs: true, + }, + } +} diff --git a/internal/provider/example/updater/servers.go b/internal/provider/example/updater/servers.go new file mode 100644 index 00000000..16560edf --- /dev/null +++ b/internal/provider/example/updater/servers.go @@ -0,0 +1,122 @@ +package updater + +import ( + "context" + "fmt" + "sort" + + "github.com/qdm12/gluetun/internal/constants/vpn" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/common" +) + +func (u *Updater) FetchServers(ctx context.Context, minServers int) ( + servers []models.Server, err error) { + // FetchServers obtains information for each VPN server + // for the VPN service provider. + // + // You should aim at obtaining as much information as possible + // for each server, such as their location information. + // Required fields for each server are: + // - the `VPN` protocol string field + // - the `Hostname` string field + // - the `IPs` IP slice field + // - have one network protocol set, either `TCP` or `UDP` + // - If `VPN` is `wireguard`, the `WgPubKey` field to be set + // + // The information obtention can be done in different ways + // or by combining ways, depending on how the provider exposes + // this information. Some common ones are listed below: + // + // - you can use u.client to fetch structured (usually JSON) + // data of the servers from an HTTP API endpoint of the provider. + // Example in: `internal/provider/mullvad/updater` + // - you can use u.unzipper to download, unzip and parse a zip + // file of OpenVPN configuration files. + // Example in: `internal/provider/fastestvpn/updater` + // - you can use u.parallelResolver to resolve all hostnames + // found in parallel to obtain their corresponding IP addresses. + // Example in: `internal/provider/fastestvpn/updater` + // + // The following is an example code which fetches server + // information from an HTTP API endpoint of the provider, + // and then resolves in parallel all hostnames to get their + // IP addresses. You should pay attention to the following: + // - we check multiple times we have enough servers + // before continuing processing. + // - hosts are deduplicated to reduce parallel resolution + // load. + // - servers are sorted at the end. + // + // Once you are done, please check all the TODO comments + // in this package and address them. + data, err := fetchAPI(ctx, u.client) + if err != nil { + return nil, fmt.Errorf("fetching API: %w", err) + } + + uniqueHosts := make(map[string]struct{}, len(data.Servers)) + + for _, serverData := range data.Servers { + if serverData.OpenVPNHostname != "" { + uniqueHosts[serverData.OpenVPNHostname] = struct{}{} + } + + if serverData.WireguardHostname != "" { + uniqueHosts[serverData.WireguardHostname] = struct{}{} + } + } + + if len(uniqueHosts) < minServers { + return nil, fmt.Errorf("%w: %d and expected at least %d", + common.ErrNotEnoughServers, len(uniqueHosts), minServers) + } + + hosts := make([]string, 0, len(uniqueHosts)) + for host := range uniqueHosts { + hosts = append(hosts, host) + } + + resolveSettings := parallelResolverSettings(hosts) + hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings) + for _, warning := range warnings { + u.warner.Warn(warning) + } + if err != nil { + return nil, fmt.Errorf("resolving hosts: %w", err) + } + + if len(hostToIPs) < minServers { + return nil, fmt.Errorf("%w: %d and expected at least %d", + common.ErrNotEnoughServers, len(servers), minServers) + } + + maxServers := 2 * len(data.Servers) //nolint:gomnd + servers = make([]models.Server, 0, maxServers) + for _, serverData := range data.Servers { + server := models.Server{ + Country: serverData.Country, + Region: serverData.Region, + City: serverData.City, + WgPubKey: serverData.WgPubKey, + UDP: true, + } + if serverData.OpenVPNHostname != "" { + server.VPN = vpn.OpenVPN + server.TCP = true + server.Hostname = serverData.OpenVPNHostname + server.IPs = hostToIPs[serverData.OpenVPNHostname] + servers = append(servers, server) + } + if serverData.WireguardHostname != "" { + server.VPN = vpn.Wireguard + server.Hostname = serverData.WireguardHostname + server.IPs = hostToIPs[serverData.WireguardHostname] + servers = append(servers, server) + } + } + + sort.Sort(models.SortableServers(servers)) + + return servers, nil +} diff --git a/internal/provider/example/updater/updater.go b/internal/provider/example/updater/updater.go new file mode 100644 index 00000000..496ac31a --- /dev/null +++ b/internal/provider/example/updater/updater.go @@ -0,0 +1,26 @@ +package updater + +import ( + "net/http" + + "github.com/qdm12/gluetun/internal/provider/common" +) + +type Updater struct { + // TODO: remove fields not used by the updater + client *http.Client + unzipper common.Unzipper + parallelResolver common.ParallelResolver + warner common.Warner +} + +func New(warner common.Warner, unzipper common.Unzipper, + client *http.Client, parallelResolver common.ParallelResolver) *Updater { + // TODO: remove arguments not used by the updater + return &Updater{ + client: client, + unzipper: unzipper, + parallelResolver: parallelResolver, + warner: warner, + } +}