This is an automated email from the git hooks/post-receive script.
meskio pushed a commit to branch main in repository pluggable-transports/obfs4.
commit 68b17054153565c6fd938aecbbd0132e643207bb Author: meskio meskio@torproject.org AuthorDate: Wed Oct 19 13:02:45 2022 +0200
Cherry-pick meek uTLS support
Mostly from f01e92dd. --- go.mod | 5 +- go.sum | 18 ++++ transports/meeklite/meek.go | 29 ++++- transports/meeklite/transport.go | 222 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 7 deletions(-)
diff --git a/go.mod b/go.mod index 49e491b..0dea33e 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ require ( filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 git.torproject.org/pluggable-transports/goptlib.git v1.0.0 github.com/dchest/siphash v1.2.1 + github.com/refraction-networking/utls v1.1.5 gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 )
go 1.16 diff --git a/go.sum b/go.sum index b542a6b..dd093aa 100644 --- a/go.sum +++ b/go.sum @@ -2,17 +2,35 @@ filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 h1:iJoUgXvhags filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.torproject.org/pluggable-transports/goptlib.git v1.0.0 h1:ElTwFFPKf/tA6x5nuIk9g49JZzS4T5WN+eTQTjqd00A= git.torproject.org/pluggable-transports/goptlib.git v1.0.0/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/refraction-networking/utls v1.1.5 h1:JtrojoNhbUQkBqEg05sP3gDgDj6hIEAAVKbI9lx4n6w= +github.com/refraction-networking/utls v1.1.5/go.mod h1:jRQxtYi7nkq1p28HF2lwOH5zQm9aC8rpK0O9lIIzGh8= gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb h1:qRSZHsODmAP5qDvb3YsO7Qnf3TRiVbGxNG/WYnlM4/o= gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb/go.mod h1:gvdJuZuO/tPZyhEV8K3Hmoxv/DWud5L4qEQxfYjEUTo= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/transports/meeklite/meek.go b/transports/meeklite/meek.go index 17c7a67..8f9fe35 100644 --- a/transports/meeklite/meek.go +++ b/transports/meeklite/meek.go @@ -44,6 +44,8 @@ import ( "sync" "time"
+ utls "github.com/refraction-networking/utls" + "git.torproject.org/pluggable-transports/goptlib.git" "gitlab.com/yawning/obfs4.git/transports/base" ) @@ -51,6 +53,7 @@ import ( const ( urlArg = "url" frontArg = "front" + utlsArg = "utls"
maxChanBacklog = 16
@@ -73,6 +76,8 @@ var ( type meekClientArgs struct { url *gourl.URL front string + + utls *utls.ClientHelloID }
func (ca *meekClientArgs) Network() string { @@ -104,13 +109,19 @@ func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) { // Parse the (optional) front argument. ca.front, _ = args.Get(frontArg)
+ // Parse the (optional) utls argument. + utlsOpt, _ := args.Get(utlsArg) + if ca.utls, err = parseClientHelloID(utlsOpt); err != nil { + return nil, err + } + return ca, nil }
type meekConn struct { - args *meekClientArgs - sessionID string - transport *http.Transport + args *meekClientArgs + sessionID string + roundTripper http.RoundTripper
closeOnce sync.Once workerWrChan chan []byte @@ -242,7 +253,7 @@ func (c *meekConn) roundTrip(sndBuf []byte) (recvBuf []byte, err error) { req.Header.Set("X-Session-Id", c.sessionID) req.Header.Set("User-Agent", "")
- resp, err = c.transport.RoundTrip(req) + resp, err = c.roundTripper.RoundTrip(req) if err != nil { return nil, err } @@ -343,10 +354,18 @@ func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs) return nil, err }
+ var rt http.RoundTripper + switch ca.utls { + case nil: + rt = &http.Transport{Dial: dialFn} + default: + rt = newRoundTripper(dialFn, ca.utls) + } + conn := &meekConn{ args: ca, sessionID: id, - transport: &http.Transport{Dial: dialFn}, + roundTripper: rt, workerWrChan: make(chan []byte, maxChanBacklog), workerRdChan: make(chan []byte, maxChanBacklog), workerCloseChan: make(chan struct{}), diff --git a/transports/meeklite/transport.go b/transports/meeklite/transport.go new file mode 100644 index 0000000..82ae9fe --- /dev/null +++ b/transports/meeklite/transport.go @@ -0,0 +1,222 @@ +/* + * 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" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + utls "github.com/refraction-networking/utls" + "gitlab.com/yawning/obfs4.git/transports/base" + "golang.org/x/net/http2" +) + +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, + "hellochrome_auto": &utls.HelloChrome_Auto, + "hellochrome_58": &utls.HelloChrome_58, + "hellochrome_62": &utls.HelloChrome_62, + "hellochrome_70": &utls.HelloChrome_70, + "helloios_auto": &utls.HelloIOS_Auto, + "helloios_11_1": &utls.HelloIOS_11_1, + } + defaultClientHello = &utls.HelloFirefox_Auto +) + +type roundTripper struct { + sync.Mutex + + clientHelloID *utls.ClientHelloID + dialFn base.DialFunc + transport http.RoundTripper + + initConn net.Conn +} + +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": + rt.transport = newHTTPTransport(rt.dialFn, nil) + 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 + } + + conn := utls.UClient(rawConn, &utls.Config{ + ServerName: host, + + // `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) + + 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. + rt.transport = newHTTPTransport(nil, rt.dialTLS) + } + + // 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) + } + pInt, _ := net.LookupPort("tcp", u.Scheme) + + return net.JoinHostPort(u.Host, strconv.Itoa(pInt)) +} + +func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID) http.RoundTripper { + return &roundTripper{ + clientHelloID: clientHelloID, + dialFn: dialFn, + } +} + +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 + } + } + return nil, fmt.Errorf("invalid ClientHelloID: '%v'", s) +} + +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, + } +} + +func init() { + // Attempt to increase compatibility, there's an encrypted link + // underneath, and this doesn't (shouldn't) affect the external + // fingerprint. + utls.EnableWeakCiphers() +}