-
Notifications
You must be signed in to change notification settings - Fork 46
/
resolver.go
196 lines (171 loc) · 5.96 KB
/
resolver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package engineresolver
//
// Implementation of model.Resolver
//
import (
"context"
"errors"
"math/rand"
"net"
"net/url"
"sync"
"time"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/legacy/multierror"
"github.com/ooni/probe-cli/v3/internal/logx"
"github.com/ooni/probe-cli/v3/internal/model"
)
// Resolver is the session resolver. Resolver will try to use
// a bunch of DoT/DoH resolvers before falling back to the
// system resolver. The relative priorities of the resolver
// are stored onto the KVStore such that we can remember them
// and therefore we can generally give preference to underlying
// DoT/DoH resolvers that work better.
//
// Make sure you fill the mandatory fields (indicated below)
// before using this data structure.
//
// You MUST NOT modify public fields of this structure once it
// has been created, because that MAY lead to data races.
type Resolver struct {
// ByteCounter is the OPTIONAL byte counter. It will count
// the bytes used by any child resolver except for the
// system resolver, whose bytes ARE NOT counted. If this
// field is not set, then we won't count the bytes.
ByteCounter *bytecounter.Counter
// KVStore is the MANDATORY key-value store where you
// want us to write statistics about which resolver is
// working better in your network.
KVStore model.KeyValueStore
// Logger is the OPTIONAL logger you want us to use
// to emit log messages.
Logger model.Logger
// ProxyURL is the OPTIONAL URL of the socks5 proxy
// we should be using. If not set, then we WON'T use
// any proxy. If set, then we WON'T use any http3
// based resolvers and we WON'T use the system resolver.
ProxyURL *url.URL
// jsonCodec is the OPTIONAL JSON Codec to use. If not set,
// we will construct a default codec.
jsonCodec jsonCodec
// mu provides synchronisation of internal fields.
mu sync.Mutex
// newChildResolverFn is the OPTIONAL function to override
// the construction of a new resolver in unit tests
newChildResolverFn func(h3 bool, URL string) (model.Resolver, error)
// once ensures that CloseIdleConnection is
// run just once.
once sync.Once
// res maps a URL to a child resolver. We will
// construct child resolvers just once and we
// will track them into this field.
res map[string]model.Resolver
}
// CloseIdleConnections closes the idle connections, if any. This
// function is guaranteed to be idempotent.
func (r *Resolver) CloseIdleConnections() {
r.once.Do(r.closeall)
}
// errLookupNotImplemented indicates a given lookup type is not implemented.
var errLookupNotImplemented = errors.New("sessionresolver: lookup not implemented")
// LookupHTTPS implements Resolver.LookupHTTPS.
func (r *Resolver) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
return nil, errLookupNotImplemented
}
// LookupNS implements Resolver.LookupNS.
func (r *Resolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
return nil, errLookupNotImplemented
}
// ErrLookupHost indicates that LookupHost failed.
var ErrLookupHost = errors.New("sessionresolver: LookupHost failed")
// LookupHost implements Resolver.LookupHost. This function returns a
// multierror.Union error on failure, so you can see individual errors
// and get a better picture of what's been going wrong.
func (r *Resolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
state := r.readstatedefault()
r.maybeConfusion(state, time.Now().UnixNano())
defer r.writestate(state)
me := multierror.New(ErrLookupHost)
for _, e := range state {
if r.ProxyURL != nil && r.shouldSkipWithProxy(e) {
r.logger().Infof("sessionresolver: skipping with proxy: %+v", e)
continue // we cannot proxy this URL so ignore it
}
// Hotfix: if the context has been canceled from the outside avoid
// doing a dnslookup, which would mark the resolver as not WAI.
//
// See https://github.com/ooni/probe/issues/2544
if err := ctx.Err(); err != nil {
me.Add(newErrWrapper(err, e.URL))
continue
}
addrs, err := r.lookupHost(ctx, e, hostname)
if err == nil {
return addrs, nil
}
me.Add(newErrWrapper(err, e.URL))
}
return nil, me
}
func (r *Resolver) shouldSkipWithProxy(e *resolverinfo) bool {
URL, err := url.Parse(e.URL)
if err != nil {
return true // please skip
}
switch URL.Scheme {
case "https", "dot", "tcp":
return false // we can handle this
default:
return true // please skip
}
}
func (r *Resolver) lookupHost(ctx context.Context, ri *resolverinfo, hostname string) ([]string, error) {
const ewma = 0.9 // the last sample is very important
re, err := r.getresolver(ri.URL)
if err != nil {
r.logger().Warnf("sessionresolver: getresolver: %s", err.Error())
ri.Score = 0 // this is a hard error
return nil, err
}
op := logx.NewOperationLogger(
r.logger(), "sessionresolver: lookup %s using %s", hostname, ri.URL)
addrs, err := timeLimitedLookup(ctx, re, hostname)
op.Stop(err)
if err == nil {
ri.Score = ewma*1.0 + (1-ewma)*ri.Score // increase score
return addrs, nil
}
ri.Score = ewma*0.0 + (1-ewma)*ri.Score // decrease score
return nil, err
}
// maybeConfusion will rearrange the first elements of the vector
// with low probability, so giving other resolvers a chance
// to run and show that they are also viable. We do not fully
// reorder the vector because that could lead to long runtimes.
//
// The return value is only meaningful for testing.
func (r *Resolver) maybeConfusion(state []*resolverinfo, seed int64) int {
rng := rand.New(rand.NewSource(seed)) // #nosec G404 -- not really important
const confusion = 0.3
if rng.Float64() >= confusion {
return -1
}
switch len(state) {
case 0, 1: // nothing to do
return 0
case 2:
state[0], state[1] = state[1], state[0]
return 2
default:
state[0], state[2] = state[2], state[0]
return 3
}
}
// Network implements Resolver.Network.
func (r *Resolver) Network() string {
return "sessionresolver"
}
// Address implements Resolver.Address.
func (r *Resolver) Address() string {
return ""
}