Просмотр исходного кода

Add support for new capabilities

Rod Hynes 10 лет назад
Родитель
Сommit
7def87065f
6 измененных файлов с 139 добавлено и 74 удалено
  1. 2 2
      psiphon/config.go
  2. 95 64
      psiphon/meekConn.go
  3. 2 1
      psiphon/notice.go
  4. 4 2
      psiphon/serverApi.go
  5. 4 0
      psiphon/serverEntry.go
  6. 32 5
      psiphon/tunnel.go

+ 2 - 2
psiphon/config.go

@@ -150,8 +150,8 @@ type Config struct {
 
 	// TunnelProtocol indicates which protocol to use. Valid values include:
 	// "SSH", "OSSH", "UNFRONTED-MEEK-OSSH", "UNFRONTED-MEEK-HTTPS-OSSH",
-	// "FRONTED-MEEK-OSSH". For the default, "", the best performing protocol
-	// is used.
+	// "FRONTED-MEEK-OSSH", "FRONTED-MEEK-HTTP-OSSH". For the default, "",
+	// the best performing protocol is used.
 	TunnelProtocol string
 
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to halt

+ 95 - 64
psiphon/meekConn.go

@@ -72,6 +72,7 @@ const (
 // through a CDN.
 type MeekConn struct {
 	frontingAddress      string
+	useHTTPS             bool
 	url                  *url.URL
 	cookie               *http.Cookie
 	pendingConns         *Conns
@@ -103,10 +104,10 @@ type transporter interface {
 // 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.
-// Fronting always uses HTTPS. Otherwise, HTTPS is optional.
 func DialMeek(
 	serverEntry *ServerEntry, sessionId string,
-	useHttps bool, frontingAddress string,
+	useHTTPS, useSNI bool,
+	frontingAddress, frontingHost string,
 	config *DialConfig) (meek *MeekConn, err error) {
 
 	// Configure transport
@@ -121,78 +122,107 @@ func DialMeek(
 	*meekConfig = *config
 	meekConfig.PendingConns = pendingConns
 
-	// host is both what is dialed and what ends up in the HTTP Host header
-	host := fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
+	var host string
 	var dialer Dialer
 	var proxyUrl func(*http.Request) (*url.URL, error)
 
-	if useHttps || frontingAddress != "" {
-		// 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.
-
-		customTLSConfig := &CustomTLSConfig{
-			Dial:                          NewTCPDialer(meekConfig),
-			Timeout:                       meekConfig.ConnectTimeout,
-			SendServerName:                false,
-			SkipVerify:                    true,
-			UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
-			TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
-		}
+	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,
+			}
+
+			dialer = NewCustomTLSDialer(customTLSConfig)
+
+		} else { // !useHTTPS
 
-		if frontingAddress != "" {
-			// In this case, host is not what is dialed but is what ends up in the HTTP Host header
-			host = serverEntry.MeekFrontingHost
-			customTLSConfig.FrontingAddr = fmt.Sprintf("%s:%d", frontingAddress, 443)
+			dialer = func(string, string) (net.Conn, error) {
+				return NewTCPDialer(meekConfig)("tcp", frontingAddress+":80")
+			}
 		}
 
-		dialer = NewCustomTLSDialer(customTLSConfig)
+	} else { // frontingAddress == ""
 
-	} else {
+		// 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 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)
+		if useHTTPS {
+
+			customTLSConfig := &CustomTLSConfig{
+				Dial:                          NewTCPDialer(meekConfig),
+				Timeout:                       meekConfig.ConnectTimeout,
+				SendServerName:                useSNI,
+				SkipVerify:                    true,
+				UseIndistinguishableTLS:       config.UseIndistinguishableTLS,
+				TrustedCACertificatesFilename: config.TrustedCACertificatesFilename,
 			}
-			proxyUrl = http.ProxyURL(url)
-			meekConfig.UpstreamProxyUrl = ""
+
+			dialer = NewCustomTLSDialer(customTLSConfig)
+
+		} else { // !useHTTPS
+
+			host = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
+
+			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 = ""
+			}
+
+			dialer = NewTCPDialer(meekConfig)
 		}
 
-		dialer = NewTCPDialer(meekConfig)
 	}
 
 	// Scheme is always "http". Otherwise http.Transport will try to do another TLS
@@ -240,6 +270,7 @@ func DialMeek(
 	// sendBuffer.
 	meek = &MeekConn{
 		frontingAddress:      frontingAddress,
+		useHTTPS:             useHTTPS,
 		url:                  url,
 		cookie:               cookie,
 		pendingConns:         pendingConns,
@@ -495,7 +526,7 @@ func (meek *MeekConn) roundTrip(sendPayload []byte) (receivedPayload io.ReadClos
 		return nil, ContextError(err)
 	}
 
-	if meek.frontingAddress != "" && nil == net.ParseIP(meek.frontingAddress) {
+	if meek.useHTTPS {
 		request.Header.Set("X-Psiphon-Fronting-Address", meek.frontingAddress)
 	}
 

+ 2 - 1
psiphon/notice.go

@@ -220,7 +220,8 @@ func NoticeFrontedMeekStats(ipAddress string, frontedMeekStats *FrontedMeekStats
 	outputNotice("NoticeFrontedMeekStats", false, "ipAddress", ipAddress,
 		"frontingAddress", frontedMeekStats.frontingAddress,
 		"resolvedIPAddress", frontedMeekStats.resolvedIPAddress,
-		"enabledSNI", frontedMeekStats.enabledSNI)
+		"enabledSNI", frontedMeekStats.enabledSNI,
+		"frontingHost", frontedMeekStats.frontingHost)
 }
 
 // NoticeLocalProxyError reports a local proxy error message. Repetitive

+ 4 - 2
psiphon/serverApi.go

@@ -52,12 +52,12 @@ type ServerContext struct {
 }
 
 // FrontedMeekStats holds extra stats that are only gathered for
-// FRONTED-MEEK-OSSH. Specifically, which fronting address was selected, what
-// IP address a fronting domain resolved to, and whether SNI was enabled.
+// FRONTED-MEEK-OSSH, FRONTED-MEEK-HTTP-OSSH.
 type FrontedMeekStats struct {
 	frontingAddress   string
 	resolvedIPAddress string
 	enabledSNI        bool
+	frontingHost      string
 }
 
 // nextTunnelNumber is a monotonically increasing number assigned to each
@@ -635,6 +635,8 @@ func makeBaseRequestUrl(tunnel *Tunnel, port, sessionId string) string {
 		} else {
 			requestUrl.WriteString("0")
 		}
+		requestUrl.WriteString("&fronting_host=")
+		requestUrl.WriteString(tunnel.frontedMeekStats.frontingHost)
 	}
 
 	if tunnel.serverEntry.Region != "" {

+ 4 - 0
psiphon/serverEntry.go

@@ -35,10 +35,12 @@ const (
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK       = "UNFRONTED-MEEK-OSSH"
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS = "UNFRONTED-MEEK-HTTPS-OSSH"
 	TUNNEL_PROTOCOL_FRONTED_MEEK         = "FRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP    = "FRONTED-MEEK-HTTP-OSSH"
 )
 
 var SupportedTunnelProtocols = []string{
 	TUNNEL_PROTOCOL_FRONTED_MEEK,
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
 	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
@@ -66,9 +68,11 @@ type ServerEntry struct {
 	MeekCookieEncryptionPublicKey string   `json:"meekCookieEncryptionPublicKey"`
 	MeekObfuscatedKey             string   `json:"meekObfuscatedKey"`
 	MeekFrontingHost              string   `json:"meekFrontingHost"`
+	MeekFrontingHosts             []string `json:"meekFrontingHosts"`
 	MeekFrontingDomain            string   `json:"meekFrontingDomain"`
 	MeekFrontingAddresses         []string `json:"meekFrontingAddresses"`
 	MeekFrontingAddressesRegex    string   `json:"meekFrontingAddressesRegex"`
+	MeekFrontingDisableSNI        bool     `json:"meekFrontingDisableSNI"`
 
 	// These local fields are not expected to be present in downloaded server
 	// entries. They are added by the client to record and report stats about

+ 32 - 5
psiphon/tunnel.go

@@ -348,22 +348,33 @@ func dialSsh(
 	// So depending on which protocol is used, multiple layers are initialized.
 	port := 0
 	useMeek := false
-	useMeekHttps := false
+	useMeekHTTPS := false
+	useMeekSNI := false
 	useFronting := false
 	useObfuscatedSsh := false
 	switch selectedProtocol {
 	case TUNNEL_PROTOCOL_FRONTED_MEEK:
 		useMeek = true
-		useMeekHttps = 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
+		useMeekHTTPS = true
+		useMeekSNI = false
 		useObfuscatedSsh = true
 		port = serverEntry.SshObfuscatedPort
 	case TUNNEL_PROTOCOL_OBFUSCATED_SSH:
@@ -374,6 +385,7 @@ func dialSsh(
 	}
 
 	frontingAddress := ""
+	frontingHost := ""
 	if useFronting {
 		if len(serverEntry.MeekFrontingAddressesRegex) > 0 {
 
@@ -397,6 +409,17 @@ func dialSsh(
 			}
 			frontingAddress = serverEntry.MeekFrontingAddresses[index]
 		}
+
+		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
+		}
 	}
 
 	NoticeConnectingServer(
@@ -430,7 +453,8 @@ func dialSsh(
 		ResolvedIPCallback:            setResolvedIPAddress,
 	}
 	if useMeek {
-		conn, err = DialMeek(serverEntry, sessionId, useMeekHttps, frontingAddress, dialConfig)
+		conn, err = DialMeek(
+			serverEntry, sessionId, useMeekHTTPS, useMeekSNI, frontingAddress, frontingHost, dialConfig)
 		if err != nil {
 			return nil, nil, nil, ContextError(err)
 		}
@@ -525,11 +549,14 @@ func dialSsh(
 		return nil, nil, nil, ContextError(result.err)
 	}
 
-	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK {
+	if selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
+		selectedProtocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP {
+
 		frontedMeekStats = &FrontedMeekStats{
 			frontingAddress:   frontingAddress,
 			resolvedIPAddress: resolvedIPAddress.Load().(string),
 			enabledSNI:        false,
+			frontingHost:      frontingHost,
 		}
 
 		NoticeFrontedMeekStats(serverEntry.IpAddress, frontedMeekStats)