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网络。


HarmonyOS码上奇行
7k 声望2.8k 粉丝