Browse Source

Add support for HTTP/2

- Due to use of a custom tls.Conn type, "net/http" will never use
  HTTP/2, so pre-dial one TLS connection and check the negotiated
  application protocol and explicitly create a http2.Transport when
  required.

- Pre-dial is only performed for HTTPS.

- Since http2.Transport doesn't support ResponseHeaderTimeout,
  implement round trip timeout using the newer Context mechanism.
  This impacts both HTTP and HTTPS and sets the timeout window
  to include the entire round trip, which closes a TODO.

- Add allowance for status code 200 when 206 is expected. This
  matches an allowance added for remote server list download.
Rod Hynes 8 years ago
parent
commit
aaedd523b3
1 changed files with 142 additions and 15 deletions
  1. 142 15
      psiphon/meekConn.go

+ 142 - 15
psiphon/meekConn.go

@@ -23,6 +23,7 @@ import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
 	"crypto/rand"
 	"crypto/rand"
+	golangtls "crypto/tls"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
@@ -33,13 +34,16 @@ import (
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
+	"sync/atomic"
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Inc/goarista/monotime"
 	"github.com/Psiphon-Inc/goarista/monotime"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tls"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
+	"golang.org/x/net/http2"
 )
 )
 
 
 // MeekConn is based on meek-client.go from Tor and Psiphon:
 // MeekConn is based on meek-client.go from Tor and Psiphon:
@@ -138,6 +142,7 @@ type MeekConn struct {
 	additionalHeaders       http.Header
 	additionalHeaders       http.Header
 	cookie                  *http.Cookie
 	cookie                  *http.Cookie
 	pendingConns            *common.Conns
 	pendingConns            *common.Conns
+	cachedTLSDialer         *cachedTLSDialer
 	transport               transporter
 	transport               transporter
 	mutex                   sync.Mutex
 	mutex                   sync.Mutex
 	isClosed                bool
 	isClosed                bool
@@ -156,9 +161,7 @@ type MeekConn struct {
 
 
 // transporter is implemented by both http.Transport and upstreamproxy.ProxyAuthTransport.
 // transporter is implemented by both http.Transport and upstreamproxy.ProxyAuthTransport.
 type transporter interface {
 type transporter interface {
-	CancelRequest(req *http.Request)
 	CloseIdleConnections()
 	CloseIdleConnections()
-	RegisterProtocol(scheme string, rt http.RoundTripper)
 	RoundTrip(req *http.Request) (resp *http.Response, err error)
 	RoundTrip(req *http.Request) (resp *http.Response, err error)
 }
 }
 
 
@@ -177,7 +180,7 @@ func DialMeek(
 	// Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections,
 	// Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections,
 	// which may be interrupted on MeekConn.Close(). This code previously used the establishTunnel
 	// which may be interrupted on MeekConn.Close(). This code previously used the establishTunnel
 	// pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport
 	// pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport
-	// connections while MeekConn is still in use
+	// connections while MeekConn is still in use.
 	pendingConns := new(common.Conns)
 	pendingConns := new(common.Conns)
 
 
 	// Use a copy of DialConfig with the meek pendingConns
 	// Use a copy of DialConfig with the meek pendingConns
@@ -185,11 +188,21 @@ func DialMeek(
 	*meekDialConfig = *dialConfig
 	*meekDialConfig = *dialConfig
 	meekDialConfig.PendingConns = pendingConns
 	meekDialConfig.PendingConns = pendingConns
 
 
+	var scheme string
+	var cachedTLSDialer *cachedTLSDialer
 	var transport transporter
 	var transport transporter
 	var additionalHeaders http.Header
 	var additionalHeaders http.Header
 	var proxyUrl func(*http.Request) (*url.URL, error)
 	var proxyUrl func(*http.Request) (*url.URL, error)
 
 
+	// Close any cached pre-dialed conn in error cases
+	defer func() {
+		if cachedTLSDialer != nil {
+			cachedTLSDialer.Close()
+		}
+	}()
+
 	if meekConfig.UseHTTPS {
 	if meekConfig.UseHTTPS {
+
 		// Custom TLS dialer:
 		// Custom TLS dialer:
 		//
 		//
 		//  1. ignores the HTTP request address and uses the fronting domain
 		//  1. ignores the HTTP request address and uses the fronting domain
@@ -223,6 +236,8 @@ func DialMeek(
 		// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
 		// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
 		// some short period. This is mitigated by the "impaired" protocol classification mechanism.
 		// some short period. This is mitigated by the "impaired" protocol classification mechanism.
 
 
+		scheme = "https"
+
 		tlsConfig := &CustomTLSConfig{
 		tlsConfig := &CustomTLSConfig{
 			DialAddr:                      meekConfig.DialAddress,
 			DialAddr:                      meekConfig.DialAddress,
 			Dial:                          NewTCPDialer(meekDialConfig),
 			Dial:                          NewTCPDialer(meekDialConfig),
@@ -238,15 +253,83 @@ func DialMeek(
 			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
 			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
 		}
 		}
 
 
-		dialer := NewCustomTLSDialer(tlsConfig)
+		tlsDialer := NewCustomTLSDialer(tlsConfig)
 
 
-		// TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip
-		transport = &http.Transport{
-			Dial: dialer,
-			ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT,
+		// Pre-dial one TLS connection in order to inspect the negotiated
+		// application protocol. Then we create an HTTP/2 or HTTP/1.1 transport
+		// depending on which protocol was negotiated. The TLS dialer
+		// is assumed to negotiate only "h2" or "http/1.1"; or not negotiate
+		// an application protocol.
+		//
+		// We cannot rely on net/http's HTTP/2 support since it's only
+		// activated when http.Transport.DialTLS returns a golang crypto/tls.Conn;
+		// e.g., https://github.com/golang/go/blob/c8aec4095e089ff6ac50d18e97c3f46561f14f48/src/net/http/transport.go#L1040
+		//
+		// The pre-dialed connection is stored in a cachedTLSDialer, which will
+		// return the cached pre-dialed connection to its first Dial caller, and
+		// use the tlsDialer for all other Dials.
+		//
+		// cachedTLSDialer.Close() must be called on all exits paths from this
+		// function and in meek.Close() to ensure the cached conn is closed in
+		// any case where no Dial call is made.
+		//
+		// The pre-dial must be interruptible so that DialMeek doesn't block and
+		// hang/delay a shutdown or end of establishment. So the pre-dial uses
+		// the Controller's PendingConns, not the MeekConn PendingConns. For this
+		// purpose, a special preDialer is configured.
+		//
+		// Only one pre-dial attempt is made; there are no retries. This differs
+		// from roundTrip, which retries and may redial for each retry. Retries
+		// at the pre-dial phase are less useful since there's no active session
+		// to preserve, and establishment will simply try another server. Note
+		// that the underlying TCPDial may still try multiple IP addreses when
+		// the destination is a domain and ir resolves to multiple IP adresses.
+
+		preConfig := &CustomTLSConfig{}
+		*preConfig = *tlsConfig
+		preConfig.Dial = NewTCPDialer(dialConfig)
+		preDialer := NewCustomTLSDialer(preConfig)
+
+		// As DialAddr is set in the CustomTLSConfig, no address is required here.
+		preConn, err := preDialer("tcp", "")
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
+		// Cancel interruptibility to keep this connection alive after establishment.
+		dialConfig.PendingConns.Remove(preConn)
+
+		isHTTP2 := false
+		if tlsConn, ok := preConn.(*tls.Conn); ok {
+			state := tlsConn.ConnectionState()
+			if state.NegotiatedProtocolIsMutual &&
+				state.NegotiatedProtocol == "h2" {
+				isHTTP2 = true
+			}
+		}
+
+		cachedTLSDialer = NewCachedTLSDialer(preConn, tlsDialer)
+
+		// transports will use this pointer since cachedTLSDialer gets set to nil
+		dialer := cachedTLSDialer
+
+		if isHTTP2 {
+			NoticeInfo("negotiated HTTP/2 for %s", meekConfig.DialAddress)
+			transport = &http2.Transport{
+				DialTLS: func(network, addr string, _ *golangtls.Config) (net.Conn, error) {
+					return dialer.Dial(network, addr)
+				},
+			}
+		} else {
+			transport = &http.Transport{
+				DialTLS: dialer.Dial,
+			}
 		}
 		}
+
 	} else {
 	} else {
 
 
+		scheme = "http"
+
 		// The dialer ignores address that http.Transport will pass in (derived
 		// The dialer ignores address that http.Transport will pass in (derived
 		// from the HTTP request URL) and always dials meekConfig.DialAddress.
 		// from the HTTP request URL) and always dials meekConfig.DialAddress.
 		dialer := func(string, string) (net.Conn, error) {
 		dialer := func(string, string) (net.Conn, error) {
@@ -277,7 +360,6 @@ func DialMeek(
 		httpTransport := &http.Transport{
 		httpTransport := &http.Transport{
 			Proxy: proxyUrl,
 			Proxy: proxyUrl,
 			Dial:  dialer,
 			Dial:  dialer,
-			ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT,
 		}
 		}
 		if proxyUrl != nil {
 		if proxyUrl != nil {
 			// Wrap transport with a transport that can perform HTTP proxy auth negotiation
 			// Wrap transport with a transport that can perform HTTP proxy auth negotiation
@@ -290,10 +372,8 @@ func DialMeek(
 		}
 		}
 	}
 	}
 
 
-	// Scheme is always "http". Otherwise http.Transport will try to do another TLS
-	// handshake inside the explicit TLS session (in fronting mode).
 	url := &url.URL{
 	url := &url.URL{
-		Scheme: "http",
+		Scheme: scheme,
 		Host:   meekConfig.HostHeader,
 		Host:   meekConfig.HostHeader,
 		Path:   "/",
 		Path:   "/",
 	}
 	}
@@ -340,6 +420,7 @@ func DialMeek(
 		additionalHeaders:       additionalHeaders,
 		additionalHeaders:       additionalHeaders,
 		cookie:                  cookie,
 		cookie:                  cookie,
 		pendingConns:            pendingConns,
 		pendingConns:            pendingConns,
+		cachedTLSDialer:         cachedTLSDialer,
 		transport:               transport,
 		transport:               transport,
 		isClosed:                false,
 		isClosed:                false,
 		runContext:              runContext,
 		runContext:              runContext,
@@ -355,6 +436,9 @@ func DialMeek(
 		fullSendBuffer:          make(chan *bytes.Buffer, 1),
 		fullSendBuffer:          make(chan *bytes.Buffer, 1),
 	}
 	}
 
 
+	// cachedTLSDialer will now be closed in meek.Close()
+	cachedTLSDialer = nil
+
 	meek.emptyReceiveBuffer <- new(bytes.Buffer)
 	meek.emptyReceiveBuffer <- new(bytes.Buffer)
 	meek.emptySendBuffer <- new(bytes.Buffer)
 	meek.emptySendBuffer <- new(bytes.Buffer)
 	meek.relayWaitGroup.Add(1)
 	meek.relayWaitGroup.Add(1)
@@ -375,6 +459,35 @@ func DialMeek(
 	return meek, nil
 	return meek, nil
 }
 }
 
 
+type cachedTLSDialer struct {
+	usedCachedConn int32
+	cachedConn     net.Conn
+	dialer         Dialer
+}
+
+func NewCachedTLSDialer(cachedConn net.Conn, dialer Dialer) *cachedTLSDialer {
+	return &cachedTLSDialer{
+		cachedConn: cachedConn,
+		dialer:     dialer,
+	}
+}
+
+func (c *cachedTLSDialer) Dial(network, addr string) (net.Conn, error) {
+	if atomic.CompareAndSwapInt32(&c.usedCachedConn, 0, 1) {
+		conn := c.cachedConn
+		c.cachedConn = nil
+		return conn, nil
+	}
+	return c.dialer(network, addr)
+}
+
+func (c *cachedTLSDialer) Close() {
+	if atomic.CompareAndSwapInt32(&c.usedCachedConn, 0, 1) {
+		c.cachedConn.Close()
+		c.cachedConn = nil
+	}
+}
+
 // Close terminates the meek connection. Close waits for the relay processing goroutine
 // Close terminates the meek connection. Close waits for the relay processing goroutine
 // to stop and releases HTTP transport resources.
 // to stop and releases HTTP transport resources.
 // A mutex is required to support net.Conn concurrency semantics.
 // A mutex is required to support net.Conn concurrency semantics.
@@ -388,6 +501,9 @@ func (meek *MeekConn) Close() (err error) {
 	if !isClosed {
 	if !isClosed {
 		meek.stopRunning()
 		meek.stopRunning()
 		meek.pendingConns.CloseAll()
 		meek.pendingConns.CloseAll()
+		if meek.cachedTLSDialer != nil {
+			meek.cachedTLSDialer.Close()
+		}
 		meek.relayWaitGroup.Wait()
 		meek.relayWaitGroup.Wait()
 		meek.transport.CloseIdleConnections()
 		meek.transport.CloseIdleConnections()
 	}
 	}
@@ -713,8 +829,13 @@ func (meek *MeekConn) roundTrip(sendBuffer *bytes.Buffer) (int64, error) {
 			request.ContentLength = int64(contentLength)
 			request.ContentLength = int64(contentLength)
 		}
 		}
 
 
-		// Note: meek.stopRunning() will abort a round trip in flight
-		request = request.WithContext(meek.runContext)
+		// - meek.stopRunning() will abort a round trip in flight
+		// - round trip will abort if it exceeds MEEK_ROUND_TRIP_TIMEOUT
+		requestContext, cancelFunc := context.WithTimeout(
+			meek.runContext,
+			MEEK_ROUND_TRIP_TIMEOUT)
+		defer cancelFunc()
+		request = request.WithContext(requestContext)
 
 
 		meek.addAdditionalHeaders(request)
 		meek.addAdditionalHeaders(request)
 
 
@@ -750,7 +871,10 @@ func (meek *MeekConn) roundTrip(sendBuffer *bytes.Buffer) (int64, error) {
 
 
 		if err == nil {
 		if err == nil {
 
 
-			if response.StatusCode != expectedStatusCode {
+			if response.StatusCode != expectedStatusCode &&
+				// Certain http servers return 200 OK where we expect 206, so accept that.
+				!(expectedStatusCode == http.StatusPartialContent && response.StatusCode == http.StatusOK) {
+
 				// Don't retry when the status code is incorrect
 				// Don't retry when the status code is incorrect
 				response.Body.Close()
 				response.Body.Close()
 				return 0, common.ContextError(
 				return 0, common.ContextError(
@@ -799,6 +923,9 @@ func (meek *MeekConn) roundTrip(sendBuffer *bytes.Buffer) (int64, error) {
 			}
 			}
 		}
 		}
 
 
+		// Release context resources now.
+		cancelFunc()
+
 		// Either the request failed entirely, or there was a failure
 		// Either the request failed entirely, or there was a failure
 		// streaming the response payload. Always retry once. Then
 		// streaming the response payload. Always retry once. Then
 		// retry if time remains; when the next delay exceeds the time
 		// retry if time remains; when the next delay exceeds the time