Преглед изворни кода

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

Rod Hynes пре 8 година
родитељ
комит
7b6953d160

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

@@ -59,6 +59,8 @@ typedef NS_ENUM(NSInteger, PsiphonConnectionState)
  @protocol TunneledAppDelegate
  @protocol TunneledAppDelegate
  Used to communicate with the application that is using the PsiphonTunnel framework,
  Used to communicate with the application that is using the PsiphonTunnel framework,
  and retrieve config info from it.
  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>
 @protocol TunneledAppDelegate <NSObject>
 
 

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

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