Commit 4d453dab authored by Yawning Angel's avatar Yawning Angel 🖕

transports/meeklite: uTLS for ClientHello camouflage

There's still some interesting oddities depending on remote server and
what fingerprint is chosen, but I can watch videos online with the
chosen settings and the TBB Azure bridge.

Note: Despite what people are claiming in the Tor Browser bug tracker
it isn't all that hard to use the built in http client with utls.  And
yes, the `transport.go` code does negotiate correctly in a standalone
test case (apart from compatibility related oddities).
parent 816cff15
Changes in version 0.0.9 - UNRELEASED:
- Various meek_lite code cleanups and bug fixes.
- Bug 29077: uTLS for ClientHello camouflage (meek_lite).
Changes in version 0.0.8 - 2019-01-20:
- Bug 24793: Send the correct authorization HTTP header for basic auth.
This diff is collapsed.
require ( v0.0.0-20180321061416-7d56ec4f381e v0.0.0-20190117054722-15f83653abbc v0.0.0-20170116200512-5312a6153412 v1.2.0 v0.0.0-20181015023909-0c41d7ab0a0e v0.0.0-20181011144130-49bb7cea24b1 v1.2.1 v0.0.0-20190105024908-a89e7e6da482 v0.0.0-20190103213133-ff983b9c42bc v0.0.0-20190119204137-ed066c81e75e v0.0.0-20190116161447-11f53e031339 // indirect v0.3.0 // indirect
) v0.0.0-20180321061416-7d56ec4f381e h1:PYcONLFUhr00kGrq7Mf14JRtoXHG7BOSKIfIha0Hu5Q= v0.0.0-20180321061416-7d56ec4f381e/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q= v0.0.0-20190117054722-15f83653abbc h1:k14lBVrBVgZx22Eg5eqQshT12PAsXBfAJK3vm65D/YE= v0.0.0-20190117054722-15f83653abbc/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q= v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= v1.2.0 h1:YWOShuhvg0GqbQpMa60QlCGtEyf7O7HC1Jf0VjdQ60M= v1.2.0/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= v0.0.0-20181015023909-0c41d7ab0a0e h1:IzypfodbhbnViNUO/MEh0FzCUooG97cIGfdggUrUSyU= v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= v0.0.0-20190105024908-a89e7e6da482 h1:mP4Zgxxydw8ljKIA3zHWmzrTKizyULi7vQrRtxqQNmo= v0.0.0-20190105024908-a89e7e6da482/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0= v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q= v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0= v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
......@@ -108,9 +108,9 @@ func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) {
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 +242,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
......@@ -346,7 +346,7 @@ func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs)
conn := &meekConn{
args: ca,
sessionID: id,
transport: &http.Transport{Dial: dialFn},
roundTripper: newRoundTripper(dialFn),
workerWrChan: make(chan []byte, maxChanBacklog),
workerRdChan: make(chan []byte, maxChanBacklog),
workerCloseChan: make(chan struct{}),
* 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
* 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 <>.
package meeklite
import (
utls ""
var errProtocolNegotiated = errors.New("meek_lite: protocol negotiated")
type roundTripper struct {
transport http.RoundTripper
dialFn base.DialFunc
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 = &http.Transport{Dial: rt.dialFn}
return nil
case "https":
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")
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.
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
// TODO: Make this configurable. What "works" is host dependent.
// * HelloChrome_Auto - Failures in a stand alone testcase against
// * HelloFirefox_Auto - Fails with the azure bridge, incompatible group.
// * HelloIOS_Auto - Seems to work.
// Since HelloChrome_Auto works with azure, that's what'll be used for
// now, since that's what the overwelming vast majority of people will
// use.
conn := utls.UClient(rawConn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
if err = conn.Handshake(); err != nil {
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}
// Assume the remote peer is speaking HTTP 1.x + TLS.
rt.transport = &http.Transport{DialTLS: 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)
return net.JoinHostPort(u.Host, u.Scheme)
func newRoundTripper(dialFn base.DialFunc) http.RoundTripper {
return &roundTripper{
dialFn: dialFn,
func init() {
// Attempt to increase compatibility, there's an encrypted link
// underneath, and this doesn't (shouldn't) affect the external
// fingerprint.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment