/
dialer.go
379 lines (322 loc) · 11 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
// package fronted provides a client and server for domain-fronted proxying
// using enproxy proxies.
package fronted
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"encoding/asn1"
"github.com/getlantern/connpool"
"github.com/getlantern/enproxy"
"github.com/getlantern/golog"
"github.com/getlantern/proxy"
"github.com/getlantern/tlsdialer"
)
const (
CONNECT = "CONNECT" // HTTP CONNECT method
)
var (
log = golog.LoggerFor("fronted")
// Cutoff for logging warnings about a dial having taken a long time.
longDialLimit = 10 * time.Second
// idleTimeout needs to be small enough that we stop using connections
// before the upstream server/CDN closes them itself.
// TODO: make this configurable.
idleTimeout = 10 * time.Second
)
// Dialer is a domain-fronted proxy.Dialer.
type Dialer interface {
proxy.Dialer
// HttpClientUsing creates a simple domain-fronted HTTP client using the
// specified Masquerade.
HttpClientUsing(masquerade *Masquerade) *http.Client
// NewDirectDomainFronter creates an HttpClient that domain-fronts but instead
// of using enproxy proxies routes to the destination server directly from the
// CDN. This is useful for web properties registered on the CDN itself, for
// example geo.getiantem.org.
NewDirectDomainFronter() *http.Client
}
// Config captures the configuration of a domain-fronted dialer.
type Config struct {
// Host: the host (e.g. getiantem.org)
Host string
// Port: the port (e.g. 443)
Port int
// Masquerades: the Masquerades to use when domain-fronting. These will be
// verified when the Dialer starts.
Masquerades []*Masquerade
// MaxMasquerades: the maximum number of masquerades to verify. If 0,
// the masquerades are uncapped.
MaxMasquerades int
// PoolSize: if greater than 0, outbound connections will be pooled in an
// eagerly loading connection pool. This can reduce latency when using
// enproxy.
PoolSize int
// InsecureSkipVerify: if true, server's certificate is not verified.
InsecureSkipVerify bool
// RootCAs: optional CertPool specifying the root CAs to use for verifying
// servers
RootCAs *x509.CertPool
// BufferRequests: if true, requests to the proxy will be buffered and sent
// with identity encoding. If false, they'll be streamed with chunked
// encoding.
BufferRequests bool
// DialTimeoutMillis: how long to wait on dialing server before timing out
// (defaults to 30 seconds)
DialTimeoutMillis int
// RedialAttempts: number of times to try redialing. The total number of
// dial attempts will be 1 + RedialAttempts.
RedialAttempts int
// Weight: relative weight versus other servers (for round-robin)
Weight int
// QOS: relative quality of service offered. Should be >= 0, with higher
// values indicating higher QOS.
QOS int
// OnDial: optional callback that gets invoked whenever we dial the server.
// The Conn and error returned from this callback will be used in lieu of
// the originals.
OnDial func(conn net.Conn, err error) (net.Conn, error)
// OnDialStats is an optional callback that will get called on every dial to
// the server to report stats on what was dialed and how long each step
// took.
OnDialStats func(success bool, domain, addr string, resolutionTime, connectTime, handshakeTime time.Duration)
}
// dialer implements the proxy.Dialer interface by dialing domain-fronted
// servers.
type dialer struct {
Config
masquerades *verifiedMasqueradeSet
connPool connpool.Pool
enproxyConfig *enproxy.Config
tlsConfigs map[string]*tls.Config
tlsConfigsMutex sync.Mutex
}
// NewDialer creates a new Dialer for the given Config.
// WARNING - depending on configuration, this Dialer may contain a connection
// pool and/or a set of Masquerades that will leak resources. Make sure to call
// Close() to clean these up when the Dialer is no longer in use.
func NewDialer(cfg Config) Dialer {
d := &dialer{
Config: cfg,
tlsConfigs: make(map[string]*tls.Config),
}
if d.Masquerades != nil {
if d.MaxMasquerades == 0 {
d.MaxMasquerades = len(d.Masquerades)
}
d.masquerades = d.verifiedMasquerades()
}
if cfg.PoolSize > 0 {
d.connPool = connpool.New(connpool.Config{
Size: cfg.PoolSize,
ClaimTimeout: idleTimeout,
Dial: d.dialServer,
})
}
d.enproxyConfig = d.enproxyConfigWith(func(addr string) (net.Conn, error) {
var conn net.Conn
var err error
if d.connPool != nil {
conn, err = d.connPool.Get()
} else {
conn, err = d.dialServer()
}
if d.OnDial != nil {
conn, err = d.OnDial(conn, err)
}
return conn, err
})
return d
}
// Dial dials upstream using domain-fronting.
func (d *dialer) Dial(network, addr string) (net.Conn, error) {
if !strings.Contains(network, "tcp") {
return nil, fmt.Errorf("Protocol %s is not supported, only tcp is supported", network)
}
return enproxy.Dial(addr, d.enproxyConfig)
}
// Close closes the dialer, in particular closing the underlying connection
// pool.
func (d *dialer) Close() error {
if d.connPool != nil {
// We close the connPool on a goroutine so as not to wait for Close to finish
go d.connPool.Close()
}
if d.masquerades != nil {
go d.masquerades.stop()
}
return nil
}
func (d *dialer) HttpClientUsing(masquerade *Masquerade) *http.Client {
enproxyConfig := d.enproxyConfigWith(func(addr string) (net.Conn, error) {
return d.dialServerWith(masquerade)
})
return &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return enproxy.Dial(addr, enproxyConfig)
},
},
}
}
// DirectDomainTransport is a wrapper struct enabling us to modify the protocol of outgoing
// requests to make them all HTTP instead of potentially HTTPS, which breaks our particular
// implemenation of direct domain fronting.
type DirectDomainTransport struct {
http.Transport
}
func (ddf *DirectDomainTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
// The connection is already encrypted by domain fronting. We need to rewrite URLs starting
// with "https://" to "http://", lest we get an error for doubling up on TLS.
// The RoundTrip interface requires that we not modify the memory in the request, so we just
// create a copy.
norm := new(http.Request)
*norm = *req // includes shallow copies of maps, but okay
norm.URL = new(url.URL)
*norm.URL = *req.URL
norm.URL.Scheme = "http"
return ddf.Transport.RoundTrip(norm)
}
// Creates a new http.Client that does direct domain fronting.
func (d *dialer) NewDirectDomainFronter() *http.Client {
log.Debugf("Creating new direct domain fronter.")
return &http.Client{
Transport: &DirectDomainTransport{
Transport: http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
log.Debugf("Dialing %s with direct domain fronter", addr)
return d.dialServer()
},
TLSHandshakeTimeout: 40 * time.Second,
DisableKeepAlives: true,
},
},
}
}
func (d *dialer) enproxyConfigWith(dialProxy func(addr string) (net.Conn, error)) *enproxy.Config {
return &enproxy.Config{
DialProxy: dialProxy,
NewRequest: func(upstreamHost string, method string, body io.Reader) (req *http.Request, err error) {
if upstreamHost == "" {
// No specific host requested, use configured one
upstreamHost = d.Host
}
return http.NewRequest(method, "http://"+upstreamHost+"/", body)
},
BufferRequests: d.BufferRequests,
IdleTimeout: idleTimeout, // TODO: make this configurable
}
}
func (d *dialer) dialServer() (net.Conn, error) {
var masquerade *Masquerade
if d.masquerades != nil {
masquerade = d.masquerades.nextVerified()
}
return d.dialServerWith(masquerade)
}
func (d *dialer) dialServerWith(masquerade *Masquerade) (net.Conn, error) {
dialTimeout := time.Duration(d.DialTimeoutMillis) * time.Millisecond
if dialTimeout == 0 {
dialTimeout = 30 * time.Second
}
// Note - we need to suppress the sending of the ServerName in the client
// handshake to make host-spoofing work with Fastly. If the client Hello
// includes a server name, Fastly checks to make sure that this matches the
// Host header in the HTTP request and if they don't match, it returns
// a 400 Bad Request error.
sendServerNameExtension := false
cwt, err := tlsdialer.DialForTimings(
&net.Dialer{
Timeout: dialTimeout,
},
"tcp",
d.addressForServer(masquerade),
sendServerNameExtension,
d.tlsConfig(masquerade))
if d.OnDialStats != nil {
domain := ""
if masquerade != nil {
domain = masquerade.Domain
}
resultAddr := ""
if err == nil {
resultAddr = cwt.Conn.RemoteAddr().String()
}
d.OnDialStats(err == nil, domain, resultAddr, cwt.ResolutionTime, cwt.ConnectTime, cwt.HandshakeTime)
}
if err != nil && masquerade != nil {
err = fmt.Errorf("Unable to dial masquerade %s: %s", masquerade.Domain, err)
}
return cwt.Conn, err
}
// Get the address to dial for reaching the server
func (d *dialer) addressForServer(masquerade *Masquerade) string {
return fmt.Sprintf("%s:%d", d.serverHost(masquerade), d.Port)
}
func (d *dialer) serverHost(masquerade *Masquerade) string {
serverHost := d.Host
if masquerade != nil {
if masquerade.IpAddress != "" {
serverHost = masquerade.IpAddress
} else if masquerade.Domain != "" {
serverHost = masquerade.Domain
}
}
return serverHost
}
// tlsInfo is a temporary function that could help catching a bug. See this
// related PR: https://github.com/getlantern/lantern/issues/2398
func (d *dialer) tlsInfo(masquerade *Masquerade) string {
var data []string
serverName := d.Host
if masquerade != nil {
serverName = masquerade.Domain
}
var certpool []string
subjects := d.RootCAs.Subjects()
var dest pkix.RDNSequence
for i, _ := range subjects {
_, err := asn1.Unmarshal(subjects[i], &dest)
if err != nil {
certpool = append(certpool, fmt.Sprintf("Error[%d]: %q", i, err.Error()))
} else {
certpool = append(certpool, fmt.Sprintf("RDNSequence[%d]: %q", i, dest))
}
}
data = append(data, fmt.Sprintf("Insecure Skip Verify: %v", d.InsecureSkipVerify))
data = append(data, fmt.Sprintf("Host: %v", d.Host))
data = append(data, fmt.Sprintf("Masquerade Domain: %v", masquerade.Domain))
data = append(data, fmt.Sprintf("Server Name: %v", serverName))
data = append(data, fmt.Sprintf("x509 cert pool subjects: %#s", strings.Join(certpool, " | ")))
return strings.Join(data, ", ")
}
// tlsConfig builds a tls.Config for dialing the upstream host. Constructed
// tls.Configs are cached on a per-masquerade basis to enable client session
// caching and reduce the amount of PEM certificate parsing.
func (d *dialer) tlsConfig(masquerade *Masquerade) *tls.Config {
d.tlsConfigsMutex.Lock()
defer d.tlsConfigsMutex.Unlock()
serverName := d.Host
if masquerade != nil {
serverName = masquerade.Domain
}
tlsConfig := d.tlsConfigs[serverName]
if tlsConfig == nil {
tlsConfig = &tls.Config{
ClientSessionCache: tls.NewLRUClientSessionCache(1000),
InsecureSkipVerify: d.InsecureSkipVerify,
ServerName: serverName,
RootCAs: d.RootCAs,
}
d.tlsConfigs[serverName] = tlsConfig
}
return tlsConfig
}