Xcode → File → New → Project → iOS → App
Product Name: DemoVPN
Interface: SwiftUI
Language: Swift
打开 Project(左侧导航),选择DemoVPN target → Signing & Capabilities → 点击 “+ Capability”
File → New → Target → Network Extension
Product Name: PacketTunnel
Provider Type: Packet Tunnel
打开 Project(左侧导航),选择PacketTunnel target → Signing & Capabilities → 点击 “+ Capability”
安装Rust: https://www.rust-lang.org/tools/install
安装 GCC or Clang.
拉取代码
git clone https://github.com/eycorsican/leaf
编译leaf.xcframework
sh scripts/build_apple_xcframework.sh
生成的leaf.xcframework位置: target/apple/release/leaf.xcframework
在项目导航栏里新建一个group: Leaf
,把leaf.xcframework拖入该group下
打开 Project(左侧导航),选择PacketTunnel target → Build Settings → Framework Search Paths → 输入$(PROJECT_DIR)/Leaf
打开 Project(左侧导航),选择PacketTunnel target → Build Phases → Link Binary With Libraries → 添加leaf.xcframework
在PacketTunnel项目下创建Bridging Header文件: PacketTunnel-Bridging-Header.h
,在头文件中添加以下内容
#include "leaf.h"
打开 Project(左侧导航),选择Extension target → Build Settings → Objective-C Bridging Header → 输入PacketTunnel/PacketTunnel-Bridging-Header.h
PacketTunnelProvider.swift
位于PacketTunnel
项目下,修改代码实现leaf启动/关闭。
以下示例代码实现tunnel流量由leaf接管,leaf直连的功能。进一步实现流量代理的功能可以通过改变leaf配置字符串实现。
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
enum PacketTunnelError: Error {
case tunnelNetworkSettings
case tunnelFileDescriptor
}
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
// 配置Tunnel网络
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "240.0.0.10")
let ipv4 = NEIPv4Settings(addresses: ["240.0.0.1"], subnetMasks: ["255.255.255.0"])
ipv4.includedRoutes = [NEIPv4Route.default()]
settings.ipv4Settings = ipv4
settings.dnsSettings = NEDNSSettings(servers: ["114.114.114.114"])
settings.mtu = 1500
setTunnelNetworkSettings(settings) { [weak self] error in
guard error == nil else {
completionHandler(PacketTunnelError.tunnelNetworkSettings)
return
}
var tunFd: Int32 = 0
if let fd = self?.tunnelFileDescriptor {
tunFd = fd
} else {
completionHandler(PacketTunnelError.tunnelFileDescriptor)
return
}
// 异步线程中启动leaf
DispatchQueue.global(qos: .userInitiated).async { [] in
// leaf配置字符串
let leafConfig = """
{
"log": {
"level": "TRACE"
},
"inbounds": [
{
"tag": "tun-in",
"protocol": "tun",
"settings": {
"fd": \(tunFd)
}
}
],
"outbounds": [
{
"tag": "direct-out",
"protocol": "direct"
}
]
}
"""
_ = leafConfig.withCString { cstr in
leaf_run_with_config_string(0, cstr)
}
}
completionHandler(nil)
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
// 关闭leaf
leaf_shutdown(0)
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
// Add code here to handle the message.
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
// Add code here to get ready to sleep.
completionHandler()
}
override func wake() {
// Add code here to wake up.
}
// 获取系统创建的tunnel文件描述符
private var tunnelFileDescriptor: Int32? {
var buf = [CChar](repeating: 0, count: Int(IFNAMSIZ))
for fd: Int32 in 0...1024 {
var len = socklen_t(buf.count)
if getsockopt(fd, 2, 2, &buf, &len) == 0 && String(cString: buf).hasPrefix("utun") {
return fd
}
}
return nil
}
}
在DemoVPN
项目下创建VPN控制类VPNManager.swift
,该类主要控制VPN启动和关闭以及查询当前VPN状态。
import NetworkExtension
final class VPNManager {
static let shared = VPNManager()
func start(completion: @escaping (Error?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, loadError in
if let e = loadError {
completion(e);
return
}
let manager = managers?.first ?? NETunnelProviderManager()
let proto = NETunnelProviderProtocol()
// 必须与Extension的Bundle Identifier相同
proto.providerBundleIdentifier = "com.tongqijie.ios.DemoVPN.PacketTunnel"
proto.serverAddress = "DemoVPN"
manager.protocolConfiguration = proto
manager.localizedDescription = "DemoVPN"
manager.isEnabled = true
manager.saveToPreferences { saveError in
if let e = saveError {
completion(e); return
}
do {
try manager.connection.startVPNTunnel()
completion(nil)
} catch {
completion(error)
}
}
}
}
func stop(completion: @escaping (Error?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, loadError in
if let e = loadError {
completion(e);
return
}
guard let manager = managers?.first else {
completion(nil); return
}
manager.connection.stopVPNTunnel()
completion(nil)
}
}
// 查询VPN状态是否开启
func isActive(completion: @escaping (Bool) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { managers, error in
guard error == nil, let manager = managers?.first else {
completion(false)
return
}
manager.loadFromPreferences { error in
if let _ = error {
completion(false)
return
}
let status = manager.connection.status
let active = (status == .connected || status == .connecting)
completion(active)
}
}
}
}
ContentView.swift
文件位于DemoVPN
项目下,修改该文件,在App主界面上添加一个VPN按钮
import SwiftUI
struct ContentView: View {
@State private var isActive = false
var body: some View {
VStack {
Button(action: {
if isActive {
VPNManager.shared.stop { error in
if let _ = error {
return
}
isActive = false
}
} else {
VPNManager.shared.start { error in
if let _ = error {
return
}
isActive = true
}
}
}) {
Text(isActive ? "Stop" : "Start")
.foregroundColor(.white)
.frame(width: 100, height: 100)
.background(isActive ? Color.green : Color.gray)
.clipShape(Circle())
.font(.title)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.edgesIgnoringSafeArea(.all)
.contentShape(Rectangle())
.onAppear {
VPNManager.shared.isActive { active in
isActive = active
}
}
}
}
#Preview {
ContentView()
}
至此,一个最小化iOS VPN应用已经实现了。至于如何接管流量,使用哪种协议,均可以通过修改leaf配置字符串实现。
leaf配置文档: https://github.com/eycorsican/leaf/blob/master/README.zh.md
目前发现该文档有点过时了,json字段有点出入,跟实际代码有差异,建议对比代码进行配置https://github.com/eycorsican/leaf/blob/master/leaf/src/config/json/config.rs