From 760f935965787168c1b84509ca89b883fbc114c0 Mon Sep 17 00:00:00 2001 From: Iurii Egorov Date: Sun, 30 Jun 2024 12:19:38 +0300 Subject: [PATCH] iOS Xray support (#864) Xray for ios --- client/3rd-prebuilt | 2 +- client/3rd/amneziawg-apple | 2 +- client/containers/containers_defs.cpp | 1 + client/ios/networkextension/CMakeLists.txt | 6 + client/platforms/ios/HevSocksTunnel.swift | 73 ++++++++ client/platforms/ios/NELogController.swift | 4 + .../ios/PacketTunnelProvider+Xray.swift | 159 ++++++++++++++++++ .../platforms/ios/PacketTunnelProvider.swift | 13 +- client/platforms/ios/ios_controller.h | 2 + client/platforms/ios/ios_controller.mm | 26 +++ 10 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 client/platforms/ios/HevSocksTunnel.swift create mode 100644 client/platforms/ios/PacketTunnelProvider+Xray.swift diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index ea49bf87..ff8445c8 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit ea49bf8796afbc5bd70a0f98f4d99c9ea4792d80 +Subproject commit ff8445c8aa1cda38497bb6f6cb0e520f5a3c8de0 diff --git a/client/3rd/amneziawg-apple b/client/3rd/amneziawg-apple index 0829e99e..76e7db55 160000 --- a/client/3rd/amneziawg-apple +++ b/client/3rd/amneziawg-apple @@ -1 +1 @@ -Subproject commit 0829e99ea9f4508fd1d4742546b62145d17587bb +Subproject commit 76e7db556a6d7e2582f9481df91db188a46c009c diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp index 2f2f8367..e695b06c 100644 --- a/client/containers/containers_defs.cpp +++ b/client/containers/containers_defs.cpp @@ -285,6 +285,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c) case DockerContainer::WireGuard: return true; case DockerContainer::OpenVpn: return true; case DockerContainer::Awg: return true; + case DockerContainer::Xray: return true; case DockerContainer::Cloak: return true; // case DockerContainer::ShadowSocks: return true; diff --git a/client/ios/networkextension/CMakeLists.txt b/client/ios/networkextension/CMakeLists.txt index 80f3f1f1..7302ba68 100644 --- a/client/ios/networkextension/CMakeLists.txt +++ b/client/ios/networkextension/CMakeLists.txt @@ -50,10 +50,12 @@ set_target_properties("networkextension" PROPERTIES find_library(FW_ASSETS_LIBRARY AssetsLibrary) find_library(FW_MOBILE_CORE MobileCoreServices) find_library(FW_UI_KIT UIKit) +find_library(FW_LIBRESOLV libresolv.9.tbd) target_link_libraries(networkextension PRIVATE ${FW_ASSETS_LIBRARY}) target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE}) target_link_libraries(networkextension PRIVATE ${FW_UI_KIT}) +target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV}) target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\") target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1) @@ -80,12 +82,14 @@ target_sources(networkextension PRIVATE ${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift + ${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift ${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift ${CLIENT_ROOT_DIR}/platforms/ios/Log.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift + ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift ${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift ${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm ) @@ -114,3 +118,5 @@ target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR}) target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/ios/arm64/libwg-go.a) + +target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework) diff --git a/client/platforms/ios/HevSocksTunnel.swift b/client/platforms/ios/HevSocksTunnel.swift new file mode 100644 index 00000000..a86a0758 --- /dev/null +++ b/client/platforms/ios/HevSocksTunnel.swift @@ -0,0 +1,73 @@ +import HevSocks5Tunnel + +public enum Socks5Tunnel { + + private static var tunnelFileDescriptor: Int32? { + var ctlInfo = ctl_info() + withUnsafeMutablePointer(to: &ctlInfo.ctl_name) { + $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) { + _ = strcpy($0, "com.apple.net.utun_control") + } + } + for fd: Int32 in 0...1024 { + var addr = sockaddr_ctl() + var ret: Int32 = -1 + var len = socklen_t(MemoryLayout.size(ofValue: addr)) + withUnsafeMutablePointer(to: &addr) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + ret = getpeername(fd, $0, &len) + } + } + if ret != 0 || addr.sc_family != AF_SYSTEM { + continue + } + if ctlInfo.ctl_id == 0 { + ret = ioctl(fd, CTLIOCGINFO, &ctlInfo) + if ret != 0 { + continue + } + } + if addr.sc_id == ctlInfo.ctl_id { + return fd + } + } + return nil + } + + private static var interfaceName: String? { + guard let tunnelFileDescriptor = self.tunnelFileDescriptor else { + return nil + } + var buffer = [UInt8](repeating: 0, count: Int(IFNAMSIZ)) + return buffer.withUnsafeMutableBufferPointer { mutableBufferPointer in + guard let baseAddress = mutableBufferPointer.baseAddress else { + return nil + } + var ifnameSize = socklen_t(IFNAMSIZ) + let result = getsockopt( + tunnelFileDescriptor, + 2 /* SYSPROTO_CONTROL */, + 2 /* UTUN_OPT_IFNAME */, + baseAddress, + &ifnameSize + ) + if result == 0 { + return String(cString: baseAddress) + } else { + return nil + } + } + } + + @discardableResult + public static func run(withConfig filePath: String) -> Int32 { + guard let fileDescriptor = self.tunnelFileDescriptor else { + fatalError("Get tunnel file descriptor failed.") + } + return hev_socks5_tunnel_main(filePath.cString(using: .utf8), fileDescriptor) + } + + public static func quit() { + hev_socks5_tunnel_quit() + } +} diff --git a/client/platforms/ios/NELogController.swift b/client/platforms/ios/NELogController.swift index e1d71e60..257dc087 100644 --- a/client/platforms/ios/NELogController.swift +++ b/client/platforms/ios/NELogController.swift @@ -13,6 +13,10 @@ public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { neLog(type, title: "OVPN: \(title)", message: message) } +public func xrayLog(_ type: OSLogType, title: String = "", message: String) { + neLog(type, title: "XRAY: \(title)", message: message) +} + public func neLog(_ type: OSLogType, title: String = "", message: String) { Log.log(type, title: "NE: \(title)", message: message) } diff --git a/client/platforms/ios/PacketTunnelProvider+Xray.swift b/client/platforms/ios/PacketTunnelProvider+Xray.swift new file mode 100644 index 00000000..f618be59 --- /dev/null +++ b/client/platforms/ios/PacketTunnelProvider+Xray.swift @@ -0,0 +1,159 @@ +import Foundation +import NetworkExtension +import WireGuardKitGo + +enum XrayErrors: Error { + case noXrayConfig + case cantSaveXrayConfig + case cantParseListenAndPort + case cantSaveHevSocksConfig +} + +extension Constants { + static let cachesDirectory: URL = { + if let cachesDirectoryURL = FileManager.default.urls(for: .cachesDirectory, + in: .userDomainMask).first { + return cachesDirectoryURL + } else { + fatalError("Unable to retrieve caches directory.") + } + }() +} + +extension PacketTunnelProvider { + func startXray(completionHandler: @escaping (Error?) -> Void) { + + // Xray configuration + guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfiguration = protocolConfiguration.providerConfiguration, + let xrayConfigData = providerConfiguration[Constants.xrayConfigKey] as? Data else { + xrayLog(.error, message: "Can't get xray configuration") + completionHandler(XrayErrors.noXrayConfig) + return + } + + // Tunnel settings + let ipv6Enabled = true + let hideVPNIcon = false + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "254.1.1.1") + settings.mtu = 9000 + + settings.ipv4Settings = { + let settings = NEIPv4Settings(addresses: ["198.18.0.1"], subnetMasks: ["255.255.0.0"]) + settings.includedRoutes = [NEIPv4Route.default()] + return settings + }() + + settings.ipv6Settings = { + guard ipv6Enabled else { + return nil + } + let settings = NEIPv6Settings(addresses: ["fd6e:a81b:704f:1211::1"], networkPrefixLengths: [64]) + settings.includedRoutes = [NEIPv6Route.default()] + if hideVPNIcon { + settings.excludedRoutes = [NEIPv6Route(destinationAddress: "::", networkPrefixLength: 128)] + } + return settings + }() + + let dns = ["8.8.4.4","1.1.1.1"] + settings.dnsSettings = NEDNSSettings(servers: dns) + + setTunnelNetworkSettings(settings) { [weak self] error in + if let error { + completionHandler(error) + return + } + + // Launch xray + self?.setupAndStartXray(configData: xrayConfigData) { xrayError in + if let xrayError { + completionHandler(xrayError) + return + } + + // Launch hevSocks + self?.setupAndRunTun2socks(configData: xrayConfigData, + completionHandler: completionHandler) + } + } + } + + func stopXray(completionHandler: () -> Void) { + Socks5Tunnel.quit() + LibXrayStopXray() + completionHandler() + } + + private func setupAndStartXray(configData: Data, + completionHandler: @escaping (Error?) -> Void) { + let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path + guard FileManager.default.createFile(atPath: path, contents: configData) else { + xrayLog(.error, message: "Can't save xray configuration") + completionHandler(XrayErrors.cantSaveXrayConfig) + return + } + + LibXrayRunXray(nil, + path, + Int64.max) + + completionHandler(nil) + xrayLog(.info, message: "Xray started") + } + + private func setupAndRunTun2socks(configData: Data, + completionHandler: @escaping (Error?) -> Void) { + var port = 10808 + var address = "::1" + + let jsonDict = try? JSONSerialization.jsonObject(with: configData, options: []) as? [String: Any] + + guard let jsonDict else { + xrayLog(.error, message: "Can't parse address and port for hevSocks") + completionHandler(XrayErrors.cantParseListenAndPort) + return + } + + // Xray listen and port should be the same as port and address in hevSocks + if let inbounds = jsonDict["inbounds"] as? [[String: Any]], let inbound = inbounds.first { + if let listen = inbound["listen"] as? String { + address = listen + address.removeAll { $0 == "[" || $0 == "]" } + } + if let portFromConfig = inbound["port"] as? Int { + port = portFromConfig + } + } + + let config = """ + tunnel: + mtu: 9000 + socks5: + port: \(port) + address: \(address) + udp: 'udp' + misc: + task-stack-size: 20480 + connect-timeout: 5000 + read-write-timeout: 60000 + log-file: stderr + log-level: error + limit-nofile: 65535 + """ + + let configurationFilePath = Constants.cachesDirectory.appendingPathComponent("config.yml", isDirectory: false).path + guard FileManager.default.createFile(atPath: configurationFilePath, contents: config.data(using: .utf8)!) else { + xrayLog(.info, message: "Cant save hevSocks configuration") + completionHandler(XrayErrors.cantSaveHevSocksConfig) + return + } + + DispatchQueue.global().async { + xrayLog(.info, message: "Hev socks started") + completionHandler(nil) + Socks5Tunnel.run(withConfig: configurationFilePath) + } + } +} diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index f78db345..9a5a5846 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -5,7 +5,8 @@ import Darwin import OpenVPNAdapter enum TunnelProtoType: String { - case wireguard, openvpn + case wireguard, openvpn, xray + } struct Constants { @@ -13,6 +14,7 @@ struct Constants { static let processQueueName = "org.amnezia.process-packets" static let kActivationAttemptId = "activationAttemptId" static let ovpnConfigKey = "ovpn" + static let xrayConfigKey = "xray" static let wireGuardConfigKey = "wireguard" static let loggerTag = "NET" @@ -91,6 +93,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { protoType = .openvpn } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { protoType = .wireguard + } else if (providerConfiguration?[Constants.xrayConfigKey] as? Data) != nil { + protoType = .xray } } @@ -107,6 +111,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider { completionHandler: completionHandler) case .openvpn: startOpenVPN(completionHandler: completionHandler) + case .xray: + startXray(completionHandler: completionHandler) + } } @@ -124,6 +131,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { case .openvpn: stopOpenVPN(with: reason, completionHandler: completionHandler) + case .xray: + stopXray(completionHandler: completionHandler) } } @@ -138,6 +147,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { handleWireguardStatusMessage(messageData, completionHandler: completionHandler) case .openvpn: handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) + case .xray: + break; } } diff --git a/client/platforms/ios/ios_controller.h b/client/platforms/ios/ios_controller.h index a36bbef5..8e13eaa9 100644 --- a/client/platforms/ios/ios_controller.h +++ b/client/platforms/ios/ios_controller.h @@ -72,9 +72,11 @@ private: bool setupCloak(); bool setupWireGuard(); bool setupAwg(); + bool setupXray(); bool startOpenVPN(const QString &config); bool startWireGuard(const QString &jsonConfig); + bool startXray(const QString &jsonConfig); void startTunnel(); diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index 44924452..a2819c6c 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -216,6 +216,9 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur if (proto == amnezia::Proto::Awg) { return setupAwg(); } + if (proto == amnezia::Proto::Xray) { + return setupXray(); + } return false; } @@ -501,6 +504,15 @@ bool IosController::setupWireGuard() return startWireGuard(wgConfigDocStr); } +bool IosController::setupXray() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Xray)].toObject(); + QJsonDocument xrayConfigDoc(config); + QString xrayConfigStr(xrayConfigDoc.toJson(QJsonDocument::Compact)); + + return startXray(xrayConfigStr); +} + bool IosController::setupAwg() { QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); @@ -590,6 +602,20 @@ bool IosController::startWireGuard(const QString &config) startTunnel(); } +bool IosController::startXray(const QString &config) +{ + qDebug() << "IosController::startXray"; + + NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; + tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; + tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]}; + tunnelProtocol.serverAddress = m_serverAddress; + + m_currentTunnel.protocolConfiguration = tunnelProtocol; + + startTunnel(); +} + void IosController::startTunnel() { NSString *protocolName = @"Unknown";