基于网络的VPN连接实现

好难的昵称
发布于 2024-8-1 16:38
浏览
0收藏

VPN全称为虚拟私人网络(Virtual Private Network),是常用于连接中、大型企业或团体间私人网络的通讯方法,利用隧道协议(Tunneling Protocol)来达到发送端认证、消息保密与准确性等功能。

使用过程中外网的用户可以使用 vpn client 连接组织搭建的 vpn server 以建立通信隧道,随后便建立了虚拟的私人网络,处于外网的 worker 和内网中的 server 可以相互通信。

场景一:手机应用配置VPN客户端转发请求到远程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”参数,

配置修改后界面如下:

基于网络的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创建成功时日志打印如下图:

基于网络的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文件中配置方法:

基于网络的VPN连接实现-鸿蒙开发者社区

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

分类
已于2024-8-1 16:38:57修改
收藏
回复
举报
回复
    相关推荐