基于网络的VPN连接实现
VPN全称为虚拟私人网络(Virtual Private Network),是常用于连接中、大型企业或团体间私人网络的通讯方法,利用隧道协议(Tunneling Protocol)来达到发送端认证、消息保密与准确性等功能。
使用过程中外网的用户可以使用 vpn client 连接组织搭建的 vpn server 以建立通信隧道,随后便建立了虚拟的私人网络,处于外网的 worker 和内网中的 server 可以相互通信。
场景一:手机应用配置VPN客户端转发请求到远程VPN服务端访问互联网,实现建立基本VPN服务能力
效果图
启动界面如下图示:
点击'启动vpnExt'按钮,会弹窗提示是否使用vpn权限连接。
方案描述
当前提供三方VPN能力主要用于创建虚拟网卡及配置VPN路由信息,连接隧道过程及内部连接的协议需要应用内部自行实现,创建过程可参考如下:
1、项目中建立VpnAbility.ets文件,继承调用VpnExtensionAbility提供VPN创建、销毁等生命周期能力。
2、ability实现后在entry-module.json5中,添加extensionAbilities相关配置。
3、设置want参数指定的启动目标,启用VPN服务。
核心代码
1、项目中建立VpnAbility.ets文件,继承调用VpnExtensionAbility提供VPN创建、销毁等生命周期能力,参考文档:@ohos.app.ability.VpnExtensionAbility。
private VpnConnection: vpnExt.VpnConnection;
onCreate(want: Want) {
console.info(TAG, `onCreate, want: ${want.abilityName}`);
this.VpnConnection = vpnExt.createVpnConnection(this.context);
console.info("createVpnConnection success");
}
onRequest(want: Want, startId: number) {
console.info(TAG, `onRequest, want: ${want.abilityName}`);
}
onConnect(want: Want) {
console.info(TAG, `onConnect, want: ${want.abilityName}`);
return null;
}
onDisconnect(want: Want) {
console.info(TAG, `onDisconnect, want: ${want.abilityName}`);
}
onDestroy() {
this.Destroy();
console.info(TAG, `onDestroy`);
}
Destroy() {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Destroy');
vpn_client.stopVpn(g_tunnelFd);
this.VpnConnection.destroy().then(() => {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Destroy Success');
}).catch((err : Error) => {
hilog.error(0x0000, 'developTag', 'vpn Destroy Failed: %{public}s', JSON.stringify(err) ?? '');
})
}
2、module.json5文件中配置extensionAbilities参数,样例如下:
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryAbility/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:startIcon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.GET_NETWORK_INFO"
}
],
"extensionAbilities": [
{
"name": "MyVpnExtAbility",
"srcEntry": "./ets/vpnAbility/MyVpnExtAbility.ets",
"type": "vpn"
}
]
}
}
如首次添加"type": "vpn"时报红,“ctrl+左键”点击"type",在"enum"中添加“vpn”参数,
配置修改后界面如下:
3、设置want参数指定的启动目标,启用VPN服务。
let want: Want = {
deviceId: "",
bundleName: "com.example.myvpndemo",
abilityName: "MyVpnExtAbility",
};
@Entry
@Component
struct StartVpn {
build() {
Row() {
Column() {
Button($r('app.string.btn_start_vpnExt')).onClick(() => {
vpnext.startVpnExtensionAbility(want); //启用VPN服务
}).fontSize(50)
}
.width('100%')
}
.height('100%')
}
}
4、创建 VPN连接 网络,有关参数说明可参考:vpnExtension.VpnConfig。
class Config {
addresses: AddressWithPrefix[];
mtu: number;
dnsAddresses: string[];
trustedApplications: string[];
blockedApplications: string[];
constructor(
tunIp: string,
blockedAppName: string
) {
this.addresses = [
new AddressWithPrefix(new Address(tunIp, 1), 24)
];
this.mtu = 1400;
this.dnsAddresses = ["114.114.114.114"];
this.trustedApplications = [];
this.blockedApplications = [blockedAppName];
}
}
let config = new Config(this.tunIp, this.blockedAppName);
try {
this.VpnConnection.create(config).then((data) => {
g_tunFd = data;
hilog.error(0x0000, 'developTag', 'tunfd: %{public}s', JSON.stringify(data) ?? '');
vpn_client.startVpn(g_tunFd, g_tunnelFd);
})
} catch (error) {
hilog.error(0x0000, 'developTag', 'vpn setUp fail: %{public}s', JSON.stringify(error) ?? '');
}
VPN创建成功时日志打印如下图:
5、销毁VPN连接。
let want: Want = {
deviceId: "",
bundleName: "com.example.myvpndemo",
abilityName: "MyVpnExtAbility",
};
let g_tunnelFd = -1;
@Entry
@Component
struct StopVpn {
@State message: string = 'VPN';
@State vpnServerIp: string = '192.168.3.49 ';
@State tunIp: string = '10.0.0.5';
@State routeAddr: string = '192.168.214.0';
@State prefix: string = '24';
@State blockedAppName: string = 'com.example.baidumyapplication';
private context = getContext(this) as common.VpnExtensionContext;
private VpnConnection: vpnext.VpnConnection = vpnext.createVpnConnection(this.context);
Destroy() {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Destroy');
vpn_client.stopVpn(g_tunnelFd);
this.VpnConnection.destroy().then(() => {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Destroy Success');
}).catch((err : Error) => {
hilog.error(0x0000, 'developTag', 'vpn Destroy Failed: %{public}s', JSON.stringify(err) ?? '');
})
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(35)
.fontWeight(FontWeight.Bold)
.onClick(() => {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Client');
})
Button('stop vpn').onClick(() => {
this.Destroy();
}).fontSize(50)
Button('stop vpnExt').onClick(() => {
vpnext.stopVpnExtensionAbility(want);
}).fontSize(50)
}.width('100%')
}.height('100%')
}
}
场景二:使用当前提供VPN能力建立隧道连接,实现连接过程中VPN数据包的传递
方案描述
建立隧道有关能力可通过NDK侧代码段实现:
1、建立vpn_client.cpp文件,写入vpn隧道通信启动、停止有关能力。
2、NDK添加可导出配置的使用接口能力。
3、页面中调用能力import引入。
核心代码
1、建立vpn_client.cpp文件,写入vpn隧道通信有关能力。
#define MAKE_FILE_NAME (strrchr(__FILE__, '/') + 1)
#define NETMANAGER_VPN_LOGE(fmt, ...) \
OH_LOG_Print(LOG_APP, LOG_ERROR, 0x15b0, "NetMgrVpn", "vpn [%{public}s %{public}d] " fmt, MAKE_FILE_NAME, \
__LINE__, ##__VA_ARGS__)
#define NETMANAGER_VPN_LOGI(fmt, ...) \
OH_LOG_Print(LOG_APP, LOG_INFO, 0x15b0, "NetMgrVpn", "vpn [%{public}s %{public}d] " fmt, MAKE_FILE_NAME, \
__LINE__, ##__VA_ARGS__)
#define NETMANAGER_VPN_LOGD(fmt, ...) \
OH_LOG_Print(LOG_APP, LOG_DEBUG, 0x15b0, "NetMgrVpn", "vpn [%{public}s %{public}d] " fmt, MAKE_FILE_NAME, \
__LINE__, ##__VA_ARGS__)
constexpr int BUFFER_SIZE = 2048;
constexpr int ERRORAGAIN = 11;
struct FdInfo {
int32_t tunFd = 0;
int32_t tunnelFd = 0;
struct sockaddr_in serverAddr;
};
static FdInfo g_fdInfo;
static bool g_threadRunF = false;
static std::thread g_threadt1;
static std::thread g_threadt2;
static constexpr const int MAX_STRING_LENGTH = 1024;
static std::string GetStringFromValueUtf8(napi_env env, napi_value value)
{
std::string result;
char str[MAX_STRING_LENGTH] = {0};
size_t length = 0;
napi_get_value_string_utf8(env, value, str, MAX_STRING_LENGTH, &length);
if (length > 0) {
return result.append(str, length);
}
return result;
}
//获取隧道能力
static void HandleReadTunfd(FdInfo fdInfo)
{
uint8_t buffer[BUFFER_SIZE] = {0};
while (g_threadRunF) {
if (fdInfo.tunFd <= 0) {
sleep(1);
continue;
}
int ret = read(fdInfo.tunFd, buffer, sizeof(buffer));
if (ret <= 0) {
if (errno != ERRORAGAIN) {
sleep(1);
}
continue;
}
// Read the data from the virtual network interface and send it to the client through a TCP tunnel.
NETMANAGER_VPN_LOGD("buffer: %{public}s, len: %{public}d", buffer, ret);
ret = sendto(fdInfo.tunnelFd, buffer, ret, 0,
reinterpret_cast<struct sockaddr *>(&fdInfo.serverAddr), sizeof(fdInfo.serverAddr));
if (ret <= 0) {
NETMANAGER_VPN_LOGE("send to server[%{public}s:%{public}d] failed, ret: %{public}d, error: %{public}s",
inet_ntoa(fdInfo.serverAddr.sin_addr), ntohs(fdInfo.serverAddr.sin_port), ret,
strerror(errno));
continue;
}
}
}
static void HandleTcpReceived(FdInfo fdInfo)
{
int addrlen = sizeof(struct sockaddr_in);
uint8_t buffer[BUFFER_SIZE] = {0};
while (g_threadRunF) {
if (fdInfo.tunnelFd <= 0) {
sleep(1);
continue;
}
int length = recvfrom(fdInfo.tunnelFd, buffer, sizeof(buffer), 0,
reinterpret_cast<struct sockaddr *>(&fdInfo.serverAddr),
reinterpret_cast<socklen_t *>(&addrlen));
if (length < 0) {
if (errno != EAGAIN) {
NETMANAGER_VPN_LOGE("read tun device error: %{public}d %{public}d", errno, fdInfo.tunnelFd);
}
continue;
}
NETMANAGER_VPN_LOGI("from [%{public}s:%{public}d] data: %{public}s, len: %{public}d",
inet_ntoa(fdInfo.serverAddr.sin_addr), ntohs(fdInfo.serverAddr.sin_port), buffer, length);
int ret = write(fdInfo.tunFd, buffer, length);
if (ret <= 0) {
NETMANAGER_VPN_LOGE("error Write To Tunfd, errno: %{public}d", errno);
}
}
}
//通信能力创建
static napi_value TcpConnect(napi_env env, napi_callback_info info)
{
size_t numArgs = 2;
size_t argc = numArgs;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int32_t port = 0;
napi_get_value_int32(env, args[1], &port);
std::string ipAddr = GetStringFromValueUtf8(env, args[0]);
NETMANAGER_VPN_LOGI("ip: %{public}s port: %{public}d", ipAddr.c_str(), port);
int32_t sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockFd == -1) {
NETMANAGER_VPN_LOGE("socket() error");
return 0;
}
struct timeval timeout = {1, 0};
setsockopt(sockFd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<char *>(&timeout), sizeof(struct timeval));
memset(&g_fdInfo.serverAddr, 0, sizeof(g_fdInfo.serverAddr));
g_fdInfo.serverAddr.sin_family = AF_INET;
g_fdInfo.serverAddr.sin_addr.s_addr = inet_addr(ipAddr.c_str()); // server's IP addr
g_fdInfo.serverAddr.sin_port = htons(port); // port
NETMANAGER_VPN_LOGI("Connection successful\n");
napi_value tunnelFd;
napi_create_int32(env, sockFd, &tunnelFd);
return tunnelFd;
}
//vpn启用当前隧道连接
static napi_value StartVpn(napi_env env, napi_callback_info info)
{
size_t numArgs = 2;
size_t argc = numArgs;
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_get_value_int32(env, args[0], &g_fdInfo.tunFd);
napi_get_value_int32(env, args[1], &g_fdInfo.tunnelFd);
if (g_threadRunF) {
g_threadRunF = false;
g_threadt1.join();
g_threadt2.join();
}
g_threadRunF = true;
std::thread tt1(HandleReadTunfd, g_fdInfo);
std::thread tt2(HandleTcpReceived, g_fdInfo);
g_threadt1 = std::move(tt1);
g_threadt2 = std::move(tt2);
NETMANAGER_VPN_LOGI("StartVpn successful\n");
napi_value retValue;
napi_create_int32(env, 0, &retValue);
return retValue;
}
//vpn停止当前隧道连接
static napi_value StopVpn(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int32_t tunnelFd;
napi_get_value_int32(env, args[0], &tunnelFd);
if (tunnelFd) {
close(tunnelFd);
tunnelFd = 0;
}
if (g_threadRunF) {
g_threadRunF = false;
g_threadt1.join();
g_threadt2.join();
}
NETMANAGER_VPN_LOGI("StopVpn successful\n");
napi_value retValue;
napi_create_int32(env, 0, &retValue);
return retValue;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{"tcpConnect", nullptr, TcpConnect, nullptr, nullptr, nullptr, napi_default, nullptr},
{"startVpn", nullptr, StartVpn, nullptr, nullptr, nullptr, napi_default, nullptr},
{"stopVpn", nullptr, StopVpn, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
// .nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
NETMANAGER_VPN_LOGI("vpn 15b0 HELLO ~~~~~~~~~~");
napi_module_register(&demoModule);
}
2、NDK添加可导出配置的使用接口能力,以当前执行使用项目为例,index.d.ts文件中配置方法:
3、页面中调用能力import引入。
import vpn_client from 'libvpn_client.so';
let want: Want = {
deviceId: "",
bundleName: "com.example.myvpndemo",
abilityName: "MyVpnExtAbility",
};
//g_tunFd连接前设置生成的标识符,g_tunnelFd表示连接隧道成功后对应的表示符
let g_tunFd = -1;
let g_tunnelFd = -1;
@Entry
@Component
struct StartVpn {
@State message: string = 'Toy VPN';
@State vpnServerIp: string = '192.168.3.49';
@State tunIp: string = '10.0.0.5';
@State prefix: string = '24';
@State blockedAppName: string = 'com.example.baidumyapplication';
private context = getContext(this) as common.VpnExtensionContext;
private VpnConnection: vpnext.VpnConnection = vpnext.createVpnConnection(this.context);
//创建隧道连接
CreateTunnel() {
g_tunnelFd = vpn_client.tcpConnect(this.vpnServerIp, 8888);
}
Protect() {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Protect');
this.VpnConnection.protect(g_tunnelFd).then(() => {
hilog.info(0x0000, 'developTag', '%{public}s', 'vpn Protect Success');
}).catch((err : Error) => {
hilog.error(0x0000, 'developTag', 'vpn Protect Failed %{public}s', JSON.stringify(err) ?? '');
})
}
}
具体实现可参考:VPN连接。
常见问题
Q:vpn连接后如何判断?
A:可使用connection模块中getNetCapabilities能力获取,返回netBearType参数为4,即当前使用了VPN网络。