avatar
童琦杰
Oct 9, 2025Technology

如何从零开始开发一款iOS VPN应用

创建App、配置DemoVPN

Xcode → File → New → Project → iOS → App

  • Product Name: DemoVPN

  • Interface: SwiftUI

  • Language: Swift

打开 Project(左侧导航),选择DemoVPN target → Signing & Capabilities → 点击 “+ Capability”

  • Network Extensions → 勾选 Packet Tunnel

创建Target、配置PacketTunnel

File → New → Target → Network Extension

  • Product Name: PacketTunnel

  • Provider Type: Packet Tunnel

打开 Project(左侧导航),选择PacketTunnel target → Signing & Capabilities → 点击 “+ Capability”

  • Network Extensions → 勾选 Packet Tunnel

编译leaf.xcframework

安装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

集成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,在头文件中添加以下内容

PacketTunnel-Bridging-Header.h
cpp
#include "leaf.h"

打开 Project(左侧导航),选择Extension target → Build Settings → Objective-C Bridging Header → 输入PacketTunnel/PacketTunnel-Bridging-Header.h

编写PacketTunnelProvider

PacketTunnelProvider.swift位于PacketTunnel项目下,修改代码实现leaf启动/关闭。

以下示例代码实现tunnel流量由leaf接管,leaf直连的功能。进一步实现流量代理的功能可以通过改变leaf配置字符串实现。

PacketTunnel/PacketTunnelProvider.swift
swift
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
    }
}

编写VPNManager

DemoVPN项目下创建VPN控制类VPNManager.swift,该类主要控制VPN启动和关闭以及查询当前VPN状态。

DemoVPN/VPNManager.swift
swift
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

ContentView.swift文件位于DemoVPN项目下,修改该文件,在App主界面上添加一个VPN按钮

DemoVPN/ContentView.swift
swift
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

© 2015-2022 tongqijie.com 版权所有沪ICP备17000682号