chore: merge probe-engine into probe-cli (#201)

This is how I did it:

1. `git clone https://github.com/ooni/probe-engine internal/engine`

2. ```
(cd internal/engine && git describe --tags)
v0.23.0
```

3. `nvim go.mod` (merging `go.mod` with `internal/engine/go.mod`

4. `rm -rf internal/.git internal/engine/go.{mod,sum}`

5. `git add internal/engine`

6. `find . -type f -name \*.go -exec sed -i 's@/ooni/probe-engine@/ooni/probe-cli/v3/internal/engine@g' {} \;`

7. `go build ./...` (passes)

8. `go test -race ./...` (temporary failure on RiseupVPN)

9. `go mod tidy`

10. this commit message

Once this piece of work is done, we can build a new version of `ooniprobe` that
is using `internal/engine` directly. We need to do more work to ensure all the
other functionality in `probe-engine` (e.g. making mobile packages) are still WAI.

Part of https://github.com/ooni/probe/issues/1335
This commit is contained in:
Simone Basso
2021-02-02 12:05:47 +01:00
committed by GitHub
parent b1ce300c8d
commit d57c78bc71
535 changed files with 66182 additions and 23 deletions
+3
View File
@@ -0,0 +1,3 @@
# Package github.com/ooni/probe-engine/geolocate
Package geolocate implements IP lookup, resolver lookup, and geolocation.
+31
View File
@@ -0,0 +1,31 @@
package geolocate
import (
"context"
"net/http"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
)
type avastResponse struct {
IP string `json:"ip"`
}
func avastIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
var v avastResponse
err := (httpx.Client{
BaseURL: "https://ip-info.ff.avast.com",
HTTPClient: httpClient,
Logger: logger,
UserAgent: userAgent,
}).GetJSON(ctx, "/v1/info", &v)
if err != nil {
return DefaultProbeIP, err
}
return v.IP, nil
}
+26
View File
@@ -0,0 +1,26 @@
package geolocate
import (
"context"
"net"
"net/http"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
)
func TestIPLookupWorksUsingAvast(t *testing.T) {
ip, err := avastIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}
+27
View File
@@ -0,0 +1,27 @@
package geolocate
import (
"io/ioutil"
"net/http"
"time"
)
type FakeTransport struct {
Err error
Resp *http.Response
}
func (txp FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
time.Sleep(10 * time.Microsecond)
if req.Body != nil {
ioutil.ReadAll(req.Body)
req.Body.Close()
}
if txp.Err != nil {
return nil, txp.Err
}
txp.Resp.Request = req // non thread safe but it doesn't matter
return txp.Resp, nil
}
func (txp FakeTransport) CloseIdleConnections() {}
+242
View File
@@ -0,0 +1,242 @@
// Package geolocate implements IP lookup, resolver lookup, and geolocation.
package geolocate
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/ooni/probe-cli/v3/internal/engine/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/version"
)
const (
// DefaultProbeASN is the default probe ASN as number.
DefaultProbeASN uint = 0
// DefaultProbeCC is the default probe CC.
DefaultProbeCC = "ZZ"
// DefaultProbeIP is the default probe IP.
DefaultProbeIP = model.DefaultProbeIP
// DefaultProbeNetworkName is the default probe network name.
DefaultProbeNetworkName = ""
// DefaultResolverASN is the default resolver ASN.
DefaultResolverASN uint = 0
// DefaultResolverIP is the default resolver IP.
DefaultResolverIP = "127.0.0.2"
// DefaultResolverNetworkName is the default resolver network name.
DefaultResolverNetworkName = ""
)
var (
// DefaultProbeASNString is the default probe ASN as a string.
DefaultProbeASNString = fmt.Sprintf("AS%d", DefaultProbeASN)
// DefaultResolverASNString is the default resolver ASN as a string.
DefaultResolverASNString = fmt.Sprintf("AS%d", DefaultResolverASN)
)
var (
// ErrMissingResourcesManager indicates that no resources
// manager has been configured inside of Config.
ErrMissingResourcesManager = errors.New("geolocate: ResourcesManager is nil")
)
// Logger is the definition of Logger used by this package.
type Logger interface {
Debugf(format string, v ...interface{})
}
// Results contains geolocate results
type Results struct {
// ASN is the autonomous system number
ASN uint
// CountryCode is the country code
CountryCode string
// DidResolverLookup indicates whether we did a resolver lookup.
DidResolverLookup bool
// NetworkName is the network name
NetworkName string
// IP is the probe IP
ProbeIP string
// ResolverASN is the resolver ASN
ResolverASN uint
// ResolverIP is the resolver IP
ResolverIP string
// ResolverNetworkName is the resolver network name
ResolverNetworkName string
}
// ASNString returns the ASN as a string
func (r *Results) ASNString() string {
return fmt.Sprintf("AS%d", r.ASN)
}
type probeIPLookupper interface {
LookupProbeIP(ctx context.Context) (addr string, err error)
}
type asnLookupper interface {
LookupASN(path string, ip string) (asn uint, network string, err error)
}
type countryLookupper interface {
LookupCC(path string, ip string) (cc string, err error)
}
type resolverIPLookupper interface {
LookupResolverIP(ctx context.Context) (addr string, err error)
}
// ResourcesManager manages the required resources.
type ResourcesManager interface {
// ASNDatabasePath returns the path of the ASN database.
ASNDatabasePath() string
// CountryDatabasePath returns the path of the country database.
CountryDatabasePath() string
// MaybeUpdateResources ensures that the required resources
// have been downloaded and are current.
MaybeUpdateResources(ctx context.Context) error
}
// Config contains configuration for a geolocate Task.
type Config struct {
// EnableResolverLookup indicates whether we want to
// perform the optional resolver lookup.
EnableResolverLookup bool
// HTTPClient is the HTTP client to use. If not set, then
// we will use the http.DefaultClient.
HTTPClient *http.Client
// Logger is the logger to use. If not set, then we will
// use a logger that discards all messages.
Logger Logger
// ResourcesManager is the mandatory resources manager. If not
// set, we will not be able to perform any lookup.
ResourcesManager ResourcesManager
// UserAgent is the user agent to use. If not set, then
// we will use a default user agent.
UserAgent string
}
// Must ensures that NewTask is successful.
func Must(task *Task, err error) *Task {
runtimex.PanicOnError(err, "NewTask failed")
return task
}
// NewTask creates a new instance of Task from config.
func NewTask(config Config) (*Task, error) {
if config.HTTPClient == nil {
config.HTTPClient = http.DefaultClient
}
if config.Logger == nil {
config.Logger = model.DiscardLogger
}
if config.ResourcesManager == nil {
return nil, ErrMissingResourcesManager
}
if config.UserAgent == "" {
config.UserAgent = fmt.Sprintf("ooniprobe-engine/%s", version.Version)
}
return &Task{
countryLookupper: mmdbLookupper{},
enableResolverLookup: config.EnableResolverLookup,
probeIPLookupper: ipLookupClient{
HTTPClient: config.HTTPClient,
Logger: config.Logger,
UserAgent: config.UserAgent,
},
probeASNLookupper: mmdbLookupper{},
resolverASNLookupper: mmdbLookupper{},
resolverIPLookupper: resolverLookupClient{},
resourcesManager: config.ResourcesManager,
}, nil
}
// Task performs a geolocation. You must create a new
// instance of Task using the NewTask factory.
type Task struct {
countryLookupper countryLookupper
enableResolverLookup bool
probeIPLookupper probeIPLookupper
probeASNLookupper asnLookupper
resolverASNLookupper asnLookupper
resolverIPLookupper resolverIPLookupper
resourcesManager ResourcesManager
}
// Run runs the task.
func (op Task) Run(ctx context.Context) (*Results, error) {
var err error
out := &Results{
ASN: DefaultProbeASN,
CountryCode: DefaultProbeCC,
NetworkName: DefaultProbeNetworkName,
ProbeIP: DefaultProbeIP,
ResolverASN: DefaultResolverASN,
ResolverIP: DefaultResolverIP,
ResolverNetworkName: DefaultResolverNetworkName,
}
if err := op.resourcesManager.MaybeUpdateResources(ctx); err != nil {
return out, fmt.Errorf("MaybeUpdateResource failed: %w", err)
}
ip, err := op.probeIPLookupper.LookupProbeIP(ctx)
if err != nil {
return out, fmt.Errorf("lookupProbeIP failed: %w", err)
}
out.ProbeIP = ip
asn, networkName, err := op.probeASNLookupper.LookupASN(
op.resourcesManager.ASNDatabasePath(), out.ProbeIP)
if err != nil {
return out, fmt.Errorf("lookupASN failed: %w", err)
}
out.ASN = asn
out.NetworkName = networkName
cc, err := op.countryLookupper.LookupCC(
op.resourcesManager.CountryDatabasePath(), out.ProbeIP)
if err != nil {
return out, fmt.Errorf("lookupProbeCC failed: %w", err)
}
out.CountryCode = cc
if op.enableResolverLookup {
out.DidResolverLookup = true
// Note: ignoring the result of lookupResolverIP and lookupASN
// here is intentional. We don't want this (~minor) failure
// to influence the result of the overall lookup. Another design
// here could be that of retrying the operation N times?
resolverIP, err := op.resolverIPLookupper.LookupResolverIP(ctx)
if err != nil {
return out, nil
}
out.ResolverIP = resolverIP
resolverASN, resolverNetworkName, err := op.resolverASNLookupper.LookupASN(
op.resourcesManager.ASNDatabasePath(), out.ResolverIP,
)
if err != nil {
return out, nil
}
out.ResolverASN = resolverASN
out.ResolverNetworkName = resolverNetworkName
}
return out, nil
}
+395
View File
@@ -0,0 +1,395 @@
package geolocate
import (
"context"
"errors"
"testing"
)
type taskResourcesManager struct {
asnDatabasePath string
countryDatabasePath string
err error
}
func (c taskResourcesManager) ASNDatabasePath() string {
return c.asnDatabasePath
}
func (c taskResourcesManager) CountryDatabasePath() string {
return c.countryDatabasePath
}
func (c taskResourcesManager) MaybeUpdateResources(ctx context.Context) error {
return c.err
}
func TestLocationLookupCannotUpdateResources(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{err: expected},
}
ctx := context.Background()
out, err := op.Run(ctx)
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != DefaultProbeASN {
t.Fatal("invalid ASN value")
}
if out.CountryCode != DefaultProbeCC {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != DefaultProbeNetworkName {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != DefaultProbeIP {
t.Fatal("invalid ProbeIP value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatal("invalid ResolverASN value")
}
if out.ResolverIP != DefaultResolverIP {
t.Fatal("invalid ResolverIP value")
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
type taskProbeIPLookupper struct {
ip string
err error
}
func (c taskProbeIPLookupper) LookupProbeIP(ctx context.Context) (string, error) {
return c.ip, c.err
}
func TestLocationLookupCannotLookupProbeIP(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{err: expected},
}
ctx := context.Background()
out, err := op.Run(ctx)
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != DefaultProbeASN {
t.Fatal("invalid ASN value")
}
if out.CountryCode != DefaultProbeCC {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != DefaultProbeNetworkName {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != DefaultProbeIP {
t.Fatal("invalid ProbeIP value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatal("invalid ResolverASN value")
}
if out.ResolverIP != DefaultResolverIP {
t.Fatal("invalid ResolverIP value")
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
type taskASNLookupper struct {
err error
asn uint
name string
}
func (c taskASNLookupper) LookupASN(path string, ip string) (uint, string, error) {
return c.asn, c.name, c.err
}
func TestLocationLookupCannotLookupProbeASN(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{err: expected},
}
ctx := context.Background()
out, err := op.Run(ctx)
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != DefaultProbeASN {
t.Fatal("invalid ASN value")
}
if out.CountryCode != DefaultProbeCC {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != DefaultProbeNetworkName {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatal("invalid ResolverASN value")
}
if out.ResolverIP != DefaultResolverIP {
t.Fatal("invalid ResolverIP value")
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
type taskCCLookupper struct {
err error
cc string
}
func (c taskCCLookupper) LookupCC(path string, ip string) (string, error) {
return c.cc, c.err
}
func TestLocationLookupCannotLookupProbeCC(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
countryLookupper: taskCCLookupper{cc: "US", err: expected},
}
ctx := context.Background()
out, err := op.Run(ctx)
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != 1234 {
t.Fatal("invalid ASN value")
}
if out.CountryCode != DefaultProbeCC {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != "1234.com" {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatal("invalid ResolverASN value")
}
if out.ResolverIP != DefaultResolverIP {
t.Fatal("invalid ResolverIP value")
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
type taskResolverIPLookupper struct {
ip string
err error
}
func (c taskResolverIPLookupper) LookupResolverIP(ctx context.Context) (string, error) {
return c.ip, c.err
}
func TestLocationLookupCannotLookupResolverIP(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
countryLookupper: taskCCLookupper{cc: "IT"},
resolverIPLookupper: taskResolverIPLookupper{err: expected},
enableResolverLookup: true,
}
ctx := context.Background()
out, err := op.Run(ctx)
if err != nil {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != 1234 {
t.Fatal("invalid ASN value")
}
if out.CountryCode != "IT" {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != "1234.com" {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.DidResolverLookup != true {
t.Fatal("invalid DidResolverLookup value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatal("invalid ResolverASN value")
}
if out.ResolverIP != DefaultResolverIP {
t.Fatal("invalid ResolverIP value")
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
func TestLocationLookupCannotLookupResolverNetworkName(t *testing.T) {
expected := errors.New("mocked error")
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
countryLookupper: taskCCLookupper{cc: "IT"},
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
resolverASNLookupper: taskASNLookupper{err: expected},
enableResolverLookup: true,
}
ctx := context.Background()
out, err := op.Run(ctx)
if err != nil {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != 1234 {
t.Fatal("invalid ASN value")
}
if out.CountryCode != "IT" {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != "1234.com" {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.DidResolverLookup != true {
t.Fatal("invalid DidResolverLookup value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
}
if out.ResolverIP != "4.3.2.1" {
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
func TestLocationLookupSuccessWithResolverLookup(t *testing.T) {
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
countryLookupper: taskCCLookupper{cc: "IT"},
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
resolverASNLookupper: taskASNLookupper{asn: 4321, name: "4321.com"},
enableResolverLookup: true,
}
ctx := context.Background()
out, err := op.Run(ctx)
if err != nil {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != 1234 {
t.Fatal("invalid ASN value")
}
if out.CountryCode != "IT" {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != "1234.com" {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.DidResolverLookup != true {
t.Fatal("invalid DidResolverLookup value")
}
if out.ResolverASN != 4321 {
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
}
if out.ResolverIP != "4.3.2.1" {
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
}
if out.ResolverNetworkName != "4321.com" {
t.Fatal("invalid ResolverNetworkName value")
}
}
func TestLocationLookupSuccessWithoutResolverLookup(t *testing.T) {
op := Task{
resourcesManager: taskResourcesManager{},
probeIPLookupper: taskProbeIPLookupper{ip: "1.2.3.4"},
probeASNLookupper: taskASNLookupper{asn: 1234, name: "1234.com"},
countryLookupper: taskCCLookupper{cc: "IT"},
resolverIPLookupper: taskResolverIPLookupper{ip: "4.3.2.1"},
resolverASNLookupper: taskASNLookupper{asn: 4321, name: "4321.com"},
}
ctx := context.Background()
out, err := op.Run(ctx)
if err != nil {
t.Fatalf("not the error we expected: %+v", err)
}
if out.ASN != 1234 {
t.Fatal("invalid ASN value")
}
if out.CountryCode != "IT" {
t.Fatal("invalid CountryCode value")
}
if out.NetworkName != "1234.com" {
t.Fatal("invalid NetworkName value")
}
if out.ProbeIP != "1.2.3.4" {
t.Fatal("invalid ProbeIP value")
}
if out.DidResolverLookup != false {
t.Fatal("invalid DidResolverLookup value")
}
if out.ResolverASN != DefaultResolverASN {
t.Fatalf("invalid ResolverASN value: %+v", out.ResolverASN)
}
if out.ResolverIP != DefaultResolverIP {
t.Fatalf("invalid ResolverIP value: %+v", out.ResolverIP)
}
if out.ResolverNetworkName != DefaultResolverNetworkName {
t.Fatal("invalid ResolverNetworkName value")
}
}
func TestSmoke(t *testing.T) {
maybeFetchResources(t)
config := Config{
EnableResolverLookup: true,
ResourcesManager: taskResourcesManager{
asnDatabasePath: asnDBPath,
countryDatabasePath: countryDBPath,
},
}
task := Must(NewTask(config))
result, err := task.Run(context.Background())
if err != nil {
t.Fatal(err)
}
if result == nil {
t.Fatal("expected non nil result")
}
// we already checked above that the returned
// value is okay for all codepaths.
}
func TestNewTaskWithNoResourcesManager(t *testing.T) {
task, err := NewTask(Config{})
if !errors.Is(err, ErrMissingResourcesManager) {
t.Fatal("not the error we expected")
}
if task != nil {
t.Fatal("expected nil task here")
}
}
+15
View File
@@ -0,0 +1,15 @@
package geolocate
import (
"context"
"net/http"
)
func invalidIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
return "invalid IP", nil
}
+30
View File
@@ -0,0 +1,30 @@
package geolocate
import (
"context"
"net/http"
"strings"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
)
func ipConfigIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
data, err := (httpx.Client{
BaseURL: "https://ipconfig.io",
HTTPClient: httpClient,
Logger: logger,
UserAgent: httpheader.CLIUserAgent(),
}).FetchResource(ctx, "/")
if err != nil {
return DefaultProbeIP, err
}
ip := strings.Trim(string(data), "\r\n\t ")
logger.Debugf("ipconfig: body: %s", ip)
return ip, nil
}
@@ -0,0 +1,26 @@
package geolocate
import (
"context"
"net"
"net/http"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
)
func TestIPLookupWorksUsingIPConfig(t *testing.T) {
ip, err := ipConfigIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}
+33
View File
@@ -0,0 +1,33 @@
package geolocate
import (
"context"
"net/http"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
)
type ipInfoResponse struct {
IP string `json:"ip"`
}
func ipInfoIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
var v ipInfoResponse
err := (httpx.Client{
Accept: "application/json",
BaseURL: "https://ipinfo.io",
HTTPClient: httpClient,
Logger: logger,
UserAgent: httpheader.CLIUserAgent(), // we must be a CLI client
}).GetJSON(ctx, "/", &v)
if err != nil {
return DefaultProbeIP, err
}
return v.IP, nil
}
+26
View File
@@ -0,0 +1,26 @@
package geolocate
import (
"context"
"net"
"net/http"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
)
func TestIPLookupWorksUsingIPInfo(t *testing.T) {
ip, err := ipInfoIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}
+113
View File
@@ -0,0 +1,113 @@
package geolocate
import (
"context"
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"sync"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/internal/multierror"
)
var (
// ErrAllIPLookuppersFailed indicates that we failed with looking
// up the probe IP for with all the lookuppers that we tried.
ErrAllIPLookuppersFailed = errors.New("all IP lookuppers failed")
// ErrInvalidIPAddress indicates that the code returned to us a
// string that actually isn't a valid IP address.
ErrInvalidIPAddress = errors.New("lookupper did not return a valid IP")
)
type lookupFunc func(
ctx context.Context, client *http.Client,
logger Logger, userAgent string,
) (string, error)
type method struct {
name string
fn lookupFunc
}
var (
methods = []method{
{
name: "avast",
fn: avastIPLookup,
},
{
name: "ipconfig",
fn: ipConfigIPLookup,
},
{
name: "ipinfo",
fn: ipInfoIPLookup,
},
{
name: "stun_ekiga",
fn: stunEkigaIPLookup,
},
{
name: "stun_google",
fn: stunGoogleIPLookup,
},
{
name: "ubuntu",
fn: ubuntuIPLookup,
},
}
once sync.Once
)
type ipLookupClient struct {
// HTTPClient is the HTTP client to use
HTTPClient *http.Client
// Logger is the logger to use
Logger Logger
// UserAgent is the user agent to use
UserAgent string
}
func makeSlice() []method {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
ret := make([]method, len(methods))
perm := r.Perm(len(methods))
for idx, randIdx := range perm {
ret[idx] = methods[randIdx]
}
return ret
}
func (c ipLookupClient) doWithCustomFunc(
ctx context.Context, fn lookupFunc,
) (string, error) {
ip, err := fn(ctx, c.HTTPClient, c.Logger, c.UserAgent)
if err != nil {
return DefaultProbeIP, err
}
if net.ParseIP(ip) == nil {
return DefaultProbeIP, fmt.Errorf("%w: %s", ErrInvalidIPAddress, ip)
}
c.Logger.Debugf("iplookup: IP: %s", ip)
return ip, nil
}
func (c ipLookupClient) LookupProbeIP(ctx context.Context) (string, error) {
union := multierror.New(ErrAllIPLookuppersFailed)
for _, method := range makeSlice() {
c.Logger.Debugf("iplookup: using %s", method.name)
ip, err := c.doWithCustomFunc(ctx, method.fn)
if err == nil {
return ip, nil
}
union.Add(err)
}
return DefaultProbeIP, union
}
@@ -0,0 +1,56 @@
package geolocate
import (
"context"
"errors"
"net"
"net/http"
"testing"
"github.com/apex/log"
)
func TestIPLookupGood(t *testing.T) {
ip, err := (ipLookupClient{
HTTPClient: http.DefaultClient,
Logger: log.Log,
UserAgent: "ooniprobe-engine/0.1.0",
}).LookupProbeIP(context.Background())
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatal("not an IP address")
}
}
func TestIPLookupAllFailed(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately cancel to cause Do() to fail
ip, err := (ipLookupClient{
HTTPClient: http.DefaultClient,
Logger: log.Log,
UserAgent: "ooniprobe-engine/0.1.0",
}).LookupProbeIP(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatal("expected an error here")
}
if ip != DefaultProbeIP {
t.Fatal("expected the default IP here")
}
}
func TestIPLookupInvalidIP(t *testing.T) {
ctx := context.Background()
ip, err := (ipLookupClient{
HTTPClient: http.DefaultClient,
Logger: log.Log,
UserAgent: "ooniprobe-engine/0.1.0",
}).doWithCustomFunc(ctx, invalidIPLookup)
if !errors.Is(err, ErrInvalidIPAddress) {
t.Fatal("expected an error here")
}
if ip != DefaultProbeIP {
t.Fatal("expected the default IP here")
}
}
+53
View File
@@ -0,0 +1,53 @@
package geolocate
import (
"net"
"github.com/oschwald/geoip2-golang"
)
type mmdbLookupper struct{}
func (mmdbLookupper) LookupASN(path, ip string) (asn uint, org string, err error) {
asn, org = DefaultProbeASN, DefaultProbeNetworkName
db, err := geoip2.Open(path)
if err != nil {
return
}
defer db.Close()
record, err := db.ASN(net.ParseIP(ip))
if err != nil {
return
}
asn = record.AutonomousSystemNumber
if record.AutonomousSystemOrganization != "" {
org = record.AutonomousSystemOrganization
}
return
}
// LookupASN returns the ASN and the organization associated with the
// given ip using the ASN database at path.
func LookupASN(path, ip string) (asn uint, org string, err error) {
return (mmdbLookupper{}).LookupASN(path, ip)
}
func (mmdbLookupper) LookupCC(path, ip string) (cc string, err error) {
cc = DefaultProbeCC
db, err := geoip2.Open(path)
if err != nil {
return
}
defer db.Close()
record, err := db.Country(net.ParseIP(ip))
if err != nil {
return
}
// With MaxMind DB we used record.RegisteredCountry.IsoCode but that does
// not seem to work with the db-ip.com database. The record is empty, at
// least for my own IP address in Italy. --Simone (2020-02-25)
if record.Country.IsoCode != "" {
cc = record.Country.IsoCode
}
return
}
@@ -0,0 +1,103 @@
package geolocate
import (
"context"
"net/http"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/resources"
)
const (
asnDBPath = "../testdata/asn.mmdb"
countryDBPath = "../testdata/country.mmdb"
ipAddr = "35.204.49.125"
)
func maybeFetchResources(t *testing.T) {
c := &resources.Client{
HTTPClient: http.DefaultClient,
Logger: log.Log,
UserAgent: "ooniprobe-engine/0.1.0",
WorkDir: "../testdata/",
}
if err := c.Ensure(context.Background()); err != nil {
t.Fatal(err)
}
}
func TestLookupASN(t *testing.T) {
maybeFetchResources(t)
asn, org, err := LookupASN(asnDBPath, ipAddr)
if err != nil {
t.Fatal(err)
}
if asn <= 0 {
t.Fatal("unexpected ASN value")
}
if len(org) <= 0 {
t.Fatal("unexpected org value")
}
}
func TestLookupASNInvalidFile(t *testing.T) {
maybeFetchResources(t)
asn, org, err := LookupASN("/nonexistent", ipAddr)
if err == nil {
t.Fatal("expected an error here")
}
if asn != DefaultProbeASN {
t.Fatal("expected a zero ASN")
}
if org != DefaultProbeNetworkName {
t.Fatal("expected an empty org")
}
}
func TestLookupASNInvalidIP(t *testing.T) {
maybeFetchResources(t)
asn, org, err := LookupASN(asnDBPath, "xxx")
if err == nil {
t.Fatal("expected an error here")
}
if asn != DefaultProbeASN {
t.Fatal("expected a zero ASN")
}
if org != DefaultProbeNetworkName {
t.Fatal("expected an empty org")
}
}
func TestLookupCC(t *testing.T) {
maybeFetchResources(t)
cc, err := (mmdbLookupper{}).LookupCC(countryDBPath, ipAddr)
if err != nil {
t.Fatal(err)
}
if len(cc) != 2 {
t.Fatal("does not seem a country code")
}
}
func TestLookupCCInvalidFile(t *testing.T) {
maybeFetchResources(t)
cc, err := (mmdbLookupper{}).LookupCC("/nonexistent", ipAddr)
if err == nil {
t.Fatal("expected an error here")
}
if cc != DefaultProbeCC {
t.Fatal("expected an empty cc")
}
}
func TestLookupCCInvalidIP(t *testing.T) {
maybeFetchResources(t)
cc, err := (mmdbLookupper{}).LookupCC(asnDBPath, "xxx")
if err == nil {
t.Fatal("expected an error here")
}
if cc != DefaultProbeCC {
t.Fatal("expected an empty cc")
}
}
@@ -0,0 +1,35 @@
package geolocate
import (
"context"
"errors"
"net"
)
var (
// ErrNoIPAddressReturned indicates that no IP address was
// returned by a specific DNS resolver.
ErrNoIPAddressReturned = errors.New("geolocate: no IP address returned")
)
type dnsResolver interface {
LookupHost(ctx context.Context, host string) (addrs []string, err error)
}
type resolverLookupClient struct{}
func (rlc resolverLookupClient) do(ctx context.Context, r dnsResolver) (string, error) {
var ips []string
ips, err := r.LookupHost(ctx, "whoami.akamai.net")
if err != nil {
return "", err
}
if len(ips) < 1 {
return "", ErrNoIPAddressReturned
}
return ips[0], nil
}
func (rlc resolverLookupClient) LookupResolverIP(ctx context.Context) (ip string, err error) {
return rlc.do(ctx, &net.Resolver{})
}
@@ -0,0 +1,50 @@
package geolocate
import (
"context"
"errors"
"testing"
)
func TestLookupResolverIP(t *testing.T) {
addr, err := (resolverLookupClient{}).LookupResolverIP(context.Background())
if err != nil {
t.Fatal(err)
}
if addr == "" {
t.Fatal("expected a non-empty string")
}
}
type brokenHostLookupper struct {
err error
}
func (bhl brokenHostLookupper) LookupHost(ctx context.Context, host string) ([]string, error) {
return nil, bhl.err
}
func TestLookupResolverIPFailure(t *testing.T) {
expected := errors.New("mocked error")
rlc := resolverLookupClient{}
addr, err := rlc.do(context.Background(), brokenHostLookupper{
err: expected,
})
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if len(addr) != 0 {
t.Fatal("expected an empty address")
}
}
func TestLookupResolverIPNoAddressReturned(t *testing.T) {
rlc := resolverLookupClient{}
addr, err := rlc.do(context.Background(), brokenHostLookupper{})
if !errors.Is(err, ErrNoIPAddressReturned) {
t.Fatalf("not the error we expected: %+v", err)
}
if len(addr) != 0 {
t.Fatal("expected an empty address")
}
}
+92
View File
@@ -0,0 +1,92 @@
package geolocate
import (
"context"
"net/http"
"github.com/pion/stun"
)
type stunClient interface {
Close() error
Start(m *stun.Message, h stun.Handler) error
}
type stunConfig struct {
Dial func(network string, address string) (stunClient, error)
Endpoint string
Logger Logger
}
func stunDialer(network string, address string) (stunClient, error) {
return stun.Dial(network, address)
}
func stunIPLookup(ctx context.Context, config stunConfig) (string, error) {
config.Logger.Debugf("STUNIPLookup: start using %s", config.Endpoint)
ip, err := func() (string, error) {
dial := config.Dial
if dial == nil {
dial = stunDialer
}
clnt, err := dial("udp", config.Endpoint)
if err != nil {
return DefaultProbeIP, err
}
defer clnt.Close()
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
errch, ipch := make(chan error, 1), make(chan string, 1)
err = clnt.Start(message, func(ev stun.Event) {
if ev.Error != nil {
errch <- ev.Error
return
}
var xorAddr stun.XORMappedAddress
if err := xorAddr.GetFrom(ev.Message); err != nil {
errch <- err
return
}
ipch <- xorAddr.IP.String()
})
if err != nil {
return DefaultProbeIP, err
}
select {
case err := <-errch:
return DefaultProbeIP, err
case ip := <-ipch:
return ip, nil
case <-ctx.Done():
return DefaultProbeIP, ctx.Err()
}
}()
if err != nil {
config.Logger.Debugf("STUNIPLookup: failure using %s: %+v", config.Endpoint, err)
return DefaultProbeIP, err
}
return ip, nil
}
func stunEkigaIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
return stunIPLookup(ctx, stunConfig{
Endpoint: "stun.ekiga.net:3478",
Logger: logger,
})
}
func stunGoogleIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
return stunIPLookup(ctx, stunConfig{
Endpoint: "stun.l.google.com:19302",
Logger: logger,
})
}
+154
View File
@@ -0,0 +1,154 @@
package geolocate
import (
"context"
"errors"
"net"
"net/http"
"testing"
"time"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
"github.com/pion/stun"
)
func TestSTUNIPLookupCanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // stop immediately
ip, err := stunIPLookup(ctx, stunConfig{
Endpoint: "stun.ekiga.net:3478",
Logger: log.Log,
})
if !errors.Is(err, context.Canceled) {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the IP address we expected: %+v", ip)
}
}
func TestSTUNIPLookupDialFailure(t *testing.T) {
expected := errors.New("mocked error")
ctx := context.Background()
ip, err := stunIPLookup(ctx, stunConfig{
Dial: func(network, address string) (stunClient, error) {
return nil, expected
},
Endpoint: "stun.ekiga.net:3478",
Logger: log.Log,
})
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the IP address we expected: %+v", ip)
}
}
type MockableSTUNClient struct {
StartErr error
Event stun.Event
}
func (c MockableSTUNClient) Close() error {
return nil
}
func (c MockableSTUNClient) Start(m *stun.Message, h stun.Handler) error {
if c.StartErr != nil {
return c.StartErr
}
go func() {
<-time.After(100 * time.Millisecond)
h(c.Event)
}()
return nil
}
func TestSTUNIPLookupStartReturnsError(t *testing.T) {
expected := errors.New("mocked error")
ctx := context.Background()
ip, err := stunIPLookup(ctx, stunConfig{
Dial: func(network, address string) (stunClient, error) {
return MockableSTUNClient{StartErr: expected}, nil
},
Endpoint: "stun.ekiga.net:3478",
Logger: log.Log,
})
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the IP address we expected: %+v", ip)
}
}
func TestSTUNIPLookupStunEventContainsError(t *testing.T) {
expected := errors.New("mocked error")
ctx := context.Background()
ip, err := stunIPLookup(ctx, stunConfig{
Dial: func(network, address string) (stunClient, error) {
return MockableSTUNClient{Event: stun.Event{
Error: expected,
}}, nil
},
Endpoint: "stun.ekiga.net:3478",
Logger: log.Log,
})
if !errors.Is(err, expected) {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the IP address we expected: %+v", ip)
}
}
func TestSTUNIPLookupCannotDecodeMessage(t *testing.T) {
ctx := context.Background()
ip, err := stunIPLookup(ctx, stunConfig{
Dial: func(network, address string) (stunClient, error) {
return MockableSTUNClient{Event: stun.Event{
Message: &stun.Message{},
}}, nil
},
Endpoint: "stun.ekiga.net:3478",
Logger: log.Log,
})
if !errors.Is(err, stun.ErrAttributeNotFound) {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the IP address we expected: %+v", ip)
}
}
func TestIPLookupWorksUsingSTUNEkiga(t *testing.T) {
ip, err := stunEkigaIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}
func TestIPLookupWorksUsingSTUNGoogle(t *testing.T) {
ip, err := stunGoogleIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}
+38
View File
@@ -0,0 +1,38 @@
package geolocate
import (
"context"
"encoding/xml"
"net/http"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpx"
)
type ubuntuResponse struct {
XMLName xml.Name `xml:"Response"`
IP string `xml:"Ip"`
}
func ubuntuIPLookup(
ctx context.Context,
httpClient *http.Client,
logger Logger,
userAgent string,
) (string, error) {
data, err := (httpx.Client{
BaseURL: "https://geoip.ubuntu.com/",
HTTPClient: httpClient,
Logger: logger,
UserAgent: userAgent,
}).FetchResource(ctx, "/lookup")
if err != nil {
return DefaultProbeIP, err
}
logger.Debugf("ubuntu: body: %s", string(data))
var v ubuntuResponse
err = xml.Unmarshal(data, &v)
if err != nil {
return DefaultProbeIP, err
}
return v.IP, nil
}
+48
View File
@@ -0,0 +1,48 @@
package geolocate
import (
"context"
"io/ioutil"
"net"
"net/http"
"strings"
"testing"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/engine/internal/httpheader"
)
func TestUbuntuParseError(t *testing.T) {
ip, err := ubuntuIPLookup(
context.Background(),
&http.Client{Transport: FakeTransport{
Resp: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("<")),
},
}},
log.Log,
httpheader.UserAgent(),
)
if err == nil || !strings.HasPrefix(err.Error(), "XML syntax error") {
t.Fatalf("not the error we expected: %+v", err)
}
if ip != DefaultProbeIP {
t.Fatalf("not the expected IP address: %s", ip)
}
}
func TestIPLookupWorksUsingUbuntu(t *testing.T) {
ip, err := ubuntuIPLookup(
context.Background(),
http.DefaultClient,
log.Log,
httpheader.UserAgent(),
)
if err != nil {
t.Fatal(err)
}
if net.ParseIP(ip) == nil {
t.Fatalf("not an IP address: '%s'", ip)
}
}