Browse Source

Merge branch 'master' of https://github.com/Psiphon-Labs/psiphon-tunnel-core

Rod Hynes 8 years ago
parent
commit
7b6953d160

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

@@ -59,6 +59,8 @@ typedef NS_ENUM(NSInteger, PsiphonConnectionState)
  @protocol TunneledAppDelegate
  Used to communicate with the application that is using the PsiphonTunnel framework,
  and retrieve config info from it.
+
+ All delegate methods will be called on a single serial dispatch queue. They will be made asynchronously unless otherwise noted (specifically when calling getPsiphonConfig and getEmbeddedServerEntries).
  */
 @protocol TunneledAppDelegate <NSObject>
 

+ 121 - 53
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -37,6 +37,10 @@
 @end
 
 @implementation PsiphonTunnel {
+    dispatch_queue_t workQueue;
+    dispatch_queue_t callbackQueue;
+    dispatch_semaphore_t noticeHandlingSemaphore;
+
     _Atomic PsiphonConnectionState connectionState;
 
     _Atomic NSInteger localSocksProxyPort;
@@ -49,11 +53,17 @@
 }
 
 - (id)init {
-    atomic_init(&connectionState, PsiphonConnectionStateDisconnected);
-    atomic_init(&localSocksProxyPort, 0);
-    atomic_init(&localHttpProxyPort, 0);
-    reachability = [Reachability reachabilityForInternetConnection];
-    tunnelWholeDevice = FALSE;
+    self.tunneledAppDelegate = nil;
+
+    self->workQueue = dispatch_queue_create("com.psiphon3.library.WorkQueue", DISPATCH_QUEUE_SERIAL);
+    self->callbackQueue = dispatch_queue_create("com.psiphon3.library.CallbackQueue", DISPATCH_QUEUE_SERIAL);
+    self->noticeHandlingSemaphore = dispatch_semaphore_create(1);
+
+    atomic_init(&self->connectionState, PsiphonConnectionStateDisconnected);
+    atomic_init(&self->localSocksProxyPort, 0);
+    atomic_init(&self->localHttpProxyPort, 0);
+    self->reachability = [Reachability reachabilityForInternetConnection];
+    self->tunnelWholeDevice = FALSE;
 
     return self;
 }
@@ -88,6 +98,9 @@
     return [self start];
 }
 
+/*!
+ Start the tunnel. If the tunnel is already started it will be stopped first.
+ */
 - (BOOL)start {
     @synchronized (PsiphonTunnel.self) {
         [self stop];
@@ -102,7 +115,11 @@
             return FALSE;
         }
 
-        NSString *embeddedServerEntries = [self.tunneledAppDelegate getEmbeddedServerEntries];
+        __block NSString *embeddedServerEntries = nil;
+        dispatch_sync(self->callbackQueue, ^{
+            embeddedServerEntries = [self.tunneledAppDelegate getEmbeddedServerEntries];
+        });
+
         if (embeddedServerEntries == nil) {
             [self logMessage:@"Error getting embedded server entries from delegate"];
             return FALSE;
@@ -117,7 +134,7 @@
                            configStr,
                            embeddedServerEntries,
                            self,
-                           tunnelWholeDevice, // useDeviceBinder
+                           self->tunnelWholeDevice, // useDeviceBinder
                            useIPv6Synthesizer,
                            &e);
             
@@ -143,6 +160,9 @@
     }
 }
 
+/*!
+ Start the tunnel if it's not already started.
+ */
 - (BOOL)startIfNeeded {
     PsiphonConnectionState connState = [self getConnectionState];
     BOOL localProxyAlive = [self isLocalProxyAlive];
@@ -157,9 +177,7 @@
 
     // Otherwise we're already connected, so let the app know via the same signaling
     // that we'd use if we were doing a connection sequence.
-    dispatch_async(dispatch_get_main_queue(), ^{
-        [self changeConnectionStateTo:connState evenIfSameState:YES];
-    });
+    [self changeConnectionStateTo:connState evenIfSameState:YES];
 
     return TRUE;
 }
@@ -175,8 +193,8 @@
         
         [self logMessage: @"Psiphon library stopped"];
 
-        atomic_store(&localSocksProxyPort, 0);
-        atomic_store(&localHttpProxyPort, 0);
+        atomic_store(&self->localSocksProxyPort, 0);
+        atomic_store(&self->localHttpProxyPort, 0);
 
         [self changeConnectionStateTo:PsiphonConnectionStateDisconnected evenIfSameState:NO];
     }
@@ -184,17 +202,17 @@
 
 // See comment in header.
 - (PsiphonConnectionState)getConnectionState {
-    return atomic_load(&connectionState);
+    return atomic_load(&self->connectionState);
 }
 
 // See comment in header.
 - (NSInteger)getLocalSocksProxyPort {
-    return atomic_load(&localSocksProxyPort);
+    return atomic_load(&self->localSocksProxyPort);
 }
 
 // See comment in header.
 - (NSInteger)getLocalHttpProxyPort {
-    return atomic_load(&localHttpProxyPort);
+    return atomic_load(&self->localHttpProxyPort);
 }
 
 // See comment in header.
@@ -217,11 +235,14 @@
            publicKey:(NSString * _Nonnull)b64EncodedPublicKey
         uploadServer:(NSString * _Nonnull)uploadServer
  uploadServerHeaders:(NSString * _Nonnull)uploadServerHeaders {
-    NSString *connectionConfigJson = [self getConfig];
-    if (connectionConfigJson == nil) {
-       [self logMessage:@"Error getting config for feedback upload"];
-    }
-    GoPsiSendFeedback(connectionConfigJson, feedbackJson, b64EncodedPublicKey, uploadServer, @"", uploadServerHeaders);
+    dispatch_async(self->workQueue, ^{
+        NSString *connectionConfigJson = [self getConfig];
+        if (connectionConfigJson == nil) {
+           [self logMessage:@"Error getting config for feedback upload"];
+        }
+
+        GoPsiSendFeedback(connectionConfigJson, feedbackJson, b64EncodedPublicKey, uploadServer, @"", uploadServerHeaders);
+    });
 }
 
 
@@ -237,8 +258,11 @@
         [self logMessage:@"tunneledApp delegate lost"];
         return nil;
     }
-    
-    NSString *configStr = [self.tunneledAppDelegate getPsiphonConfig];
+
+    __block NSString *configStr = nil;
+    dispatch_sync(self->callbackQueue, ^{
+        configStr = [self.tunneledAppDelegate getPsiphonConfig];
+    });
     if (configStr == nil) {
         [self logMessage:@"Error getting config from delegate"];
         return nil;
@@ -384,7 +408,7 @@
     //
 
     // We'll record our state about what mode we're in.
-    tunnelWholeDevice = ([config[@"TunnelWholeDevice"] integerValue] == 1);
+    self->tunnelWholeDevice = ([config[@"TunnelWholeDevice"] integerValue] == 1);
 
     // Other optional fields not being altered. If not set, their defaults will be used:
     // * TunnelWholeDevice
@@ -509,7 +533,9 @@
     }
     else if ([noticeType isEqualToString:@"Exiting"]) {
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onExiting)]) {
-            [self.tunneledAppDelegate onExiting];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onExiting];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"AvailableEgressRegions"]) {
@@ -520,7 +546,9 @@
         }
 
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onAvailableEgressRegions:)]) {
-            [self.tunneledAppDelegate onAvailableEgressRegions:regions];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onAvailableEgressRegions:regions];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"SocksProxyPortInUse"]) {
@@ -531,7 +559,9 @@
         }
 
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onSocksProxyPortInUse:)]) {
-            [self.tunneledAppDelegate onSocksProxyPortInUse:[port integerValue]];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onSocksProxyPortInUse:[port integerValue]];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"HttpProxyPortInUse"]) {
@@ -542,7 +572,9 @@
         }
 
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onHttpProxyPortInUse:)]) {
-            [self.tunneledAppDelegate onHttpProxyPortInUse:[port integerValue]];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onHttpProxyPortInUse:[port integerValue]];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"ListeningSocksProxyPort"]) {
@@ -554,10 +586,12 @@
 
         NSInteger portInt = [port integerValue];
 
-        atomic_store(&localSocksProxyPort, portInt);
+        atomic_store(&self->localSocksProxyPort, portInt);
 
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onListeningSocksProxyPort:)]) {
-            [self.tunneledAppDelegate onListeningSocksProxyPort:portInt];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onListeningSocksProxyPort:portInt];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"ListeningHttpProxyPort"]) {
@@ -569,10 +603,12 @@
 
         NSInteger portInt = [port integerValue];
 
-        atomic_store(&localHttpProxyPort, portInt);
+        atomic_store(&self->localHttpProxyPort, portInt);
 
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onListeningHttpProxyPort:)]) {
-            [self.tunneledAppDelegate onListeningHttpProxyPort:portInt];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onListeningHttpProxyPort:portInt];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"UpstreamProxyError"]) {
@@ -583,7 +619,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onUpstreamProxyError:)]) {
-            [self.tunneledAppDelegate onUpstreamProxyError:message];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onUpstreamProxyError:message];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"ClientUpgradeDownloaded"]) {
@@ -600,7 +638,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onHomepage:)]) {
-            [self.tunneledAppDelegate onHomepage:url];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onHomepage:url];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"ClientRegion"]) {
@@ -611,7 +651,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onClientRegion:)]) {
-            [self.tunneledAppDelegate onClientRegion:region];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onClientRegion:region];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"SplitTunnelRegion"]) {
@@ -622,7 +664,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onSplitTunnelRegion:)]) {
-            [self.tunneledAppDelegate onSplitTunnelRegion:region];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onSplitTunnelRegion:region];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"Untunneled"]) {
@@ -633,7 +677,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onUntunneledAddress:)]) {
-            [self.tunneledAppDelegate onUntunneledAddress:address];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onUntunneledAddress:address];
+            });
         }
     }
     else if ([noticeType isEqualToString:@"BytesTransferred"]) {
@@ -647,7 +693,9 @@
         }
         
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onBytesTransferred::)]) {
-            [self.tunneledAppDelegate onBytesTransferred:[sent longLongValue]:[received longLongValue]];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onBytesTransferred:[sent longLongValue]:[received longLongValue]];
+            });
         }
     }
     
@@ -669,7 +717,7 @@
 #pragma mark - GoPsiPsiphonProvider protocol implementation (private)
 
 - (BOOL)bindToDevice:(long)fileDescriptor error:(NSError **)error {
-    if (!tunnelWholeDevice) {
+    if (!self->tunnelWholeDevice) {
         return FALSE;
     }
     
@@ -758,7 +806,7 @@
 }
 
 - (long)hasNetworkConnectivity {
-    BOOL hasConnectivity = [reachability currentReachabilityStatus] != NotReachable;
+    BOOL hasConnectivity = [self->reachability currentReachabilityStatus] != NotReachable;
 
     if (!hasConnectivity) {
         // changeConnectionStateTo self-throttles, so even if called multiple
@@ -781,7 +829,15 @@
 }
 
 - (void)notice:(NSString *)noticeJSON {
-    [self handlePsiphonNotice:noticeJSON];
+    // To prevent out-of-control memory usage, we want to limit the number of notices
+    // we asynchronously queue. Note that this means we'll start blocking Go threads
+    // after the first notice, but that's still preferable to a memory explosion.
+    dispatch_semaphore_wait(self->noticeHandlingSemaphore, DISPATCH_TIME_FOREVER);
+
+    dispatch_async(self->workQueue, ^{
+        [self handlePsiphonNotice:noticeJSON];
+        dispatch_semaphore_signal(self->noticeHandlingSemaphore);
+    });
 }
 
 
@@ -789,18 +845,22 @@
 
 - (void)logMessage:(NSString *)message {
     if ([self.tunneledAppDelegate respondsToSelector:@selector(onDiagnosticMessage:)]) {
-        [self.tunneledAppDelegate onDiagnosticMessage:message];
+        dispatch_async(self->callbackQueue, ^{
+            [self.tunneledAppDelegate onDiagnosticMessage:message];
+        });
     }
 }
 
 - (void)changeConnectionStateTo:(PsiphonConnectionState)newState evenIfSameState:(BOOL)forceNotification {
     // Store the new state and get the old state.
-    PsiphonConnectionState oldState = atomic_exchange(&connectionState, newState);
+    PsiphonConnectionState oldState = atomic_exchange(&self->connectionState, newState);
 
     // If the state has changed, inform the app.
     if (forceNotification || oldState != newState) {
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onConnectionStateChangedFrom:to:)]) {
-            [self.tunneledAppDelegate onConnectionStateChangedFrom:oldState to:newState];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onConnectionStateChangedFrom:oldState to:newState];
+            });
         }
 
         if (newState == PsiphonConnectionStateDisconnected) {
@@ -808,15 +868,21 @@
         }
         else if (newState == PsiphonConnectionStateConnecting &&
                  [self.tunneledAppDelegate respondsToSelector:@selector(onConnecting)]) {
-            [self.tunneledAppDelegate onConnecting];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onConnecting];
+            });
         }
         else if (newState == PsiphonConnectionStateConnected &&
                  [self.tunneledAppDelegate respondsToSelector:@selector(onConnected)]) {
-            [self.tunneledAppDelegate onConnected];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onConnected];
+            });
         }
         else if (newState == PsiphonConnectionStateWaitingForNetwork &&
                  [self.tunneledAppDelegate respondsToSelector:@selector(onStartedWaitingForNetworkConnectivity)]) {
-            [self.tunneledAppDelegate onStartedWaitingForNetworkConnectivity];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onStartedWaitingForNetworkConnectivity];
+            });
         }
     }
 }
@@ -868,14 +934,14 @@
 // time for the tunnel to notice the network is gone (depending on attempts to
 // use the tunnel).
 - (void)startInternetReachabilityMonitoring {
-    previousNetworkStatus = [reachability currentReachabilityStatus];
+    self->previousNetworkStatus = [self->reachability currentReachabilityStatus];
 
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(internetReachabilityChanged:) name:kReachabilityChangedNotification object:nil];
-    [reachability startNotifier];
+    [self->reachability startNotifier];
 }
 
 - (void)stopInternetReachabilityMonitoring {
-    [reachability stopNotifier];
+    [self->reachability stopNotifier];
     [[NSNotificationCenter defaultCenter] removeObserver:self name:kReachabilityChangedNotification object:nil];
 }
 
@@ -891,14 +957,16 @@
     PsiphonConnectionState currentConnectionState = [self getConnectionState];
 
     if (currentConnectionState == PsiphonConnectionStateConnected &&
-        previousNetworkStatus != NotReachable &&
-        previousNetworkStatus != networkStatus) {
+        self->previousNetworkStatus != NotReachable &&
+        self->previousNetworkStatus != networkStatus) {
         if ([self.tunneledAppDelegate respondsToSelector:@selector(onDeviceInternetConnectivityInterrupted)]) {
-            [self.tunneledAppDelegate onDeviceInternetConnectivityInterrupted];
+            dispatch_async(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onDeviceInternetConnectivityInterrupted];
+            });
         }
     }
 
-    previousNetworkStatus = networkStatus;
+    self->previousNetworkStatus = networkStatus;
 }
 
 /*!