update ios project
update ios project
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// FileManager+Helpers.swift
|
||||
// PacketTunnel
|
||||
//
|
||||
// Created by Jerry Bool on 2022/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
static var appGroupId = "group.com.vpntunnel.vpnzeus"
|
||||
|
||||
private var sharedFolderURL: URL? {
|
||||
let appGroupId = FileManager.appGroupId
|
||||
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
||||
Logger.log("Cannot obtain shared folder URL", to: Logger.vpnLogFile)
|
||||
return nil
|
||||
}
|
||||
return sharedFolderURL
|
||||
}
|
||||
|
||||
var vpnLogFile: URL? {
|
||||
sharedFolderURL?.appendingPathComponent("log")
|
||||
}
|
||||
|
||||
var leafLogFile: URL? {
|
||||
sharedFolderURL?.appendingPathComponent("leaf.log")
|
||||
}
|
||||
|
||||
var leafConfFile: URL? {
|
||||
sharedFolderURL?.appendingPathComponent("leaf.conf")
|
||||
}
|
||||
|
||||
var leafConfTemplateFile: URL? {
|
||||
Bundle.main.url(forResource: "template", withExtension: "conf")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// URL+Helpers.swift
|
||||
// PacketTunnel
|
||||
//
|
||||
// Created by Jerry Bool on 2022/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
|
||||
var contents: String? {
|
||||
guard self.isFileURL else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try String(contentsOf: self)
|
||||
}
|
||||
catch {
|
||||
Logger.log(error.localizedDescription, to: Logger.vpnLogFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func truncate() -> Self {
|
||||
if isFileURL {
|
||||
try? "".write(to: self, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// LeafAdapter.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Jerry Bool on 2022/12/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
//import LeafFFI
|
||||
import leaf
|
||||
|
||||
public enum LeafAdapterError: Error {
|
||||
/// Failure to locate tunnel file descriptor.
|
||||
case cannotLocateTunnelFileDescriptor
|
||||
|
||||
/// Failure to perform an operation in such state.
|
||||
case invalidState
|
||||
|
||||
/// Failure to set network settings.
|
||||
case setNetworkSettings(Error)
|
||||
|
||||
/// Failure to set tunnel configuration
|
||||
case setTunnelConfiguration(Int32)
|
||||
|
||||
/// Failure to start Leaf FFI.
|
||||
case startLeafFFI
|
||||
}
|
||||
|
||||
/// Enum representing internal state of the `LeafAdapter`
|
||||
private enum State {
|
||||
/// The tunnel is stopped
|
||||
case stopped
|
||||
|
||||
/// The tunnel is up and running
|
||||
case started
|
||||
|
||||
/// The tunnel is temporarily shutdown due to device going offline
|
||||
case temporaryShutdown
|
||||
}
|
||||
|
||||
private extension Network.NWPath.Status {
|
||||
/// Returns `true` if the path is potentially satisfiable.
|
||||
var isSatisfiable: Bool {
|
||||
switch self {
|
||||
case .requiresConnection, .satisfied:
|
||||
return true
|
||||
case .unsatisfied:
|
||||
return false
|
||||
@unknown default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LeafAdapater {
|
||||
private static var sharedLeafAdapater: LeafAdapater = {
|
||||
return LeafAdapater()
|
||||
}()
|
||||
|
||||
public class func shared() -> LeafAdapater {
|
||||
return sharedLeafAdapater
|
||||
}
|
||||
|
||||
/// Leaf instance id
|
||||
public static let leafId: UInt16 = 666
|
||||
|
||||
/// Network routes monitor.
|
||||
private var networkMonitor: NWPathMonitor?
|
||||
|
||||
/// Packet tunnel provider.
|
||||
private static weak var packetTunnelProvider: NEPacketTunnelProvider?
|
||||
|
||||
/// Private queue used to synchronize access to `LeafAdapter` members.
|
||||
private let workQueue = DispatchQueue(label: "LeafAdapterWorkQueue")
|
||||
|
||||
/// Adapter state.
|
||||
private var state: State = .stopped
|
||||
|
||||
var tunnelFd: 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 /* IGMP */, 2, &buf, &len) == 0 && String(cString: buf).hasPrefix("utun") {
|
||||
return fd
|
||||
}
|
||||
}
|
||||
|
||||
return LeafAdapater.packetTunnelProvider?.packetFlow.value(forKey: "socket.fileDescriptor") as? Int32
|
||||
}
|
||||
|
||||
/// Set PacketTunnelProvider instance
|
||||
/// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored
|
||||
/// as a weak
|
||||
public static func setPacketTunnelProvider(with packetTunnelProvider: NEPacketTunnelProvider) {
|
||||
LeafAdapater.packetTunnelProvider = packetTunnelProvider
|
||||
}
|
||||
|
||||
/// Designated initializer.
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Cancel network monitor
|
||||
networkMonitor?.cancel()
|
||||
|
||||
// Shutdown the tunnel
|
||||
if case .started = self.state {
|
||||
leaf_shutdown(LeafAdapater.leafId)
|
||||
}
|
||||
}
|
||||
|
||||
public func setRuntimeConfiguration(conf: String?, completionHandler: @escaping (LeafAdapterError?) -> Void) {
|
||||
guard let conf = conf else {
|
||||
completionHandler(.startLeafFFI)
|
||||
return
|
||||
}
|
||||
|
||||
let file = FileManager.default.leafConfFile
|
||||
|
||||
try! conf.write(to: file!, atomically: true, encoding: .utf8)
|
||||
|
||||
setenv("LOG_NO_COLOR", "true", 1)
|
||||
|
||||
let result = leaf_test_config(file?.path)
|
||||
guard result == 0 else {
|
||||
completionHandler(.setTunnelConfiguration(result))
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
||||
/// Returns a runtime configuration.
|
||||
/// - Parameter completionHandler: completion handler.
|
||||
public func getRuntimeConfiguration(completionHandler: @escaping (String?) -> Void) {
|
||||
workQueue.async {
|
||||
let fm = FileManager.default
|
||||
|
||||
if let conf = fm.leafConfFile?.contents {
|
||||
completionHandler(conf)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the tunnel tunnel.
|
||||
/// - Parameters:
|
||||
/// - tunnelConfiguration: tunnel configuration.
|
||||
/// - completionHandler: completion handler.
|
||||
public func start(completionHandler: @escaping (LeafAdapterError?) -> Void) {
|
||||
workQueue.async {
|
||||
guard case .stopped = self.state else {
|
||||
completionHandler(.invalidState)
|
||||
return
|
||||
}
|
||||
|
||||
let networkMonitor = NWPathMonitor()
|
||||
networkMonitor.pathUpdateHandler = { [weak self] path in
|
||||
self?.didReceivePathUpdate(path: path)
|
||||
}
|
||||
networkMonitor.start(queue: self.workQueue)
|
||||
|
||||
let tunFd = self.tunnelFd != nil ? String(self.tunnelFd!) : nil
|
||||
|
||||
// Reset log file.
|
||||
FileManager.default.leafLogFile?.truncate()
|
||||
|
||||
let fm = FileManager.default
|
||||
let file = fm.leafConfFile
|
||||
var conf = file?.contents ?? ""
|
||||
|
||||
conf = conf
|
||||
.replacingOccurrences(of: "{{leafLogFile}}", with: fm.leafLogFile?.path ?? "")
|
||||
.replacingOccurrences(of: "{{tunFd}}", with: tunFd ?? "")
|
||||
|
||||
try! conf.write(to: file!, atomically: true, encoding: .utf8)
|
||||
|
||||
setenv("LOG_NO_COLOR", "true", 1)
|
||||
|
||||
leaf_run(LeafAdapater.leafId, file?.path)
|
||||
|
||||
self.state = .started
|
||||
self.networkMonitor = networkMonitor
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the tunnel.
|
||||
/// - Parameter completionHandler: completion handler.
|
||||
public func stop(completionHandler: @escaping (LeafAdapterError?) -> Void) {
|
||||
workQueue.async {
|
||||
switch self.state {
|
||||
case .started:
|
||||
leaf_shutdown(LeafAdapater.leafId)
|
||||
|
||||
case .temporaryShutdown:
|
||||
break
|
||||
|
||||
case .stopped:
|
||||
completionHandler(.invalidState)
|
||||
return
|
||||
}
|
||||
|
||||
self.networkMonitor?.cancel()
|
||||
self.networkMonitor = nil
|
||||
|
||||
self.state = .stopped
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update runtime configuration.
|
||||
/// - Parameters:
|
||||
/// - tunnelConfiguration: tunnel configuration.
|
||||
/// - completionHandler: completion handler.
|
||||
public func update(conf: String?, completionHandler: @escaping (LeafAdapterError?) -> Void) {
|
||||
workQueue.async {
|
||||
if case .stopped = self.state {
|
||||
completionHandler(.invalidState)
|
||||
return
|
||||
}
|
||||
|
||||
// Tell the system that the tunnel is going to reconnect using new configuration.
|
||||
// This will broadcast the `NEVPNStatusDidChange` notification to the GUI process.
|
||||
LeafAdapater.packetTunnelProvider?.reasserting = true
|
||||
defer {
|
||||
LeafAdapater.packetTunnelProvider?.reasserting = false
|
||||
}
|
||||
|
||||
switch self.state {
|
||||
case .started:
|
||||
self.setRuntimeConfiguration(conf: conf, completionHandler: completionHandler)
|
||||
|
||||
leaf_reload(LeafAdapater.leafId)
|
||||
|
||||
self.state = .started
|
||||
|
||||
case .temporaryShutdown:
|
||||
self.state = .temporaryShutdown
|
||||
|
||||
case .stopped:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method used by network path monitor.
|
||||
/// - Parameter path: new network path
|
||||
private func didReceivePathUpdate(path: Network.NWPath) {
|
||||
switch self.state {
|
||||
case .started:
|
||||
if path.status.isSatisfiable {
|
||||
|
||||
} else {
|
||||
self.state = .temporaryShutdown
|
||||
leaf_shutdown(LeafAdapater.leafId)
|
||||
}
|
||||
|
||||
case .temporaryShutdown:
|
||||
guard path.status.isSatisfiable else { return }
|
||||
self.state = .started
|
||||
|
||||
case .stopped:
|
||||
// no-op
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Logger.swift
|
||||
// PacketTunnel
|
||||
//
|
||||
// Created by Jerry Bool on 2022/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Logger {
|
||||
|
||||
static let ENABLE_LOGGING = true
|
||||
|
||||
static var vpnLogFile: URL? = {
|
||||
FileManager.default.vpnLogFile?.truncate()
|
||||
}()
|
||||
|
||||
static func log(_ message: String, to: URL?) {
|
||||
guard ENABLE_LOGGING,
|
||||
let url = to,
|
||||
let data = message.trimmingCharacters(in: .whitespacesAndNewlines).appending("\n").data(using: .utf8),
|
||||
let fh = try? FileHandle(forUpdating: url)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
fh.closeFile()
|
||||
}
|
||||
|
||||
fh.seekToEndOfFile()
|
||||
fh.write(data)
|
||||
}
|
||||
|
||||
private static var logFsObject: DispatchSourceFileSystemObject? {
|
||||
didSet {
|
||||
oldValue?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private static var logText = ""
|
||||
|
||||
static func tailFile(_ url: URL?, _ update: ((_ logText: String) -> Void)? = nil) {
|
||||
|
||||
// Stop and remove the previous watched content.
|
||||
// (Will implicitely call #stop through `didSet` hook!)
|
||||
logFsObject = nil
|
||||
|
||||
guard let url = url,
|
||||
let fh = try? FileHandle(forReadingFrom: url)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let ui = {
|
||||
let data = fh.readDataToEndOfFile()
|
||||
|
||||
if let content = String(data: data, encoding: .utf8) {
|
||||
logText.append(content)
|
||||
|
||||
update?(logText)
|
||||
}
|
||||
}
|
||||
|
||||
logText = ""
|
||||
ui()
|
||||
|
||||
logFsObject = DispatchSource.makeFileSystemObjectSource(
|
||||
fileDescriptor: fh.fileDescriptor,
|
||||
eventMask: [.extend, .delete, .link],
|
||||
queue: .main)
|
||||
|
||||
logFsObject?.setEventHandler {
|
||||
guard let data = logFsObject?.data else {
|
||||
return
|
||||
}
|
||||
|
||||
if data.contains(.delete) || data.contains(.link) {
|
||||
DispatchQueue.main.async {
|
||||
tailFile(url, update)
|
||||
}
|
||||
}
|
||||
|
||||
if data.contains(.extend) {
|
||||
ui()
|
||||
}
|
||||
}
|
||||
|
||||
logFsObject?.setCancelHandler {
|
||||
try? fh.close()
|
||||
|
||||
logText = ""
|
||||
}
|
||||
|
||||
logFsObject?.resume()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import NetworkExtension
|
||||
//import LeafFFI
|
||||
import leaf
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
private lazy var adapter: LeafAdapater = {
|
||||
LeafAdapater.setPacketTunnelProvider(with: self)
|
||||
return LeafAdapater.shared()
|
||||
}()
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
let ipv4 = NEIPv4Settings(addresses: ["198.18.20.20"], subnetMasks: ["255.255.255.0"])
|
||||
ipv4.includedRoutes = [NEIPv4Route.default()]
|
||||
|
||||
let ipv6 = NEIPv6Settings(addresses: ["FD00::9999:9999"], networkPrefixLengths: [7])
|
||||
ipv6.includedRoutes = [NEIPv6Route.default()]
|
||||
|
||||
let dns = NEDNSSettings(servers: ["1.1.1.1"])
|
||||
// https://developer.apple.com/forums/thread/116033
|
||||
// Mention special Tor domains here, so the OS doesn't drop onion domain
|
||||
// resolve requests immediately.
|
||||
dns.matchDomains = ["", "onion", "exit"]
|
||||
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "198.18.20.200")
|
||||
settings.ipv4Settings = ipv4
|
||||
settings.ipv6Settings = ipv6
|
||||
settings.dnsSettings = dns
|
||||
settings.proxySettings = nil
|
||||
settings.mtu = 1500
|
||||
|
||||
self.adapter.start(completionHandler: completionHandler)
|
||||
|
||||
setTunnelNetworkSettings(settings) { error in
|
||||
if let error = error {
|
||||
return completionHandler(error)
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
// self.adapter.stop { error in
|
||||
// if let error = error {
|
||||
// Logger.log(error.localizedDescription, to: Logger.vpnLogFile)
|
||||
// }
|
||||
//
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// TunnelConfiguration.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Jerry Bool on 2022/12/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class TunnelConfiguration {
|
||||
public var name: String?
|
||||
|
||||
public var content: String?
|
||||
|
||||
public init(name: String?, content: String?) {
|
||||
self.name = name
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
extension TunnelConfiguration: Equatable {
|
||||
public static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
|
||||
return lhs.name == rhs.name &&
|
||||
lhs.name == rhs.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
//import LeafFFI
|
||||
import leaf
|
||||
|
||||
extension NEVPNStatus: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .disconnected: return "Disconnected"
|
||||
case .invalid: return "Invalid"
|
||||
case .connected: return "Connected"
|
||||
case .connecting: return "Connecting"
|
||||
case .disconnecting: return "Disconnecting"
|
||||
case .reasserting: return "Reasserting"
|
||||
default: return "Unknowed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class VPNManager {
|
||||
public var manager = NETunnelProviderManager.shared()
|
||||
|
||||
private static var sharedVPNManager: VPNManager = {
|
||||
return VPNManager()
|
||||
}()
|
||||
|
||||
public class func shared() -> VPNManager {
|
||||
return sharedVPNManager
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func getStatus() -> NEVPNStatus {
|
||||
return manager.connection.status
|
||||
}
|
||||
|
||||
public func getConnectedDate() -> Date? {
|
||||
return manager.connection.connectedDate
|
||||
}
|
||||
|
||||
public func loadVPNPreference(completion: @escaping (Error?) -> Void) {
|
||||
NETunnelProviderManager.loadAllFromPreferences() { managers, error in
|
||||
guard let managers = managers, error == nil else {
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
|
||||
if managers.count == 0 {
|
||||
let newManager = NETunnelProviderManager()
|
||||
newManager.protocolConfiguration = NETunnelProviderProtocol()
|
||||
newManager.localizedDescription = "UUVPN"
|
||||
newManager.protocolConfiguration?.serverAddress = "iLeaf"
|
||||
newManager.saveToPreferences { error in
|
||||
guard error == nil else {
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
newManager.loadFromPreferences { error in
|
||||
self.manager = newManager
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.manager = managers[0]
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func enableVPNManager(completion: @escaping (Error?) -> Void) {
|
||||
manager.isEnabled = true
|
||||
manager.saveToPreferences { error in
|
||||
guard error == nil else {
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
self.manager.loadFromPreferences { error in
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleVPNConnection(completion: @escaping (Error?) -> Void) {
|
||||
if self.manager.connection.status == .disconnected || self.manager.connection.status == .invalid {
|
||||
do {
|
||||
try self.manager.connection.startVPNTunnel()
|
||||
} catch {
|
||||
completion(error)
|
||||
}
|
||||
} else {
|
||||
self.manager.connection.stopVPNTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[General]
|
||||
loglevel = info
|
||||
dns-server = 223.5.5.5, 114.114.114.114
|
||||
routing-domain-resolve = true
|
||||
|
||||
# Local HTTP CONNECT proxy
|
||||
interface = 127.0.0.1
|
||||
port = 1087
|
||||
|
||||
# Local SOCKS5 proxy with UDP Associate support
|
||||
socks-interface = 127.0.0.1
|
||||
socks-port = 1086
|
||||
|
||||
[Proxy]
|
||||
Direct = direct
|
||||
Reject = reject
|
||||
|
||||
[Rule]
|
||||
EXTERNAL, site:cn, Direct
|
||||
FINAL, Direct
|
||||
Reference in New Issue
Block a user