diff --git a/internal/experiment/openvpn/endpoint.go b/internal/experiment/openvpn/endpoint.go index 9c41a62c4..df255527d 100644 --- a/internal/experiment/openvpn/endpoint.go +++ b/internal/experiment/openvpn/endpoint.go @@ -25,10 +25,6 @@ type endpoint struct { // IPAddr is the IP Address for this endpoint. IPAddr string - // DomainName is an optional domain name that we use internally to get the IP address. - // This is just a convenience field, the experiments should always be done against a canonical IPAddr. - DomainName string - // Obfuscation is any obfuscation method use to connect to this endpoint. // Valid values are: obfs4, none. Obfuscation string @@ -36,10 +32,6 @@ type endpoint struct { // Port is the Port for this endpoint. Port string - // PreferredCountries is an optional array of country codes. Probes in these countries have preference on this - // endpoint. - PreferredCountries []string - // Protocol is the tunneling protocol (openvpn, openvpn+obfs4). Protocol string diff --git a/internal/experiment/openvpn/richerinput.go b/internal/experiment/openvpn/richerinput.go index f3c88fc14..1cd7384c0 100644 --- a/internal/experiment/openvpn/richerinput.go +++ b/internal/experiment/openvpn/richerinput.go @@ -3,16 +3,26 @@ package openvpn import ( "context" "fmt" + "slices" + "time" "github.com/ooni/probe-cli/v3/internal/experimentconfig" + "github.com/ooni/probe-cli/v3/internal/legacy/netx" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/reflectx" "github.com/ooni/probe-cli/v3/internal/targetloading" ) -// defaultProvider is the provider we will request from API in case we got no provider set -// in the CLI options. -var defaultProvider = "riseupvpn" +// defaultOONIHostnames is the array of hostnames that will return valid +// endpoints to be probed. Do note that this is a workaround for the lack +// of a backend service. +var defaultOONIEndpoints = []string{ + "a.composer-presenter.com", + "a.goodyear2dumpster.com", +} + +// maxDefaultOONIAddresses is how many IPs to use from the +// set of resolved IPs. +var maxDefaultOONIAddresses = 3 // providerAuthentication is a map so that we know which kind of credentials we // need to fill in the openvpn options for each known provider. @@ -91,16 +101,6 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err tl.loader.Logger.Warnf("Error loading OpenVPN targets from cli") } - // If inputs and files are all empty and there are no options, let's use the backend - if len(tl.loader.StaticInputs) <= 0 && len(tl.loader.SourceFiles) <= 0 && - reflectx.StructOrStructPtrIsZero(tl.options) { - targets, err := tl.loadFromBackend(ctx) - if err == nil { - return targets, nil - } - tl.loader.Logger.Warnf("Error fetching OpenVPN targets from backend") - } - // Build the list of targets that we should measure. var targets []model.ExperimentTarget for _, input := range inputs { @@ -117,93 +117,50 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err return tl.loadFromDefaultEndpoints() } -func makeTargetListPerProtocol(cc string, num int) []model.ExperimentTarget { - targets := []model.ExperimentTarget{} - var reverse bool - switch num { - case 1, 2: - // for single or few picks, we start the list in the natural order - reverse = false - default: - // for multiple picks, we start the list from the bottom, so that we can lookup - // custom country campaigns first. - reverse = true - } - if inputsUDP, err := pickOONIOpenVPNTargets("udp", cc, num, reverse); err == nil { - for _, t := range inputsUDP { - targets = append(targets, - &Target{ - Config: pickFromDefaultOONIOpenVPNConfig(), - URL: t, - }) - } - } - if inputsTCP, err := pickOONIOpenVPNTargets("tcp", cc, num, reverse); err == nil { - for _, t := range inputsTCP { - targets = append(targets, - &Target{ - Config: pickFromDefaultOONIOpenVPNConfig(), - URL: t, - }) - } - } - return targets +// TODO: move to targets. +func lookupHost(ctx context.Context, hostname string, r model.Resolver) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + return r.LookupHost(ctx, hostname) } func (tl *targetLoader) loadFromDefaultEndpoints() ([]model.ExperimentTarget, error) { - cc := tl.session.ProbeCC() - - tl.loader.Logger.Warnf("Using default OpenVPN endpoints") - tl.loader.Logger.Warnf("Picking endpoints for %s", cc) - - var targets []model.ExperimentTarget - switch cc { - case "RU", "CN", "IR", "EG", "NL": - // we want to cover all of our bases for a few interest countries - targets = makeTargetListPerProtocol(cc, 20) - default: - targets = makeTargetListPerProtocol(cc, 1) - } - return targets, nil -} - -func (tl *targetLoader) loadFromBackend(ctx context.Context) ([]model.ExperimentTarget, error) { - if tl.options.Provider == "" { - tl.options.Provider = defaultProvider + resolver := netx.NewResolver(netx.Config{ + BogonIsError: false, + Logger: tl.session.Logger(), + Saver: nil, + }) + + addrs := []string{} + + // get the set of all IPs for all the hostnames we have. + for _, hostname := range defaultOONIEndpoints { + resolved, err := lookupHost(context.Background(), hostname, resolver) + if err != nil { + tl.loader.Logger.Warnf("Cannot resolve %s", hostname) + continue + } + for _, ipaddr := range resolved { + if !slices.Contains(addrs, ipaddr) { + addrs = append(addrs, ipaddr) + } + } } - targets := make([]model.ExperimentTarget, 0) - provider := tl.options.Provider + fmt.Println(">>> ADDRS", addrs) - apiConfig, err := tl.session.FetchOpenVPNConfig(ctx, provider, tl.session.ProbeCC()) - if err != nil { - tl.session.Logger().Warnf("Cannot fetch openvpn config: %v", err) - return nil, err - } - - auth, ok := providerAuthentication[provider] - if !ok { - return nil, fmt.Errorf("%w: unknown authentication for provider %s", targetloading.ErrInvalidInput, provider) - } + // TODO: filter bogons (here), return err if nil - for _, input := range apiConfig.Inputs { - config := &Config{ - Auth: "SHA512", - Cipher: "AES-256-GCM", - } - switch auth { - case AuthCertificate: - config.SafeCA = apiConfig.Config.CA - config.SafeCert = apiConfig.Config.Cert - config.SafeKey = apiConfig.Config.Key - case AuthUserPass: - // TODO(ainghazal): implement (surfshark, etc) + tl.loader.Logger.Warnf("Picking from default OpenVPN endpoints") + targets := []model.ExperimentTarget{} + if inputs, err := pickOONIOpenVPNTargets(addrs); err == nil { + for _, url := range inputs { + targets = append(targets, + &Target{ + Config: pickFromDefaultOONIOpenVPNConfig(), + URL: url, + }) } - targets = append(targets, &Target{ - URL: input, - Config: config, - }) } - return targets, nil } diff --git a/internal/experiment/openvpn/targets.go b/internal/experiment/openvpn/targets.go index e797760a4..2770357f0 100644 --- a/internal/experiment/openvpn/targets.go +++ b/internal/experiment/openvpn/targets.go @@ -3,142 +3,94 @@ package openvpn import ( "fmt" "math/rand" - "net" - "slices" ) +// TODO: deprecate, move to function below // defaultOpenVPNEndpoints contain a list of all default endpoints -// to be tried, in the order that we want the name resolution to happen. +// to be tried. We will fill in the IP Addresses. var defaultOpenVPNEndpoints = []endpoint{ - // default domain. this should work fine for most places. { - IPAddr: "", - DomainName: "openvpn-server1.ooni.io", Obfuscation: "none", Port: "1194", Protocol: "openvpn", Provider: "oonivpn", - Transport: "tcp", - }, - { IPAddr: "", - DomainName: "openvpn-server1.ooni.io", - Obfuscation: "none", - Port: "1194", - Protocol: "openvpn", - Provider: "oonivpn", - Transport: "udp", + Transport: "", }, - // alt domain 1. still same endpoint ports, one udp and one tcp. - // TODO(ain): update to real domain names { IPAddr: "", - DomainName: "alt-domain1.example.org", Obfuscation: "none", - Port: "1194", - Protocol: "openvpn", - Provider: "oonivpn", - Transport: "udp", - }, - { - IPAddr: "", - DomainName: "alt-domain1.example.org", - Obfuscation: "none", - Port: "1194", + Port: "443", Protocol: "openvpn", Provider: "oonivpn", Transport: "tcp", }, - // alt domain 2. still same endpoint ports, one udp and one tcp. - // TODO(ain): update to real domain names { IPAddr: "", - DomainName: "alt-domain2.example.org", Obfuscation: "none", Port: "53", Protocol: "openvpn", Provider: "oonivpn", Transport: "udp", }, - { - IPAddr: "", - DomainName: "alt-domain2.example.org", +} + +// pickOONIOpenVPNTargets returns an array of input URIs from the list of available endpoints, up to max, +// for the given transport. By default, we use the first endpoint that resolves to an IP. If reverseOrder +// is specified, we reverse the list before attempting resolution. +func pickOONIOpenVPNTargets(ipaddrList []string) ([]string, error) { + // Step 1. Create endpoint list. + endpoints := []endpoint{} + for _, ipAddr := range ipaddrList { + // 1. Probe the canonical 1194/udp and 1194/tcp ports + endpoints = append(endpoints, endpoint{ + Obfuscation: "none", + Port: "1194", + Protocol: "openvpn", + Provider: "oonivpn", + IPAddr: ipAddr, + Transport: "tcp", + }) + endpoints = append(endpoints, endpoint{ + Obfuscation: "none", + Port: "1194", + Protocol: "openvpn", + Provider: "oonivpn", + IPAddr: ipAddr, + Transport: "udp", + }) + + } + + // Pick one IP and sample on non-standard ports + // to confirm if this one goes through. + extra := ipaddrList[rand.Intn(len(ipaddrList))] + endpoints = append(endpoints, endpoint{ Obfuscation: "none", - Port: "443", Protocol: "openvpn", Provider: "oonivpn", - Transport: "tcp", - }, - // alt domain 3. this is reserved. - // TODO(ain): update to real domain names - { - IPAddr: "", - DomainName: "alt-domain3.example.org", + IPAddr: extra, + Port: "53", + Transport: "udp", + }) + endpoints = append(endpoints, endpoint{ Obfuscation: "none", - Port: "443", Protocol: "openvpn", Provider: "oonivpn", + IPAddr: extra, + Port: "443", Transport: "tcp", - PreferredCountries: []string{ - "AM", "AZ", "BY", "GE", "KZ", "KG", "LT", "MD", "RU", "TJ", "TM", "UA", "UZ", - "IR", "CN", "EG"}, - }, - // TODO: add more backup domains here -} - -// this is a safety toggle: it's on purpose that the experiment will receive no -// input if the resolution fails. This also implies that we have no way of knowing if this -// target has been blocked at the level of DNS. -// TODO(ain,mehul): we might want to try resolving with other techniques (DoT etc), -// and perhaps also transform DNS failure into a specific failure of the experiment, not -// a skip. -// TODO(ain): update the openvpn spec to reflect the CURRENT state of delivering the targets. -// If the probe services ever gets deployed, this step will not be needed anymore. -func resolveTarget(domain string) (string, error) { - ips, err := net.LookupIP(domain) - if err != nil { - return "", err - } - if len(ips) > 0 { - return ips[0].String(), nil - } - return "", fmt.Errorf("cannot resolve %v", domain) -} + }) -// pickOONIOpenVPNTargets returns an array of input URIs from the list of available endpoints, up to max, -// for the given transport. By default, we use the first endpoint that resolves to an IP. If reverseOrder -// is specified, we reverse the list before attempting resolution. -func pickOONIOpenVPNTargets(transport string, cc string, max int, reverseOrder bool) ([]string, error) { - endpoints := slices.Clone(defaultOpenVPNEndpoints)[:] - if reverseOrder { - slices.Reverse(endpoints) - } + // Step 2. Create targets for the selected endpoints. targets := make([]string, 0) - for _, endpoint := range endpoints { - if endpoint.Transport != transport { - continue - } - if len(endpoint.PreferredCountries) > 0 && !slices.Contains(endpoint.PreferredCountries, cc) { - // not for us - continue - } - // Do note that this will get the wrong result if we got DNS poisoning. - // When analyzing this data, you should be careful about bogus IPs. - ip, err := resolveTarget(endpoint.DomainName) - if err != nil { - continue - } - endpoint.IPAddr = ip - - targets = append(targets, endpoint.AsInputURI()) - if len(targets) == max { - return targets, nil - } + for _, e := range endpoints { + targets = append(targets, e.AsInputURI()) } if len(targets) > 0 { return targets, nil } - return nil, fmt.Errorf("cannot find any endpoint for %s", transport) + return nil, fmt.Errorf("cannot find any usable endpoint") } func pickFromDefaultOONIOpenVPNConfig() *Config { diff --git a/internal/experiment/openvpn/targets_test.go b/internal/experiment/openvpn/targets_test.go index 704750bb8..54e202d7f 100644 --- a/internal/experiment/openvpn/targets_test.go +++ b/internal/experiment/openvpn/targets_test.go @@ -1,64 +1,9 @@ package openvpn import ( - "net/url" "testing" - - "github.com/google/go-cmp/cmp" ) -func Test_resolveTarget(t *testing.T) { - // TODO: mustHaveExternalNetwork() equivalent. - if testing.Short() { - t.Skip("skip test in short mode") - } - - _, err := resolveTarget("google.com") - - if err != nil { - if err.Error() == "connection_refused" { - // connection_refused is raised when running this test - // on the restricted network for coverage tests. - // so we bail out - return - } - t.Fatal("should be able to resolve the target") - } - - _, err = resolveTarget("nothing.corp") - if err == nil { - t.Fatal("should not be able to resolve the target") - } - - _, err = resolveTarget("asfasfasfasfasfafs.ooni.io") - if err == nil { - t.Fatal("should not be able to resolve the target") - } -} - -func Test_pickOpenVPNTargets(t *testing.T) { - urls, err := pickOONIOpenVPNTargets("udp", "IT", 1, false) - if err != nil { - if err.Error() == "connection_refused" { - // connection_refused is raised when running this test - // on the restricted network for coverage tests. - // so we bail out - return - } - t.Fatal("unexpected error") - } - expected := "openvpn://oonivpn.corp?address=37.218.243.98:1194&transport=udp" - - got, err := url.QueryUnescape(urls[0]) - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(got, expected); diff != "" { - t.Fatal(diff) - } -} - func Test_pickFromDefaultOONIOpenVPNConfig(t *testing.T) { pick := pickFromDefaultOONIOpenVPNConfig()