From 72053321d0b6c64fe1399a4b9dfb19af48cdebbb Mon Sep 17 00:00:00 2001 From: JPSE Date: Tue, 28 Jul 2026 09:45:03 +0700 Subject: [PATCH 1/3] feat: add vpn checking --- README.md | 10 ++++-- .../safe_device/SafeDevicePlugin.java | 3 ++ .../isVpnEnabled/isVpnEnabled.java | 33 +++++++++++++++++++ example/lib/main.dart | 5 +++ example/pubspec.lock | 2 +- .../SafeDeviceJailbreakDetection.m | 28 ++++++++++++++++ .../Sources/safe_device/SafeDevicePlugin.m | 3 ++ .../SafeDeviceJailbreakDetection.h | 3 ++ lib/safe_device.dart | 10 ++++++ test/safe_device_test.dart | 17 ++++++++++ 10 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/xamdesign/safe_device/isVpnEnabled/isVpnEnabled.java 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/java/com/xamdesign/safe_device/SafeDevicePlugin.java b/android/src/main/java/com/xamdesign/safe_device/SafeDevicePlugin.java index 6c0603d..3dcf835 100644 --- a/android/src/main/java/com/xamdesign/safe_device/SafeDevicePlugin.java +++ b/android/src/main/java/com/xamdesign/safe_device/SafeDevicePlugin.java @@ -11,6 +11,7 @@ import com.xamdesign.safe_device.MockLocation.LocationAssistant; import com.xamdesign.safe_device.Rooted.RootedCheck; import com.xamdesign.safe_device.SafeDeviceConfig; +import com.xamdesign.safe_device.isVpnEnabled.IsVpnEnabled; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; @@ -85,6 +86,8 @@ public void onMethodCall(MethodCall call, final MethodChannel.Result result) { } } else if (call.method.equals("rootDetectionDetails")) { result.success(RootedCheck.getRootDetectionDetails(context)); + } else if (call.method.equals("isVpnEnabled")) { + result.success(IsVpnEnabled.isVpnActive(context)); } else if (call.method.equals("init")) { @SuppressWarnings("unchecked") Map 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..fe6e05f --- /dev/null +++ b/android/src/main/java/com/xamdesign/safe_device/isVpnEnabled/isVpnEnabled.java @@ -0,0 +1,33 @@ +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 network = connectivityManager.getActiveNetwork(); + if (network == null) { + return false; + } + + NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); + } +} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 6ff9ea0..e1acf8e 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 isVpnActive = false; bool isDevelopmentModeEnable = false; Map jailbreakDetails = {}; Map rootDetectionDetails = {}; @@ -50,6 +51,7 @@ class _MyAppState extends State { isOnExternalStorage = await SafeDevice.isOnExternalStorage; isSafeDevice = await SafeDevice.isSafeDevice; isDevelopmentModeEnable = await SafeDevice.isDevelopmentModeEnable; + // iOS-specific enhanced jailbreak detection if (Platform.isIOS) { @@ -109,6 +111,9 @@ class _MyAppState extends State { style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 8), + ElevatedButton(onPressed: () async { + isVpnActive = await SafeDevice.isVpnEnabled(); + }, child: Text("VPN is ${isVpnActive}")), if (jailbreakDetails.containsKey('isSimulator')) Card( color: jailbreakDetails['isSimulator'] == true 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..7417f2d 100644 --- a/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m +++ b/ios/safe_device/Sources/safe_device/SafeDeviceJailbreakDetection.m @@ -7,6 +7,7 @@ #import "SafeDeviceJailbreakDetection.h" #import +#import @implementation SafeDeviceJailbreakDetection @@ -126,6 +127,33 @@ + (BOOL)isSimulator { #endif } +#pragma mark - VPN Detection + ++ (BOOL)isVPNConnected { + CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings(); + if (!proxySettings) { + return NO; + } + + NSDictionary *settings = (__bridge_transfer NSDictionary *)proxySettings; + NSDictionary *scopedSettings = settings[@"__SCOPED__"]; + if (![scopedSettings isKindOfClass:[NSDictionary class]]) { + return NO; + } + + for (NSString *key in scopedSettings.allKeys) { + if ([key hasPrefix:@"tap"] || + [key hasPrefix:@"tun"] || + [key hasPrefix:@"ppp"] || + [key hasPrefix:@"ipsec"] || + [key hasPrefix:@"utun"]) { + return YES; + } + } + + return NO; +} + #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..752d18e 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 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); + }); } From d1623369da431b37c051c151464e2d5bf11b651e Mon Sep 17 00:00:00 2001 From: JPSE Date: Tue, 28 Jul 2026 10:13:06 +0700 Subject: [PATCH 2/3] refactor: style format code and display the vpn checker on the main.dart --- example/lib/main.dart | 10 ++++++---- lib/safe_device.dart | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index e1acf8e..1335ad6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,7 +19,7 @@ class _MyAppState extends State { bool isRealDevice = false; bool isOnExternalStorage = false; bool isSafeDevice = false; - bool isVpnActive = false; + bool isVpnEnabled = false; bool isDevelopmentModeEnable = false; Map jailbreakDetails = {}; Map rootDetectionDetails = {}; @@ -41,6 +41,7 @@ class _MyAppState extends State { isOnExternalStorage = false; isSafeDevice = false; isDevelopmentModeEnable = false; + isVpnEnabled = false; jailbreakDetails = {}; rootDetectionDetails = {}; }); @@ -51,6 +52,7 @@ 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 @@ -66,6 +68,7 @@ class _MyAppState extends State { setState(() { this.isJailBroken = isJailBroken; + this.isVpnEnabled = isVpnEnabled; this.isJailBrokenCustom = isJailBrokenCustom; this.isMockLocation = isMockLocation; this.isRealDevice = isRealDevice; @@ -111,10 +114,8 @@ class _MyAppState extends State { style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), SizedBox(height: 8), - ElevatedButton(onPressed: () async { - isVpnActive = await SafeDevice.isVpnEnabled(); - }, child: Text("VPN is ${isVpnActive}")), if (jailbreakDetails.containsKey('isSimulator')) + Card( color: jailbreakDetails['isSimulator'] == true ? Colors.blue.shade50 @@ -217,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/lib/safe_device.dart b/lib/safe_device.dart index 752d18e..20d0e78 100644 --- a/lib/safe_device.dart +++ b/lib/safe_device.dart @@ -166,7 +166,7 @@ class SafeDevice { return {}; // iOS devices return empty map for Android-specific method } - static Future isVpnEnabled() async { + static Future get isVpnEnabled async { ensureInitiated(); try { return await _channel.invokeMethod('isVpnEnabled') ?? false; From de279bc80781ba97e0179330b007d10ee0590bb6 Mon Sep 17 00:00:00 2001 From: JPSE Date: Tue, 28 Jul 2026 10:33:50 +0700 Subject: [PATCH 3/3] refactor:add dependencies --- android/src/main/AndroidManifest.xml | 1 + .../isVpnEnabled/isVpnEnabled.java | 14 +++--- .../SafeDeviceJailbreakDetection.m | 48 +++++++++++-------- 3 files changed, 36 insertions(+), 27 deletions(-) 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 @@ + #import - +#include @implementation SafeDeviceJailbreakDetection #pragma mark - Jailbreak Detection Paths @@ -130,28 +130,34 @@ + (BOOL)isSimulator { #pragma mark - VPN Detection + (BOOL)isVPNConnected { - CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings(); - if (!proxySettings) { - return NO; - } - - NSDictionary *settings = (__bridge_transfer NSDictionary *)proxySettings; - NSDictionary *scopedSettings = settings[@"__SCOPED__"]; - if (![scopedSettings isKindOfClass:[NSDictionary class]]) { - return NO; - } - - for (NSString *key in scopedSettings.allKeys) { - if ([key hasPrefix:@"tap"] || - [key hasPrefix:@"tun"] || - [key hasPrefix:@"ppp"] || - [key hasPrefix:@"ipsec"] || - [key hasPrefix:@"utun"]) { - return YES; + 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; } } - - return NO; + + // Free memory + freeifaddrs(interfaces); + + return isVPN; } #pragma mark - Main Detection Method