transport.go 7.24 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
 * Copyright (c) 2019 Yawning Angel <yawning at schwanenlied dot me>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package meeklite

import (
	"crypto/tls"
22
	"crypto/x509"
23
24
25
26
27
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
28
	"strconv"
29
30
31
	"strings"
	"sync"

32
	"gitlab.com/yawning/obfs4.git/common/log"
33
	"gitlab.com/yawning/obfs4.git/transports/base"
34
35
	utls "gitlab.com/yawning/utls.git"
	"golang.org/x/net/http2"
36
37
)

38
39
40
41
42
43
44
45
46
47
48
49
50
var (
	errProtocolNegotiated = errors.New("meek_lite: protocol negotiated")

	// This should be kept in sync with what is available in utls.
	clientHelloIDMap = map[string]*utls.ClientHelloID{
		"hellogolang":           nil, // Don't bother with utls.
		"hellorandomized":       &utls.HelloRandomized,
		"hellorandomizedalpn":   &utls.HelloRandomizedALPN,
		"hellorandomizednoalpn": &utls.HelloRandomizedNoALPN,
		"hellofirefox_auto":     &utls.HelloFirefox_Auto,
		"hellofirefox_55":       &utls.HelloFirefox_55,
		"hellofirefox_56":       &utls.HelloFirefox_56,
		"hellofirefox_63":       &utls.HelloFirefox_63,
51
		"hellofirefix_65":       &utls.HelloFirefox_65,
52
53
54
55
		"hellochrome_auto":      &utls.HelloChrome_Auto,
		"hellochrome_58":        &utls.HelloChrome_58,
		"hellochrome_62":        &utls.HelloChrome_62,
		"hellochrome_70":        &utls.HelloChrome_70,
56
		"hellochrome_72":        &utls.HelloChrome_72,
57
		"hellochrome_83":        &utls.HelloChrome_83,
58
59
		"helloios_auto":         &utls.HelloIOS_Auto,
		"helloios_11_1":         &utls.HelloIOS_11_1,
60
		"helloios_12_1":         &utls.HelloIOS_12_1,
61
	}
62
	defaultClientHello = &utls.HelloFirefox_Auto
63
)
64
65
66
67

type roundTripper struct {
	sync.Mutex

68
69
70
	clientHelloID *utls.ClientHelloID
	dialFn        base.DialFunc
	transport     http.RoundTripper
71

72
73
	initConn    net.Conn
	disableHPKP bool
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
}

func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	// Note: This isn't protected with a lock, since the meeklite ioWorker
	// serializes RoundTripper requests.
	//
	// This also assumes that req.URL.Host will remain constant for the
	// lifetime of the roundTripper, which is a valid assumption for meeklite.
	if rt.transport == nil {
		if err := rt.getTransport(req); err != nil {
			return nil, err
		}
	}
	return rt.transport.RoundTrip(req)
}

func (rt *roundTripper) getTransport(req *http.Request) error {
	switch strings.ToLower(req.URL.Scheme) {
	case "http":
93
		rt.transport = newHTTPTransport(rt.dialFn, nil)
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
		return nil
	case "https":
	default:
		return fmt.Errorf("meek_lite: invalid URL scheme: '%v'", req.URL.Scheme)
	}

	_, err := rt.dialTLS("tcp", getDialTLSAddr(req.URL))
	switch err {
	case errProtocolNegotiated:
	case nil:
		// Should never happen.
		panic("meek_lite: dialTLS returned no error when determining transport")
	default:
		return err
	}

	return nil
}

func (rt *roundTripper) dialTLS(network, addr string) (net.Conn, error) {
	// Unlike rt.transport, this is protected by a critical section
	// since past the initial manual call from getTransport, the HTTP
	// client will be the caller.
	rt.Lock()
	defer rt.Unlock()

	// If we have the connection from when we determined the HTTPS
	// transport to use, return that.
	if conn := rt.initConn; conn != nil {
		rt.initConn = nil
		return conn, nil
	}

	rawConn, err := rt.dialFn(network, addr)
	if err != nil {
		return nil, err
	}

	var host string
	if host, _, err = net.SplitHostPort(addr); err != nil {
		host = addr
	}

137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
	var verifyPeerCertificateFn func([][]byte, [][]*x509.Certificate) error
	if !rt.disableHPKP {
		if pinHost, ok := builtinPinDB.HasPins(host); ok {
			if rt.transport == nil {
				log.Debugf("meek_lite - HPKP enabled for host: %v", pinHost)
			}
			verifyPeerCertificateFn = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
				if !builtinPinDB.Validate(pinHost, verifiedChains) {
					log.Errorf("meek_lite - HPKP validation failure, potential MITM for host: %v", pinHost)
					return fmt.Errorf("meek_lite: HPKP validation failure for host: %v", pinHost)
				}
				return nil
			}
		}
	} else if rt.transport == nil {
		log.Warnf("meek_lite - HPKP disabled for host: %v", host)
	}

155
156
157
158
159
160
161
162
163
	conn := utls.UClient(rawConn, &utls.Config{
		ServerName:            host,
		VerifyPeerCertificate: verifyPeerCertificateFn,

		// `crypto/tls` gradually ramps up the record size.  While this is
		// a good optimization and is a relatively common server feature,
		// neither Firefox nor Chromium appear to use such optimizations.
		DynamicRecordSizingDisabled: true,
	}, *rt.clientHelloID)
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
	if err = conn.Handshake(); err != nil {
		conn.Close()
		return nil, err
	}

	if rt.transport != nil {
		return conn, nil
	}

	// No http.Transport constructed yet, create one based on the results
	// of ALPN.
	switch conn.ConnectionState().NegotiatedProtocol {
	case http2.NextProtoTLS:
		// The remote peer is speaking HTTP 2 + TLS.
		rt.transport = &http2.Transport{DialTLS: rt.dialTLSHTTP2}
	default:
		// Assume the remote peer is speaking HTTP 1.x + TLS.
181
		rt.transport = newHTTPTransport(nil, rt.dialTLS)
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
	}

	// Stash the connection just established for use servicing the
	// actual request (should be near-immediate).
	rt.initConn = conn

	return nil, errProtocolNegotiated
}

func (rt *roundTripper) dialTLSHTTP2(network, addr string, cfg *tls.Config) (net.Conn, error) {
	return rt.dialTLS(network, addr)
}

func getDialTLSAddr(u *url.URL) string {
	host, port, err := net.SplitHostPort(u.Host)
	if err == nil {
		return net.JoinHostPort(host, port)
	}
200
	pInt, _ := net.LookupPort("tcp", u.Scheme)
201

202
	return net.JoinHostPort(u.Host, strconv.Itoa(pInt))
203
204
}

205
func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID, disableHPKP bool) http.RoundTripper {
206
	return &roundTripper{
207
208
		clientHelloID: clientHelloID,
		dialFn:        dialFn,
209
		disableHPKP:   disableHPKP,
210
211
212
213
214
215
216
217
218
219
220
221
222
223
	}
}

func parseClientHelloID(s string) (*utls.ClientHelloID, error) {
	s = strings.ToLower(s)
	switch s {
	case "none":
		return nil, nil
	case "":
		return defaultClientHello, nil
	default:
		if ret := clientHelloIDMap[s]; ret != nil {
			return ret, nil
		}
224
	}
225
	return nil, fmt.Errorf("invalid ClientHelloID: '%v'", s)
226
227
}

228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
func newHTTPTransport(dialFn, dialTLSFn base.DialFunc) *http.Transport {
	base := (http.DefaultTransport).(*http.Transport)

	return &http.Transport{
		Dial:    dialFn,
		DialTLS: dialTLSFn,

		// Use default configuration values, taken from the runtime.
		MaxIdleConns:          base.MaxIdleConns,
		IdleConnTimeout:       base.IdleConnTimeout,
		TLSHandshakeTimeout:   base.TLSHandshakeTimeout,
		ExpectContinueTimeout: base.ExpectContinueTimeout,
	}
}

243
func init() {
244
245
246
	// Attempt to increase compatibility and performance, there's an
	// encrypted link underneath, and this doesn't (shouldn't) affect
	// the external fingerprint.
247
	utls.EnableWeakCiphers()
248
	utls.EnableVartimeGroups()
249
	utls.EnableVartimeAES()
250
}