1// Package geolocate implements IP lookup, resolver lookup, and geolocation.
2package geolocate
3
4import (
5	"context"
6	"fmt"
7
8	"github.com/ooni/probe-cli/v3/internal/engine/model"
9	"github.com/ooni/probe-cli/v3/internal/engine/netx"
10	"github.com/ooni/probe-cli/v3/internal/engine/runtimex"
11	"github.com/ooni/probe-cli/v3/internal/version"
12)
13
14const (
15	// DefaultProbeASN is the default probe ASN as number.
16	DefaultProbeASN uint = 0
17
18	// DefaultProbeCC is the default probe CC.
19	DefaultProbeCC = "ZZ"
20
21	// DefaultProbeIP is the default probe IP.
22	DefaultProbeIP = model.DefaultProbeIP
23
24	// DefaultProbeNetworkName is the default probe network name.
25	DefaultProbeNetworkName = ""
26
27	// DefaultResolverASN is the default resolver ASN.
28	DefaultResolverASN uint = 0
29
30	// DefaultResolverIP is the default resolver IP.
31	DefaultResolverIP = "127.0.0.2"
32
33	// DefaultResolverNetworkName is the default resolver network name.
34	DefaultResolverNetworkName = ""
35)
36
37var (
38	// DefaultProbeASNString is the default probe ASN as a string.
39	DefaultProbeASNString = fmt.Sprintf("AS%d", DefaultProbeASN)
40
41	// DefaultResolverASNString is the default resolver ASN as a string.
42	DefaultResolverASNString = fmt.Sprintf("AS%d", DefaultResolverASN)
43)
44
45// Logger is the definition of Logger used by this package.
46type Logger interface {
47	Debug(msg string)
48	Debugf(format string, v ...interface{})
49	Info(msg string)
50	Infof(format string, v ...interface{})
51	Warn(msg string)
52	Warnf(format string, v ...interface{})
53}
54
55// Results contains geolocate results
56type Results struct {
57	// ASN is the autonomous system number
58	ASN uint
59
60	// CountryCode is the country code
61	CountryCode string
62
63	// didResolverLookup indicates whether we did a resolver lookup.
64	didResolverLookup bool
65
66	// NetworkName is the network name
67	NetworkName string
68
69	// IP is the probe IP
70	ProbeIP string
71
72	// ResolverASN is the resolver ASN
73	ResolverASN uint
74
75	// ResolverIP is the resolver IP
76	ResolverIP string
77
78	// ResolverNetworkName is the resolver network name
79	ResolverNetworkName string
80}
81
82// ASNString returns the ASN as a string
83func (r *Results) ASNString() string {
84	return fmt.Sprintf("AS%d", r.ASN)
85}
86
87type probeIPLookupper interface {
88	LookupProbeIP(ctx context.Context) (addr string, err error)
89}
90
91type asnLookupper interface {
92	LookupASN(ip string) (asn uint, network string, err error)
93}
94
95type countryLookupper interface {
96	LookupCC(ip string) (cc string, err error)
97}
98
99type resolverIPLookupper interface {
100	LookupResolverIP(ctx context.Context) (addr string, err error)
101}
102
103// Resolver is a DNS resolver.
104type Resolver interface {
105	LookupHost(ctx context.Context, domain string) ([]string, error)
106	Network() string
107	Address() string
108}
109
110// Config contains configuration for a geolocate Task.
111type Config struct {
112	// Resolver is the resolver we should use when
113	// making requests for discovering the IP. When
114	// this field is not set, we use the stdlib.
115	Resolver Resolver
116
117	// Logger is the logger to use. If not set, then we will
118	// use a logger that discards all messages.
119	Logger Logger
120
121	// UserAgent is the user agent to use. If not set, then
122	// we will use a default user agent.
123	UserAgent string
124}
125
126// Must ensures that NewTask is successful.
127func Must(task *Task, err error) *Task {
128	runtimex.PanicOnError(err, "NewTask failed")
129	return task
130}
131
132// NewTask creates a new instance of Task from config.
133func NewTask(config Config) (*Task, error) {
134	if config.Logger == nil {
135		config.Logger = model.DiscardLogger
136	}
137	if config.UserAgent == "" {
138		config.UserAgent = fmt.Sprintf("ooniprobe-engine/%s", version.Version)
139	}
140	if config.Resolver == nil {
141		config.Resolver = netx.NewResolver(
142			netx.Config{Logger: config.Logger})
143	}
144	return &Task{
145		countryLookupper:     mmdbLookupper{},
146		probeIPLookupper:     ipLookupClient(config),
147		probeASNLookupper:    mmdbLookupper{},
148		resolverASNLookupper: mmdbLookupper{},
149		resolverIPLookupper:  resolverLookupClient{},
150	}, nil
151}
152
153// Task performs a geolocation. You must create a new
154// instance of Task using the NewTask factory.
155type Task struct {
156	countryLookupper     countryLookupper
157	probeIPLookupper     probeIPLookupper
158	probeASNLookupper    asnLookupper
159	resolverASNLookupper asnLookupper
160	resolverIPLookupper  resolverIPLookupper
161}
162
163// Run runs the task.
164func (op Task) Run(ctx context.Context) (*Results, error) {
165	var err error
166	out := &Results{
167		ASN:                 DefaultProbeASN,
168		CountryCode:         DefaultProbeCC,
169		NetworkName:         DefaultProbeNetworkName,
170		ProbeIP:             DefaultProbeIP,
171		ResolverASN:         DefaultResolverASN,
172		ResolverIP:          DefaultResolverIP,
173		ResolverNetworkName: DefaultResolverNetworkName,
174	}
175	ip, err := op.probeIPLookupper.LookupProbeIP(ctx)
176	if err != nil {
177		return out, fmt.Errorf("lookupProbeIP failed: %w", err)
178	}
179	out.ProbeIP = ip
180	asn, networkName, err := op.probeASNLookupper.LookupASN(out.ProbeIP)
181	if err != nil {
182		return out, fmt.Errorf("lookupASN failed: %w", err)
183	}
184	out.ASN = asn
185	out.NetworkName = networkName
186	cc, err := op.countryLookupper.LookupCC(out.ProbeIP)
187	if err != nil {
188		return out, fmt.Errorf("lookupProbeCC failed: %w", err)
189	}
190	out.CountryCode = cc
191	out.didResolverLookup = true
192	// Note: ignoring the result of lookupResolverIP and lookupASN
193	// here is intentional. We don't want this (~minor) failure
194	// to influence the result of the overall lookup. Another design
195	// here could be that of retrying the operation N times?
196	resolverIP, err := op.resolverIPLookupper.LookupResolverIP(ctx)
197	if err != nil {
198		return out, nil // intentional
199	}
200	out.ResolverIP = resolverIP
201	resolverASN, resolverNetworkName, err := op.resolverASNLookupper.LookupASN(
202		out.ResolverIP,
203	)
204	if err != nil {
205		return out, nil // intentional
206	}
207	out.ResolverASN = resolverASN
208	out.ResolverNetworkName = resolverNetworkName
209	return out, nil
210}
211