diff --git a/README.md b/README.md
index 5498458..2bc83ef 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-Flutter (Null-Safety) Jailbroken, root, emulator and mock location detection.
+Flutter (Null-Safety) Jailbroken, root, emulator, mock location, and VPN detection.
## Getting Started
@@ -65,12 +65,18 @@ Checks whether device is real or emulator
bool isRealDevice = await SafeDevice.isRealDevice;
```
-bool isMockLocation = await SafeDevice.isMockLocation;
+Checks whether mock location is active
```
bool isMockLocation = await SafeDevice.isMockLocation;
```
+Check whether VPN connection is active on device
+
+```
+bool isVpnActive = await SafeDevice.isVpnEnabled();
+```
+
**Android:** If mock location check is disabled via config, the check always returns `false` and no location updates are started. If enabled (default), the check is active.
**iOS:** The config is stored for future use but does not affect current detection logic. Mock location detection on iOS is based on jailbreak/emulator status.
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 6fec74f..d99b652 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
configMap = (Map) call.arguments;
diff --git a/android/src/main/java/com/xamdesign/safe_device/isVpnEnabled/isVpnEnabled.java b/android/src/main/java/com/xamdesign/safe_device/isVpnEnabled/isVpnEnabled.java
new file mode 100644
index 0000000..5b8ee28
--- /dev/null
+++ b/android/src/main/java/com/xamdesign/safe_device/isVpnEnabled/isVpnEnabled.java
@@ -0,0 +1,35 @@
+package com.xamdesign.safe_device.isVpnEnabled;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.os.Build;
+
+public class IsVpnEnabled {
+ public static boolean isVpnActive(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return false;
+ }
+
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ Network[] networks = connectivityManager.getAllNetworks();
+ for (Network network : networks) {
+ NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
+ if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 6ff9ea0..1335ad6 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -19,6 +19,7 @@ class _MyAppState extends State {
bool isRealDevice = false;
bool isOnExternalStorage = false;
bool isSafeDevice = false;
+ bool isVpnEnabled = false;
bool isDevelopmentModeEnable = false;
Map jailbreakDetails = {};
Map rootDetectionDetails = {};
@@ -40,6 +41,7 @@ class _MyAppState extends State {
isOnExternalStorage = false;
isSafeDevice = false;
isDevelopmentModeEnable = false;
+ isVpnEnabled = false;
jailbreakDetails = {};
rootDetectionDetails = {};
});
@@ -50,6 +52,8 @@ class _MyAppState extends State {
isOnExternalStorage = await SafeDevice.isOnExternalStorage;
isSafeDevice = await SafeDevice.isSafeDevice;
isDevelopmentModeEnable = await SafeDevice.isDevelopmentModeEnable;
+ isVpnEnabled = await SafeDevice.isVpnEnabled;
+
// iOS-specific enhanced jailbreak detection
if (Platform.isIOS) {
@@ -64,6 +68,7 @@ class _MyAppState extends State {
setState(() {
this.isJailBroken = isJailBroken;
+ this.isVpnEnabled = isVpnEnabled;
this.isJailBrokenCustom = isJailBrokenCustom;
this.isMockLocation = isMockLocation;
this.isRealDevice = isRealDevice;
@@ -110,6 +115,7 @@ class _MyAppState extends State {
),
SizedBox(height: 8),
if (jailbreakDetails.containsKey('isSimulator'))
+
Card(
color: jailbreakDetails['isSimulator'] == true
? Colors.blue.shade50
@@ -212,6 +218,7 @@ class _MyAppState extends State {
mainAxisSize: MainAxisSize.min,
children: [
buildInfoRow('isJailBroken()', isJailBroken),
+ buildInfoRow('isVpnEnabled()', isVpnEnabled),
if (Platform.isIOS) ...[
buildInfoRow('isJailBrokenCustom()', isJailBrokenCustom),
],
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 943fae1..ef9df06 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -198,7 +198,7 @@ packages:
path: ".."
relative: true
source: path
- version: "1.3.10"
+ version: "1.4.1"
sky_engine:
dependency: transitive
description: flutter
diff --git a/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m b/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m
index 652d6e5..39e517e 100644
--- a/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m
+++ b/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m
@@ -7,7 +7,8 @@
#import "SafeDeviceJailbreakDetection.h"
#import
-
+#import
+#include
@implementation SafeDeviceJailbreakDetection
#pragma mark - Jailbreak Detection Paths
@@ -126,6 +127,39 @@ + (BOOL)isSimulator {
#endif
}
+#pragma mark - VPN Detection
+
++ (BOOL)isVPNConnected {
+ BOOL isVPN = NO;
+ struct ifaddrs *interfaces = NULL;
+ struct ifaddrs *temp_addr = NULL;
+
+ // Retrieve the current interfaces - returns 0 on success
+ if (getifaddrs(&interfaces) == 0) {
+ // Loop through linked list of interfaces
+ temp_addr = interfaces;
+ while (temp_addr != NULL) {
+ if (temp_addr->ifa_addr != NULL && (temp_addr->ifa_addr->sa_family == AF_INET || temp_addr->ifa_addr->sa_family == AF_INET6)) {
+ NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name];
+ if ([interfaceName containsString:@"tap"] ||
+ [interfaceName containsString:@"tun"] ||
+ [interfaceName containsString:@"ppp"] ||
+ [interfaceName containsString:@"ipsec"] ||
+ [interfaceName containsString:@"utun"]) {
+ isVPN = YES;
+ break;
+ }
+ }
+ temp_addr = temp_addr->ifa_next;
+ }
+ }
+
+ // Free memory
+ freeifaddrs(interfaces);
+
+ return isVPN;
+}
+
#pragma mark - Main Detection Method
+ (BOOL)isJailbroken {
diff --git a/ios/safe_device/Sources/safe_device/SafeDevicePlugin.m b/ios/safe_device/Sources/safe_device/SafeDevicePlugin.m
index e99fe57..95f5059 100644
--- a/ios/safe_device/Sources/safe_device/SafeDevicePlugin.m
+++ b/ios/safe_device/Sources/safe_device/SafeDevicePlugin.m
@@ -28,6 +28,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
result([NSNumber numberWithBool:[self isJailBrokenCustom]]);
}else if ([@"jailbreakDetails" isEqualToString:call.method]) {
result([self jailbreakDetails]);
+ }else if ([@"isVpnEnabled" isEqualToString:call.method]) {
+ result([NSNumber numberWithBool:[SafeDeviceJailbreakDetection isVPNConnected]]);
}else if ([@"canMockLocation" isEqualToString:call.method]) {
//For now we have returned if device is Jail Broken or if it's not real device. There is no
//strong detection of Mock location in iOS
@@ -176,6 +178,7 @@ - (NSDictionary*)jailbreakDetails {
@"hasObviousJailbreakSigns": @(obviousJailbreak),
@"dttResult": @([DTTJailbreakDetection isJailbroken]),
@"customResult": @([SafeDeviceJailbreakDetection isJailbroken]),
+ @"isVpnConnected": @([SafeDeviceJailbreakDetection isVPNConnected]),
@"finalResult": @([self isJailBroken])
};
}
diff --git a/ios/safe_device/Sources/safe_device/include/safe_device/SafeDeviceJailbreakDetection.h b/ios/safe_device/Sources/safe_device/include/safe_device/SafeDeviceJailbreakDetection.h
index 16f8966..ec60466 100644
--- a/ios/safe_device/Sources/safe_device/include/safe_device/SafeDeviceJailbreakDetection.h
+++ b/ios/safe_device/Sources/safe_device/include/safe_device/SafeDeviceJailbreakDetection.h
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Main jailbreak detection method
+ (BOOL)isJailbroken;
+/// Check whether a VPN connection is currently active.
++ (BOOL)isVPNConnected;
+
/// Get detailed breakdown of jailbreak detection methods
+ (NSDictionary *)getJailbreakDetails;
diff --git a/lib/safe_device.dart b/lib/safe_device.dart
index 38ece80..20d0e78 100644
--- a/lib/safe_device.dart
+++ b/lib/safe_device.dart
@@ -165,4 +165,14 @@ class SafeDevice {
}
return {}; // iOS devices return empty map for Android-specific method
}
+
+ static Future get isVpnEnabled async {
+ ensureInitiated();
+ try {
+ return await _channel.invokeMethod('isVpnEnabled') ?? false;
+ } catch (e) {
+ print('Error checking VPN status: $e');
+ return false;
+ }
+ }
}
diff --git a/test/safe_device_test.dart b/test/safe_device_test.dart
index 41fbd59..8957e3f 100644
--- a/test/safe_device_test.dart
+++ b/test/safe_device_test.dart
@@ -1,5 +1,6 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:safe_device/safe_device.dart';
void main() {
const MethodChannel channel = MethodChannel('safe_device');
@@ -24,4 +25,20 @@ void main() {
test('getPlatformVersion', () async {
//expect(await SafeDevice.platformVersion, '42');
});
+
+ test('isVpnEnabled forwards the platform call', () async {
+ final binaryMessenger =
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+ binaryMessenger.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
+ if (methodCall.method == 'init') {
+ return null;
+ }
+ if (methodCall.method == 'isVpnEnabled') {
+ return true;
+ }
+ return null;
+ });
+
+ expect(await SafeDevice.isVpnEnabled(), isTrue);
+ });
}