forked from ooni/probe-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dialer.go
390 lines (345 loc) · 13.3 KB
/
dialer.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
package netxlite
//
// Code for dialing TCP or UDP net.Conn-like connections
//
import (
"context"
"errors"
"net"
"sync"
"time"
"github.com/ooni/probe-cli/v3/internal/model"
)
// NewDialerWithStdlibResolver is equivalent to creating a system resolver
// using NewStdlibResolver and then a dialer using NewDialerWithResolver where
// the resolver argument is the previously created resolver.
func NewDialerWithStdlibResolver(dl model.DebugLogger) model.Dialer {
reso := NewStdlibResolver(dl)
return NewDialerWithResolver(dl, reso)
}
// NewDialerWithResolver is equivalent to calling WrapDialer with
// the dialer argument being equal to &DialerSystem{}.
func NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer {
return WrapDialer(dl, r, &DialerSystem{}, w...)
}
// WrapDialer wraps an existing Dialer to add extra functionality
// such as separting DNS lookup and connecting, error wrapping, logging, etc.
//
// When possible use NewDialerWithResolver or NewDialerWithoutResolver
// instead of using this rather low-level function.
//
// # Arguments
//
// 1. logger is used to emit debug messages (MUST NOT be nil);
//
// 2. resolver is the resolver to use when dialing for endpoint
// addresses containing domain names (MUST NOT be nil);
//
// 3. baseDialer is the dialer to wrap (MUST NOT be nil);
//
// 4. wrappers is a list of zero or more functions allowing you to
// modify the behavior of the returned dialer (see below). Please note
// that this function will just ignore any nil wrapper.
//
// # Return value
//
// The returned dialer is an opaque type consisting of the composition of
// several simple dialers. The following pseudo code illustrates the general
// behavior of the returned composed dialer:
//
// addrs, err := dnslookup()
// if err != nil {
// return nil, err
// }
// errors := []error{}
// for _, a := range addrs {
// conn, err := tcpconnect(a)
// if err != nil {
// errors = append(errors, err)
// continue
// }
// return conn, nil
// }
// return nil, errors[0]
//
// The following table describes the structure of the returned dialer:
//
// +-------+-----------------+------------------------------------------+
// | Index | Name | Description |
// +-------+-----------------+------------------------------------------+
// | 0 | base | the baseDialer argument |
// +-------+-----------------+------------------------------------------+
// | 1 | errWrapper | wraps Go errors to be consistent with |
// | | | OONI df-007-errors spec |
// +-------+-----------------+------------------------------------------+
// | 2 | ??? | if there are wrappers, result of calling |
// | | | the first one on the errWrapper dialer |
// +-------+-----------------+------------------------------------------+
// | ... | ... | ... |
// +-------+-----------------+------------------------------------------+
// | N | ??? | if there are wrappers, result of calling |
// | | | the last one on the N-1 dialer |
// +-------+-----------------+------------------------------------------+
// | N+1 | logger (inner) | logs TCP connect operations |
// +-------+-----------------+------------------------------------------+
// | N+2 | resolver | DNS lookup and try connect each IP in |
// | | | sequence until one of them succeeds |
// +-------+-----------------+------------------------------------------+
// | N+3 | logger (outer) | logs the overall dial operation |
// +-------+-----------------+------------------------------------------+
//
// The list of wrappers allows to insert modified dialers in the correct
// place for observing and saving I/O events (connect, read, etc.).
//
// # Remarks
//
// When the resolver is &NullResolver{} any attempt to perform DNS resolutions
// in the dialer at index N+2 will fail with ErrNoResolver.
//
// Otherwise, the dialer at index N+2 will try each resolver IP address
// sequentially. In case of failure, such a resolver will return the first
// error that occurred. This implementation strategy is a QUIRK that is
// documented at TODO(https://github.com/ooni/probe/issues/1779).
//
// If the baseDialer is &DialerSystem{}, there will be a fixed TCP connect
// timeout for each connect operation. Because there may be multiple IP
// addresses per dial, the overall timeout would be a multiple of the timeout
// of a single connect operation. You may want to use the context to reduce
// the overall time spent trying all addresses and timing out.
func WrapDialer(logger model.DebugLogger, resolver model.Resolver,
baseDialer model.Dialer, wrappers ...model.DialerWrapper) (outDialer model.Dialer) {
outDialer = &dialerErrWrapper{
Dialer: baseDialer,
}
for _, wrapper := range wrappers {
if wrapper == nil {
continue // ignore as documented
}
outDialer = wrapper.WrapDialer(outDialer) // extend with user-supplied constructors
}
return &dialerLogger{
Dialer: &dialerResolverWithTracing{
Dialer: &dialerLogger{
Dialer: outDialer,
DebugLogger: logger,
operationSuffix: "_address",
},
Resolver: resolver,
},
DebugLogger: logger,
}
}
// NewDialerWithoutResolver is equivalent to calling NewDialerWithResolver
// with the resolver argument being &NullResolver{}.
func NewDialerWithoutResolver(dl model.DebugLogger, w ...model.DialerWrapper) model.Dialer {
return NewDialerWithResolver(dl, &NullResolver{}, w...)
}
// DialerSystem is a model.Dialer that uses the stdlib's net.Dialer
// to construct the new SimpleDialer used for dialing. This dialer has
// a fixed timeout for each connect operation equal to 15 seconds.
type DialerSystem struct {
// timeout is the OPTIONAL timeout (for testing).
timeout time.Duration
}
var _ model.Dialer = &DialerSystem{}
const dialerDefaultTimeout = 15 * time.Second
func (d *DialerSystem) configuredTimeout() time.Duration {
t := d.timeout
if t <= 0 {
t = dialerDefaultTimeout
}
return t
}
func (d *DialerSystem) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return TProxy.DialContext(ctx, d.configuredTimeout(), network, address)
}
func (d *DialerSystem) CloseIdleConnections() {
// nothing to do here
}
// dialerResolverWithTracing combines dialing with domain name resolution and
// implements hooks to trace TCP (or UDP) connect operations.
type dialerResolverWithTracing struct {
Dialer model.Dialer
Resolver model.Resolver
}
var _ model.Dialer = &dialerResolverWithTracing{}
// DialContext implements model.Dialer.DialContext. Specifically this
// method performs the following operations:
//
// 1. resolve the domain inside the address using a resolver;
//
// 2. cycle through the available IP addresses and try to dial each of them;
//
// 3. trace the TCP (or UDP) connect and allow wrapping the returned conn.
func (d *dialerResolverWithTracing) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
// QUIRK: this routine and the related routines in quirks.go cannot
// be changed easily until we use events tracing to measure.
//
// Reference issue: TODO(https://github.com/ooni/probe/issues/1779).
onlyhost, onlyport, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
addrs, err := d.lookupHost(ctx, onlyhost)
if err != nil {
return nil, err
}
addrs = quirkSortIPAddrs(addrs)
var errorslist []error
trace := ContextTraceOrDefault(ctx)
for _, addr := range addrs {
target := net.JoinHostPort(addr, onlyport)
started := trace.TimeNow()
conn, err := d.Dialer.DialContext(ctx, network, target)
finished := trace.TimeNow()
// TODO(bassosimone): to make the code robust to future refactoring we have
// moved error wrapping inside this type. This change opens up the possibility
// of simplifying the dialing chain by removing dialerErrWrapper. We'll be
// able to implement this refactoring once netx is gone. We cannot complete
// this refactoring _before_ because WrapDialer inserts extra wrappers
// provided by netx in the dialers chain _before_ this dialer and the dialers
// that netx insert assume that they wrap a dialer with error wrapping.
//
// Because error wrapping should be idempotent, it should not be a problem
// to have two error wrapping dialers in the chain except that, of course, it
// would be less efficient than just having a single wrapper.
err = MaybeNewErrWrapper(ClassifyGenericError, ConnectOperation, err)
trace.OnConnectDone(started, network, onlyhost, target, err, finished)
if err == nil {
conn = &dialerErrWrapperConn{conn}
return trace.MaybeWrapNetConn(conn), nil
}
errorslist = append(errorslist, err)
}
return nil, quirkReduceErrors(errorslist)
}
// lookupHost ensures we correctly handle IP addresses.
func (d *dialerResolverWithTracing) lookupHost(ctx context.Context, hostname string) ([]string, error) {
if net.ParseIP(hostname) != nil {
return []string{hostname}, nil
}
return d.Resolver.LookupHost(ctx, hostname)
}
func (d *dialerResolverWithTracing) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
d.Resolver.CloseIdleConnections()
}
// dialerLogger is a Dialer with logging.
type dialerLogger struct {
// Dialer is the underlying dialer.
Dialer model.Dialer
// DebugLogger is the underlying logger.
DebugLogger model.DebugLogger
// operationSuffix is appended to the operation name.
//
// We use this suffix to distinguish the output from dialing
// with the output from dialing an IP address when we are
// using a dialer without resolver, where otherwise both lines
// would read something like `dial 8.8.8.8:443...`
operationSuffix string
}
var _ model.Dialer = &dialerLogger{}
func (d *dialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
d.DebugLogger.Debugf("dial%s %s/%s...", d.operationSuffix, address, network)
start := time.Now()
conn, err := d.Dialer.DialContext(ctx, network, address)
elapsed := time.Since(start)
if err != nil {
d.DebugLogger.Debugf("dial%s %s/%s... %s in %s", d.operationSuffix,
address, network, err, elapsed)
return nil, err
}
d.DebugLogger.Debugf("dial%s %s/%s... ok in %s", d.operationSuffix,
address, network, elapsed)
return conn, nil
}
func (d *dialerLogger) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
}
// ErrNoConnReuse is the type of error returned when you create a
// "single use" dialer or a "single use" TLS dialer and you dial
// more than once, which is not supported by such a dialer.
var ErrNoConnReuse = errors.New("cannot reuse connection")
// NewSingleUseDialer returns a "single use" dialer. The first
// dial will succed and return conn regardless of the network
// and address arguments passed to DialContext. Any subsequent
// dial returns ErrNoConnReuse.
func NewSingleUseDialer(conn net.Conn) model.Dialer {
return &dialerSingleUse{conn: conn}
}
// dialerSingleUse is the Dialer returned by NewSingleDialer.
type dialerSingleUse struct {
mu sync.Mutex
conn net.Conn
}
var _ model.Dialer = &dialerSingleUse{}
func (s *dialerSingleUse) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
defer s.mu.Unlock()
s.mu.Lock()
if s.conn == nil {
return nil, ErrNoConnReuse
}
var conn net.Conn
conn, s.conn = s.conn, nil
return conn, nil
}
func (s *dialerSingleUse) CloseIdleConnections() {
// nothing to do
}
// dialerErrWrapper is a dialer that performs error wrapping. The connection
// returned by the DialContext function will also perform error wrapping.
type dialerErrWrapper struct {
Dialer model.Dialer
}
var _ model.Dialer = &dialerErrWrapper{}
func (d *dialerErrWrapper) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil {
return nil, NewErrWrapper(ClassifyGenericError, ConnectOperation, err)
}
return &dialerErrWrapperConn{Conn: conn}, nil
}
func (d *dialerErrWrapper) CloseIdleConnections() {
d.Dialer.CloseIdleConnections()
}
// dialerErrWrapperConn is a net.Conn that performs error wrapping.
type dialerErrWrapperConn struct {
net.Conn
}
var _ net.Conn = &dialerErrWrapperConn{}
func (c *dialerErrWrapperConn) Read(b []byte) (int, error) {
count, err := c.Conn.Read(b)
if err != nil {
return 0, NewErrWrapper(ClassifyGenericError, ReadOperation, err)
}
return count, nil
}
func (c *dialerErrWrapperConn) Write(b []byte) (int, error) {
count, err := c.Conn.Write(b)
if err != nil {
return 0, NewErrWrapper(ClassifyGenericError, WriteOperation, err)
}
return count, nil
}
func (c *dialerErrWrapperConn) Close() error {
err := c.Conn.Close()
if err != nil {
return NewErrWrapper(ClassifyGenericError, CloseOperation, err)
}
return nil
}
// ErrNoDialer is the type of error returned by "null" dialers
// when you attempt to dial with them.
var ErrNoDialer = errors.New("no configured dialer")
// NewNullDialer returns a dialer that always fails with ErrNoDialer.
func NewNullDialer() model.Dialer {
return &nullDialer{}
}
type nullDialer struct{}
var _ model.Dialer = &nullDialer{}
func (*nullDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return nil, ErrNoDialer
}
func (*nullDialer) CloseIdleConnections() {
// nothing to do
}