Przeglądaj źródła

Merge branch 'master' of https://github.com/rod-hynes/psiphon-tunnel-core into tactics

Rod Hynes 8 lat temu
rodzic
commit
b0ee9c3156

+ 13 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h

@@ -331,6 +331,19 @@ Swift: @code func onInternetReachabilityChanged(_ currentReachability: Reachabil
  */
  */
 - (BOOL)start:(BOOL)ifNeeded;
 - (BOOL)start:(BOOL)ifNeeded;
 
 
+
+/*!
+ Force stops the tunnel and reconnects with the current session ID.
+ Retuns with FALSE immediately if no session ID has already been generated.
+
+ @note On the first connection `start:` method should always be used to generate a
+ session ID.
+
+ @return TRUE if the connection start was successful, FALSE otherwise.
+ Swift: @code func startWithCurrentSessionID() @endcode
+ */
+- (BOOL)stopAndReconnectWithCurrentSessionID;
+
 /*!
 /*!
  Stop the tunnel (regardless of its current connection state).
  Stop the tunnel (regardless of its current connection state).
  Swift: @code func stop() @endcode
  Swift: @code func stop() @endcode

+ 11 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -149,6 +149,17 @@
     return [self start];
     return [self start];
 }
 }
 
 
+// See comment in header
+- (BOOL)stopAndReconnectWithCurrentSessionID {
+
+    // Proceed only if a session ID has alreaby been generated.
+    if (self.sessionID == nil) {
+        return FALSE;
+    }
+
+    return [self start];
+}
+
 /*!
 /*!
  Start the tunnel. If the tunnel is already started it will be stopped first.
  Start the tunnel. If the tunnel is already started it will be stopped first.
  Assumes self.sessionID has been initialized -- i.e., assumes that
  Assumes self.sessionID has been initialized -- i.e., assumes that

+ 21 - 2
psiphon/server/trafficRules.go

@@ -86,8 +86,14 @@ type TrafficRulesFilter struct {
 
 
 	// AuthorizedAccessTypes specifies a list of access types, at least
 	// AuthorizedAccessTypes specifies a list of access types, at least
 	// one of which the client must have presented an active authorization
 	// one of which the client must have presented an active authorization
-	// for.
+	// for and which must not be revoked.
+	// AuthorizedAccessTypes is ignored when AuthorizationsRevoked is true.
 	AuthorizedAccessTypes []string
 	AuthorizedAccessTypes []string
+
+	// AuthorizationsRevoked indicates whether the client's authorizations
+	// must have been revoked. When true, authorizations must have been
+	// revoked. When omitted or false, this field is ignored.
+	AuthorizationsRevoked bool
 }
 }
 
 
 // TrafficRules specify the limits placed on client traffic.
 // TrafficRules specify the limits placed on client traffic.
@@ -395,11 +401,24 @@ func (set *TrafficRulesSet) GetTrafficRules(
 			}
 			}
 		}
 		}
 
 
-		if len(filteredRules.Filter.AuthorizedAccessTypes) > 0 {
+		if filteredRules.Filter.AuthorizationsRevoked {
 			if !state.completed {
 			if !state.completed {
 				continue
 				continue
 			}
 			}
 
 
+			if !state.authorizationsRevoked {
+				continue
+			}
+
+		} else if len(filteredRules.Filter.AuthorizedAccessTypes) > 0 {
+			if !state.completed {
+				continue
+			}
+
+			if state.authorizationsRevoked {
+				continue
+			}
+
 			if !common.ContainsAny(filteredRules.Filter.AuthorizedAccessTypes, state.authorizedAccessTypes) {
 			if !common.ContainsAny(filteredRules.Filter.AuthorizedAccessTypes, state.authorizedAccessTypes) {
 				continue
 				continue
 			}
 			}

+ 105 - 45
psiphon/server/tunnelServer.go

@@ -272,20 +272,21 @@ type sshServer struct {
 	// Note: 64-bit ints used with atomic operations are placed
 	// Note: 64-bit ints used with atomic operations are placed
 	// at the start of struct to ensure 64-bit alignment.
 	// at the start of struct to ensure 64-bit alignment.
 	// (https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
 	// (https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
-	lastAuthLog             int64
-	authFailedCount         int64
-	support                 *SupportServices
-	establishTunnels        int32
-	concurrentSSHHandshakes semaphore.Semaphore
-	shutdownBroadcast       <-chan struct{}
-	sshHostKey              ssh.Signer
-	clientsMutex            sync.Mutex
-	stoppingClients         bool
-	acceptedClientCounts    map[string]map[string]int64
-	clients                 map[string]*sshClient
-	oslSessionCacheMutex    sync.Mutex
-	oslSessionCache         *cache.Cache
-	activeAuthorizationIDs  sync.Map
+	lastAuthLog                  int64
+	authFailedCount              int64
+	support                      *SupportServices
+	establishTunnels             int32
+	concurrentSSHHandshakes      semaphore.Semaphore
+	shutdownBroadcast            <-chan struct{}
+	sshHostKey                   ssh.Signer
+	clientsMutex                 sync.Mutex
+	stoppingClients              bool
+	acceptedClientCounts         map[string]map[string]int64
+	clients                      map[string]*sshClient
+	oslSessionCacheMutex         sync.Mutex
+	oslSessionCache              *cache.Cache
+	authorizationSessionIDsMutex sync.Mutex
+	authorizationSessionIDs      map[string]string
 }
 }
 
 
 func newSSHServer(
 func newSSHServer(
@@ -330,6 +331,7 @@ func newSSHServer(
 		acceptedClientCounts:    make(map[string]map[string]int64),
 		acceptedClientCounts:    make(map[string]map[string]int64),
 		clients:                 make(map[string]*sshClient),
 		clients:                 make(map[string]*sshClient),
 		oslSessionCache:         oslSessionCache,
 		oslSessionCache:         oslSessionCache,
+		authorizationSessionIDs: make(map[string]string),
 	}, nil
 	}, nil
 }
 }
 
 
@@ -494,11 +496,11 @@ func (sshServer *sshServer) registerEstablishedClient(client *sshClient) bool {
 	}
 	}
 
 
 	// In the case of a duplicate client sessionID, the previous client is closed.
 	// In the case of a duplicate client sessionID, the previous client is closed.
-	// - Well-behaved clients generate pick a random sessionID that should be
-	//   unique (won't accidentally conflict) and hard to guess (can't be targeted
-	//   by a malicious client).
+	// - Well-behaved clients generate a random sessionID that should be unique (won't
+	//   accidentally conflict) and hard to guess (can't be targeted by a malicious
+	//   client).
 	// - Clients reuse the same sessionID when a tunnel is unexpectedly disconnected
 	// - Clients reuse the same sessionID when a tunnel is unexpectedly disconnected
-	//   and resestablished. In this case, when the same server is selected, this logic
+	//   and reestablished. In this case, when the same server is selected, this logic
 	//   will be hit; closing the old, dangling client is desirable.
 	//   will be hit; closing the old, dangling client is desirable.
 	// - Multi-tunnel clients should not normally use one server for multiple tunnels.
 	// - Multi-tunnel clients should not normally use one server for multiple tunnels.
 	existingClient := sshServer.clients[client.sessionID]
 	existingClient := sshServer.clients[client.sessionID]
@@ -729,6 +731,31 @@ func (sshServer *sshServer) getClientHandshaked(
 	return completed, exhausted, nil
 	return completed, exhausted, nil
 }
 }
 
 
+func (sshServer *sshServer) revokeClientAuthorizations(sessionID string) {
+	sshServer.clientsMutex.Lock()
+	client := sshServer.clients[sessionID]
+	sshServer.clientsMutex.Unlock()
+
+	if client == nil {
+		return
+	}
+
+	// sshClient.handshakeState.authorizedAccessTypes is not cleared. Clearing
+	// authorizedAccessTypes may cause sshClient.logTunnel to fail to log
+	// access types. As the revocation may be due to legitimate use of an
+	// authorization in multiple sessions by a single client, useful metrics
+	// would be lost.
+
+	client.Lock()
+	client.handshakeState.authorizationsRevoked = true
+	client.Unlock()
+
+	// Select and apply new traffic rules, as filtered by the client's new
+	// authorization state.
+
+	client.setTrafficRules()
+}
+
 func (sshServer *sshServer) expectClientDomainBytes(
 func (sshServer *sshServer) expectClientDomainBytes(
 	sessionID string) (bool, error) {
 	sessionID string) (bool, error) {
 
 
@@ -900,6 +927,7 @@ type handshakeState struct {
 	apiProtocol           string
 	apiProtocol           string
 	apiParams             common.APIParameters
 	apiParams             common.APIParameters
 	authorizedAccessTypes []string
 	authorizedAccessTypes []string
+	authorizationsRevoked bool
 	expectDomainBytes     bool
 	expectDomainBytes     bool
 }
 }
 
 
@@ -1139,7 +1167,11 @@ func (sshClient *sshClient) passwordCallback(conn ssh.ConnMetadata, password []b
 		sshPasswordPayload.ClientCapabilities, protocol.CLIENT_CAPABILITY_SERVER_REQUESTS)
 		sshPasswordPayload.ClientCapabilities, protocol.CLIENT_CAPABILITY_SERVER_REQUESTS)
 
 
 	sshClient.Lock()
 	sshClient.Lock()
+
+	// After this point, sshClient.sessionID is read-only as it will be read
+	// without obtaining sshClient.Lock.
 	sshClient.sessionID = sessionID
 	sshClient.sessionID = sessionID
+
 	sshClient.supportsServerRequests = supportsServerRequests
 	sshClient.supportsServerRequests = supportsServerRequests
 	geoIPData := sshClient.geoIPData
 	geoIPData := sshClient.geoIPData
 	sshClient.Unlock()
 	sshClient.Unlock()
@@ -1786,21 +1818,23 @@ func (sshClient *sshClient) setHandshakeState(
 		return nil, nil, common.ContextError(errors.New("handshake already completed"))
 		return nil, nil, common.ContextError(errors.New("handshake already completed"))
 	}
 	}
 
 
-	// Verify the authorizations submitted by the client. Verified, active (non-expired)
-	// access types will be available for traffic rules filtering.
+	// Verify the authorizations submitted by the client. Verified, active
+	// (non-expired) access types will be available for traffic rules
+	// filtering.
 	//
 	//
-	// When an authorization is active but expires while the client is connected, the
-	// client is disconnected to ensure the access is revoked. This is implemented by
-	// setting a timer to perform the disconnect at the expiry time of the soonest
-	// expiring authorization.
+	// When an authorization is active but expires while the client is
+	// connected, the client is disconnected to ensure the access is reset.
+	// This is implemented by setting a timer to perform the disconnect at the
+	// expiry time of the soonest expiring authorization.
 	//
 	//
-	// sshServer.activeAuthorizationIDs tracks the unique IDs of active authorizations
-	// and is used to detect and prevent multiple malicious clients from reusing a
-	// single authorization (within the scope of this server).
-
-	// authorizationIDs and authorizedAccessTypes are returned to the client and logged,
-	// respectively; initialize to empty lists so the protocol/logs don't need to handle
-	// 'null' values.
+	// sshServer.authorizationSessionIDs tracks the unique mapping of active
+	// authorization IDs to client session IDs  and is used to detect and
+	// prevent multiple malicious clients from reusing a single authorization
+	// (within the scope of this server).
+
+	// authorizationIDs and authorizedAccessTypes are returned to the client
+	// and logged, respectively; initialize to empty lists so the
+	// protocol/logs don't need to handle 'null' values.
 	authorizationIDs := make([]string, 0)
 	authorizationIDs := make([]string, 0)
 	authorizedAccessTypes := make([]string, 0)
 	authorizedAccessTypes := make([]string, 0)
 	var stopTime time.Time
 	var stopTime time.Time
@@ -1825,18 +1859,6 @@ func (sshClient *sshClient) setHandshakeState(
 
 
 		authorizationID := base64.StdEncoding.EncodeToString(verifiedAuthorization.ID)
 		authorizationID := base64.StdEncoding.EncodeToString(verifiedAuthorization.ID)
 
 
-		// A client may reconnect while the server still has an active sshClient for that
-		// client session. In this case, the previous sshClient is closed by the new
-		// client's call to sshServer.registerEstablishedClient.
-		// This is assumed to call sshClient.releaseAuthorizations which will remove
-		// the client's authorization IDs before this check is reached.
-
-		if _, exists := sshClient.sshServer.activeAuthorizationIDs.LoadOrStore(authorizationID, true); exists {
-			log.WithContextFields(
-				LogFields{"ID": verifiedAuthorization.ID}).Warning("duplicate active authorization")
-			continue
-		}
-
 		if common.Contains(authorizedAccessTypes, verifiedAuthorization.AccessType) {
 		if common.Contains(authorizedAccessTypes, verifiedAuthorization.AccessType) {
 			log.WithContextFields(
 			log.WithContextFields(
 				LogFields{"accessType": verifiedAuthorization.AccessType}).Warning("duplicate authorization access type")
 				LogFields{"accessType": verifiedAuthorization.AccessType}).Warning("duplicate authorization access type")
@@ -1851,6 +1873,39 @@ func (sshClient *sshClient) setHandshakeState(
 		}
 		}
 	}
 	}
 
 
+	// Associate all verified authorizationIDs with this client's session ID.
+	// Handle cases where previous associations exist:
+	//
+	// - Multiple malicious clients reusing a single authorization. In this
+	//   case, authorizations are revoked from the previous client.
+	//
+	// - The client reconnected with a new session ID due to user toggling.
+	//   This case is expected due to server affinity. This cannot be
+	//   distinguished from the previous case and the same action is taken;
+	//   this will have no impact on a legitimate client as the previous
+	//   session is dangling.
+	//
+	// - The client automatically reconnected with the same session ID. This
+	//   case is not expected as sshServer.registerEstablishedClient
+	//   synchronously calls sshClient.releaseAuthorizations; as a safe guard,
+	//   this case is distinguished and no revocation action is taken.
+
+	sshClient.sshServer.authorizationSessionIDsMutex.Lock()
+	for _, authorizationID := range authorizationIDs {
+		sessionID, ok := sshClient.sshServer.authorizationSessionIDs[authorizationID]
+		if ok && sessionID != sshClient.sessionID {
+
+			log.WithContextFields(
+				LogFields{"authorizationID": authorizationID}).Warning("duplicate active authorization")
+
+			// Invoke asynchronously to avoid deadlocks.
+			// TODO: invoke only once for each distinct sessionID?
+			go sshClient.sshServer.revokeClientAuthorizations(sessionID)
+		}
+		sshClient.sshServer.authorizationSessionIDs[authorizationID] = sshClient.sessionID
+	}
+	sshClient.sshServer.authorizationSessionIDsMutex.Unlock()
+
 	if len(authorizationIDs) > 0 {
 	if len(authorizationIDs) > 0 {
 
 
 		sshClient.Lock()
 		sshClient.Lock()
@@ -1866,9 +1921,14 @@ func (sshClient *sshClient) setHandshakeState(
 		// Note: termination of the stopTimer goroutine is not synchronized.
 		// Note: termination of the stopTimer goroutine is not synchronized.
 
 
 		sshClient.releaseAuthorizations = func() {
 		sshClient.releaseAuthorizations = func() {
-			for _, ID := range authorizationIDs {
-				sshClient.sshServer.activeAuthorizationIDs.Delete(ID)
+			sshClient.sshServer.authorizationSessionIDsMutex.Lock()
+			for _, authorizationID := range authorizationIDs {
+				sessionID, ok := sshClient.sshServer.authorizationSessionIDs[authorizationID]
+				if ok && sessionID == sshClient.sessionID {
+					delete(sshClient.sshServer.authorizationSessionIDs, authorizationID)
+				}
 			}
 			}
+			sshClient.sshServer.authorizationSessionIDsMutex.Unlock()
 		}
 		}
 
 
 		sshClient.stopTimer = time.AfterFunc(
 		sshClient.stopTimer = time.AfterFunc(