iOS Xray support (#864)

Xray for ios
This commit is contained in:
Iurii Egorov 2024-06-30 12:19:38 +03:00 committed by GitHub
parent eeeb2805c5
commit 760f935965
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 285 additions and 3 deletions

@ -1 +1 @@
Subproject commit ea49bf8796afbc5bd70a0f98f4d99c9ea4792d80 Subproject commit ff8445c8aa1cda38497bb6f6cb0e520f5a3c8de0

@ -1 +1 @@
Subproject commit 0829e99ea9f4508fd1d4742546b62145d17587bb Subproject commit 76e7db556a6d7e2582f9481df91db188a46c009c

View file

@ -285,6 +285,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::WireGuard: return true; case DockerContainer::WireGuard: return true;
case DockerContainer::OpenVpn: return true; case DockerContainer::OpenVpn: return true;
case DockerContainer::Awg: return true; case DockerContainer::Awg: return true;
case DockerContainer::Xray: return true;
case DockerContainer::Cloak: case DockerContainer::Cloak:
return true; return true;
// case DockerContainer::ShadowSocks: return true; // case DockerContainer::ShadowSocks: return true;

View file

@ -50,10 +50,12 @@ set_target_properties("networkextension" PROPERTIES
find_library(FW_ASSETS_LIBRARY AssetsLibrary) find_library(FW_ASSETS_LIBRARY AssetsLibrary)
find_library(FW_MOBILE_CORE MobileCoreServices) find_library(FW_MOBILE_CORE MobileCoreServices)
find_library(FW_UI_KIT UIKit) 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_ASSETS_LIBRARY})
target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE}) target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE})
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT}) 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 -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\")
target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1) 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/Array+ConcurrentMap.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift ${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.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/NELogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift ${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift ${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.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/WGConfig.swift
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm ${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_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/wireguard/ios/arm64/libwg-go.a)
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework)

View file

@ -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()
}
}

View file

@ -13,6 +13,10 @@ public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "OVPN: \(title)", message: message) 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) { public func neLog(_ type: OSLogType, title: String = "", message: String) {
Log.log(type, title: "NE: \(title)", message: message) Log.log(type, title: "NE: \(title)", message: message)
} }

View file

@ -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)
}
}
}

View file

@ -5,7 +5,8 @@ import Darwin
import OpenVPNAdapter import OpenVPNAdapter
enum TunnelProtoType: String { enum TunnelProtoType: String {
case wireguard, openvpn case wireguard, openvpn, xray
} }
struct Constants { struct Constants {
@ -13,6 +14,7 @@ struct Constants {
static let processQueueName = "org.amnezia.process-packets" static let processQueueName = "org.amnezia.process-packets"
static let kActivationAttemptId = "activationAttemptId" static let kActivationAttemptId = "activationAttemptId"
static let ovpnConfigKey = "ovpn" static let ovpnConfigKey = "ovpn"
static let xrayConfigKey = "xray"
static let wireGuardConfigKey = "wireguard" static let wireGuardConfigKey = "wireguard"
static let loggerTag = "NET" static let loggerTag = "NET"
@ -91,6 +93,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
protoType = .openvpn protoType = .openvpn
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
protoType = .wireguard protoType = .wireguard
} else if (providerConfiguration?[Constants.xrayConfigKey] as? Data) != nil {
protoType = .xray
} }
} }
@ -107,6 +111,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
completionHandler: completionHandler) completionHandler: completionHandler)
case .openvpn: case .openvpn:
startOpenVPN(completionHandler: completionHandler) startOpenVPN(completionHandler: completionHandler)
case .xray:
startXray(completionHandler: completionHandler)
} }
} }
@ -124,6 +131,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
case .openvpn: case .openvpn:
stopOpenVPN(with: reason, stopOpenVPN(with: reason,
completionHandler: completionHandler) completionHandler: completionHandler)
case .xray:
stopXray(completionHandler: completionHandler)
} }
} }
@ -138,6 +147,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
handleWireguardStatusMessage(messageData, completionHandler: completionHandler) handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .openvpn: case .openvpn:
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
case .xray:
break;
} }
} }

View file

@ -72,9 +72,11 @@ private:
bool setupCloak(); bool setupCloak();
bool setupWireGuard(); bool setupWireGuard();
bool setupAwg(); bool setupAwg();
bool setupXray();
bool startOpenVPN(const QString &config); bool startOpenVPN(const QString &config);
bool startWireGuard(const QString &jsonConfig); bool startWireGuard(const QString &jsonConfig);
bool startXray(const QString &jsonConfig);
void startTunnel(); void startTunnel();

View file

@ -216,6 +216,9 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
if (proto == amnezia::Proto::Awg) { if (proto == amnezia::Proto::Awg) {
return setupAwg(); return setupAwg();
} }
if (proto == amnezia::Proto::Xray) {
return setupXray();
}
return false; return false;
} }
@ -501,6 +504,15 @@ bool IosController::setupWireGuard()
return startWireGuard(wgConfigDocStr); 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() bool IosController::setupAwg()
{ {
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject();
@ -590,6 +602,20 @@ bool IosController::startWireGuard(const QString &config)
startTunnel(); 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() void IosController::startTunnel()
{ {
NSString *protocolName = @"Unknown"; NSString *protocolName = @"Unknown";