diff --git a/README.md b/README.md index 5498458..2bc83ef 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Safe Device Safe Device -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); + }); }