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

Merge pull request #79 from rod-hynes/master

Changes for new fronting capabilities and for Windows/Go release
Rod Hynes 11 лет назад
Родитель
Сommit
8ad6bdaf3f
7 измененных файлов с 92 добавлено и 38 удалено
  1. 1 2
      psiphon/config.go
  2. 2 12
      psiphon/controller.go
  3. 39 13
      psiphon/meekConn.go
  4. 7 2
      psiphon/notice.go
  5. 1 1
      psiphon/serverEntry.go
  6. 2 0
      psiphon/splitTunnel.go
  7. 40 8
      psiphon/tunnel.go

+ 1 - 2
psiphon/config.go

@@ -29,7 +29,7 @@ import (
 // TODO: allow all params to be configured
 
 const (
-	VERSION                                      = "0.0.7"
+	VERSION                                      = "0.0.8"
 	DATA_STORE_FILENAME                          = "psiphon.db"
 	CONNECTION_WORKER_POOL_SIZE                  = 10
 	TUNNEL_POOL_SIZE                             = 1
@@ -91,7 +91,6 @@ type Config struct {
 	SplitTunnelRoutesUrlFormat          string
 	SplitTunnelRoutesSignaturePublicKey string
 	SplitTunnelDnsServer                string
-	AlternateMeekFrontingAddresses      map[string][]string
 }
 
 // LoadConfig parses and validates a JSON format Psiphon config JSON

+ 2 - 12
psiphon/controller.go

@@ -630,18 +630,8 @@ loop:
 				break
 			}
 
-			// Override fronting domain, if configured to do so.
-			// TODO: we could generate multiple candidates from
-			// the current server entry when there are many
-			// AlternateMeekFrontingAddresses for this MeekFrontingHost.
-			if addresses, ok := controller.config.AlternateMeekFrontingAddresses[serverEntry.MeekFrontingDomain]; ok {
-				index, err := MakeSecureRandomInt(len(addresses))
-				if err == nil {
-					address := addresses[index]
-					NoticeAlert("using alternate address for %s: %s", serverEntry.MeekFrontingDomain, address)
-					serverEntry.MeekFrontingDomain = address
-				}
-			}
+			// TODO: here we could generate multiple candidates from the
+			// server entry when there are many MeekFrontingAddresses.
 
 			select {
 			case controller.candidateServerEntries <- serverEntry:

+ 39 - 13
psiphon/meekConn.go

@@ -88,10 +88,11 @@ type MeekConn struct {
 // 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.
-// useFronting assumes caller has already checked server entry capabilities.
+// When frontingAddress is not "", fronting is used. This option assumes caller has
+// already checked server entry capabilities.
 func DialMeek(
 	serverEntry *ServerEntry, sessionId string,
-	useFronting bool, config *DialConfig) (meek *MeekConn, err error) {
+	frontingAddress string, config *DialConfig) (meek *MeekConn, err error) {
 
 	// Configure transport
 	// Note: MeekConn has its own PendingConns to manage the underlying HTTP transport connections,
@@ -109,26 +110,51 @@ func DialMeek(
 	var dialer Dialer
 	var proxyUrl func(*http.Request) (*url.URL, error)
 
-	if useFronting {
+	if frontingAddress != "" {
 		// In this case, host is not what is dialed but is what ends up in the HTTP Host header
 		host = serverEntry.MeekFrontingHost
 
-		// We skip verifying the server certificate when the host address is an IP address. In the
-		// short term, this is a circumvention weakness: it's vulnerable to an active MiM attack
-		// which injects its own cert and decrypts the TLS and reads the custom Host header.
-		// We need to know which server cert to expect in order to perform verification in this case.
-		skipVerify := (net.ParseIP(serverEntry.MeekFrontingDomain) != nil)
-
 		// Custom TLS dialer:
-		//  - ignores the HTTP request address and uses the fronting domain
-		//  - disables SNI -- SNI breaks fronting when used with CDNs that support SNI on the server side.
+		//
+		//  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 similar to the "unidentified protocol" attack outlined in selectProtocol().
+		// A similar weighted selection defense may be appropriate.
+
 		dialer = NewCustomTLSDialer(
 			&CustomTLSConfig{
 				Dial:           NewTCPDialer(meekConfig),
 				Timeout:        meekConfig.ConnectTimeout,
-				FrontingAddr:   fmt.Sprintf("%s:%d", serverEntry.MeekFrontingDomain, 443),
+				FrontingAddr:   fmt.Sprintf("%s:%d", frontingAddress, 443),
 				SendServerName: false,
-				SkipVerify:     skipVerify,
+				SkipVerify:     true,
 			})
 	} else {
 		// In this case, host is both what is dialed and what ends up in the HTTP Host header

+ 7 - 2
psiphon/notice.go

@@ -110,9 +110,9 @@ func NoticeCandidateServers(region, protocol string, count int) {
 }
 
 // NoticeConnectingServer is details on a connection attempt
-func NoticeConnectingServer(ipAddress, region, protocol, frontingDomain string) {
+func NoticeConnectingServer(ipAddress, region, protocol, frontingAddress string) {
 	outputNotice("ConnectingServer", false, "ipAddress", ipAddress, "region",
-		region, "protocol", protocol, "frontingDomain", frontingDomain)
+		region, "protocol", protocol, "frontingAddress", frontingAddress)
 }
 
 // NoticeActiveTunnel is a successful connection that is used as an active tunnel for port forwarding
@@ -169,6 +169,11 @@ func NoticeUntunneled(address string) {
 	outputNotice("Untunneled", true, "address", address)
 }
 
+// NoticeSplitTunnelRegion reports that split tunnel is on for the given region.
+func NoticeSplitTunnelRegion(region string) {
+	outputNotice("SplitTunnelRegion", true, "region", region)
+}
+
 type noticeObject struct {
 	NoticeType string          `json:"noticeType"`
 	Data       json.RawMessage `json:"data"`

+ 1 - 1
psiphon/serverEntry.go

@@ -49,8 +49,8 @@ type ServerEntry struct {
 	MeekServerPort                int      `json:"meekServerPort"`
 	MeekCookieEncryptionPublicKey string   `json:"meekCookieEncryptionPublicKey"`
 	MeekObfuscatedKey             string   `json:"meekObfuscatedKey"`
-	MeekFrontingDomain            string   `json:"meekFrontingDomain"`
 	MeekFrontingHost              string   `json:"meekFrontingHost"`
+	MeekFrontingAddresses         []string `json:"MeekFrontingAddresses"`
 }
 
 // DecodeServerEntry extracts server entries from the encoding

+ 2 - 0
psiphon/splitTunnel.go

@@ -201,6 +201,8 @@ func (classifier *SplitTunnelClassifier) setRoutes(tunnel *Tunnel) {
 		NoticeAlert("failed to install split tunnel routes: %s", err)
 		return
 	}
+
+	NoticeSplitTunnelRegion(tunnel.session.clientRegion)
 }
 
 // getRoutes makes a web request to download fresh routes data for the

+ 40 - 8
psiphon/tunnel.go

@@ -295,17 +295,38 @@ func selectProtocol(config *Config, serverEntry *ServerEntry) (selectedProtocol
 		}
 		selectedProtocol = config.TunnelProtocol
 	} else {
-		// Order of SupportedTunnelProtocols is default preference order
+		// Pick at random from the supported protocols. This ensures that we'll eventually
+		// try all possible protocols. Depending on network configuration, it may be the
+		// case that some protocol is only available through multi-capability servers,
+		// and a simplr ranked preference of protocols could lead to that protocol never
+		// being selected.
+
+		// TODO: this is a good spot to apply protocol selection weightings. This would be
+		// to defend against an attack where the adversary, for example, classifies OSSH as
+		// an "unidentified" protocol; allows it to connect; but then kills the underlying
+		// TCP connection after a short time. Since OSSH has less latency than other protocols
+		// that may bypass an "unidentified" filter, other protocols which would be otherwise
+		// classified and not killed might never be selected for use.
+		// So one proposed defense is to add negative selection weights to the protocol
+		// associated with failed tunnels (controller.failedTunnels) with short session
+		// durations.
+
+		candidateProtocols := make([]string, 0)
 		for _, protocol := range SupportedTunnelProtocols {
 			requiredCapability := strings.TrimSuffix(protocol, "-OSSH")
 			if Contains(serverEntry.Capabilities, requiredCapability) {
-				selectedProtocol = protocol
-				break
+				candidateProtocols = append(candidateProtocols, protocol)
 			}
 		}
-		if selectedProtocol == "" {
+		if len(candidateProtocols) == 0 {
 			return "", ContextError(fmt.Errorf("server does not have any supported capabilities"))
 		}
+
+		index, err := MakeSecureRandomInt(len(candidateProtocols))
+		if err != nil {
+			return "", ContextError(err)
+		}
+		selectedProtocol = candidateProtocols[index]
 	}
 	return selectedProtocol, nil
 }
@@ -340,15 +361,26 @@ func dialSsh(
 		port = serverEntry.SshPort
 	}
 
-	frontingDomain := ""
+	frontingAddress := ""
 	if useFronting {
-		frontingDomain = serverEntry.MeekFrontingDomain
+
+		// 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]
 	}
 	NoticeConnectingServer(
 		serverEntry.IpAddress,
 		serverEntry.Region,
 		selectedProtocol,
-		frontingDomain)
+		frontingAddress)
 
 	// Create the base transport: meek or direct connection
 	dialConfig := &DialConfig{
@@ -361,7 +393,7 @@ func dialSsh(
 		DnsServerGetter:          config.DnsServerGetter,
 	}
 	if useMeek {
-		conn, err = DialMeek(serverEntry, sessionId, useFronting, dialConfig)
+		conn, err = DialMeek(serverEntry, sessionId, frontingAddress, dialConfig)
 		if err != nil {
 			return nil, nil, nil, ContextError(err)
 		}