Browse Source

Refactored tunnel/meekConn
* Clearer flow and separation of logic for various meek modes.
* All setup cases are now in tunnel.go; meekConn.go simply uses explicit config params.
* API stats are explicit meek config params -- actual on-the-wire values
* Emit meek config params for "Connecting" attempts in addition to successful connections.
* Enable pluggable host name transformation.

Rod Hynes 10 years ago
parent
commit
1675820071
9 changed files with 408 additions and 277 deletions
  1. 8 0
      psiphon/config.go
  2. 5 0
      psiphon/controller.go
  3. 140 128
      psiphon/meekConn.go
  4. 37 22
      psiphon/net.go
  5. 27 11
      psiphon/notice.go
  6. 3 3
      psiphon/opensslConn.go
  7. 26 17
      psiphon/serverApi.go
  8. 11 9
      psiphon/tlsDialer.go
  9. 151 87
      psiphon/tunnel.go

+ 8 - 0
psiphon/config.go

@@ -208,6 +208,10 @@ type Config struct {
 	// This parameter is only applicable to library deployments.
 	DnsServerGetter DnsServerGetter
 
+	// HostNameTransformer is an interface that enables pluggable hostname
+	// transformation circumvention strategies.
+	HostNameTransformer HostNameTransformer
+
 	// TargetServerEntry is an encoded server entry. When specified, this server entry
 	// is used exclusively and all other known servers are ignored.
 	TargetServerEntry string
@@ -359,6 +363,10 @@ func LoadConfig(configJson []byte) (*Config, error) {
 		return nil, ContextError(errors.New("DnsServerGetter interface must be set at runtime"))
 	}
 
+	if config.HostNameTransformer != nil {
+		return nil, ContextError(errors.New("HostNameTransformer interface must be set at runtime"))
+	}
+
 	if config.EmitDiagnosticNotices {
 		setEmitDiagnosticNotices(true)
 	}

+ 5 - 0
psiphon/controller.go

@@ -73,6 +73,11 @@ func NewController(config *Config) (controller *Controller, err error) {
 	// Needed by regen, at least
 	rand.Seed(int64(time.Now().Nanosecond()))
 
+	// Supply a default HostNameTransformer
+	if config.HostNameTransformer == nil {
+		config.HostNameTransformer = &IdentityHostNameTransformer{}
+	}
+
 	// Generate a session ID for the Psiphon server API. This session ID is
 	// used across all tunnels established by the controller.
 	sessionId, err := MakeSessionId()

+ 140 - 128
psiphon/meekConn.go

@@ -59,6 +59,37 @@ const (
 	MEEK_ROUND_TRIP_TIMEOUT        = 20 * time.Second
 )
 
+// MeekConfig specifies the behavior of a MeekConn
+type MeekConfig struct {
+
+	// DialAddress is the actual network address to dial to establish a
+	// connection to the meek server. This may be either a fronted or
+	// direct address. The address must be in the form "host:port",
+	// where host may be a domain name or IP address.
+	DialAddress string
+
+	// UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
+	UseHTTPS bool
+
+	// SNIServerName is the value to place in the TLS SNI server_name
+	// field when HTTPS is used.
+	SNIServerName string
+
+	// HostHeader is the value to place in the HTTP request Host header.
+	HostHeader string
+
+	// TransformedHostName records whether a HostNameTransformer
+	// transformation is in effect. This value is used for stats reporting.
+	TransformedHostName bool
+
+	// The following values are used to create the obfuscated meek cookie.
+
+	PsiphonServerAddress          string
+	SessionID                     string
+	MeekCookieEncryptionPublicKey string
+	MeekObfuscatedKey             string
+}
+
 // MeekConn is a network connection that tunnels TCP over HTTP and supports "fronting". Meek sends
 // client->server flow in HTTP request bodies and receives server->client flow in HTTP response bodies.
 // Polling is used to achieve full duplex TCP.
@@ -71,9 +102,8 @@ const (
 // MeekConn also operates in unfronted mode, in which plain HTTP connections are made without routing
 // through a CDN.
 type MeekConn struct {
-	frontingAddress      string
-	useHTTPS             bool
 	url                  *url.URL
+	additionalHeaders    map[string]string
 	cookie               *http.Cookie
 	pendingConns         *Conns
 	transport            transporter
@@ -105,10 +135,8 @@ type transporter interface {
 // When frontingAddress is not "", fronting is used. This option assumes caller has
 // already checked server entry capabilities.
 func DialMeek(
-	serverEntry *ServerEntry, sessionId string,
-	useHTTPS, useSNI bool,
-	frontingAddress, frontingHost string,
-	config *DialConfig) (meek *MeekConn, err error) {
+	meekConfig *MeekConfig,
+	dialConfig *DialConfig) (meek *MeekConn, err error) {
 
 	// Configure transport
 	// Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections,
@@ -118,136 +146,121 @@ func DialMeek(
 	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 = frontingHost
-
-		if 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.
-
-			customTLSConfig := &CustomTLSConfig{
-				FrontingAddr:                  fmt.Sprintf("%s:%d", frontingAddress, 443),
-				Dial:                          NewTCPDialer(meekConfig),
-				Timeout:                       meekConfig.ConnectTimeout,
-				SendServerName:                useSNI,
-				SkipVerify:                    true,
-				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
-				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
-			}
+	meekDialConfig := new(DialConfig)
+	*meekDialConfig = *dialConfig
+	meekDialConfig.PendingConns = pendingConns
 
-			dialer = NewCustomTLSDialer(customTLSConfig)
+	var transport transporter
 
-		} else { // !useHTTPS
+	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,
+		})
+
+		transport = &http.Transport{
+			Dial: dialer,
+			ResponseHeaderTimeout: MEEK_ROUND_TRIP_TIMEOUT,
+		}
+	} else {
 
-			dialer = func(string, string) (net.Conn, error) {
-				return NewTCPDialer(meekConfig)("tcp", frontingAddress+":80")
+		// For HTTP meek, we let the http.Transport handle proxying. http.Transport will
+		// put the the HTTP server address in the HTTP request line. In this case, we can
+		// use an HTTP proxy that does not support CONNECT.
+		var proxyUrl func(*http.Request) (*url.URL, error)
+		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, ContextError(err)
 			}
+			proxyUrl = http.ProxyURL(url)
+			meekDialConfig.UpstreamProxyUrl = ""
 		}
 
-	} else { // frontingAddress == ""
-
-		// 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 useHTTPS {
-
-			customTLSConfig := &CustomTLSConfig{
-				Dial:                          NewTCPDialer(meekConfig),
-				Timeout:                       meekConfig.ConnectTimeout,
-				SendServerName:                useSNI,
-				SkipVerify:                    true,
-				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
-				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
-			}
+		// 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)
+		}
 
-			dialer = NewCustomTLSDialer(customTLSConfig)
-
-		} else { // !useHTTPS
-
-			if strings.HasPrefix(meekConfig.UpstreamProxyUrl, "http://") {
-				// 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 = ""
+		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)
+			if err != nil {
+				return nil, ContextError(err)
 			}
-
-			dialer = NewTCPDialer(meekConfig)
+		} 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:   host,
+		Host:   meekConfig.HostHeader,
 		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)
+
+	var additionalHeaders map[string]string
+	if meekConfig.UseHTTPS {
+		host, _, err := net.SplitHostPort(meekConfig.DialAddress)
 		if err != nil {
 			return nil, ContextError(err)
 		}
-	} else {
-		transport = httpTransport
+		additionalHeaders = map[string]string{
+			"X-Psiphon-Fronting-Address": host,
+		}
+	}
+
+	cookie, err := makeCookie(meekConfig)
+	if err != nil {
+		return nil, ContextError(err)
 	}
 
 	// The main loop of a MeekConn is run in the relay() goroutine.
@@ -267,9 +280,8 @@ func DialMeek(
 	// Write() calls and relay() are synchronized in a similar way, using a single
 	// sendBuffer.
 	meek = &MeekConn{
-		frontingAddress:      frontingAddress,
-		useHTTPS:             useHTTPS,
 		url:                  url,
+		additionalHeaders:    additionalHeaders,
 		cookie:               cookie,
 		pendingConns:         pendingConns,
 		transport:            transport,
@@ -290,7 +302,7 @@ func DialMeek(
 	go meek.relay()
 
 	// Enable interruption
-	if !config.PendingConns.Add(meek) {
+	if !dialConfig.PendingConns.Add(meek) {
 		meek.Close()
 		return nil, ContextError(errors.New("pending connections already closed"))
 	}
@@ -524,16 +536,16 @@ func (meek *MeekConn) roundTrip(sendPayload []byte) (receivedPayload io.ReadClos
 		return nil, ContextError(err)
 	}
 
-	if meek.useHTTPS {
-		request.Header.Set("X-Psiphon-Fronting-Address", meek.frontingAddress)
-	}
-
 	// Don't use the default user agent ("Go 1.1 package http").
 	// For now, just omit the header (net/http/request.go: "may be blank to not send the header").
 	request.Header.Set("User-Agent", "")
 
 	request.Header.Set("Content-Type", "application/octet-stream")
 
+	for name, value := range meek.additionalHeaders {
+		request.Header.Set(name, value)
+	}
+
 	request.AddCookie(meek.cookie)
 
 	// The retry mitigates intermittent failures between the client and front/server.
@@ -624,13 +636,13 @@ type meekCookieData struct {
 // all consequent HTTP requests
 // In unfronted meek mode, the cookie is visible over the adversary network, so the
 // cookie is encrypted and obfuscated.
-func makeCookie(serverEntry *ServerEntry, sessionId string) (cookie *http.Cookie, err error) {
+func makeCookie(meekConfig *MeekConfig) (cookie *http.Cookie, err error) {
 
 	// Make the JSON data
-	serverAddress := fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
+	serverAddress := meekConfig.PsiphonServerAddress
 	cookieData := &meekCookieData{
 		ServerAddress:       serverAddress,
-		SessionID:           sessionId,
+		SessionID:           meekConfig.SessionID,
 		MeekProtocolVersion: MEEK_PROTOCOL_VERSION,
 	}
 	serializedCookie, err := json.Marshal(cookieData)
@@ -647,7 +659,7 @@ func makeCookie(serverEntry *ServerEntry, sessionId string) (cookie *http.Cookie
 	// different messages if the messages are sent to two different public keys."
 	var nonce [24]byte
 	var publicKey [32]byte
-	decodedPublicKey, err := base64.StdEncoding.DecodeString(serverEntry.MeekCookieEncryptionPublicKey)
+	decodedPublicKey, err := base64.StdEncoding.DecodeString(meekConfig.MeekCookieEncryptionPublicKey)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -663,7 +675,7 @@ func makeCookie(serverEntry *ServerEntry, sessionId string) (cookie *http.Cookie
 
 	// Obfuscate the encrypted data
 	obfuscator, err := NewObfuscator(
-		&ObfuscatorConfig{Keyword: serverEntry.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
+		&ObfuscatorConfig{Keyword: meekConfig.MeekObfuscatedKey, MaxPadding: MEEK_COOKIE_MAX_PADDING})
 	if err != nil {
 		return nil, ContextError(err)
 	}

+ 37 - 22
psiphon/net.go

@@ -97,11 +97,6 @@ type DialConfig struct {
 	ResolvedIPCallback func(string)
 }
 
-// DeviceBinder defines the interface to the external BindToDevice provider
-type DeviceBinder interface {
-	BindToDevice(fileDescriptor int) error
-}
-
 // NetworkConnectivityChecker defines the interface to the external
 // HasNetworkConnectivity provider
 type NetworkConnectivityChecker interface {
@@ -109,12 +104,31 @@ type NetworkConnectivityChecker interface {
 	HasNetworkConnectivity() int
 }
 
+// DeviceBinder defines the interface to the external BindToDevice provider
+type DeviceBinder interface {
+	BindToDevice(fileDescriptor int) error
+}
+
 // DnsServerGetter defines the interface to the external GetDnsServer provider
 type DnsServerGetter interface {
 	GetPrimaryDnsServer() string
 	GetSecondaryDnsServer() string
 }
 
+// HostNameTransformer defines the interface for pluggable hostname
+// transformation circumvention strategies.
+type HostNameTransformer interface {
+	TransformHostName(hostname string) (string, bool)
+}
+
+// IdentityHostNameTransformer is the default HostNameTransformer, which
+// returns the hostname unchanged.
+type IdentityHostNameTransformer struct{}
+
+func (IdentityHostNameTransformer) TransformHostName(hostname string) (string, bool) {
+	return hostname, false
+}
+
 // TimeoutError implements the error interface
 type TimeoutError struct{}
 
@@ -276,29 +290,15 @@ func MakeUntunneledHttpsClient(
 	requestUrl string,
 	requestTimeout time.Duration) (*http.Client, string, error) {
 
-	// Note: IndistinguishableTLS mode doesn't support VerifyLegacyCertificate
-	useIndistinguishableTLS := dialConfig.UseIndistinguishableTLS && verifyLegacyCertificate == nil
-
-	dialer := NewCustomTLSDialer(
-		// Note: when verifyLegacyCertificate is not nil, some
-		// of the other CustomTLSConfig is overridden.
-		&CustomTLSConfig{
-			Dial: NewTCPDialer(dialConfig),
-			VerifyLegacyCertificate:       verifyLegacyCertificate,
-			SendServerName:                true,
-			SkipVerify:                    false,
-			UseIndistinguishableTLS:       useIndistinguishableTLS,
-			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
-		})
+	// Change the scheme to "http"; otherwise http.Transport will try to do
+	// another TLS handshake inside the explicit TLS session. Also need to
+	// force an explicit port, as the default for "http", 80, won't talk TLS.
 
 	urlComponents, err := url.Parse(requestUrl)
 	if err != nil {
 		return nil, "", ContextError(err)
 	}
 
-	// Change the scheme to "http"; otherwise http.Transport will try to do
-	// another TLS handshake inside the explicit TLS session. Also need to
-	// force an explicit port, as the default for "http", 80, won't talk TLS.
 	urlComponents.Scheme = "http"
 	host, port, err := net.SplitHostPort(urlComponents.Host)
 	if err != nil {
@@ -311,6 +311,21 @@ func MakeUntunneledHttpsClient(
 	}
 	urlComponents.Host = net.JoinHostPort(host, port)
 
+	// Note: IndistinguishableTLS mode doesn't support VerifyLegacyCertificate
+	useIndistinguishableTLS := dialConfig.UseIndistinguishableTLS && verifyLegacyCertificate == nil
+
+	dialer := NewCustomTLSDialer(
+		// Note: when verifyLegacyCertificate is not nil, some
+		// of the other CustomTLSConfig is overridden.
+		&CustomTLSConfig{
+			Dial: NewTCPDialer(dialConfig),
+			VerifyLegacyCertificate:       verifyLegacyCertificate,
+			SNIServerName:                 host,
+			SkipVerify:                    false,
+			UseIndistinguishableTLS:       useIndistinguishableTLS,
+			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
+		})
+
 	transport := &http.Transport{
 		Dial: dialer,
 	}

+ 27 - 11
psiphon/notice.go

@@ -137,9 +137,24 @@ func NoticeAvailableEgressRegions(regions []string) {
 }
 
 // NoticeConnectingServer is details on a connection attempt
-func NoticeConnectingServer(ipAddress, region, protocol, frontingAddress string) {
-	outputNotice("ConnectingServer", true, false, "ipAddress", ipAddress, "region",
-		region, "protocol", protocol, "frontingAddress", frontingAddress)
+func NoticeConnectingServer(ipAddress, region, protocol, directTCPDialAddress string, meekConfig *MeekConfig) {
+	if meekConfig == nil {
+		outputNotice("ConnectingServer", true, false,
+			"ipAddress", ipAddress,
+			"region", region,
+			"protocol", protocol,
+			"directTCPDialAddress", directTCPDialAddress)
+	} else {
+		outputNotice("ConnectingServer", true, false,
+			"ipAddress", ipAddress,
+			"region", region,
+			"protocol", protocol,
+			"meekDialAddress", meekConfig.DialAddress,
+			"meekUseHTTPS", meekConfig.UseHTTPS,
+			"meekSNIServerName", meekConfig.SNIServerName,
+			"meekHostHeader", meekConfig.HostHeader,
+			"meekTransformedHostName", meekConfig.TransformedHostName)
+	}
 }
 
 // NoticeActiveTunnel is a successful connection that is used as an active tunnel for port forwarding
@@ -263,14 +278,15 @@ func NoticeLocalProxyError(proxyType string, err error) {
 		"LocalProxyError", true, false, "message", err.Error())
 }
 
-// NoticeFrontedMeekStats reports extra network details for a
-// FRONTED-MEEK-OSSH or FRONTED-MEEK-HTTP-OSSH tunnel connection.
-func NoticeFrontedMeekStats(ipAddress string, frontedMeekStats *FrontedMeekStats) {
-	outputNotice("NoticeFrontedMeekStats", true, false, "ipAddress", ipAddress,
-		"frontingAddress", frontedMeekStats.frontingAddress,
-		"resolvedIPAddress", frontedMeekStats.resolvedIPAddress,
-		"enabledSNI", frontedMeekStats.enabledSNI,
-		"frontingHost", frontedMeekStats.frontingHost)
+// NoticeConnectedMeekStats reports extra network details for a meek tunnel connection.
+func NoticeConnectedMeekStats(ipAddress string, meekStats *MeekStats) {
+	outputNotice("NoticeConnectedMeekStats", true, false,
+		"ipAddress", ipAddress,
+		"serverAddress", meekStats.ServerAddress,
+		"resolvedIPAddress", meekStats.ResolvedIPAddress,
+		"sniServerName", meekStats.SNIServerName,
+		"hostHeader", meekStats.HostHeader,
+		"transformedHostName", meekStats.TransformedHostName)
 }
 
 // NoticeBuildInfo reports build version info.

+ 3 - 3
psiphon/opensslConn.go

@@ -96,12 +96,12 @@ func newOpenSSLConn(rawConn net.Conn, hostname string, config *CustomTLSConfig)
 		return nil, ContextError(err)
 	}
 
-	if config.SendServerName {
+	if config.SNIServerName {
 		// Explicitly exclude IPs:
 		// - "Literal IPv4 and IPv6 addresses are not permitted": https://tools.ietf.org/html/rfc6066#page-6.
 		// - OpenSSL does not appear to enforce this rule itself.
-		if net.ParseIP(hostname) == nil {
-			err = conn.SetTlsExtHostName(hostname)
+		if net.ParseIP(config.SNIServerName) == nil {
+			err = conn.SetTlsExtHostName(config.SNIServerName)
 			if err != nil {
 				return nil, ContextError(err)
 			}

+ 26 - 17
psiphon/serverApi.go

@@ -51,13 +51,13 @@ type ServerContext struct {
 	serverHandshakeTimestamp string
 }
 
-// FrontedMeekStats holds extra stats that are only gathered for
-// FRONTED-MEEK-OSSH, FRONTED-MEEK-HTTP-OSSH.
-type FrontedMeekStats struct {
-	frontingAddress   string
-	resolvedIPAddress string
-	enabledSNI        bool
-	frontingHost      string
+// MeekStats holds extra stats that are only gathered for meek tunnels.
+type MeekStats struct {
+	ServerAddress       string
+	ResolvedIPAddress   string
+	SNIServerName       string
+	HostHeader          string
+	TransformedHostName bool
 }
 
 // nextTunnelNumber is a monotonically increasing number assigned to each
@@ -623,20 +623,29 @@ func makeBaseRequestUrl(tunnel *Tunnel, port, sessionId string) string {
 		requestUrl.WriteString("&device_region=")
 		requestUrl.WriteString(tunnel.config.DeviceRegion)
 	}
-
-	if tunnel.frontedMeekStats != nil {
-		requestUrl.WriteString("&fronting_address=")
-		requestUrl.WriteString(tunnel.frontedMeekStats.frontingAddress)
-		requestUrl.WriteString("&fronting_resolved_ip_address=")
-		requestUrl.WriteString(tunnel.frontedMeekStats.resolvedIPAddress)
-		requestUrl.WriteString("&fronting_enabled_sni=")
-		if tunnel.frontedMeekStats.enabledSNI {
+	if tunnel.meekStats != nil {
+		if tunnel.meekStats.ServerAddress != "" {
+			requestUrl.WriteString("&meek_server_address=")
+			requestUrl.WriteString(tunnel.meekStats.ServerAddress)
+		}
+		if tunnel.meekStats.ResolvedIPAddress != "" {
+			requestUrl.WriteString("&meek_resolved_ip_address=")
+			requestUrl.WriteString(tunnel.meekStats.ResolvedIPAddress)
+		}
+		if tunnel.meekStats.SNIServerName != "" {
+			requestUrl.WriteString("&meek_sni_server_name=")
+			requestUrl.WriteString(tunnel.meekStats.SNIServerName)
+		}
+		if tunnel.meekStats.HostHeader != "" {
+			requestUrl.WriteString("&meek_host_header=")
+			requestUrl.WriteString(tunnel.meekStats.HostHeader)
+		}
+		requestUrl.WriteString("&meek_transformed_host_name=")
+		if tunnel.meekStats.TransformedHostName {
 			requestUrl.WriteString("1")
 		} else {
 			requestUrl.WriteString("0")
 		}
-		requestUrl.WriteString("&fronting_host=")
-		requestUrl.WriteString(tunnel.frontedMeekStats.frontingHost)
 	}
 
 	if tunnel.serverEntry.Region != "" {

+ 11 - 9
psiphon/tlsDialer.go

@@ -91,12 +91,14 @@ type CustomTLSConfig struct {
 	// connection dial and TLS handshake.
 	Timeout time.Duration
 
-	// FrontingAddr overrides the "addr" input to Dial when specified
-	FrontingAddr string
+	// DialAddr overrides the "addr" input to Dial when specified
+	DialAddr string
 
-	// SendServerName specifies whether to use SNI
-	// (tlsdialer functionality)
-	SendServerName bool
+	// SNIServerName specifies the value to set in the SNI
+	// server_name field. When blank, SNI is omitted. Note that
+	// underlying TLS code also automatically omits SNI when
+	// the server_name is an IP address.
+	SNIServerName string
 
 	// SkipVerify completely disables server certificate verification.
 	SkipVerify bool
@@ -151,8 +153,8 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 	}
 
 	dialAddr := addr
-	if config.FrontingAddr != "" {
-		dialAddr = config.FrontingAddr
+	if config.DialAddr != "" {
+		dialAddr = config.DialAddr
 	}
 
 	rawConn, err := config.Dial(network, dialAddr)
@@ -172,12 +174,12 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 		tlsConfig.InsecureSkipVerify = true
 	}
 
-	if config.SendServerName && config.VerifyLegacyCertificate == nil {
+	if config.SNIServerName != "" && config.VerifyLegacyCertificate == nil {
 		// Set the ServerName and rely on the usual logic in
 		// tls.Conn.Handshake() to do its verification.
 		// Note: Go TLS will automatically omit this ServerName when it's an IP address
 		if net.ParseIP(hostname) == nil {
-			tlsConfig.ServerName = hostname
+			tlsConfig.ServerName = config.SNIServerName
 		}
 	} else {
 		// No SNI.

+ 151 - 87
psiphon/tunnel.go

@@ -78,7 +78,7 @@ type Tunnel struct {
 	signalPortForwardFailure chan struct{}
 	totalPortForwardFailures int
 	startTime                time.Time
-	frontedMeekStats         *FrontedMeekStats
+	meekStats                *MeekStats
 }
 
 // EstablishTunnel first makes a network transport connection to the
@@ -105,7 +105,7 @@ func EstablishTunnel(
 	}
 
 	// Build transport layers and establish SSH connection
-	conn, sshClient, frontedMeekStats, err := dialSsh(
+	conn, sshClient, meekStats, err := dialSsh(
 		config, pendingConns, serverEntry, selectedProtocol, sessionId)
 	if err != nil {
 		return nil, ContextError(err)
@@ -134,7 +134,7 @@ func EstablishTunnel(
 		// A buffer allows at least one signal to be sent even when the receiver is
 		// not listening. Senders should not block.
 		signalPortForwardFailure: make(chan struct{}, 1),
-		frontedMeekStats:         frontedMeekStats,
+		meekStats:                meekStats,
 	}
 
 	// Create a new Psiphon API server context for this tunnel. This includes
@@ -334,91 +334,156 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 	return selectedProtocol, nil
 }
 
+// selectFrontingParameters is a helper which selects/generates meek fronting
+// parameters where the server entry provides multiple options or patterns.
+func selectFrontingParameters(
+	serverEntry *ServerEntry) (frontingAddress, frontingHost string, err error) {
+
+	if len(serverEntry.MeekFrontingAddressesRegex) > 0 {
+
+		// Generate a front address based on the regex.
+
+		frontingAddress, err = regen.Generate(serverEntry.MeekFrontingAddressesRegex)
+		if err != nil {
+			return "", "", ContextError(err)
+		}
+	} else {
+
+		// Randomly select, for this connection attempt, one front address for
+		// fronting-capable servers.
+
+		if len(serverEntry.MeekFrontingAddresses) == 0 {
+			return "", "", ContextError(errors.New("MeekFrontingAddresses is empty"))
+		}
+		index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingAddresses))
+		if err != nil {
+			return "", "", ContextError(err)
+		}
+		frontingAddress = serverEntry.MeekFrontingAddresses[index]
+	}
+
+	if len(serverEntry.MeekFrontingHosts) > 0 {
+		index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingHosts))
+		if err != nil {
+			return "", "", ContextError(err)
+		}
+		frontingHost = serverEntry.MeekFrontingHosts[index]
+	} else {
+		// Backwards compatibility case
+		frontingHost = serverEntry.MeekFrontingHost
+	}
+
+	return
+}
+
+// initMeekConfig is a helper that creates a MeekConfig suitable for the
+// selected meek tunnel protocol.
+func initMeekConfig(
+	config *Config,
+	serverEntry *ServerEntry,
+	selectedProtocol,
+	sessionId string) (*MeekConfig, error) {
+
+	// The meek protocol always uses OSSH
+	psiphonServerAddress := fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
+
+	var dialAddress string
+	useHTTPS := false
+	var SNIServerName, hostHeader string
+	transformedHostName := false
+
+	switch selectedProtocol {
+	case TUNNEL_PROTOCOL_FRONTED_MEEK:
+		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
+		if err != nil {
+			return nil, ContextError(err)
+		}
+		dialAddress = fmt.Sprintf("%s:443", frontingAddress)
+		useHTTPS = true
+		if !serverEntry.MeekFrontingDisableSNI {
+			SNIServerName, transformedHostName =
+				config.HostNameTransformer.TransformHostName(frontingAddress)
+		}
+		hostHeader = frontingHost
+
+	case TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP:
+		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
+		if err != nil {
+			return nil, ContextError(err)
+		}
+		dialAddress = fmt.Sprintf("%s:80", frontingAddress)
+		hostHeader = frontingHost
+
+	case TUNNEL_PROTOCOL_UNFRONTED_MEEK:
+		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
+		hostname := serverEntry.IpAddress
+		hostname, transformedHostName = config.HostNameTransformer.TransformHostName(hostname)
+		if serverEntry.MeekServerPort == 80 {
+			hostHeader = hostname
+		} else {
+			hostHeader = fmt.Sprintf("%s:%d", hostname, serverEntry.MeekServerPort)
+		}
+
+	case TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS:
+		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
+		useHTTPS = true
+		SNIServerName, transformedHostName =
+			config.HostNameTransformer.TransformHostName(serverEntry.IpAddress)
+		hostHeader = dialAddress
+
+	default:
+		return nil, ContextError(errors.New("unexpected selectedProtocol"))
+	}
+
+	// The unnderlying TLS will automatically disable SNI for IP address server name
+	// values; we have this explicit check here so we record the correct value for stats.
+	if net.ParseIP(SNIServerName) != nil {
+		SNIServerName = ""
+	}
+
+	return &MeekConfig{
+		DialAddress:                   dialAddress,
+		UseHTTPS:                      useHTTPS,
+		SNIServerName:                 SNIServerName,
+		HostHeader:                    hostHeader,
+		TransformedHostName:           transformedHostName,
+		PsiphonServerAddress:          psiphonServerAddress,
+		SessionID:                     sessionId,
+		MeekCookieEncryptionPublicKey: serverEntry.MeekCookieEncryptionPublicKey,
+		MeekObfuscatedKey:             serverEntry.MeekObfuscatedKey,
+	}, nil
+}
+
 // dialSsh is a helper that builds the transport layers and establishes the SSH connection.
-// When  FRONTED-MEEK-OSSH is selected, additional FrontedMeekStats are recorded and returned.
+// When a meek protocols is selected, additional MeekStats are recorded and returned.
 func dialSsh(
 	config *Config,
 	pendingConns *Conns,
 	serverEntry *ServerEntry,
 	selectedProtocol,
 	sessionId string) (
-	conn net.Conn, sshClient *ssh.Client, frontedMeekStats *FrontedMeekStats, err error) {
+	conn net.Conn, sshClient *ssh.Client, meekStats *MeekStats, err error) {
 
 	// The meek protocols tunnel obfuscated SSH. Obfuscated SSH is layered on top of SSH.
 	// So depending on which protocol is used, multiple layers are initialized.
-	port := 0
-	useMeek := false
-	useMeekHTTPS := false
-	useMeekSNI := false
-	useFronting := false
+
 	useObfuscatedSsh := false
+	var directTCPDialAddress string
+	var meekConfig *MeekConfig
+
 	switch selectedProtocol {
-	case TUNNEL_PROTOCOL_FRONTED_MEEK:
-		useMeek = true
-		useMeekHTTPS = true
-		useMeekSNI = !serverEntry.MeekFrontingDisableSNI
-		useFronting = true
-		useObfuscatedSsh = true
-	case TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP:
-		useMeek = true
-		useMeekHTTPS = false
-		useMeekSNI = false
-		useFronting = true
-		useObfuscatedSsh = true
-	case TUNNEL_PROTOCOL_UNFRONTED_MEEK:
-		useMeek = true
-		useMeekHTTPS = false
-		useMeekSNI = false
-		useObfuscatedSsh = true
-		port = serverEntry.SshObfuscatedPort
-	case TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS:
-		useMeek = true
-		useMeekHTTPS = true
-		useMeekSNI = false
-		useObfuscatedSsh = true
-		port = serverEntry.SshObfuscatedPort
 	case TUNNEL_PROTOCOL_OBFUSCATED_SSH:
 		useObfuscatedSsh = true
-		port = serverEntry.SshObfuscatedPort
-	case TUNNEL_PROTOCOL_SSH:
-		port = serverEntry.SshPort
-	}
-
-	frontingAddress := ""
-	frontingHost := ""
-	if useFronting {
-		if len(serverEntry.MeekFrontingAddressesRegex) > 0 {
+		directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedPort)
 
-			// Generate a front address based on the regex.
-
-			frontingAddress, err = regen.Generate(serverEntry.MeekFrontingAddressesRegex)
-			if err != nil {
-				return nil, nil, nil, ContextError(err)
-			}
-		} else {
-
-			// Randomly select, for this connection attempt, one front address for
-			// fronting-capable servers.
-
-			if len(serverEntry.MeekFrontingAddresses) == 0 {
-				return nil, nil, nil, ContextError(errors.New("MeekFrontingAddresses is empty"))
-			}
-			index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingAddresses))
-			if err != nil {
-				return nil, nil, nil, ContextError(err)
-			}
-			frontingAddress = serverEntry.MeekFrontingAddresses[index]
-		}
+	case TUNNEL_PROTOCOL_SSH:
+		directTCPDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshPort)
 
-		if len(serverEntry.MeekFrontingHosts) > 0 {
-			index, err := MakeSecureRandomInt(len(serverEntry.MeekFrontingHosts))
-			if err != nil {
-				return nil, nil, nil, ContextError(err)
-			}
-			frontingHost = serverEntry.MeekFrontingHosts[index]
-		} else {
-			// Backwards compatibility case
-			frontingHost = serverEntry.MeekFrontingHost
+	default:
+		useObfuscatedSsh = true
+		meekConfig, err = initMeekConfig(config, serverEntry, selectedProtocol, sessionId)
+		if err != nil {
+			return nil, nil, nil, ContextError(err)
 		}
 	}
 
@@ -426,7 +491,8 @@ func dialSsh(
 		serverEntry.IpAddress,
 		serverEntry.Region,
 		selectedProtocol,
-		frontingAddress)
+		directTCPDialAddress,
+		meekConfig)
 
 	// Use an asynchronous callback to record the resolved IP address when
 	// dialing a domain name. Note that DialMeek doesn't immediately
@@ -452,14 +518,13 @@ func dialSsh(
 		DeviceRegion:                  config.DeviceRegion,
 		ResolvedIPCallback:            setResolvedIPAddress,
 	}
-	if useMeek {
-		conn, err = DialMeek(
-			serverEntry, sessionId, useMeekHTTPS, useMeekSNI, frontingAddress, frontingHost, dialConfig)
+	if meekConfig != nil {
+		conn, err = DialMeek(meekConfig, dialConfig)
 		if err != nil {
 			return nil, nil, nil, ContextError(err)
 		}
 	} else {
-		conn, err = DialTCP(fmt.Sprintf("%s:%d", serverEntry.IpAddress, port), dialConfig)
+		conn, err = DialTCP(directTCPDialAddress, dialConfig)
 		if err != nil {
 			return nil, nil, nil, ContextError(err)
 		}
@@ -549,20 +614,19 @@ func dialSsh(
 		return nil, nil, nil, ContextError(result.err)
 	}
 
-	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
-		selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP {
-
-		frontedMeekStats = &FrontedMeekStats{
-			frontingAddress:   frontingAddress,
-			resolvedIPAddress: resolvedIPAddress.Load().(string),
-			enabledSNI:        useMeekSNI,
-			frontingHost:      frontingHost,
+	if meekConfig != nil {
+		meekStats = &MeekStats{
+			ServerAddress:       meekConfig.DialAddress,
+			ResolvedIPAddress:   resolvedIPAddress.Load().(string),
+			SNIServerName:       meekConfig.SNIServerName,
+			HostHeader:          meekConfig.HostHeader,
+			TransformedHostName: meekConfig.TransformedHostName,
 		}
 
-		NoticeFrontedMeekStats(serverEntry.IpAddress, frontedMeekStats)
+		NoticeConnectedMeekStats(serverEntry.IpAddress, meekStats)
 	}
 
-	return conn, result.sshClient, frontedMeekStats, nil
+	return conn, result.sshClient, meekStats, nil
 }
 
 // operateTunnel monitors the health of the tunnel and performs