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