HarmonyOS 鸿蒙Next基于网络的VPN连接实现

发布于 1周前 作者 bupafengyu 来自 鸿蒙OS

HarmonyOS 鸿蒙Next基于网络的VPN连接实现
<markdown _ngcontent-jvs-c237="" class="markdownPreContainer">

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, <span class="javascript"><span class="javascript">onCreate, want: ${want.abilityName}</span></span>); this.VpnConnection = vpnExt.createVpnConnection(this.context); console.info(“createVpnConnection success”); }

onRequest(want: Want, startId: number) { console.info(TAG, <span class="javascript"><span class="javascript">onRequest, want: ${want.abilityName}</span></span>); }

onConnect(want: Want) { console.info(TAG, <span class="javascript"><span class="javascript">onConnect, want: ${want.abilityName}</span></span>); return null; }

onDisconnect(want: Want) { console.info(TAG, <span class="javascript"><span class="javascript">onDisconnect, want: ${want.abilityName}</span></span>); }

onDestroy() { this.Destroy(); console.info(TAG, <span class="javascript"><span class="javascript">onDestroy</span></span>); }

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) ?? ‘’); }) } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

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"
      }
    ]
  }
}
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

如首次添加"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%’) } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

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) ?? ‘’); } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

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%’) } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

场景二:使用当前提供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, …) </span> 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, …) </span> 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, …) </span> 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; }

<span class="hljs-keyword"><span class="hljs-keyword">int</span></span> ret = read(fdInfo.tunFd, buffer, <span class="hljs-keyword"><span class="hljs-keyword">sizeof</span></span>(buffer));
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (ret &lt;= <span class="hljs-number"><span class="hljs-number">0</span></span>) {
  <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (errno != ERRORAGAIN) {
    sleep(<span class="hljs-number"><span class="hljs-number">1</span></span>);
  }
  <span class="hljs-keyword"><span class="hljs-keyword">continue</span></span>;
}

<span class="hljs-comment"><span class="hljs-comment">// Read the data from the virtual network interface and send it to the client through a TCP tunnel.</span></span>
NETMANAGER_VPN_LOGD(<span class="hljs-string"><span class="hljs-string">"buffer: %{public}s, len: %{public}d"</span></span>, buffer, ret);
ret = sendto(fdInfo.tunnelFd, buffer, ret, <span class="hljs-number"><span class="hljs-number">0</span></span>,
  <span class="hljs-keyword"><span class="hljs-keyword">reinterpret_cast</span></span>&lt;<span class="hljs-keyword"><span class="hljs-keyword">struct</span></span> sockaddr *&gt;(&amp;fdInfo.serverAddr), <span class="hljs-keyword"><span class="hljs-keyword">sizeof</span></span>(fdInfo.serverAddr));
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (ret &lt;= <span class="hljs-number"><span class="hljs-number">0</span></span>) {
  NETMANAGER_VPN_LOGE(<span class="hljs-string"><span class="hljs-string">"send to server[%{public}s:%{public}d] failed, ret: %{public}d, error: %{public}s"</span></span>,
    inet_ntoa(fdInfo.serverAddr.sin_addr), ntohs(fdInfo.serverAddr.sin_port), ret,
    strerror(errno));
  <span class="hljs-keyword"><span class="hljs-keyword">continue</span></span>;
}

} }

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; }

<span class="hljs-keyword"><span class="hljs-keyword">int</span></span> length = recvfrom(fdInfo.tunnelFd, buffer, <span class="hljs-keyword"><span class="hljs-keyword">sizeof</span></span>(buffer), <span class="hljs-number"><span class="hljs-number">0</span></span>,
  <span class="hljs-keyword"><span class="hljs-keyword">reinterpret_cast</span></span>&lt;<span class="hljs-keyword"><span class="hljs-keyword">struct</span></span> sockaddr *&gt;(&amp;fdInfo.serverAddr),
<span class="hljs-keyword"><span class="hljs-keyword">reinterpret_cast</span></span>&lt;socklen_t *&gt;(&amp;addrlen));
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (length &lt; <span class="hljs-number"><span class="hljs-number">0</span></span>) {
  <span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (errno != EAGAIN) {
    NETMANAGER_VPN_LOGE(<span class="hljs-string"><span class="hljs-string">"read tun device error: %{public}d %{public}d"</span></span>, errno, fdInfo.tunnelFd);
  }
  <span class="hljs-keyword"><span class="hljs-keyword">continue</span></span>;
}

NETMANAGER_VPN_LOGI(<span class="hljs-string"><span class="hljs-string">"from [%{public}s:%{public}d] data: %{public}s, len: %{public}d"</span></span>,
  inet_ntoa(fdInfo.serverAddr.sin_addr), ntohs(fdInfo.serverAddr.sin_port), buffer, length);
<span class="hljs-keyword"><span class="hljs-keyword">int</span></span> ret = write(fdInfo.tunFd, buffer, length);
<span class="hljs-keyword"><span class="hljs-keyword">if</span></span> (ret &lt;= <span class="hljs-number"><span class="hljs-number">0</span></span>) {
  NETMANAGER_VPN_LOGE(<span class="hljs-string"><span class="hljs-string">"error Write To Tunfd, errno: %{public}d"</span></span>, 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); } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

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) ?? ‘’); }) } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

具体实现可参考:VPN连接

常见问题

Q:vpn连接后如何判断?

A:可使用connection模块中getNetCapabilities能力获取,返回netBearType参数为4,即当前使用了VPN网络。

</markdown>

关于HarmonyOS 鸿蒙Next基于网络的VPN连接实现的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。
7 回复

有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html

beta3 无权限

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

不是吧,还没权限呢,

请问startVpnExtensionAbility有没有遇到VPN RegisterBundleName result = 0,有授权弹框但小钥匙图标没出来。

请问,这个可以跑鸿蒙next上嘛,我看模拟器都跑不了这个功能的,

这个工程是不是太老了,提示需要升级了,升级失败,又是一顿折腾
回到顶部