vpn_sdp
SDP 版本 VPN 接入 Flutter 插件,基于零信任 SDP 安全隧道 SDK 封装,提供 iOS/Android 双平台 VPN 连接管理能力。
如果此文档显示不正常,请点击《VPN_SDP Flutter插件文档》跳转至语雀查看
一、使用前准备
注:在使用插件之前,需要大概过一下SDP厂商提供的接入文档,熟悉VPN的基本原理和使用流程,此插件依据厂商提供的《 零信任SDP移动终端(SDK)安全隧道接入方案V1.0 》文档和Demo基础上编写而成
1.1. iOS 接入流程
1.1.1 插件引入(自动集成)
在 Flutter 主工程的 pubspec.yaml 中添加依赖:
dependencies:
vpn_sdp: ^2.0.9
执行 flutter pub get 后,iOS 端通过 CocoaPods 自动集成。插件的 vpn_sdp.podspec 已声明以下 vendored_frameworks(共 18 个),pod install 时自动引入主 Target:
| 框架 | 说明 |
|---|---|
| AiSDPsdk.framework | SDP 厂商核心 SDK(Swift),提供 VPN 连接/认证/透传能力 |
| Alamofire.framework | Swift 网络库(SDK 内部依赖) |
| NIO.framework | SwiftNIO 网络框架(SDK 内部依赖) |
| NIOCore / NIOPosix / NIOHTTP1 / NIOEmbedded / NIOTLS / NIOSSL | SwiftNIO 子模块 |
| NIOConcurrencyHelpers / _NIODataStructures | SwiftNIO 并发与数据结构 |
| CNIOAtomics / CNIODarwin / CNIOHTTPParser / CNIOLinux / CNIOWindows | SwiftNIO C 层桥接 |
| CNIOBoringSSL / CNIOBoringSSLShims | BoringSSL 加密库桥接 |
同时 podspec 还声明了 资源文件 AiSDPsdkBundle.bundle(内含 SDP 隧道证书:client.crt、client.key、controllerca.crt、gatewayca.crt),也会随 Pod 自动拷贝到主 Target。
注意:以上框架和资源由 CocoaPods 自动管理,无需手动拖拽文件到 Xcode。
1.1.1.1 CocoaPods 1.17.0 兼容性修复(重要)
已知问题:CocoaPods 1.17.0 存在缺陷——
vendored_frameworks的文件引用虽已创建,但未自动加入 pod target 的链接阶段,导致pod install后编译时出现Undefined symbol: _OBJC_CLASS_$__TtC8AiSDPsdk11MacInfoTool等符号未定义错误。
修复方式:在主工程 ios/Podfile 末尾的 post_install 中添加 hook,确保每次 pod install 后 AiSDPsdk.framework 被正确链接:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
# 修复 CocoaPods 1.17.0 vendored_frameworks 链接缺失
vpn_target = installer.pods_project.targets.find { |t| t.name == 'vpn_sdp' }
if vpn_target
ai_ref = installer.pods_project.files.find { |f| f.path&.end_with?('AiSDPsdk.framework') }
if ai_ref
link_phase = vpn_target.frameworks_build_phase
unless link_phase.files.any? { |bf| bf.file_ref == ai_ref }
link_phase.add_file_reference(ai_ref)
end
end
end
end
适用范围:CocoaPods 1.17.x 版本。如果后续 CocoaPods 修复此缺陷或升级到其他版本后问题消失,可移除此 hook。
1.1.2 Swift 编译器版本兼容(重要)
已知问题:
AiSDPsdk.framework以预编译二进制形式提供,.swiftmodule为二进制格式,严格绑定编译时的 Swift 版本。当 Xcode 升级后,若新的 Swift 编译器版本与 SDK 编译时的版本不兼容,Tun 扩展(Network Extension)在import AiSDPsdk时会出现Module compiled with Swift X cannot be imported by the Swift Y compiler错误。
修复方式:在 SDK framework 的模块目录中提供 .swiftinterface 文本接口文件,使编译器在加载 .swiftmodule 失败时自动回退读取。详见 IOS_SDK_INTEGRATION_GUIDE.md 问题 3。
长期建议:联系 SDK 提供方在编译时加入
BUILD_LIBRARY_FOR_DISTRIBUTION=YES,以自动生成.swiftinterface,彻底消除 Swift 版本兼容问题。
1.1.3 手动配置步骤(Xcode 操作)
以下步骤无法通过 CocoaPods 自动化,必须在 Xcode 中手动完成:
(1)创建 Network Extension Target
- Xcode 打开
ios/Runner.xcworkspace(注意是.xcworkspace,不是.xcodeproj) - 左侧目录选中 Runner 项目 → 点击 TARGETS 区域底部 "+" 按钮
- 搜索并选择 "Network Extension" 模板
- Product Name 建议命名为 Tun(与后续
providerBundleId对应) - 编程语言选择 Swift(即使是 OC 项目也选择 Swift)
- 按向导完成创建
(2)配置 Capabilities
主 Target(Runner) 和 扩展 Target(Tun) 都需要配置:
- 在 Signing & Capabilities 选项卡中,点击 "+ Capability"
- 添加 Network Extensions,勾选 Packet Tunnel
- 添加 Personal VPN
(3)配置 entitlements
确保扩展 Target 的 .entitlements 文件包含:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourcompany.yourapp</string>
</array>
</dict>
</plist>
(4)配置扩展 Target 的 Framework 依赖
扩展 Target(Tun)不受 CocoaPods 管理,需要手动添加 AiSDPsdk.framework:
- 在扩展 Target(Tun)→ General → Frameworks, Libraries, and Embedded Content
- 点击 "+" 添加
AiSDPsdk.framework(从主工程的 Pods 中选择) - 将 Embed 设置为 "Do Not Embed"
- 如果编译提示缺少其他 framework(Alamofire、NIO 等),同样方式添加并设置为 "Do Not Embed"
(5)编译选项配置
- 主 Target(Runner)→ Build Settings → Build Options → Enable Bitcode 设置为 NO
(6)编写 PacketTunnelProvider
打开扩展 Target 的 PacketTunnelProvider.swift,替换为:
import NetworkExtension
import AiSDPsdk
class PacketTunnelProvider: PacketTunnelProviderSDK {}
Info.plist 验证:确保扩展 Target 的
Info.plist中NSExtensionPointIdentifier为com.apple.networkextension.packet-tunnel,NSExtensionPrincipalClass为$(PRODUCT_MODULE_NAME).PacketTunnelProvider。
1.1.4 iOS 集成架构总览
┌─────────────────────────────────────────────────────┐
│ Flutter App (pubspec.yaml) │
│ dependencies: │
│ vpn_sdp: ^2.0.9 │
└──────────────────┬──────────────────────────────────┘
│ flutter pub get
▼
┌─────────────────────────────────────────────────────┐
│ CocoaPods (Podfile) │
│ pod 'vpn_sdp' ←── vpn_sdp.podspec │
│ ├─ vendored_frameworks (18个 .framework) │
│ │ ├─ AiSDPsdk.framework (核心SDK) │
│ │ ├─ Alamofire.framework │
│ │ └─ SwiftNIO 系列 (16个) │
│ └─ resources │
│ └─ AiSDPsdkBundle.bundle (隧道证书) │
└──────────────────┬──────────────────────────────────┘
│ pod install (自动,无需手动拖拽)
▼
┌─────────────────────────────────────────────────────┐
│ Xcode 手动配置 (不可自动化) │
│ 1. 创建 Network Extension Target (如 Tun) │
│ 2. 主Target + 扩展Target 添加 Capabilities: │
│ - Network Extensions (Packet Tunnel) │
│ - Personal VPN │
│ 3. 扩展Target 手动添加 AiSDPsdk.framework │
│ Embed: Do Not Embed │
│ 4. Enable Bitcode → NO │
│ 5. PacketTunnelProvider 继承 PacketTunnelProviderSDK│
└─────────────────────────────────────────────────────┘
1.2. Android 接入
在应用启动之初,需要请求android.permission.BLUETOOTH_CONNECT权限,否则所有关于vpn连接的请求都将出现如下异常:
D/UDP ( 6317): 认证通道创建异常: Need android.permission.BLUETOOTH_CONNECT permission for AttributionSource { uid = 10421, packageName = com.gzdict.vpn_sdp_example, attributionTag = null, token = android.os.BinderProxy@37f742a, next = null }: getName
[ +2 ms] W/System.err( 6317): java.lang.SecurityException: Need android.permission.BLUETOOTH_CONNECT permission for AttributionSource { uid = 10421, packageName = com.gzdict.vpn_sdp_example, attributionTag = null, token = android.os.BinderProxy@37f742a, next = null }: getName
[ ] W/System.err( 6317): at android.os.Parcel.createExceptionOrNull(Parcel.java:2438)
[ +2 ms] W/System.err( 6317): at android.os.Parcel.createException(Parcel.java:2422)
[ ] W/System.err( 6317): at android.os.Parcel.readException(Parcel.java:2405)
[ ] W/System.err( 6317): at android.os.Parcel.readException(Parcel.java:2347)
[ ] W/System.err( 6317): at android.bluetooth.IBluetoothManager$Stub$Proxy.getName(IBluetoothManager.java:1194)
[ ] W/System.err( 6317): at android.bluetooth.BluetoothAdapter.getName(BluetoothAdapter.java:2345)
[ ] W/System.err( 6317): at com.aisec.sdp.util.InterfaceV2Method.verifyV2(InterfaceV2Method.java:61)
[ ] W/System.err( 6317): at com.aisec.sdp.thread.UdpAuthV2Thread.run(UdpAuthV2Thread.java:111)
关于权限请求,建议使用permission_handler插件处理,例如:
void _requestPermission() async {
if (!Platform.isAndroid) {
return;
}
var result = await Permission.bluetoothConnect.request();
if (!result.isGranted) {
/// 无权限,不可操作
}
}
接入插件,第一次调用连接方法成功后,会出现如下弹窗;点击确认后,状态栏会出现小钥匙的图标,此时就代表接入成功

1.3. Android 依赖说明(compileOnly)
本插件的 Android 端依赖(SDP 厂商 SDK aar、fastjson、dnsjava、bcpkix-jdk15to18、sunjce_provider.jar)均以 compileOnly 方式引入,不会随插件打包进宿主工程。宿主 App 需要自行集成这些依赖,否则运行时会抛出
NoClassDefFoundError/ClassNotFoundException异常。插件仓库中已不再附带
android/libs目录(原包含sdp-release-3.8.7.131_20240725.aar与sunjce_provider.jar),且插件android/build.gradle中不声明任何本地文件依赖,以上依赖必须由宿主工程完整提供,否则宿主工程编译时会因缺少文件导致构建失败(如 JetifyTransform 报错)。如需本地开发,请向 SDP 厂商获取对应的 aar 放入宿主工程的libs目录,并在宿主工程的build.gradle中配置flatDir仓库及相应依赖(参考以下示例):
repositories {
flatDir { dirs 'libs' }
}
dependencies {
implementation(name: 'sdp-release-3.8.7.131_20240725', ext: 'aar')
implementation 'com.alibaba:fastjson:1.2.70'
implementation 'dnsjava:dnsjava:2.1.7'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.68'
implementation files('libs/sunjce_provider.jar')
}
二、接口说明
2.1. verifyV3
特别注意:
- 在Android端,此方法会同
**proxyApiV2**以及**reconnect**方法互斥- 在iOS端,此方法会同
**reconnect**方法互斥
2.1.1. VPN三方认证登录,因为SDP需要知晓是否认证成功,所以需要外围系统对返回格式进行改造:
| 参数 | 说明 |
|---|---|
| code | 【0:成功 ;1:失败; 2:需要下一步认证】 |
| desc | 描述 |
| nextcmd | 二次认证方式:sms email otp |
| userName | 账号 |
| data | 接口需要返回给APP的数据,SDP会做透传 |
2.1.2. 参数如下:
| 114.135.115.224 | |||
| 55840 | |||
| reqUrl | 第三方的认证请求URL | ||
| method | http调用方式,传递post、get | post | |
| header | 使用json格式 | ||
| reqType |
请求方式:【1,2】,含义如下 1:健值对格式 2:body方式 |
2 | |
| 当传输方式选择1:使用json格式传递健值对 当传输方式选择2:使用字符串,SDP会透传 |
|||
| function | string | ||
| timeout | Duration | 防止插件异常导致结果无返回,设置一个超时时间 | 15秒 |
2.2. proxyApiV2
接口透传,返回透传结果,透传返回值亦可通过**messageStream**监听,code为**MethodCode.proxyApi**
- 在Android端,此方法会同
**verifyV3**以及**reconnect**方法互斥
2.2.1. 对第三方接口进行透传,
参数说明:
| reqUrl | 第三方的数据请求URL | ||
| method | http调用方式,传递post、get | ||
| header | 使用json格式 | ||
| reqType | 请求方式:【1,2】,含义如下 1:健值对格式 2:body方式 |
||
| 当传输方式选择1:使用json格式传递健值对 当传输方式选择2:使用字符串,SDP会透传 |
|||
2.3. reconnect vpn重连,无参数
- 在Android端,此方法会同
verifyV3以及proxyApiV2方法互斥 - 在iOS端,此方法会同
verifyV3方法互斥
2.4. disconnect
断开vpn连接,之后**messageStream**会接收到**RltCode.disconnect**消息。
2.5. messageStream
监听消息,返回SdpResult,其中code和data字段如下:
{
"code":0, #状态码
"data": {} #返回值,可能是json格式,也可能是字符串
}
2.6. setUp
初始化参数,仅iOS有效
| 变量名 | 类型 | 描述 | 默认值 |
|---|---|---|---|
| sn | String | 设备序列号 | 000000000000 |
| vpnLocalizedDesc | String | vpn描述,用于在系统设置Vpn列表中显示 | VPN_SDP |
| mac | String | 设备mac | 00:00:00:00:00:00 |
| providerBundleId | String | NetworkExtension的bundleId,例如com.example.vpn.tun |
三、示例
3.1.1. 初始化
# 依赖vpn_sdp
vpn_sdp: ^2.0.9
// 实例化vpn插件
final _vpnSdpPlugin = VpnSdp();
/// 如果有iOS,必须先调用此方法
_vpnSdpPlugin.setUp(
providerBundleId: "com.gzdict.gzyqdev.Tun",
);
/// Android端请求权限
if (!Platform.isAndroid) {
return;
}
var result = await Permission.bluetoothConnect.request();
if (!result.isGranted) {
/// 无权限,不可操作
}
3.2.2. 连接
var url = "${netDio.options.baseUrl}/km/sso/sdpLogin";
var msgCode = await _msgCode.rsaEncode();
var password = await _password.rsaEncode();
var body = {
"username": _username,
"password": password,
"captchaCode": msgCode,
};
// 在此调用verifyV3开启vpn
var result = await _vpnSdpPlugin
.verifyV3(username: _username, reqUrl: url, body: body, func: "yxt")
.toDialogRequest()
.start();
if (result != null) {
_transformData(result);
}
3.2.3. 断开连接
// 手动断开连接,一般在退出登录时调用
await _vpnSdpPlugin.disconnect();
3.2.4. 重连
手动重连,SDK内设置了主动重连功能,一般无需调用,仅在某些特殊情况提供此功能
await _vpnSdpPlugin.reconnect()
3.2.5. 透传
var url = "${netDio.options.baseUrl}/km/sso/sdpVerify";
var password = await _password.rsaEncode();
var data = {
"username": _username,
"password": password,
"captchaType": 0,
};
var result = await _vpnSdpPlugin
.proxyApiV2(
reqUrl: url,
body: data,
)
.toDialogRequest()
.start();
if (result != null) {
SmartDialog.showToast(result.toString());
}
3.2.6. 监听vpn返回的所有状态
StreamSubscription? _subscription;
.....
_subscription = _vpnSdpPlugin.messageStream.listen(
(data) {
setState(() {
_resultStr = "$data";
});
},
onError: (error) {
setState(() {
_resultStr = "$error";
});
},
);
四、错误码
以下错误均SDP提供,可能存在不完整情况,仅做参考;
4.1. Android 错误码
| code | 错误提示 | 报错原因 | 备注 |
|---|---|---|---|
| 1001 | 认证失败 | 认证接口返回的报错 | 第三方APPServer错误都在此处提示 |
| 1003 | 创建虚拟网卡失败 | 创建虚拟网卡失败 | 再认证成功后或者断链重新创建VPN失败情况会出现 |
| 1015 | 无法连接到服务器 | 连接SDP服务器超时 | (网络不通环境下会报此错误) |
| 1016 | 异常中断 | SDP服务端交互异常 | |
| 1019 | 服务器连接超时 | 连接SDP服务器超时 | (网络不通环境下会报此错误) |
| 1025 | 连接失败 | SDP控制器断开连接 | (网络不通环境下会报此错误) |
| 1040 | 验证读取超时异常 | 无法连接到SDP服务器 | (网络不通环境下会报此错误) |
| 1045 | 认证成功,vpn通道已存在 | 重复创建VPN | 如果手机已存在VPN也会报此错 |
| 9999 | 重新连接中...连接状态:true/false | 正在进行重连 | (10S检测一次,检测到连接失败就开始重连) |
| else | 后端返回的错误 | 交互异常 |
4.2. iOS 错误码
调用SDK连接的时候,新增回调方法TunConnectedWithCode的回调处理,红色code码为常见错误
(原TunConnected回调也保留,只不过没有code )
| code(后续补充) | 错误提示 | 报错原因 | 备注 |
|---|---|---|---|
| 1001 | 获取控制器地址或端口为空 | 服务器地址或端口设置 | 参数未初始化,或者是服务端配置的控制器地址未空 |
| 1002 |
敲门包发送异常,请稍后重试! | 网络不通,SPA敲门超时 | (网络不通环境下会报此错误) |
| 1003 | 连接超时 | 敲门包发送超时了 |
这种一般网络是正常的,但是SDP的敲门包发送出现问题,因为有些运营商会拦截SDP的敲门包;或者网络不好,敲门包发送超时了 |
| 1004 | 敲门超时 | 敲门包发送超时 | (网络不通环境下会报此错误) |
| 1005 | 连接关闭 | SDK与服务端的连接断开了 | 一般是用户主动关闭连接,或者网络断开,服务被关闭的时候 |
| 2001 | 连接服务器失败 | 与SDP控制器连接失败 | 可能是控制器地址连接超时或sdp服务出现异常了 |
| 2002 | 创建虚拟网卡失败 | 网关连接超时 | 多次重连也会存在此异常,或者sdk的refreshkey过期了,sdk重连也会发生 |
| 2004 | 会话过期,请重新登录 | refreshkey到期了 |
管理平台设置的会话到期时间一般是8小时,重连sdk会话已到期,需重新登录 |
| 2003 | 敲门绑定失败 | 敲门bind失败 | 网络异常了,建议切换网络或重启手机 |
| 3001 | 虚拟网卡启动配置错误 | 配置vpn出现异常 | 一般是系统出现异常了,建议重启手机 |
| 3002 | vpn异常断开 | VPN被其他app或人为关闭 | app之间抢占vpn;或进入设置页手动关闭vpn |
| else | 后端返回的错误 | SDP服务端或者第三方认证服务错误 |
调用SDK发送透传请求,调用proxyApi/proxyApiV2回调
| code | 提示信息(Any对象) | 报错原因 | 备注 |
|---|---|---|---|
| 1002 | 敲门包发送异常,请稍后重试! | 敲门包发送超时了 | 这种一般网络是正常的,但是SDP的敲门包发送出现问题,因为有些运营商会拦截SDP的敲门包;或者网络不好,敲门包发送超时了 |
| 1004 | 敲门超时 | 敲门包发送超时 | (网络不通环境下会报此错误) |
| else | 后端返回的错误 | SDP服务端或者第三方认证服务错误 |