Skip to content

Commit

Permalink
adapt to latest dns resolution strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
ainghazal committed Oct 16, 2024
1 parent 6790cdc commit 1bdabbb
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 253 deletions.
8 changes: 0 additions & 8 deletions internal/experiment/openvpn/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,13 @@ 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

// 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

Expand Down
143 changes: 50 additions & 93 deletions internal/experiment/openvpn/richerinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
146 changes: 49 additions & 97 deletions internal/experiment/openvpn/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 1bdabbb

Please sign in to comment.