如何在 NEPacketTunnelProvider 中运行 Clash

问题描述 投票:0回答:1

我创建了

PacketTunnelProvider
类,其中包含在 127.0.0.1:7890 上运行 http(s) 代理的 Clash VPN 服务器(显示
HTTP proxy listening at: [::]:7890
登录控制台)。

//
//  PacketTunnelProvider.swift
//  PacketTunnel
//
//  Created by LondonX on 2022/9/13.
//

import NetworkExtension
import ClashKit

class PacketTunnelProvider: NEPacketTunnelProvider {
    private var trafficTotalUp: Int64 = 0
    private var trafficTotalDown: Int64 = 0
    private var trafficUp: Int64 = 0
    private var trafficDown: Int64 = 0
    
    private lazy var client = AppClashClient { trafficUp, trafficDown in
        self.trafficTotalUp += trafficUp
        self.trafficTotalDown += trafficDown
        self.trafficUp = trafficUp
        self.trafficDown = trafficDown
    }
    
    override func startTunnel(options: [String : NSObject]?) async throws {
        let isSetup = setupClash()
        if (!isSetup) {
            throw MyError.runtimeError("Clash Setup failed")
        }
        let generalJson = String(data: ClashKit.ClashGetConfigGeneral()!, encoding: .utf8)
        let general = jsonToDictionary(generalJson)
        osLog("startTunnel with config: \(String(describing: (generalJson)))")
        let port = general?["port"] as? Int ?? 7890
        //192.168.0.29
        let host = "127.0.0.1"
        try await self.setTunnelNetworkSettings(initTunnelSettings(proxyHost: host, proxyPort: port))
    }
    
    override func stopTunnel(with reason: NEProviderStopReason) async {
    }
    
    override func handleAppMessage(_ messageData: Data) async -> Data? {
        let message = String(data: messageData, encoding: .utf8)
        switch(message) {
        case "notifyConfigChanged":
            _ = setupClash()
            break
        case "queryTrafficNow":
            return "\(trafficUp),\(trafficDown)".data(using: .utf8)
        case "queryTrafficTotal":
            return "\(trafficTotalUp),\(trafficTotalDown)".data(using: .utf8)
        default:
            break
        }
        return nil
    }
    
    private func setupClash() -> Bool {
        let exIdentifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as! String
        let identifier = exIdentifier.replacingOccurrences(of: ".PacketTunnel", with: "")
        let suiteName = "group.\(identifier)"
        let userDefaults = UserDefaults(suiteName: suiteName)!
        let clashHome = userDefaults.string(forKey: "clash_flt_clashHome")
        let clashHomeUrl = resolvePath(clashHome, isDir: true)
        let profilePath = userDefaults.string(forKey: "clash_flt_profilePath")
        let profileUrl = resolvePath(profilePath, isDir: false)
        let countryDBPath = userDefaults.string(forKey: "clash_flt_countryDBPath")
        let countryDBUrl = resolvePath(countryDBPath, isDir: false)
        let groupName = userDefaults.string(forKey: "clash_flt_groupName")
        let proxyName = userDefaults.string(forKey: "clash_flt_proxyName")
        osLog("setup with clashHome: \(clashHomeUrl?.path ?? ""), profilePath: \(profileUrl?.path ?? ""), countryDBPath: \(countryDBUrl?.path ?? ""), groupName: \(groupName ?? ""), proxyName: \(proxyName ?? "")")
        
        if(clashHomeUrl == nil ||
           profileUrl == nil ||
           countryDBUrl == nil ||
           groupName == nil ||
           proxyName == nil
        ) {
            osLog("\(String(describing: clashHomeUrl)), \(String(describing: profileUrl)), \(String(describing: countryDBUrl)), \(String(describing: groupName)), \(String(describing: proxyName))")
            return false
        }
        let cacheDBUrl = clashHomeUrl!.appendingPathComponent("cache.db")
        FileManager.default.createFile(atPath: cacheDBUrl.path, contents: nil)
        let fileExists = FileManager.default.fileExists(atPath: profileUrl!.path)
        osLog("profileUrl: \(profileUrl!), fileExists: \(fileExists)")
        
        let config = try? String(contentsOfFile: profilePath!)
        osLog("config: \(config ?? "")")
        if(config == nil) {
            return false
        }
        ClashKit.ClashSetup(clashHomeUrl!.path, config, client)
        let data = ClashKit.ClashGetConfigGeneral()
        let map = [groupName! : proxyName!]
        let json = dictionaryToJson(dic: map)
        ClashKit.ClashPatchSelector(json?.data(using: .utf8))
        return data != nil
    }
    
    private func initTunnelSettings(proxyHost: String, proxyPort: Int) -> NEPacketTunnelNetworkSettings {
        let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyHost)
        settings.mtu = 1440
        
        /* proxy settings */
        let proxySettings = NEProxySettings()
        proxySettings.httpServer = NEProxyServer(
            address: proxyHost,
            port: proxyPort
        )
        proxySettings.httpsServer = NEProxyServer(
            address: proxyHost,
            port: proxyPort
        )
        proxySettings.httpEnabled = true
        proxySettings.httpsEnabled = true
        proxySettings.matchDomains = [""]
        
        let ipv4Settings = NEIPv4Settings(
            addresses: ["127.0.0.1"],
            subnetMasks: ["255.255.255.255"]
        )
        settings.ipv4Settings = ipv4Settings
        settings.proxySettings = proxySettings
        return settings
    }
}

class AppClashClient: NSObject, ClashClientProtocol {
    private let trafficListener: (_ up: Int64, _ down: Int64) -> Void
    
    init(trafficListener: @escaping (_ up: Int64, _ down: Int64) -> Void) {
        self.trafficListener = trafficListener
    }
    
    func log(_ level: String?, message: String?) {
        osLog("AppClashClient[\(level ?? "")]: \(message ?? "")")
    }
    
    func traffic(_ up: Int64, down: Int64) {
        trafficListener(up, down)
    }
}


private func jsonToDictionary(_ text: String?) -> [String: Any]? {
    if (text == nil) {
        return nil
    }
    if let data = text!.data(using: .utf8) {
        do {
            return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
        } catch {
            osLog("\(error.localizedDescription)")
        }
    }
    return nil
}

private func dictionaryToJson(dic: Dictionary<String, Any>?) -> String? {
    var jsonData: Data? = nil
    do {
        if let dic = dic {
            jsonData = try JSONSerialization.data(withJSONObject: dic, options: .prettyPrinted)
        }
    } catch {
    }
    if let jsonData = jsonData {
        return String(data: jsonData, encoding: .utf8)
    }
    return nil
}

enum MyError: Error {
    case runtimeError(String)
}


private func resolvePath(_ nonSandboxPath: String?, isDir: Bool) -> URL? {
    if (nonSandboxPath == nil) {
        return nil
    }
    return URL(string: nonSandboxPath!)
}

func osLog(_ any: Any?) {
    NSLog("[ClashFlt.PacketTunnel]\(any ?? "")")
}

调用

startVPNTunnel()
后,iPhone在状态栏中显示VPN标志,但所有来自Safari的请求都卡住直到超时,没有来自Clash的日志。

我相信我的 Clash 服务器正在工作,因为我可以将

<iPhone's IP>:7890
设置为另一部手机的代理服务器,并像
[info]: [TCP] 192.168.31.191:45048 --> i.ytimg.com:443 match DomainSuffix(ytimg.com) using Proxy[proxy node 1]
.

一样记录

我还尝试在另一台设备上运行 Clash 服务器,并将

host
initTunnelSettings
参数更改为该设备的 ip,启动 VPN 并运行。

看起来

initTunnelSettings
中的一些行为阻止了请求循环的本地 Clash 服务器。

ios vpn
1个回答
0
投票

问题是我自己的

config
allow-lan: true
,这样会造成网络扩展中的网络环路

像这样覆盖配置将解决问题。

let configOverride = """
\(config!)
allow-lan: false
"""
ClashKit.ClashSetup(clashHomeUrl!.path, configOverride, client)
© www.soinside.com 2019 - 2024. All rights reserved.