コード例 #1
0
// DialMeek returns an initialized meek connection. A meek connection is
// an HTTP session which does not depend on an underlying socket connection (although
// persistent HTTP connections are used for performance). This function does not
// wait for the connection to be "established" before returning. A goroutine
// is spawned which will eventually start HTTP polling.
// When frontingAddress is not "", fronting is used. This option assumes caller has
// already checked server entry capabilities.
func DialMeek(
	serverEntry *ServerEntry, sessionId string,
	frontingAddress string, config *DialConfig) (meek *MeekConn, err error) {

	// Configure transport
	// 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
	// pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport
	// connections while MeekConn is still in use
	pendingConns := new(Conns)

	// Use a copy of DialConfig with the meek pendingConns
	meekConfig := new(DialConfig)
	*meekConfig = *config
	meekConfig.PendingConns = pendingConns

	var host string
	var dialer Dialer
	var proxyUrl func(*http.Request) (*url.URL, error)

	if frontingAddress != "" {
		// In this case, host is not what is dialed but is what ends up in the HTTP Host header
		host = serverEntry.MeekFrontingHost

		// Custom TLS dialer:
		//
		//  1. ignores the HTTP request address and uses the fronting domain
		//  2. disables SNI -- SNI breaks fronting when used with CDNs that support SNI on the server side.
		//  3. skips verifying the server cert.
		//
		// Reasoning for #3:
		//
		// With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client
		// will refuse to connect. That's not a successful outcome.
		//
		// With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively
		// targeting Psiphon and classifying the HTTP traffic by Host header or payload signature.
		//
		// However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting
		// something other than Psiphon, the client will connect. This is a successful outcome.
		//
		// What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an
		// unrelated, randomly generated domain name which cannot be used to block direct connections. The
		// Psiphon server IP is sent over meek, but it's in the encrypted cookie.
		//
		// The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol.
		// So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic
		// as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside
		// our threat model; we merely seek to evade mass blocking by taking steps that require progressively
		// more effort to block.
		//
		// There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can
		// classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server
		// selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't
		// 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.

		dialer = NewCustomTLSDialer(
			&CustomTLSConfig{
				Dial:                          NewTCPDialer(meekConfig),
				Timeout:                       meekConfig.ConnectTimeout,
				FrontingAddr:                  fmt.Sprintf("%s:%d", frontingAddress, 443),
				SendServerName:                false,
				SkipVerify:                    true,
				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
			})
	} else {
		// In the unfronted case, host is both what is dialed and what ends up in the HTTP Host header
		host = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)

		if meekConfig.UpstreamProxyUrl != "" {
			// For unfronted meek, we let the http.Transport handle proxying, as the
			// target server hostname has to be in the HTTP request line. Also, in this
			// case, we don't require the proxy to support CONNECT and so we can work
			// through HTTP proxies that don't support it.
			url, err := url.Parse(meekConfig.UpstreamProxyUrl)
			if err != nil {
				return nil, ContextError(err)
			}
			proxyUrl = http.ProxyURL(url)
			meekConfig.UpstreamProxyUrl = ""
		}

		dialer = NewTCPDialer(meekConfig)
	}

	// 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{
		Scheme: "http",
		Host:   host,
		Path:   "/",
	}
	cookie, err := makeCookie(serverEntry, sessionId)
	if err != nil {
		return nil, ContextError(err)
	}
	httpTransport := &http.Transport{
		Proxy: proxyUrl,
		Dial:  dialer,
		ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT,
	}
	var transport transporter
	if proxyUrl != nil {
		// Wrap transport with a transport that can perform HTTP proxy auth negotiation
		transport, err = upstreamproxy.NewProxyAuthTransport(httpTransport)
		if err != nil {
			return nil, ContextError(err)
		}
	} else {
		transport = httpTransport
	}

	// The main loop of a MeekConn is run in the relay() goroutine.
	// A MeekConn implements net.Conn concurrency semantics:
	// "Multiple goroutines may invoke methods on a Conn simultaneously."
	//
	// Read() calls and relay() are synchronized by exchanging control of a single
	// receiveBuffer (bytes.Buffer). This single buffer may be:
	// - in the emptyReceiveBuffer channel when it is available and empty;
	// - in the partialReadBuffer channel when it is available and contains data;
	// - in the fullReadBuffer channel when it is available and full of data;
	// - "checked out" by relay or Read when they are are writing to or reading from the
	//   buffer, respectively.
	// relay() will obtain the buffer from either the empty or partial channel but block when
	// the buffer is full. Read will obtain the buffer from the partial or full channel when
	// there is data to read but block when the buffer is empty.
	// Write() calls and relay() are synchronized in a similar way, using a single
	// sendBuffer.
	meek = &MeekConn{
		frontingAddress:      frontingAddress,
		url:                  url,
		cookie:               cookie,
		pendingConns:         pendingConns,
		transport:            transport,
		isClosed:             false,
		broadcastClosed:      make(chan struct{}),
		relayWaitGroup:       new(sync.WaitGroup),
		emptyReceiveBuffer:   make(chan *bytes.Buffer, 1),
		partialReceiveBuffer: make(chan *bytes.Buffer, 1),
		fullReceiveBuffer:    make(chan *bytes.Buffer, 1),
		emptySendBuffer:      make(chan *bytes.Buffer, 1),
		partialSendBuffer:    make(chan *bytes.Buffer, 1),
		fullSendBuffer:       make(chan *bytes.Buffer, 1),
	}
	// TODO: benchmark bytes.Buffer vs. built-in append with slices?
	meek.emptyReceiveBuffer <- new(bytes.Buffer)
	meek.emptySendBuffer <- new(bytes.Buffer)
	meek.relayWaitGroup.Add(1)
	go meek.relay()

	// Enable interruption
	if !config.PendingConns.Add(meek) {
		meek.Close()
		return nil, ContextError(errors.New("pending connections already closed"))
	}

	return meek, nil
}
コード例 #2
0
// DialMeek returns an initialized meek connection. A meek connection is
// an HTTP session which does not depend on an underlying socket connection (although
// persistent HTTP connections are used for performance). This function does not
// wait for the connection to be "established" before returning. A goroutine
// is spawned which will eventually start HTTP polling.
// When frontingAddress is not "", fronting is used. This option assumes caller has
// already checked server entry capabilities.
func DialMeek(
	meekConfig *MeekConfig,
	dialConfig *DialConfig) (meek *MeekConn, err error) {

	// Configure transport
	// 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
	// pendingConns here, but that was a lifecycle mismatch: we don't want to abort HTTP transport
	// connections while MeekConn is still in use
	pendingConns := new(common.Conns)

	// Use a copy of DialConfig with the meek pendingConns
	meekDialConfig := new(DialConfig)
	*meekDialConfig = *dialConfig
	meekDialConfig.PendingConns = pendingConns

	var transport transporter
	var additionalHeaders http.Header
	var proxyUrl func(*http.Request) (*url.URL, error)

	if meekConfig.UseHTTPS {
		// Custom TLS dialer:
		//
		//  1. ignores the HTTP request address and uses the fronting domain
		//  2. optionally disables SNI -- SNI breaks fronting when used with certain CDNs.
		//  3. skips verifying the server cert.
		//
		// Reasoning for #3:
		//
		// With a TLS MiM attack in place, and server certs verified, we'll fail to connect because the client
		// will refuse to connect. That's not a successful outcome.
		//
		// With a MiM attack in place, and server certs not verified, we'll fail to connect if the MiM is actively
		// targeting Psiphon and classifying the HTTP traffic by Host header or payload signature.
		//
		// However, in the case of a passive MiM that's just recording traffic or an active MiM that's targeting
		// something other than Psiphon, the client will connect. This is a successful outcome.
		//
		// What is exposed to the MiM? The Host header does not contain a Psiphon server IP address, just an
		// unrelated, randomly generated domain name which cannot be used to block direct connections. The
		// Psiphon server IP is sent over meek, but it's in the encrypted cookie.
		//
		// The payload (user traffic) gets its confidentiality and integrity from the underlying SSH protocol.
		// So, nothing is leaked to the MiM apart from signatures which could be used to classify the traffic
		// as Psiphon to possibly block it; but note that not revealing that the client is Psiphon is outside
		// our threat model; we merely seek to evade mass blocking by taking steps that require progressively
		// more effort to block.
		//
		// There is a subtle attack remaining: an adversary that can MiM some CDNs but not others (and so can
		// classify Psiphon traffic on some CDNs but not others) may throttle non-MiM CDNs so that our server
		// selection always chooses tunnels to the MiM CDN (without any server cert verification, we won't
		// 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.

		dialer := NewCustomTLSDialer(&CustomTLSConfig{
			DialAddr:                      meekConfig.DialAddress,
			Dial:                          NewTCPDialer(meekDialConfig),
			Timeout:                       meekDialConfig.ConnectTimeout,
			SNIServerName:                 meekConfig.SNIServerName,
			SkipVerify:                    true,
			UseIndistinguishableTLS:       meekDialConfig.UseIndistinguishableTLS,
			TrustedCACertificatesFilename: meekDialConfig.TrustedCACertificatesFilename,
		})

		// 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,
		}
	} else {

		// The dialer ignores address that http.Transport will pass in (derived
		// from the HTTP request URL) and always dials meekConfig.DialAddress.
		dialer := func(string, string) (net.Conn, error) {
			return NewTCPDialer(meekDialConfig)("tcp", meekConfig.DialAddress)
		}

		// For HTTP, and when the meekConfig.DialAddress matches the
		// meekConfig.HostHeader, we let http.Transport handle proxying.
		// http.Transport will put the the HTTP server address in the HTTP
		// request line. In this one case, we can use an HTTP proxy that does
		// not offer CONNECT support.
		if strings.HasPrefix(meekDialConfig.UpstreamProxyUrl, "http://") &&
			(meekConfig.DialAddress == meekConfig.HostHeader ||
				meekConfig.DialAddress == meekConfig.HostHeader+":80") {
			url, err := url.Parse(meekDialConfig.UpstreamProxyUrl)
			if err != nil {
				return nil, common.ContextError(err)
			}
			proxyUrl = http.ProxyURL(url)
			meekDialConfig.UpstreamProxyUrl = ""

			// Here, the dialer must use the address that http.Transport
			// passes in (which will be proxy address).
			dialer = NewTCPDialer(meekDialConfig)
		}

		// TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip
		httpTransport := &http.Transport{
			Proxy: proxyUrl,
			Dial:  dialer,
			ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT,
		}
		if proxyUrl != nil {
			// Wrap transport with a transport that can perform HTTP proxy auth negotiation
			transport, err = upstreamproxy.NewProxyAuthTransport(httpTransport, meekDialConfig.UpstreamProxyCustomHeaders)
			if err != nil {
				return nil, common.ContextError(err)
			}
		} else {
			transport = httpTransport
		}
	}

	// 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{
		Scheme: "http",
		Host:   meekConfig.HostHeader,
		Path:   "/",
	}

	if meekConfig.UseHTTPS {
		host, _, err := net.SplitHostPort(meekConfig.DialAddress)
		if err != nil {
			return nil, common.ContextError(err)
		}
		additionalHeaders = map[string][]string{
			"X-Psiphon-Fronting-Address": {host},
		}
	} else {
		if proxyUrl == nil {
			additionalHeaders = meekDialConfig.UpstreamProxyCustomHeaders
		}
	}

	cookie, err := makeMeekCookie(meekConfig)
	if err != nil {
		return nil, common.ContextError(err)
	}

	// The main loop of a MeekConn is run in the relay() goroutine.
	// A MeekConn implements net.Conn concurrency semantics:
	// "Multiple goroutines may invoke methods on a Conn simultaneously."
	//
	// Read() calls and relay() are synchronized by exchanging control of a single
	// receiveBuffer (bytes.Buffer). This single buffer may be:
	// - in the emptyReceiveBuffer channel when it is available and empty;
	// - in the partialReadBuffer channel when it is available and contains data;
	// - in the fullReadBuffer channel when it is available and full of data;
	// - "checked out" by relay or Read when they are are writing to or reading from the
	//   buffer, respectively.
	// relay() will obtain the buffer from either the empty or partial channel but block when
	// the buffer is full. Read will obtain the buffer from the partial or full channel when
	// there is data to read but block when the buffer is empty.
	// Write() calls and relay() are synchronized in a similar way, using a single
	// sendBuffer.
	meek = &MeekConn{
		url:                  url,
		additionalHeaders:    additionalHeaders,
		cookie:               cookie,
		pendingConns:         pendingConns,
		transport:            transport,
		isClosed:             false,
		broadcastClosed:      make(chan struct{}),
		relayWaitGroup:       new(sync.WaitGroup),
		emptyReceiveBuffer:   make(chan *bytes.Buffer, 1),
		partialReceiveBuffer: make(chan *bytes.Buffer, 1),
		fullReceiveBuffer:    make(chan *bytes.Buffer, 1),
		emptySendBuffer:      make(chan *bytes.Buffer, 1),
		partialSendBuffer:    make(chan *bytes.Buffer, 1),
		fullSendBuffer:       make(chan *bytes.Buffer, 1),
	}
	// TODO: benchmark bytes.Buffer vs. built-in append with slices?
	meek.emptyReceiveBuffer <- new(bytes.Buffer)
	meek.emptySendBuffer <- new(bytes.Buffer)
	meek.relayWaitGroup.Add(1)
	go meek.relay()

	// Enable interruption
	if !dialConfig.PendingConns.Add(meek) {
		meek.Close()
		return nil, common.ContextError(errors.New("pending connections already closed"))
	}

	return meek, nil
}