From 86d342cb077db9e5b6a77bc4fd96c10a69688164 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Wed, 9 Oct 2024 02:30:02 +0800 Subject: [PATCH] ios IP Layer Proxy Beta (#215) --- .../proxy/vpn/transport/protocol/IP4Header.kt | 3 +- .../transport/protocol/TCPPacketFactory.kt | 12 + .../proxy/vpn/transport/protocol/UDPHeader.kt | 5 +- ios/ProxyPin/PacketTunnelProvider.swift | 51 ++- ios/ProxyPin/vpn/Connection.swift | 105 +++++ ios/ProxyPin/vpn/ConnectionHandler.swift | 399 ++++++++++++++++++ ios/ProxyPin/vpn/ConnectionManager.swift | 159 +++++++ ios/ProxyPin/vpn/NWProtocol.swift | 13 + ios/ProxyPin/vpn/ProxyVpnService.swift | 53 +++ ios/ProxyPin/vpn/QueueFactory.swift | 29 ++ .../vpn/socket/ClientPacketWriter.swift | 33 ++ .../vpn/socket/CloseableConnection.swift | 14 + ios/ProxyPin/vpn/socket/SocketIOService.swift | 214 ++++++++++ ios/ProxyPin/vpn/transport/Packet.swift | 21 + .../vpn/transport/protocol/ICMPPacket.swift | 96 +++++ .../vpn/transport/protocol/IP4Header.swift | 161 +++++++ .../vpn/transport/protocol/TCPHeader.swift | 182 ++++++++ .../transport/protocol/TCPPacketFactory.swift | 325 ++++++++++++++ .../transport/protocol/TransportHeader.swift | 13 + .../vpn/transport/protocol/UDPHeader.swift | 112 +++++ ios/ProxyPin/vpn/utils/PacketUtil.swift | 138 ++++++ ios/Runner.xcodeproj/project.pbxproj | 108 +++++ ios/Runner/AppDelegate.swift | 11 +- ios/Runner/VpnManager.swift | 11 +- ios/Runner/pip/PictureInPictureManager.swift | 2 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/native/vpn.dart | 12 +- lib/network/bin/server.dart | 9 + lib/network/http_client.dart | 8 +- lib/network/network.dart | 6 +- lib/ui/desktop/toolbar/setting/setting.dart | 2 +- lib/ui/desktop/toolbar/ssl/ssl.dart | 2 +- lib/ui/desktop/toolbar/toolbar.dart | 12 +- lib/ui/mobile/mobile.dart | 15 +- lib/ui/mobile/setting/proxy.dart | 1 - lib/ui/mobile/widgets/remote_device.dart | 199 +++++++-- pubspec.yaml | 4 +- 38 files changed, 2458 insertions(+), 84 deletions(-) create mode 100644 ios/ProxyPin/vpn/Connection.swift create mode 100644 ios/ProxyPin/vpn/ConnectionHandler.swift create mode 100644 ios/ProxyPin/vpn/ConnectionManager.swift create mode 100644 ios/ProxyPin/vpn/NWProtocol.swift create mode 100644 ios/ProxyPin/vpn/ProxyVpnService.swift create mode 100644 ios/ProxyPin/vpn/QueueFactory.swift create mode 100644 ios/ProxyPin/vpn/socket/ClientPacketWriter.swift create mode 100644 ios/ProxyPin/vpn/socket/CloseableConnection.swift create mode 100644 ios/ProxyPin/vpn/socket/SocketIOService.swift create mode 100644 ios/ProxyPin/vpn/transport/Packet.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/IP4Header.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift create mode 100644 ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift create mode 100644 ios/ProxyPin/vpn/utils/PacketUtil.swift diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt index 7c3ac00..998a9ed 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt @@ -102,8 +102,7 @@ object IPPacketFactory { val versionAndHeaderLength: Byte = buffer.get() val ipVersion = (versionAndHeaderLength.toInt() shr 4).toByte() if (ipVersion.toInt() != IP4_VERSION) { -// throw IllegalArgumentException("Invalid IP version $ipVersion") - return null + throw IllegalArgumentException("Invalid IP version $ipVersion") } val internetHeaderLength = (versionAndHeaderLength.toInt() and 0x0F).toByte() diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt index d981d7c..5b3c5a7 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt @@ -73,6 +73,7 @@ object TCPPacketFactory { tcp.isNS = false tcp.setIsRST(true) + tcp.dataOffset = 5 tcp.options = null tcp.windowSize = 0 @@ -109,6 +110,8 @@ object TCPPacketFactory { tcp.setIsFIN(false) tcp.timeStampSender = timeSender tcp.timeStampReplyTo = timeReplyTo + tcp.dataOffset = 5 + tcp.options = null var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() if (packetData != null) { @@ -142,6 +145,8 @@ object TCPPacketFactory { tcp.setIsSYN(false) tcp.setIsPSH(false) tcp.setIsFIN(false) + tcp.dataOffset = 5 + tcp.options = null ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return createPacketData(ip, tcp, null) @@ -184,6 +189,9 @@ object TCPPacketFactory { tcp.timeStampReplyTo = tcp.timeStampSender tcp.timeStampSender = PacketUtil.currentTime + tcp.dataOffset = 5 + tcp.options = null + return Packet(ip, tcp, createPacketData(ip, tcp, null)) } @@ -209,6 +217,9 @@ object TCPPacketFactory { tcp.setIsPSH(false) tcp.setIsFIN(isFin) + tcp.dataOffset = 5 + tcp.options = null + ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() return createPacketData(ip, tcp, null) } @@ -233,6 +244,7 @@ object TCPPacketFactory { tcp.setIsACK(true) tcp.setIsFIN(true) + tcp.dataOffset = 5 tcp.options = null //窗口大小应为零 tcp.windowSize = 0 diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt index 6ddc5dc..f550f3b 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt @@ -39,7 +39,6 @@ object UDPPacketFactory { } val srcPort = udp.destinationPort val destPort = udp.sourcePort - val checksum: Short = 0 val ipHeader = ip.copy() val srcIp = ip.destinationIP val destIp = ip.sourceIP @@ -68,15 +67,19 @@ object UDPPacketFactory { var start = ipData.size val intContainer = ByteArray(4) PacketUtil.writeIntToBytes(srcPort, intContainer, 0) + //extract the last two bytes of int value System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 + PacketUtil.writeIntToBytes(destPort, intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 PacketUtil.writeIntToBytes(udpLen, intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 + + val checksum: Short = 0 PacketUtil.writeIntToBytes(checksum.toInt(), intContainer, 0) System.arraycopy(intContainer, 2, buffer, start, 2) start += 2 diff --git a/ios/ProxyPin/PacketTunnelProvider.swift b/ios/ProxyPin/PacketTunnelProvider.swift index b27fe70..9bfa1d4 100644 --- a/ios/ProxyPin/PacketTunnelProvider.swift +++ b/ios/ProxyPin/PacketTunnelProvider.swift @@ -6,10 +6,12 @@ // import NetworkExtension - +import Network +import os.log class PacketTunnelProvider: NEPacketTunnelProvider { - + private var proxyVpnService: ProxyVpnService? + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { NSLog("startTunnel") @@ -17,11 +19,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider { NSLog("[ERROR] No ProtocolConfiguration Found") exit(EXIT_FAILURE) } - let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") - NSLog(conf.debugDescription) - //http代理 + let host = conf["proxyHost"] as! String let proxyPort = conf["proxyPort"] as! Int + let ipProxy = conf["ipProxy"] as! Bool? ?? false + + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") +// let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host) + NSLog(conf.debugDescription) + //http代理 let proxySettings = NEProxySettings() proxySettings.httpEnabled = true proxySettings.httpServer = NEProxyServer(address: host, port: proxyPort) @@ -30,26 +36,49 @@ class PacketTunnelProvider: NEPacketTunnelProvider { proxySettings.matchDomains = [""] networkSettings.proxySettings = proxySettings - networkSettings.mtu = 1500 + networkSettings.mtu = 1480 let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.255"]) + + if (ipProxy){ + ipv4Settings.includedRoutes = [NEIPv4Route.default()] +// ipv4Settings.excludedRoutes = [ +// NEIPv4Route(destinationAddress: "10.0.0.0", subnetMask: "255.0.0.0"), +// NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0"), +// NEIPv4Route(destinationAddress: "127.0.0.0", subnetMask: "255.0.0.0"), +// NEIPv4Route(destinationAddress: "169.254.0.0", subnetMask: "255.255.0.0"), +// NEIPv4Route(destinationAddress: "172.16.0.0", subnetMask: "255.240.0.0"), +// NEIPv4Route(destinationAddress: "192.168.0.0", subnetMask: "255.255.0.0"), +// NEIPv4Route(destinationAddress: "17.0.0.0", subnetMask: "255.0.0.0"), +// ] + + + let dns = "114.114.114.114,8.8.8.8" + let dnsSettings = NEDNSSettings(servers: dns.components(separatedBy: ",")) + dnsSettings.matchDomains = [""] + networkSettings.dnsSettings = dnsSettings + } + networkSettings.ipv4Settings = ipv4Settings - setTunnelNetworkSettings(networkSettings) { - error in + setTunnelNetworkSettings(networkSettings) { error in guard error == nil else { - NSLog(error.debugDescription) NSLog("startTunnel Encountered an error setting up the network: \(error.debugDescription)") completionHandler(error) return } - completionHandler(nil) + if (ipProxy){ + let proxyAddress = Network.NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(proxyPort))!) + self.proxyVpnService = ProxyVpnService(packetFlow: self.packetFlow, proxyAddress: proxyAddress) + self.proxyVpnService!.start() + } + completionHandler(nil) } - NSLog("startTunnelend") } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + proxyVpnService?.stop() completionHandler() } diff --git a/ios/ProxyPin/vpn/Connection.swift b/ios/ProxyPin/vpn/Connection.swift new file mode 100644 index 0000000..33c1ca1 --- /dev/null +++ b/ios/ProxyPin/vpn/Connection.swift @@ -0,0 +1,105 @@ +// +// Connection.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + + +import Foundation +import Network +import os.log + +class Connection{ + var nwProtocol: NWProtocol + var sourceIp: UInt32 + var sourcePort: UInt16 + var destinationIp: UInt32 + var destinationPort: UInt16 + var channel: NWConnection? + + var isInitConnect: Bool = false + var isConnected: Bool = false + var isClosingConnection: Bool = false + var isAbortingConnection: Bool = false + var isAckedToFin: Bool = false + + private let connectionCloser: ConnectionManager + + init(nwProtocol: NWProtocol, sourceIp: UInt32, sourcePort: UInt16, destinationIp: UInt32, destinationPort: UInt16, connectionCloser: ConnectionManager) { + self.nwProtocol = nwProtocol + self.sourceIp = sourceIp + self.sourcePort = sourcePort + self.destinationIp = destinationIp + self.destinationPort = destinationPort + self.connectionCloser = connectionCloser + } + + //发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据 + var sendBuffer = Data() + + var hasReceivedLastSegment = false + + //从客户端接收的最后一个数据包 + var lastIpHeader: IP4Header? + var lastTcpHeader: TCPHeader? + var lastUdpHeader: UDPHeader? + + var timestampSender = 0 + var timestampReplyTo = 0 + + //从客户端接收的序列 + var recSequence: UInt32 = 0 + + //在tcp选项内的SYN期间由客户端发送 + var maxSegmentSize = 0 + + //跟踪我们发送给客户端的ack,并等待客户端返回ack + var sendUnAck: UInt32 = 0 + + //发送到客户端的下一个ack + var sendNext: UInt32 = 0 + + static func getConnectionKey(nwProtocol: NWProtocol, destIp: UInt32, destPort: UInt16, sourceIp: UInt32, sourcePort: UInt16) -> String { + let destIpString = PacketUtil.intToIPAddress(destIp) + let sourceIpString = PacketUtil.intToIPAddress(sourceIp) + return "\(nwProtocol)|\(sourceIpString):\(sourcePort)->\(destIpString):\(destPort)" + } + + func closeConnection() { + connectionCloser.closeConnection(connection: self) + } + + func addSendData(data: Data) { + QueueFactory.instance.getQueue().sync { +// sendBuffer.append(data) +// if (channel?.state != .ready) { +// return +// } + self.sendToDestination(data: data) + } + } + + //发送到目标服务器的数据 + func sendToDestination(data: Data) { +// QueueFactory.instance.getQueue().sync { + // os_log("Sending data to destination key %{public}@", log: OSLog.default, type: .debug, self.description) + //发送数据, 数据包大拆分 + + + self.channel?.send(content: data, completion: .contentProcessed { error in + if let error = error { + os_log("Error sending data to destination %{public}@: %{public}@", log: OSLog.default, type: .error, self.description, error.localizedDescription) + return + } + }) + +// } + } + + var description: String { + return Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: destinationIp, destPort: destinationPort, sourceIp: sourceIp, sourcePort: sourcePort) + } +} diff --git a/ios/ProxyPin/vpn/ConnectionHandler.swift b/ios/ProxyPin/vpn/ConnectionHandler.swift new file mode 100644 index 0000000..a97f5ee --- /dev/null +++ b/ios/ProxyPin/vpn/ConnectionHandler.swift @@ -0,0 +1,399 @@ +// +// ConnectionHandler.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/16. +// + +import Foundation +import NetworkExtension +import os.log + + +enum ProtocolType: UInt8 { + case icmp = 1, tcp = 6, udp = 17 +} + +/// Handles incoming packets and routes them to the appropriate connection. +class ConnectionHandler { + private let manager: ConnectionManager + private let writer: NEPacketTunnelFlow + private let ioService: SocketIOService + + init(manager: ConnectionManager, writer: NEPacketTunnelFlow, ioService: SocketIOService) { + self.manager = manager + self.writer = writer + self.ioService = ioService + } + + //Handle unknown raw IP packet data + public func handlePacket(packet: Data, version: NSNumber?) { + guard let ipHeader = IPPacketFactory.createIP4Header(data: packet) else { + os_log("Malformed IP packet", log: OSLog.default, type: .error) + return + } + + if ipHeader.ipVersion != 4 { + os_log("Unsupported IP version: %d", log: OSLog.default, type: .error, ipHeader.ipVersion) + return + } + +// os_log("Handling packet length:%d, protocolNumber: %d", log: OSLog.default, type: .default, packet.count, ipHeader.protocolNumber) + + var clientPacketData = packet.subdata(in: IPPacketFactory.IP4_HEADER_SIZE.. Void) { +// objc_sync_enter(lock) + closure() +// objc_sync_exit(lock) + } + + private func handleUDPPacket(clientPacketData: Data, ipHeader: IP4Header) { + guard let udpHeader = UDPPacketFactory.createUDPHeader(from: clientPacketData) else { + os_log("Malformed UDP packet", log: OSLog.default, type: .error) + return + } + + var connection = manager.getConnection( + nwProtocol: .UDP, + ip: ipHeader.destinationIP, + port: udpHeader.destinationPort, + srcIp: ipHeader.sourceIP, + srcPort: udpHeader.sourcePort + ) + + let newSession = connection == nil + if connection == nil { + connection = manager.createUDPConnection( + ip: ipHeader.destinationIP, + port: udpHeader.destinationPort, + srcIp: ipHeader.sourceIP, + srcPort: udpHeader.sourcePort + ) + } + + + guard let connection = connection else { + os_log("Failed to create UDP connection", log: OSLog.default, type: .error) + return + } + + synchronized(connection) { +// os_log("Received UDP packet", log: OSLog.default, type: .default) + if newSession { + ioService.registerSession(connection: connection) + } + + let payload = clientPacketData.subdata(in: UDPPacketFactory.UDP_HEADER_LENGTH.. 0 { +// initProxyConnect(packet: packet, destinationIP: destinationIP, destinationPort: destinationPort, connection: connection) +// os_log("Received data packet %{public}@ length:%d seq:%u", log: OSLog.default, type: .default, connection.description, dataLength, tcpHeader.sequenceNumber) + //accumulate data from client + manager.addClientData(data: tcpHeader.payload!, connection: connection) + + //send ack to client only if new data was added + sendAck(ipHeader: ipHeader, tcpHeader: tcpHeader, acceptedDataLength: dataLength, connection: connection) + + } else { +// os_log("Received ACK packet %{public}@ seq:%u", log: OSLog.default, type: .default, connection.description, tcpHeader.sequenceNumber) + //an ack from client for previously sent data + acceptAck(tcpHeader: tcpHeader, connection: connection) + if connection.isClosingConnection { + sendFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection) + } else if connection.isAckedToFin && !tcpHeader.isFIN() { + //the last ACK from client after FIN-ACK flag was sent + manager.closeConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) + } + } + + //received the last segment of data from vpn client + if tcpHeader.isPSH() { + // Tell the NIO thread to immediately send data to the destination + pushDataToDestination(connection: connection, tcpHeader: tcpHeader) + } else if tcpHeader.isFIN() { + //fin from vpn client is the last packet + //ack it + ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection) + } else if tcpHeader.isRST() { + resetTCPConnection(ip: ipHeader, tcp: tcpHeader) + } + + if !connection.isAbortingConnection { + manager.keepSessionAlive(connection: connection) + } + } + } else if tcpHeader.isFIN() { + os_log("Received FIN packet %{public}@:%d seq:%u", log: OSLog.default, type: .default, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber) + //case client sent FIN without ACK + guard let connection = manager.getConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) else { + ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: nil) + return + } + + manager.keepSessionAlive(connection: connection) + } else if tcpHeader.isRST() { + os_log("Received RST packet %{public}@:%d seq:%u", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber) + resetTCPConnection(ip: ipHeader, tcp: tcpHeader) + } else { + os_log("Unknown TCP flag", log: OSLog.default, type: .error) + } + } + + //set connection as aborting so that background worker will close it. + func resetTCPConnection(ip: IP4Header, tcp: TCPHeader) { + let session = manager.getConnection(nwProtocol: .TCP, ip: ip.destinationIP, port: tcp.destinationPort, srcIp: ip.sourceIP, srcPort: tcp.sourcePort) + if let session = session { + session.isAbortingConnection = true + } + } + + func ackFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection?) { + let ackNumber = tcpHeader.sequenceNumber + 1 + let seqNumber = tcpHeader.ackNumber + let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: true) + write(data: finAckData) +// os_log("Sent FIN-ACK packet ack# %{public}d, seq# %{public}d", log: OSLog.default, type: .default, ackNumber, seqNumber) + if let connection = connection { + manager.closeConnection(connection: connection) + } + } + + func pushDataToDestination(connection: Connection, tcpHeader: TCPHeader) { + connection.timestampReplyTo = tcpHeader.timeStampSender + connection.timestampSender = Int(Date().timeIntervalSince1970) + } + + func sendFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection) { + let ackNumber = tcpHeader.sequenceNumber + let seqNumber = tcpHeader.ackNumber + let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: false) + write(data: finAckData) + + connection.sendNext = seqNumber + 1 + connection.isClosingConnection = false + } + + //acknowledge a packet. + func acceptAck(tcpHeader: TCPHeader, connection: Connection) { + let isCorrupted = PacketUtil.isPacketCorrupted(tcpHeader: tcpHeader) + + if isCorrupted { + os_log("Packet is corrupted", log: OSLog.default, type: .error) + } + + if (tcpHeader.sequenceNumber > connection.recSequence) { + connection.recSequence = tcpHeader.sequenceNumber + } + + if tcpHeader.ackNumber >= connection.sendUnAck - 1 || tcpHeader.ackNumber == connection.sendNext { + connection.sendUnAck = tcpHeader.ackNumber + + connection.timestampReplyTo = tcpHeader.timeStampSender + connection.timestampSender = Int(Date().timeIntervalSince1970) + } else { + os_log("%{public}@ Not accepting ack# %d, it should be: %d", log: OSLog.default, type: .error, connection.description ,tcpHeader.ackNumber, connection.sendNext) + os_log("%{public}@ Previous sendUnAck: %d", log: OSLog.default, type: .error, connection.description, connection.sendUnAck) + } + } + + func sendAckForDisorder(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int) { + let ackNumber = tcpHeader.sequenceNumber + UInt32(acceptedDataLength) +// os_log("Sent disorder ack, ack# %{public}d", log: OSLog.default, type: .debug, ackNumber) + let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber) + write(data: ackData) + } + + func sendAck(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection) { + synchronized(connection) { + let ackNumber = (tcpHeader.sequenceNumber + UInt32(acceptedDataLength)) % UInt32.max + connection.recSequence = ackNumber + let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber) + self.write(data: ackData) + } + +// os_log("Sent ACK packet ack# %{public}u", log: OSLog.default, type: .default, ackNumber) + } + + private func sendLastAck(ip: IP4Header, tcp: TCPHeader) { + let data = TCPPacketFactory.createResponseAckData(ipHeader: ip, tcpHeader: tcp, ackToClient: tcp.sequenceNumber + 1) + self.write(data: data) + os_log("Sent last ACK Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort) + } + + private func sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) { + let data = TCPPacketFactory.createRstData(ipHeader: ip, tcpHeader: tcp, dataLength: dataLength) + self.write(data: data) + os_log("Sent RST Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort) + } + + //create a new client's session and SYN-ACK packet data to respond to client + private func replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Void { + ipHeader.identification = 0 + let packet = TCPPacketFactory.createSynAckPacketData(ipHeader: ipHeader, tcpHeader: tcpHeader) + + guard let tcpTransport = packet.transportHeader as? TCPHeader else { + os_log("Failed to extract TCP header from packet", log: OSLog.default, type: .error) + return + } + + let connection = manager.createTCPConnection( + ip: ipHeader.destinationIP, + port: tcpHeader.destinationPort, + srcIp: ipHeader.sourceIP, + srcPort: tcpHeader.sourcePort + ) + + if connection.lastIpHeader != nil { + resendAck(connection: connection) + return + } + + synchronized(connection) { + connection.maxSegmentSize = Int(tcpTransport.maxSegmentSize) + connection.sendUnAck = tcpTransport.sequenceNumber + connection.sendNext = tcpTransport.sequenceNumber + 1 + + //client initial sequence has been incremented by 1 and set to ack + connection.recSequence = tcpTransport.ackNumber + connection.lastIpHeader = ipHeader + connection.lastTcpHeader = tcpHeader + if connection.isInitConnect { + self.ioService.registerSession(connection: connection) + } + self.write(data: packet.buffer) +// os_log("SYN-ACK packet length:%d sent", log: OSLog.default, type: .default, packet.buffer.count) + } + } + + /** + * resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order + * packet arrives. + */ + private func resendAck(connection: Connection) { + let data = TCPPacketFactory.createResponseAckData( + ipHeader: connection.lastIpHeader!, + tcpHeader: connection.lastTcpHeader!, + ackToClient: connection.recSequence + ) +// os_log("Resending ACK packet %{public}@ ackToClient: %d", log: OSLog.default, type: .default, connection.description, connection.recSequence) + self.write(data: data) + } + + + private func write(data: Data) { + self.writer.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) + } + + private func handleICMPPacket(clientPacketData: inout Data, ipHeader: IP4Header) { + guard let requestPacket = ICMPPacketFactory.parseICMPPacket(&clientPacketData) else { + os_log("Failed to parse ICMP packet", log: OSLog.default, type: .error) + return + } + +// os_log("Handling ICMP packet type: %d", log: OSLog.default, type: .default, requestPacket.type) + if requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE { + // This is a packet from the phone, telling somebody that a destination is unreachable. + // Might be caused by issues on our end, but it's unclear what kind of issues. Regardless, + // we can't send ICMP messages ourselves or react usefully, so we drop these silently. + + return + } else if requestPacket.type != ICMPPacket.ECHO_REQUEST_TYPE { + // We only actually support outgoing ping packets. Loudly drop anything else: + os_log("Unknown ICMP type: %d", log: OSLog.default, type: .error, requestPacket.type) + return + } + + QueueFactory.instance.getQueue().async { + + if !self.isReachable(ipAddress: PacketUtil.intToIPAddress(ipHeader.destinationIP)) { + os_log("Failed ping, ignoring", log: OSLog.default, type: .default) + return + } + + let response = ICMPPacketFactory.buildSuccessPacket(requestPacket) + + // Flip the address + let destination = ipHeader.destinationIP + let source = ipHeader.sourceIP + ipHeader.sourceIP = destination + ipHeader.destinationIP = source + + let responseData = ICMPPacketFactory.packetToBuffer(ipHeader: ipHeader, packet: response) + os_log("Successful ping response", log: OSLog.default, type: .default) + self.write(data: responseData) + } + } + + private func isReachable(ipAddress: String) -> Bool { + do { + return true +// return try InetAddress.getByName(ipAddress).isReachable(timeout: 10000) + } catch { + return false + } + } +} diff --git a/ios/ProxyPin/vpn/ConnectionManager.swift b/ios/ProxyPin/vpn/ConnectionManager.swift new file mode 100644 index 0000000..35e2830 --- /dev/null +++ b/ios/ProxyPin/vpn/ConnectionManager.swift @@ -0,0 +1,159 @@ +// +// ConnectionManager.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/16. +// + +import Foundation +import Network +import os.log + +//管理VPN客户端的连接 +class ConnectionManager : CloseableConnection{ + //static let instance = ConnectionManager() + + private var table: [String: Connection] = [:] + private let tableQueue = DispatchQueue(label: "ProxyPin.ConnectionManager") + + public var proxyAddress: NWEndpoint? + + private let defaultPorts: [UInt16] = [80, 443] + +// private init() {} + + + func getConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection? { + let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) + return getConnectionByKey(key: key) + } + + func getConnectionByKey(key: String) -> Connection? { + return tableQueue.sync { + return table[key] + } + } + + func createTCPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection { + let key = Connection.getConnectionKey(nwProtocol: .TCP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) + + return tableQueue.sync { + if let existingConnection = table[key] { + return existingConnection + } + + let connection = Connection(nwProtocol: .TCP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self) + + let parameters = NWParameters.tcp + parameters.allowLocalEndpointReuse = true + parameters.includePeerToPeer = true + + let endpoint: NWEndpoint + if defaultPorts.contains(port) { + endpoint = proxyAddress! + } else { + let ipString = PacketUtil.intToIPAddress(ip) + endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: port)!) + } + + let nwConnection = NWConnection(to: endpoint, using: parameters) + + connection.channel = nwConnection + connection.isInitConnect = true + + tableQueue.async(flags: .barrier) { + self.table[key] = connection + } + os_log("Created TCP connection %{public}@", log: OSLog.default, type: .default, key) + + return connection + } + +// nwConnection.stateUpdateHandler = { state in +// switch state { +// case .ready: +// connection.isConnected = true +// os_log("Connected to %{public}@", log: OSLog.default, type: .debug, endpoint.debugDescription) +// case .failed(let error): +// connection.isConnected = false +// os_log("Failed to connect: %{public}@", log: OSLog.default, type: .error, error.localizedDescription) +// +// default: +// break +// } +// } + + } + + func createUDPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection { + let key = Connection.getConnectionKey(nwProtocol: .UDP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) + + return tableQueue.sync { + if let existingConnection = table[key] { + return existingConnection + } + + let connection = Connection(nwProtocol: .UDP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self) + + + let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host((PacketUtil.intToIPAddress(ip))), port: NWEndpoint.Port(rawValue: port)!) + + let nwConnection = NWConnection(to: endpoint, using: .udp) + connection.channel = nwConnection + + os_log("Created UDP connection %{public}@", log: OSLog.default, type: .default, key) + + connection.isConnected = true + tableQueue.async(flags: .barrier) { + self.table[key] = connection + } + + return connection + } + } + + func closeConnection(connection: Connection) { + closeConnection( + nwProtocol: connection.nwProtocol, ip: connection.destinationIp, port: connection.destinationPort, + srcIp: connection.sourceIp, srcPort: connection.sourcePort + ) + } + + // 从内存中删除连接,然后关闭套接字。 + func closeConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) { + let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort) + tableQueue.async(flags: .barrier) { + if let connection = self.table.removeValue(forKey: key) { + if connection.channel?.state != .cancelled { + connection.channel?.cancel() + os_log("Closed connection %{public}@", log: OSLog.default, type: .debug, key) + } else { + os_log("Connection %{public}@ is already cancelled", log: OSLog.default, type: .debug, key) + } + } + } + } + + //添加来自客户端的数据,该数据稍后将在接收到PSH标志时发送到目的服务器。 + func addClientData(data: Data, connection: Connection) { + guard data.count > 0 else { + return + } + + connection.addSendData(data: data) + } + + //阻止java垃圾收集器收集会话 + func keepSessionAlive(connection: Connection) { + let key = Connection.getConnectionKey( + nwProtocol: connection.nwProtocol, + destIp: connection.destinationIp, + destPort: connection.destinationPort, + sourceIp: connection.sourceIp, + sourcePort: connection.sourcePort + ) + tableQueue.async(flags: .barrier) { + self.table[key] = connection + } + } +} diff --git a/ios/ProxyPin/vpn/NWProtocol.swift b/ios/ProxyPin/vpn/NWProtocol.swift new file mode 100644 index 0000000..b979aa9 --- /dev/null +++ b/ios/ProxyPin/vpn/NWProtocol.swift @@ -0,0 +1,13 @@ +// +// NWProtocol.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + + +public enum NWProtocol { + case TCP,UDP +} diff --git a/ios/ProxyPin/vpn/ProxyVpnService.swift b/ios/ProxyPin/vpn/ProxyVpnService.swift new file mode 100644 index 0000000..5f3b0c5 --- /dev/null +++ b/ios/ProxyPin/vpn/ProxyVpnService.swift @@ -0,0 +1,53 @@ +// +// ProxyService.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation +import NetworkExtension +import Network +import os.log + +class ProxyVpnService { + private let queue: DispatchQueue = DispatchQueue(label: "ProxyPin.ProxyVpnService") + + private var packetFlow: NEPacketTunnelFlow + private var connectionHandler: ConnectionHandler + private var socketIOService: SocketIOService + + init(packetFlow: NEPacketTunnelFlow, proxyAddress: Network.NWEndpoint?) { + self.packetFlow = packetFlow + self.socketIOService = SocketIOService(clientPacketWriter: packetFlow) + let manager = ConnectionManager() + manager.proxyAddress = proxyAddress + self.connectionHandler = ConnectionHandler(manager: manager, writer: packetFlow, ioService: socketIOService) + } + + + /** + Start processing packets, this should be called after registering all IP stacks. + + A stopped interface should never start again. Create a new interface instead. + */ + func start() { + self.readPackets() + } + + func stop() { + self.socketIOService.stop() + queue.suspend() + } + + func readPackets() -> Void { + self.packetFlow.readPackets { (packets, protocols) in + +// os_log("Read %d packets", packets.count) + for (i, packet) in packets.enumerated() { + self.connectionHandler.handlePacket(packet: packet, version: protocols[i]) + } + self.readPackets() + } + } +} diff --git a/ios/ProxyPin/vpn/QueueFactory.swift b/ios/ProxyPin/vpn/QueueFactory.swift new file mode 100644 index 0000000..ed69b22 --- /dev/null +++ b/ios/ProxyPin/vpn/QueueFactory.swift @@ -0,0 +1,29 @@ +// +// QueueFactory.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + +class QueueFactory { + static let instance = QueueFactory() + + private let queue: DispatchQueue + + private init() { + queue = DispatchQueue(label: "com.network.ProxyPin.queue") + } + + func getQueue() -> DispatchQueue { + return queue + } + + func executeAsync(block: @escaping () -> Void) { + queue.async { + block() + } + } + +} diff --git a/ios/ProxyPin/vpn/socket/ClientPacketWriter.swift b/ios/ProxyPin/vpn/socket/ClientPacketWriter.swift new file mode 100644 index 0000000..de627c8 --- /dev/null +++ b/ios/ProxyPin/vpn/socket/ClientPacketWriter.swift @@ -0,0 +1,33 @@ +// +// ClientPacketWriter.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/ + +import Foundation +import NetworkExtension + +class ClientPacketWriter: NSObject { + private var packetFlow: NEPacketTunnelFlow + private let packetQueue = DispatchQueue(label: "packetQueue", attributes: .concurrent) + private var isShutdown = false + + init(packetFlow: NEPacketTunnelFlow) { + self.packetFlow = packetFlow + } + + func write(data: Data) { + if !self.isShutdown { + packetQueue.async { + self.packetFlow.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) + } + } + } + + func shutdown() { + packetQueue.async(flags: .barrier) { + self.isShutdown = true + } + } +} + diff --git a/ios/ProxyPin/vpn/socket/CloseableConnection.swift b/ios/ProxyPin/vpn/socket/CloseableConnection.swift new file mode 100644 index 0000000..eb9846e --- /dev/null +++ b/ios/ProxyPin/vpn/socket/CloseableConnection.swift @@ -0,0 +1,14 @@ +// +// CloseableConnection.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + + +protocol CloseableConnection { + /// Closes the connection + func closeConnection(connection: Connection) +} diff --git a/ios/ProxyPin/vpn/socket/SocketIOService.swift b/ios/ProxyPin/vpn/socket/SocketIOService.swift new file mode 100644 index 0000000..71703e3 --- /dev/null +++ b/ios/ProxyPin/vpn/socket/SocketIOService.swift @@ -0,0 +1,214 @@ +// +// ProxySocketIOService.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation +import NetworkExtension +import os.log + +class SocketIOService { + //private static let maxReceiveBufferSize = 16384 + private static let maxReceiveBufferSize = 1024 + + private let queue: DispatchQueue = DispatchQueue(label: "ProxyPin.SocketIOService", attributes: .concurrent) + + private var clientPacketWriter: NEPacketTunnelFlow + + private var shutdown = false + + init(clientPacketWriter: NEPacketTunnelFlow) { + self.clientPacketWriter = clientPacketWriter + } + + public func stop() { + os_log("Stopping SocketIOService", log: OSLog.default, type: .default) + queue.async(flags: .barrier) { + self.shutdown = true + } + queue.suspend() + } + + //从connection接受数据 写到client + public func registerSession(connection: Connection) { + connection.channel!.stateUpdateHandler = { state in + switch state { + + case .ready: + connection.isConnected = true + os_log("Connected to %{public}@ on receiveMessage", log: OSLog.default, type: .default, connection.description) + //接受远程服务器的数据 +// connection.sendToDestination() + self.receiveMessage(connection: connection) + case .cancelled: + connection.isConnected = false +// os_log("Connection cancelled", log: OSLog.default, type: .default) + connection.closeConnection() + case .failed(let error): + connection.isConnected = false + os_log("Failed to connect: %{public}@", log: OSLog.default, type: .error, error.localizedDescription) + connection.closeConnection() + default: + break + } + } + + connection.channel!.start(queue: self.queue) + } + + private func receiveMessage(connection: Connection) { + if (shutdown) { + os_log("SocketIOService is shutting down", log: OSLog.default, type: .default) + return + } + + if (connection.nwProtocol == .UDP) { + readUDP(connection: connection) + } else { + readTCP(connection: connection) + } + + if (connection.isAbortingConnection) { + os_log("Connection is aborting", log: OSLog.default, type: .default) + connection.closeConnection() + return + } + } + + func readTCP(connection: Connection) { +// os_log("Reading from TCP socket") + if connection.isAbortingConnection { + os_log("Connection is aborting", log: OSLog.default, type: .default) + return + } + + queue.async { + guard let channel = connection.channel else { + os_log("Invalid channel type", log: OSLog.default, type: .error) + return + } + + channel.receive(minimumIncompleteLength: 1, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in +// os_log("Received TCP data packet %{public}@ length %d", log: OSLog.default, type: .default, connection.description, data?.count ?? 0) + if let error = error { + os_log("Failed to read from TCP socket: %@", log: OSLog.default, type: .error, error as CVarArg) + self.sendFin(connection: connection) + connection.isAbortingConnection = true + return + } + + guard let data = data, !data.isEmpty else { + return + } + + self.pushDataToClient(buffer: data, connection: connection) + + // Recursively call readTCP to continue reading messages + self.receiveMessage(connection: connection) + + if (isComplete) { + self.sendFin(connection: connection) + connection.isAbortingConnection = true + return + } + } + } + } + + func synchronized(_ lock: AnyObject, closure: () -> Void) { +// objc_sync_enter(lock) + closure() +// objc_sync_exit(lock) + } + + ///create packet data and send it to VPN client + private func pushDataToClient(buffer: Data, connection: Connection) { + // Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a + // trigger to set PSH on the resulting TCP packet that goes to the VPN. + + connection.hasReceivedLastSegment = buffer.count < Self.maxReceiveBufferSize + + guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else { + os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error) + return + } + + synchronized(connection) { + let unAck = connection.sendNext + //处理益处问题 + let nextUnAck = UInt32(truncatingIfNeeded: (connection.sendNext + UInt32(buffer.count)) % UInt32.max) + connection.sendNext = nextUnAck + + let data = TCPPacketFactory.createResponsePacketData( + ipHeader: ipHeader, + tcpHeader: tcpHeader, + packetData: buffer, + isPsh: connection.hasReceivedLastSegment, + ackNumber: connection.recSequence, + seqNumber: unAck, + timeSender: connection.timestampSender, + timeReplyTo: connection.timestampReplyTo + ) + + self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) +// os_log("Sent TCP data packet to client %{public}@ length:%d ack:%u", log: OSLog.default, type: .default, connection.description, data.count, connection.recSequence) + } + } + + private func sendFin(connection: Connection) { + guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else { + os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error) + return + } + synchronized(connection) { + let data = TCPPacketFactory.createFinData( + ipHeader: ipHeader, + tcpHeader: tcpHeader, + ackNumber: connection.recSequence, + seqNumber: connection.sendNext, + timeSender: connection.timestampSender, + timeReplyTo: connection.timestampReplyTo + ) + + self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)]) + } + } + + func readUDP(connection: Connection) { + queue.async { + guard let channel = connection.channel else { + os_log("Invalid channel type", log: OSLog.default, type: .error) + return + } + + channel.receive(minimumIncompleteLength: 1, maximumLength: 4196) { (data, context, isComplete, error) in + if let error = error { + os_log("Failed to read from UDP socket: %@", log: OSLog.default, type: .error, error as CVarArg) + connection.isAbortingConnection = true + return + } + +// os_log("Received UDP data packet length %d", log: OSLog.default, type: .debug, data?.count ?? 0) + + guard let data = data, !data.isEmpty else { + return + } + + + let packetData = UDPPacketFactory.createResponsePacket( + ip: connection.lastIpHeader!, + udp: connection.lastUdpHeader!, + packetData: data + ) +// os_log("Sending UDP data packet to client", log: OSLog.default, type: .default) + + self.clientPacketWriter.writePackets([packetData], withProtocols: [NSNumber(value: AF_INET)]) + + // Recursively call receiveMessage to continue receiving messages + self.receiveMessage(connection: connection) + } + } + } +} diff --git a/ios/ProxyPin/vpn/transport/Packet.swift b/ios/ProxyPin/vpn/transport/Packet.swift new file mode 100644 index 0000000..74223b8 --- /dev/null +++ b/ios/ProxyPin/vpn/transport/Packet.swift @@ -0,0 +1,21 @@ +// +// Packet.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + + +class Packet { + var ipHeader: IP4Header + var transportHeader: TransportHeader + var buffer: Data + + init(ipHeader: IP4Header, transportHeader: TransportHeader, buffer: Data) { + self.ipHeader = ipHeader + self.transportHeader = transportHeader + self.buffer = buffer + } +} diff --git a/ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift b/ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift new file mode 100644 index 0000000..191d91c --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift @@ -0,0 +1,96 @@ +// +// ICMPPacket.swift +// ProxyPin +// +// Created by wanghongen on 2024/10/3. +// + +import Foundation + +class ICMPPacket { + // Two ICMP packets we can handle: simple ping & pong + static let ECHO_REQUEST_TYPE: UInt8 = 8 + static let ECHO_SUCCESS_TYPE: UInt8 = 0 + + // One very common packet we ignore: connection rejection. Unclear why this happens, + // random incoming connections that the phone tries to reply to? Nothing we can do though, + // as we can't forward ICMP onwards, and we can't usefully respond or react. + static let DESTINATION_UNREACHABLE_TYPE: UInt8 = 3 + + let type: UInt8 + let code: UInt8 // 0 for request, 0 for success, 0 - 15 for error subtypes + let checksum: UInt16 + let identifier: UInt16 + let sequenceNumber: UInt16 + let data: [UInt8] + + init(type: UInt8, code: UInt8, checksum: UInt16, identifier: UInt16, sequenceNumber: UInt16, data: [UInt8]) { + self.type = type + self.code = code + self.checksum = checksum + self.identifier = identifier + self.sequenceNumber = sequenceNumber + self.data = data + } + + var description: String { + return "ICMP packet type \(type)/\(code) id:\(identifier) seq:\(sequenceNumber) and \(data.count) bytes of data" + } +} + + +class ICMPPacketFactory { + + static func parseICMPPacket(_ stream: inout Data) -> ICMPPacket? { + guard stream.count >= 8 else { return nil } + + let type = stream.removeFirst() + let code = stream.removeFirst() + let checksum = stream.withUnsafeBytes { $0.load(as: UInt16.self) } + stream.removeFirst(2) + + let identifier = stream.withUnsafeBytes { $0.load(as: UInt16.self) } + stream.removeFirst(2) + let sequenceNumber = stream.withUnsafeBytes { $0.load(as: UInt16.self) } + stream.removeFirst(2) + + let data = Array(stream) + + return ICMPPacket(type: type, code: code, checksum: checksum, identifier: identifier, sequenceNumber: sequenceNumber, data: data) + } + + static func buildSuccessPacket(_ requestPacket: ICMPPacket) -> ICMPPacket { + return ICMPPacket( + type: ICMPPacket.ECHO_SUCCESS_TYPE, + code: 0, + checksum: 0, + identifier: requestPacket.identifier, + sequenceNumber: requestPacket.sequenceNumber, + data: requestPacket.data + ) + } + + static func packetToBuffer(ipHeader: IP4Header, packet: ICMPPacket) -> Data { + var buffer = Data() + buffer.append(ipHeader.toBytes()) + + var icmpDataBuffer = Data() + icmpDataBuffer.append(packet.type) + icmpDataBuffer.append(packet.code) + icmpDataBuffer.append(contentsOf: withUnsafeBytes(of: UInt16(0), Array.init)) + + if packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE { + icmpDataBuffer.append(contentsOf: packet.identifier.bytes) + icmpDataBuffer.append(contentsOf: packet.sequenceNumber.bytes) + icmpDataBuffer.append(contentsOf: packet.data) + } else { + fatalError("Can't serialize unrecognized ICMP packet type") + } + + let checksum = PacketUtil.calculateChecksum(data: icmpDataBuffer, offset: 0, length: icmpDataBuffer.count) + icmpDataBuffer.replaceSubrange(2..<4, with: checksum) + buffer.append(icmpDataBuffer) + + return buffer + } +} diff --git a/ios/ProxyPin/vpn/transport/protocol/IP4Header.swift b/ios/ProxyPin/vpn/transport/protocol/IP4Header.swift new file mode 100644 index 0000000..7c97684 --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/IP4Header.swift @@ -0,0 +1,161 @@ +// +// IP4Header.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/16. +// + +import Foundation +import os.log + +// IPv4 header data structure +class IP4Header { + var ipVersion: UInt8 // 对于IPv4,其值为4(因此命名为IPv4)。 4bit + var internetHeaderLength: UInt8 // 头部长度 4bit + var diffTypeOfService: UInt8 // 差分服务代码点 =>6位 + var ecn: UInt8 // 显式拥塞通知(ECN) + var totalLength: UInt16 // 此IP数据包的总长度 16bit + var identification: UInt16 // 主要用于唯一标识单个IP数据报的片段组。 16bit + var mayFragment: Bool // 用于指示数据报是否可以分段。 1bit + var lastFragment: Bool // 用于指示数据报是否是片段中的最后一个。 1bit + var fragmentOffset: UInt16 // 指定特定片段相对于原始未分段的IP数据报的开始的偏移量。 13bit + var timeToLive: UInt8 // 用于防止数据报持续存在。8bit + var protocolNumber: UInt8 // 定义IP数据报的数据部分中使用的协议。 8bit + var headerChecksum: UInt16 // 用于对头部进行错误检查的16位字段。 16bit + var sourceIP: UInt32 // 发送者的IPv4地址。 32bit + var destinationIP: UInt32 // 接收者的IPv4地址。 32bit + + //用于控制或识别片段的3比特字段。 + //bit 0: 保留;必须为零 + //bit 1: Don't Fragment (DF) + //bit 2: More Fragments (MF) + private var flag: UInt8 + + init( + ipVersion: UInt8, internetHeaderLength: UInt8, diffTypeOfService: UInt8, ecn: UInt8, totalLength: UInt16, identification: UInt16, + mayFragment: Bool, lastFragment: Bool, fragmentOffset: UInt16, timeToLive: UInt8, protocolNumber: UInt8, headerChecksum: UInt16, + sourceIP: UInt32, destinationIP: UInt32 + ) { + self.ipVersion = ipVersion + self.internetHeaderLength = internetHeaderLength + self.diffTypeOfService = diffTypeOfService + self.ecn = ecn + self.totalLength = totalLength + self.identification = identification + self.mayFragment = mayFragment + self.lastFragment = lastFragment + self.fragmentOffset = fragmentOffset + self.timeToLive = timeToLive + self.protocolNumber = protocolNumber + self.headerChecksum = headerChecksum + self.sourceIP = sourceIP + self.destinationIP = destinationIP + self.flag = IP4Header.initFlag(mayFragment: mayFragment, lastFragment: lastFragment) + } + + + private static func initFlag(mayFragment: Bool, lastFragment: Bool) -> UInt8 { + var initFlag: UInt8 = 0 + if mayFragment { + initFlag = 0x40 + } + if lastFragment { + initFlag |= 0x20 + } + return initFlag + } + + func setMayFragment(_ mayFragment: Bool) { + self.mayFragment = mayFragment + flag = mayFragment ? (flag | 0x40) : (flag & 0xBF) + } + + func getIPHeaderLength() -> Int { + return Int(internetHeaderLength * 4) + } + + func copy() -> IP4Header { + return IP4Header( + ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification, + mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: headerChecksum, + sourceIP: sourceIP, destinationIP: destinationIP + ) + } + + func toBytes() -> Data { + var buffer = Data() + buffer.append(UInt8((ipVersion << 4) + internetHeaderLength)) + buffer.append(UInt8((diffTypeOfService << 2) + ecn)) + + buffer.append(contentsOf: totalLength.bytes) + buffer.append(contentsOf: identification.bytes) + + //组合标志和部分片段偏移 + buffer.append(UInt8((fragmentOffset >> 8) & 0x1F) | flag) + buffer.append(UInt8(fragmentOffset & 0xFF)) + + buffer.append(timeToLive) + buffer.append(protocolNumber) + + buffer.append(contentsOf: headerChecksum.bytes) + + buffer.append(contentsOf: sourceIP.bytes) + buffer.append(contentsOf: destinationIP.bytes) + return buffer + } +} + +class IPPacketFactory { + static let IP4_HEADER_SIZE = 20 + static let IP4_VERSION: UInt8 = 0x04 + + //从给定的ByteBuffer流创建IPv4标头 + static func createIP4Header(data: Data) -> IP4Header? { + guard data.count >= IP4_HEADER_SIZE else { + return nil + } + + let buffer = [UInt8](data) + let versionAndHeaderLength = buffer[0] + let ipVersion = versionAndHeaderLength >> 4 + guard ipVersion == IP4_VERSION else { + return nil + } + + let internetHeaderLength = versionAndHeaderLength & 0x0F + let typeOfService = buffer[1] + let diffTypeOfService = typeOfService >> 2 + let ecn = typeOfService & 0x03 + let totalLength = UInt16(buffer[2]) << 8 | UInt16(buffer[3]) + let identification = UInt16(buffer[4]) << 8 | UInt16(buffer[5]) + let flagsAndFragmentOffset = UInt16(buffer[6]) << 8 | UInt16(buffer[7]) + let mayFragment = (flagsAndFragmentOffset & 0x4000) != 0 + let lastFragment = (flagsAndFragmentOffset & 0x2000) != 0 + let fragmentOffset = flagsAndFragmentOffset & 0x1FFF + let timeToLive = buffer[8] + let protocolNumber = buffer[9] + let checksum = UInt16(buffer[10]) << 8 | UInt16(buffer[11]) + let sourceIp = UInt32(buffer[12]) << 24 | UInt32(buffer[13]) << 16 | UInt32(buffer[14]) << 8 | UInt32(buffer[15]) + let desIp = UInt32(buffer[16]) << 24 | UInt32(buffer[17]) << 16 | UInt32(buffer[18]) << 8 | UInt32(buffer[19]) + + if internetHeaderLength > 5 { + // drop the IP option + for _ in 0..<(internetHeaderLength - 5) { + // Skip the IP options + } + } + + return IP4Header( + ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification, + mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: checksum, + sourceIP: sourceIp, destinationIP: desIp + ) + } + + public static func printPacket(data: Data) { + guard let ipHeader = createIP4Header(data: data) else { + return + } + os_log("IP Header: version: %{public}d, internetHeaderLength: %{public}d, diffTypeOfService: %{public}d, ecn: %{public}d, totalLength: %{public}d, identification: %{public}d, mayFragment: %{public}d, lastFragment: %{public}d, fragmentOffset: %{public}d, timeToLive: %{public}d, protocolNumber: %{public}d, headerChecksum: %{public}d, sourceIP: %{public}@, destinationIP: %{public}@", log: OSLog.default, type: .default, ipHeader.ipVersion, ipHeader.internetHeaderLength, ipHeader.diffTypeOfService, ipHeader.ecn, ipHeader.totalLength, ipHeader.identification, ipHeader.mayFragment, ipHeader.lastFragment, ipHeader.fragmentOffset, ipHeader.timeToLive, ipHeader.protocolNumber, ipHeader.headerChecksum, PacketUtil.intToIPAddress(ipHeader.sourceIP), PacketUtil.intToIPAddress(ipHeader.destinationIP)) + } +} diff --git a/ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift b/ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift new file mode 100644 index 0000000..14622ba --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift @@ -0,0 +1,182 @@ +// +// TCPHeader.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/16. +// + +import Foundation + +/// Represents a TCP header in a network packet. +class TCPHeader : TransportHeader{ + + /// Source port number (16 bits) + var sourcePort: UInt16 + /// Destination port number (16 bits) + var destinationPort: UInt16 + /// Sequence number (32 bits) + var sequenceNumber: UInt32 + /// Acknowledgment number (32 bits) + var ackNumber: UInt32 + /// Data offset (4 bits) + var dataOffset: UInt8 + var isNS: Bool = false // ECN-nonce concealment protection (experimental: see RFC 3540) + /// Flags (9 bits) + var flags: UInt8 + /// Window size (16 bits) + var windowSize: UInt16 + /// Checksum (16 bits) + var checksum: UInt16 + /// Urgent pointer (16 bits) + var urgentPointer: UInt16 + /// Options (variable length) + var options: Data? + var payload: Data? + + //Static section for constants + static let END_OF_OPTIONS_LIST: UInt8 = 0 + static let NO_OPERATION: UInt8 = 1 + static let MAX_SEGMENT_SIZE: UInt8 = 2 + static let WINDOW_SCALE: UInt8 = 3 + static let SELECTIVE_ACK_PERMITTED: UInt8 = 4 + static let TIME_STAMP: UInt8 = 8 + + init(sourcePort: UInt16, destinationPort: UInt16, sequenceNumber: UInt32, ackNumber: UInt32, dataOffset: UInt8, isNS: Bool, flags: UInt8, windowSize: UInt16, checksum: UInt16, urgentPointer: UInt16, options: Data?, payload: Data? = nil) { + self.sourcePort = sourcePort + self.destinationPort = destinationPort + self.sequenceNumber = sequenceNumber + self.ackNumber = ackNumber + self.dataOffset = dataOffset + self.isNS = isNS + self.flags = flags + self.windowSize = windowSize + self.checksum = checksum + self.urgentPointer = urgentPointer + self.options = options + self.payload = payload + } + + //options + var maxSegmentSize: UInt16 = 0 + private var windowScale: UInt8 = 0 + private var isSelectiveAckPermitted = false + var timeStampSender = 0 + var timeStampReplyTo = 0 + + func getSourcePort() -> Int { + return Int(sourcePort) + } + + func getDestinationPort() -> Int { + return Int(destinationPort) + } + + func isFIN() -> Bool { + return flags & 0x01 != 0 + } + + /// Checks if the SYN flag is set. + func isSYN() -> Bool { + return flags & 0x02 != 0 + } + + /// Checks if the RST flag is set. + func isRST() -> Bool { + return flags & 0x04 != 0 + } + + /// Checks if the PSH flag is set. + func isPSH() -> Bool { + return flags & 0x08 != 0 + } + + /// Checks if the ACK flag is set. + func isACK() -> Bool { + return flags & 0x10 != 0 + } + + /// Checks if the URG flag is set. + func isURG() -> Bool { + return flags & 0x20 != 0 + } + + /// Checks if the ECE flag is set. + func isECE() -> Bool { + return flags & 0x40 != 0 + } + + /// Checks if the CWR flag is set. + func isCWR() -> Bool { + return flags & 0x80 != 0 + } + + /// Sets or clears the RST flag. + func setIsRST(_ isRST: Bool) { + flags = isRST ? (flags | 0x04) : (flags & 0xFB) + } + + /// Sets or clears the SYN flag. + func setIsSYN(_ isSYN: Bool) { + flags = isSYN ? (flags | 0x02) : (flags & 0xFD) + } + + /// Sets or clears the FIN flag. + func setIsFIN(_ isFIN: Bool) { + flags = isFIN ? (flags | 0x01) : (flags & 0xFE) + } + + /// Sets or clears the PSH flag. + func setIsPSH(_ isPSH: Bool) { + flags = isPSH ? (flags | 0x08) : (flags & 0xF7) + } + + /// Sets or clears the ACK flag. + func setIsACK(_ isACK: Bool) { + flags = isACK ? (flags | 0x10) : (flags & 0xEF) + } + + /// Returns the length of the TCP header. + func getTCPHeaderLength() -> Int { + return Int(dataOffset) * 4 + } + + /// Converts the TCP header to a byte array. + func toBytes() -> Data { + var buffer = Data() + + buffer.append(contentsOf: sourcePort.bytes) + buffer.append(contentsOf: destinationPort.bytes) + buffer.append(contentsOf: sequenceNumber.bytes) + buffer.append(contentsOf: ackNumber.bytes) + + //is ns and data offset + let headerLength = 5 + buffer.append(UInt8((headerLength << 4) | (isNS ? 1 : 0))) + buffer.append(flags) + buffer.append(contentsOf: windowSize.bytes) + buffer.append(contentsOf: checksum.bytes) + buffer.append(contentsOf: urgentPointer.bytes) +// if let options = options { +// buffer.append(options) +// } + return buffer + } + + /// Creates a copy of the TCP header. + func copy() -> TCPHeader { + return TCPHeader( + sourcePort: sourcePort, + destinationPort: destinationPort, + sequenceNumber: sequenceNumber, + ackNumber: ackNumber, + dataOffset: dataOffset, + isNS: isNS, + flags: flags, + windowSize: windowSize, + checksum: checksum, + urgentPointer: urgentPointer, + options: options + ) + } + +} diff --git a/ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift b/ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift new file mode 100644 index 0000000..36530ed --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift @@ -0,0 +1,325 @@ +// +// TCPPacketFactory.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/16. +// +// + +import Foundation +import os.log + +/// Factory class for creating TCP packets. +class TCPPacketFactory { + + public static let TCP_HEADER_LENGTH = 20 + + //从tcp报文创建tcpHeader + static func createTCPHeader(data: Data) -> TCPHeader? { + if data.count < TCP_HEADER_LENGTH { + os_log("Data is too short to be a TCP packet", log: OSLog.default, type: .error) + return nil + } + + + var offset = 0 + + func readUInt16() -> UInt16 { + let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt16.self).bigEndian } + offset += 2 + return value + } + + func readUInt32() -> UInt32 { + let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self).bigEndian } + offset += 4 + return value + } + + let sourcePort = readUInt16() + let destinationPort = readUInt16() + + let sequenceNumber = readUInt32() + let ackNumber = readUInt32() + + let dataOffsetAndReserved = data[offset] + offset += 1 + let dataOffset = UInt8((dataOffsetAndReserved & 0xF0) >> 4) + let isNs = (dataOffsetAndReserved & 0x01) == 1 + let flags = UInt8(data[offset]) + offset += 1 + + let windowSize = readUInt16() + let checksum = readUInt16() + let urgentPointer = readUInt16() + + var optionsSize = Int(dataOffset) - 5 + var options: Data? + if (optionsSize > 0) { + optionsSize *= 4 + options = data.subdata(in: offset.. Data { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + + flipIp(ip: &ip, tcp: &tcp) + let seqNumber = tcp.ackNumber + tcp.ackNumber = ackToClient + tcp.sequenceNumber = seqNumber + + ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) + + // Set TCP flags + tcp.setIsACK(true) + tcp.setIsSYN(false) + tcp.setIsPSH(false) + tcp.setIsFIN(false) + + tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes + tcp.options = nil + + ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) + + return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) + } + + ///创建带有RST标志的数据包,以便在需要重置时发送到客户端。 + static func createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int) -> Data { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + + var ackNumber: UInt32 = 0 + var seqNumber: UInt32 = 0 + + if tcp.ackNumber > 0 { + seqNumber = tcp.ackNumber + } else { + ackNumber = tcp.sequenceNumber + UInt32(dataLength) + } + + tcp.ackNumber = ackNumber + tcp.sequenceNumber = seqNumber + + // Flip IP from source to destination + flipIp(ip: &ip, tcp: &tcp) + + ip.identification = 0 + + tcp.flags = 0 + tcp.isNS = false + tcp.setIsRST(true) + + tcp.dataOffset = 5 + tcp.options = nil + tcp.windowSize = 0 + + // Recalculate IP length + let totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() + ip.totalLength = UInt16(totalLength) + + return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) + } + + //创建发送到客户端的FIN-ACK + static func createFinAckData(ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: UInt32, seqToClient: UInt32, isFin: Bool, isAck: Bool) -> Data { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + + flipIp(ip: &ip, tcp: &tcp) + + tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes + tcp.options = nil + + tcp.ackNumber = ackToClient + tcp.sequenceNumber = seqToClient + ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) + + tcp.setIsACK(isAck) + tcp.setIsSYN(false) + tcp.setIsPSH(false) + tcp.setIsFIN(isFin) + + ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) + return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) + } + + //通过写回客户端流创建SYN-ACK数据包数据 + public static func createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Packet { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + flipIp(ip: &ip, tcp: &tcp) + + tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes + tcp.options = nil + + // ack = received sequence + 1 + let ackNumber = tcpHeader.sequenceNumber + 1 + tcp.ackNumber = ackNumber + + // Server-generated initial sequence number + let seqNumber = UInt64.random(in: 0..<100000) + tcp.sequenceNumber = UInt32(seqNumber) + + // SYN-ACK + tcp.setIsACK(true) + tcp.setIsSYN(true) + + tcp.timeStampReplyTo = tcp.timeStampSender + tcp.timeStampSender = PacketUtil.currentTime + + ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength()) + + return Packet(ipHeader: ip, transportHeader: tcp, buffer: createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)) + } + + //创建数据包数据以发送回客户端 + public static func createResponsePacketData( + ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: Data?, isPsh: Bool, + ackNumber: UInt32, seqNumber: UInt32, timeSender: Int, timeReplyTo: Int + ) -> Data { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + + flipIp(ip: &ip, tcp: &tcp) + + tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes + tcp.options = nil + + tcp.ackNumber = ackNumber + tcp.sequenceNumber = seqNumber + ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) + + // ACK is always sent + tcp.setIsACK(true) + tcp.setIsSYN(false) + tcp.setIsPSH(isPsh) + tcp.setIsFIN(false) + tcp.timeStampSender = timeSender + tcp.timeStampReplyTo = timeReplyTo + + var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength() + if let packetData = packetData { + totalLength += packetData.count + } + ip.totalLength = UInt16(totalLength) + + return createPacketData(ipHeader: ip, tcpHeader: tcp, data: packetData) + } + + //将IP从源翻转到目标 + private static func flipIp(ip: inout IP4Header, tcp: inout TCPHeader) { + let sourceIp = ip.destinationIP + let destIp = ip.sourceIP + let sourcePort = tcp.destinationPort + let destPort = tcp.sourcePort + + ip.destinationIP = destIp + ip.sourceIP = sourceIp + tcp.destinationPort = destPort + tcp.sourcePort = sourcePort + } + + public static func createFinData( + ipHeader: IP4Header, tcpHeader: TCPHeader, ackNumber: UInt32, seqNumber: UInt32, + timeSender: Int, timeReplyTo: Int + ) -> Data { + var ip = ipHeader.copy() + var tcp = tcpHeader.copy() + + flipIp(ip: &ip, tcp: &tcp) + + tcp.ackNumber = ackNumber + tcp.sequenceNumber = seqNumber + + ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) + + tcp.timeStampReplyTo = timeReplyTo + tcp.timeStampSender = timeSender + + tcp.flags = 0 + tcp.isNS = false + tcp.setIsACK(true) + tcp.setIsFIN(true) + + tcp.options = nil + tcp.windowSize = 0 + + ip.totalLength = UInt16(ip.getIPHeaderLength() + TCP_HEADER_LENGTH) + return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil) + } + + + //从tcpHeader创建tcp报文 + private static func createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: Data?) -> Data { + let dataLength = data?.count ?? 0 + + var buffer = Data() + + // Add IP header + let ipBuffer = ipHeader.toBytes() + buffer.append(ipBuffer) + + // Add TCP header + let tcpBuffer = tcpHeader.toBytes() + buffer.append(tcpBuffer) + + // Add data if exists + if let data = data { + buffer.append(data) + } + + // Zero out IP checksum + buffer[10] = 0 + buffer[11] = 0 + + // Calculate IP checksum + let ipChecksum = PacketUtil.calculateChecksum(data: buffer, offset: 0, length: ipBuffer.count) + buffer[10] = ipChecksum[0] + buffer[11] = ipChecksum[1] +// IPPacketFactory.printPacket(data: ipBuffer) + + // Zero out TCP checksum + let tcpStart = ipBuffer.count + buffer[tcpStart + 16] = 0 + buffer[tcpStart + 17] = 0 + + // Calculate TCP checksum + let tcpChecksum = PacketUtil.calculateTCPHeaderChecksum( + data: buffer, offset: tcpStart, tcpLength: tcpBuffer.count + dataLength, + sourceIP: ipHeader.sourceIP, destinationIP: ipHeader.destinationIP + ) + buffer[tcpStart + 16] = tcpChecksum[0] + buffer[tcpStart + 17] = tcpChecksum[1] + return buffer + } + + static func printPacket(data: Data) { + guard let tcpHeader = createTCPHeader(data: data) else { + os_log("Failed to create TCP header", log: OSLog.default, type: .error) + return + } + + os_log("TCP Header: sourcePort: %{public}d, destinationPort: %{public}d, sequenceNumber: %{public}u, ackNumber: %{public}u, dataOffset: %{public}d, isNS: %{public}d, flags: %{public}d, windowSize: %{public}d, checksum: %{public}u, urgentPointer: %{public}u", + log: OSLog.default, type: .default, tcpHeader.sourcePort, tcpHeader.destinationPort, tcpHeader.sequenceNumber, tcpHeader.ackNumber, tcpHeader.dataOffset, tcpHeader.isNS ? 1 : 0, tcpHeader.flags, tcpHeader.windowSize, tcpHeader.checksum, tcpHeader.urgentPointer) + } +} diff --git a/ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift b/ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift new file mode 100644 index 0000000..9c6d155 --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift @@ -0,0 +1,13 @@ +// +// TransportHeader.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation + +protocol TransportHeader { + func getSourcePort() -> Int + func getDestinationPort() -> Int +} diff --git a/ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift b/ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift new file mode 100644 index 0000000..4423b56 --- /dev/null +++ b/ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift @@ -0,0 +1,112 @@ +// +// UDPHeader.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation +import os.log + +///UDP报头的数据 +struct UDPHeader { + var sourcePort: UInt16 //源端口号 16bit + var destinationPort: UInt16 //源端口号 16bit + var length: UInt16 //UDP数据报长度 16bit + var checksum: UInt16 //校验和 16bit + + init(sourcePort: UInt16, destinationPort: UInt16, length: UInt16, checksum: UInt16) { + self.sourcePort = sourcePort + self.destinationPort = destinationPort + self.length = length + self.checksum = checksum + } +} + + +class UDPPacketFactory { + static let UDP_HEADER_LENGTH = 8 + + static func createUDPHeader(from data: Data) -> UDPHeader? { + guard data.count >= UDP_HEADER_LENGTH else { + return nil + } + + let srcPort = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt16.self).bigEndian } + let destPort = data.withUnsafeBytes { $0.load(fromByteOffset: 2, as: UInt16.self).bigEndian } + let length = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt16.self).bigEndian } + let checksum = data.withUnsafeBytes { $0.load(fromByteOffset: 6, as: UInt16.self).bigEndian } + + return UDPHeader(sourcePort: srcPort, destinationPort: destPort, length: length, checksum: checksum) + } + + + static func createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: Data?) -> Data { + var udpLen = 8 + if let packetData = packetData { + udpLen += packetData.count + } + let srcPort = udp.destinationPort + let destPort = udp.sourcePort + + let ipHeader = ip.copy() + let srcIp = ip.destinationIP + let destIp = ip.sourceIP + + ipHeader.setMayFragment(false) + ipHeader.sourceIP = srcIp + ipHeader.destinationIP = destIp + ipHeader.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId()) + + //ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length + let totalLength = ipHeader.getIPHeaderLength() + udpLen + ipHeader.totalLength = UInt16(totalLength) + + var ipData = ipHeader.toBytes() + + // clear IP checksum + ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in + bytes[10] = 0 + bytes[11] = 0 + } + + //os_log("Create UDP response packet from %{public}@:%{public}d to %{public}@:%{public}d totalLength:%{public}d", log: OSLog.default, type: .default, PacketUtil.intToIPAddress(srcIp), srcPort, PacketUtil.intToIPAddress(destIp), destPort, totalLength) + + // calculate checksum for IP header + let ipChecksum = PacketUtil.calculateChecksum(data: ipData, offset: 0, length: ipData.count) + + // write result of checksum back to buffer + ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in + bytes[10] = ipChecksum[0] + bytes[11] = ipChecksum[1] + } + + var buffer = Data() + + // copy IP header to buffer + buffer.append(ipData) + + // copy UDP header to buffer + buffer.append(contentsOf: srcPort.bytes) + buffer.append(contentsOf: destPort.bytes) + + buffer.append(contentsOf: UInt16(udpLen).bytes) + + let checksum: UInt16 = 0 + buffer.append(contentsOf: checksum.bytes) + + if let packetData = packetData { + buffer.append(packetData) + } + return buffer + } + + //打印数据包 + public static func printPacket(data: Data) { + guard let udpHeader = createUDPHeader(from: data) else { + return + } + os_log("UDP Header: sourcePort: %{public}d, destinationPort: %{public}d, length: %{public}d, checksum: %{public}d", log: OSLog.default, type: .default, udpHeader.sourcePort, udpHeader.destinationPort, udpHeader.length, udpHeader.checksum) + } + +} diff --git a/ios/ProxyPin/vpn/utils/PacketUtil.swift b/ios/ProxyPin/vpn/utils/PacketUtil.swift new file mode 100644 index 0000000..e9a7e44 --- /dev/null +++ b/ios/ProxyPin/vpn/utils/PacketUtil.swift @@ -0,0 +1,138 @@ +// +// PacketUtil.swift +// ProxyPin +// +// Created by wanghongen on 2024/9/17. +// + +import Foundation +import os.log + +class PacketUtil { + private static var packetId: Int = 0 + + static func getPacketId() -> Int { + defer { packetId += 1 } + return packetId + } + + static var currentTime: Int { + return Int(Date().timeIntervalSince1970) + } + + static func writeIntToBytes(value: UInt32, buffer: inout Data, offset: Int) { + guard buffer.count >= offset + 4 else { return } + var intValue = value.bigEndian + let intData = Data(bytes: &intValue, count: 4) + buffer.replaceSubrange(offset.. String { + return String(format: "%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF) + } + + static func calculateTCPHeaderChecksum(data: Data, offset: Int, tcpLength: Int, sourceIP: UInt32, destinationIP: UInt32) -> Data { + var bufferSize = tcpLength + 12 + var isOdd = false + if bufferSize % 2 != 0 { + bufferSize += 1 + isOdd = true + } + + var buffer = Data() + + // Add source IP + buffer.append(contentsOf: sourceIP.bytes) + // Add destination IP + buffer.append(contentsOf: destinationIP.bytes) + + // Add reserved byte and protocol (6 for TCP) + buffer.append(0) + buffer.append(6) + + // Add TCP length + buffer.append(contentsOf: UInt16(tcpLength).bytes) + + // Add TCP header and data + buffer.append(contentsOf: data[offset.. Data { + var start = offset + var sum = 0 + + while start < length { + sum += getNetworkInt(buffer: data, start: start, length: 2) + start += 2 + } + + // Carry over one's complement + while (sum >> 16) > 0 { + sum = (sum & 0xFFFF) + (sum >> 16) + } + + // Flip the bits to get one's complement + sum = ~sum + + // Extract the last two bytes of the int + let checksum = Data([UInt8(truncatingIfNeeded: (sum >> 8) & 0xFF), UInt8(truncatingIfNeeded: sum & 0xFF)]) + return checksum + } + + static func getNetworkInt(buffer: Data, start: Int, length: Int) -> Int { + var value = 0 + var end = start + min(length, 4) + if end > buffer.count { end = buffer.count } + for i in start.. Bool { + guard let options = tcpHeader.options else { + return false + } + + var i = 0 + while i < options.count { + let kind = options[i] + switch kind { + case 0, 1: + break + case 2: + i += 3 + case 3, 14: + i += 2 + case 4: + i += 1 + case 5, 15: + i += Int(options[i + 1]) - 2 + case 8: + i += 9 + case 23: + return true + default: + print("Unknown option: \(kind)") + } + i += 1 + } + return false + } +} + + +extension FixedWidthInteger { + var bytes: [UInt8] { + withUnsafeBytes(of: self.bigEndian) { Array($0) } + } +} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f67fe8d..dde381f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -21,10 +21,27 @@ 9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9B0912202A54593A001108B7 /* ProxyPin.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */; }; 9B2A10C82B4CBE32001C443F /* silience.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9B2A10C72B4CBE32001C443F /* silience.mov */; }; + 9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5125A92CAEE3350027996E /* ICMPPacket.swift */; }; 9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B70772C2A5718FB00F184A9 /* AudioManager.swift */; }; 9B7077362A5728B900F184A9 /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9B7077352A5728B900F184A9 /* silence.mp3 */; }; 9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */; }; 9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */; }; + 9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */; }; + 9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28692C97748100C2B46C /* IP4Header.swift */; }; + 9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */; }; + 9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */; }; + 9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */; }; + 9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28722C988E9D00C2B46C /* Packet.swift */; }; + 9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28742C988EC400C2B46C /* TransportHeader.swift */; }; + 9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28772C98902900C2B46C /* PacketUtil.swift */; }; + 9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28792C989A7200C2B46C /* Connection.swift */; }; + 9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */; }; + 9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */; }; + 9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */; }; + 9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */; }; + 9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */; }; + 9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28892C98C82000C2B46C /* SocketIOService.swift */; }; + 9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */; }; B375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -100,6 +117,7 @@ 9B0912272A54593A001108B7 /* ProxyPin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProxyPin.entitlements; sourceTree = ""; }; 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureView.swift; sourceTree = ""; }; 9B2A10C72B4CBE32001C443F /* silience.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = silience.mov; sourceTree = ""; }; + 9B5125A92CAEE3350027996E /* ICMPPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMPPacket.swift; sourceTree = ""; }; 9B70772C2A5718FB00F184A9 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; 9B7077352A5728B900F184A9 /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; 9B90F57C2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; @@ -107,6 +125,22 @@ 9B90F5812C183CDE007D7A81 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 9B90F5832C183CE0007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureManager.swift; sourceTree = ""; }; + 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHandler.swift; sourceTree = ""; }; + 9BCA28692C97748100C2B46C /* IP4Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP4Header.swift; sourceTree = ""; }; + 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPHeader.swift; sourceTree = ""; }; + 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPPacketFactory.swift; sourceTree = ""; }; + 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = ""; }; + 9BCA28722C988E9D00C2B46C /* Packet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Packet.swift; sourceTree = ""; }; + 9BCA28742C988EC400C2B46C /* TransportHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportHeader.swift; sourceTree = ""; }; + 9BCA28772C98902900C2B46C /* PacketUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketUtil.swift; sourceTree = ""; }; + 9BCA28792C989A7200C2B46C /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; + 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseableConnection.swift; sourceTree = ""; }; + 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWProtocol.swift; sourceTree = ""; }; + 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientPacketWriter.swift; sourceTree = ""; }; + 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyVpnService.swift; sourceTree = ""; }; + 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = ""; }; + 9BCA28892C98C82000C2B46C /* SocketIOService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOService.swift; sourceTree = ""; }; + 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPHeader.swift; sourceTree = ""; }; D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -228,6 +262,7 @@ 9B0912232A54593A001108B7 /* ProxyPin */ = { isa = PBXGroup; children = ( + 9BCA28642C97729000C2B46C /* vpn */, 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */, 9B0912262A54593A001108B7 /* Info.plist */, 9B0912272A54593A001108B7 /* ProxyPin.entitlements */, @@ -245,6 +280,62 @@ path = pip; sourceTree = ""; }; + 9BCA28642C97729000C2B46C /* vpn */ = { + isa = PBXGroup; + children = ( + 9BCA287B2C989A8700C2B46C /* socket */, + 9BCA28762C98901800C2B46C /* utils */, + 9BCA28672C97746200C2B46C /* transport */, + 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */, + 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */, + 9BCA28792C989A7200C2B46C /* Connection.swift */, + 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */, + 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */, + 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */, + ); + path = vpn; + sourceTree = ""; + }; + 9BCA28672C97746200C2B46C /* transport */ = { + isa = PBXGroup; + children = ( + 9BCA28682C97747000C2B46C /* protocol */, + 9BCA28722C988E9D00C2B46C /* Packet.swift */, + ); + path = transport; + sourceTree = ""; + }; + 9BCA28682C97747000C2B46C /* protocol */ = { + isa = PBXGroup; + children = ( + 9BCA28692C97748100C2B46C /* IP4Header.swift */, + 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */, + 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */, + 9BCA28742C988EC400C2B46C /* TransportHeader.swift */, + 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */, + 9B5125A92CAEE3350027996E /* ICMPPacket.swift */, + ); + path = protocol; + sourceTree = ""; + }; + 9BCA28762C98901800C2B46C /* utils */ = { + isa = PBXGroup; + children = ( + 9BCA28772C98902900C2B46C /* PacketUtil.swift */, + ); + path = utils; + sourceTree = ""; + }; + 9BCA287B2C989A8700C2B46C /* socket */ = { + isa = PBXGroup; + children = ( + 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */, + 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */, + 9BCA28892C98C82000C2B46C /* SocketIOService.swift */, + ); + path = socket; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -504,7 +595,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */, + 9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */, + 9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */, + 9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */, + 9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */, + 9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */, 9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */, + 9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */, + 9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */, + 9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */, + 9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */, + 9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */, + 9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */, + 9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */, + 9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */, + 9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */, + 9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */, + 9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c0b7563..f6335cb 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -12,16 +12,19 @@ import NetworkExtension GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window.rootViewController as! FlutterViewController ; - let batteryChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger); - batteryChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in + let vpnChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger); + vpnChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in if ("stopVpn" == call.method) { VpnManager.shared.disconnect() } else if ("isRunning" == call.method){ result(Bool(VpnManager.shared.isRunning())) + } else if ("restartVpn" == call.method){ + let arguments = call.arguments as? Dictionary +// VpnManager.shared.disconnect() + VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool) } else { let arguments = call.arguments as? Dictionary -// self.backgroundAudioEnable = (arguments!["backgroundAudioEnable"]) as! Bool - VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int) + VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool) } }) diff --git a/ios/Runner/VpnManager.swift b/ios/Runner/VpnManager.swift index 17e6f24..872ef08 100755 --- a/ios/Runner/VpnManager.swift +++ b/ios/Runner/VpnManager.swift @@ -14,8 +14,9 @@ enum VPNStatus { class VpnManager{ var activeVPN: NETunnelProviderManager?; - public var proxyHost: String = "127.0.0.01" + public var proxyHost: String = "127.0.0.1" public var proxyPort: Int = 9099 + public var ipProxy: Bool = false static let shared = VpnManager() var observerAdded: Bool = false @@ -73,7 +74,6 @@ class VpnManager{ // load VPN Profiles extension VpnManager{ - fileprivate func createProviderManager() -> NETunnelProviderManager { let manager = NETunnelProviderManager() let conf = NETunnelProviderProtocol() @@ -96,6 +96,7 @@ extension VpnManager{ var conf = [String:AnyObject]() conf["proxyHost"] = self.proxyHost as AnyObject conf["proxyPort"] = self.proxyPort as AnyObject + conf["ipProxy"] = self.ipProxy as AnyObject let orignConf = manager.protocolConfiguration as! NETunnelProviderProtocol @@ -140,10 +141,11 @@ extension VpnManager{ // Actions extension VpnManager{ - func connect(host: String?, port: Int?){ + func connect(host: String?, port: Int?, ipProxy: Bool? = false) { self.proxyHost = host ?? self.proxyHost self.proxyPort = port ?? self.proxyPort - + self.ipProxy = ipProxy ?? false + self.loadAndCreatePrividerManager { (manager) in guard let manager = manager else{return} do{ @@ -157,7 +159,6 @@ extension VpnManager{ func disconnect() { if (activeVPN != nil) { - print("stopVPNTunnel activeVPN") activeVPN?.connection.stopVPNTunnel() activeVPN = nil return diff --git a/ios/Runner/pip/PictureInPictureManager.swift b/ios/Runner/pip/PictureInPictureManager.swift index 7529f4b..f493ef7 100644 --- a/ios/Runner/pip/PictureInPictureManager.swift +++ b/ios/Runner/pip/PictureInPictureManager.swift @@ -177,7 +177,7 @@ class PictureInPictureManager: NSObject,AVPictureInPictureControllerDelegate { VpnManager.shared.disconnect() playButton.setImage(UIImage(systemName: "play.fill"), for: .normal) } else { - VpnManager.shared.connect(host: nil, port: proxyPort) + VpnManager.shared.connect(host: nil, port: proxyPort, ipProxy: nil) playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal) } // pipView?.addData(text: "hello") diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2c3213a..4b73541 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -236,6 +236,7 @@ "connected": "Connected", "notConnected": "Not connected", "disconnect": "Disconnect", + "ipLayerProxy": "IP Layer Proxy(Beta)", "inputAddress": "Input Address", "syncConfig": "Sync configuration", "pullConfigFail": "Failed to pull configuration, please check the network connection", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9f5c55d..1837b43 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -237,6 +237,7 @@ "notConnected": "未连接", "inputAddress": "输入地址", "disconnect": "断开连接", + "ipLayerProxy": "IP层代理(Beta)", "syncConfig": "同步配置", "pullConfigFail": "拉取配置失败, 请检查网络连接", "sync": "同步", diff --git a/lib/native/vpn.dart b/lib/native/vpn.dart index d2b05f5..4e269f5 100644 --- a/lib/native/vpn.dart +++ b/lib/native/vpn.dart @@ -6,7 +6,7 @@ class Vpn { static bool isVpnStarted = false; //vpn是否已经启动 - static startVpn(String host, int port, Configuration configuration) { + static startVpn(String host, int port, Configuration configuration, {bool? ipProxy = false}) { List? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : []; List? disallowApps; @@ -14,8 +14,8 @@ class Vpn { disallowApps = configuration.appBlacklist ?? []; } - proxyVpnChannel.invokeMethod( - "startVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); + proxyVpnChannel.invokeMethod("startVpn", + {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps, "ipProxy": ipProxy}); isVpnStarted = true; } @@ -25,15 +25,15 @@ class Vpn { } //重启vpn - static restartVpn(String host, int port, Configuration configuration) { + static restartVpn(String host, int port, Configuration configuration, {bool ipProxy = false}) { List? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : []; List? disallowApps; if (appList.isEmpty) { disallowApps = configuration.appBlacklist ?? []; } - proxyVpnChannel.invokeMethod( - "restartVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); + proxyVpnChannel.invokeMethod("restartVpn", + {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps, "ipProxy": ipProxy}); isVpnStarted = true; } diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index f7382d1..b17d2dc 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -137,6 +137,15 @@ class ProxyServer { await stop().whenComplete(() => start()); } + Future retryBind() async { + //检测端口是否被占用 + try { + await Socket.connect('127.0.0.1', port, timeout: const Duration(milliseconds: 350)); + } catch (e) { + await restart(); + } + } + ///添加监听器 addListener(EventListener listener) { listeners.add(listener); diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index b287831..7735d75 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -103,14 +103,14 @@ class HttpClients { } /// 发送get请求 - static Future get(String url, {Duration duration = const Duration(seconds: 3)}) async { + static Future get(String url, {Duration timeout = const Duration(seconds: 3)}) async { HttpRequest msg = HttpRequest(HttpMethod.get, url); - return request(HostAndPort.of(url), msg, duration: duration); + return request(HostAndPort.of(url), msg, timeout: timeout); } /// 发送请求 static Future request(HostAndPort hostAndPort, HttpRequest request, - {Duration duration = const Duration(seconds: 3)}) async { + {Duration timeout = const Duration(seconds: 3)}) async { var httpResponseHandler = HttpResponseHandler(); var client = Client() @@ -120,7 +120,7 @@ class HttpClients { Channel channel = await client.connect(hostAndPort, channelContext); await channel.write(request); - return httpResponseHandler.getResponse(duration).whenComplete(() => channel.close()); + return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close()); } /// 发送代理请求 diff --git a/lib/network/network.dart b/lib/network/network.dart index 5f16cbc..846f0d1 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -167,6 +167,7 @@ class Server extends Network { var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data); channel.serverSecureSocket(secureSocket, channelContext); } catch (error, trace) { + print('$hostAndPort ssl error: $error'); try { channelContext.processInfo ??= await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown'); @@ -181,14 +182,15 @@ class Server extends Network { } class Client extends Network { - Future connect(HostAndPort hostAndPort, ChannelContext channelContext) async { + Future connect(HostAndPort hostAndPort, ChannelContext channelContext, + {Duration timeout = const Duration(seconds: 3)}) async { String host = hostAndPort.host; //说明支持ipv6 if (host.startsWith("[") && host.endsWith(']')) { host = host.substring(host.lastIndexOf(":") + 1, host.length - 1); } - return Socket.connect(host, hostAndPort.port, timeout: const Duration(seconds: 3)).then((socket) { + return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) { if (socket.address.type != InternetAddressType.unix) { socket.setOption(SocketOption.tcpNoDelay, true); } diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index 6c955f6..c692cba 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -56,7 +56,7 @@ class _SettingState extends State { return MenuAnchor( builder: (context, controller, child) { return IconButton( - icon: const Icon(Icons.settings), + icon: const Icon(Icons.settings, size: 22), tooltip: localizations.setting, onPressed: () { if (controller.isOpen) { diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index a152749..e3b45b2 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -29,7 +29,7 @@ class _SslState extends State { return MenuAnchor( builder: (context, controller, child) { return IconButton( - icon: Icon(Icons.https, color: widget.proxyServer.enableSsl ? null : Colors.red), + icon: Icon(Icons.https, color: widget.proxyServer.enableSsl ? null : Colors.red, size: 22), tooltip: localizations.httpsProxy, onPressed: () { if (controller.isOpen) { diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index 984ee43..f000bde 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -85,21 +85,21 @@ class _ToolbarState extends State { children: [ Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 80 : 30)), SocketLaunch(proxyServer: widget.proxyServer, startup: widget.proxyServer.configuration.startup), - const Padding(padding: EdgeInsets.only(left: 20)), + const Padding(padding: EdgeInsets.only(left: 18)), IconButton( tooltip: localizations.clear, - icon: const Icon(Icons.cleaning_services_outlined), + icon: const Icon(Icons.cleaning_services_outlined, size: 22), onPressed: () { widget.requestListStateKey.currentState?.clean(); }), - const Padding(padding: EdgeInsets.only(left: 20)), + const Padding(padding: EdgeInsets.only(left: 18)), SslWidget(proxyServer: widget.proxyServer), // SSL配置 - const Padding(padding: EdgeInsets.only(left: 20)), + const Padding(padding: EdgeInsets.only(left: 18)), Setting(proxyServer: widget.proxyServer), // 设置 - const Padding(padding: EdgeInsets.only(left: 20)), + const Padding(padding: EdgeInsets.only(left: 18)), IconButton( tooltip: localizations.mobileConnect, - icon: const Icon(Icons.phone_iphone), + icon: const Icon(Icons.phone_iphone, size: 22), onPressed: () async { final ips = await localIps(); phoneConnect(ips, widget.proxyServer.port); diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index f34bf7e..19747d0 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -160,7 +160,7 @@ class MobileHomeState extends State implements EventListener, Li return; } - if (DateTime.now().millisecondsSinceEpoch - exitTime > 2000) { + if (DateTime.now().millisecondsSinceEpoch - exitTime > 1500) { exitTime = DateTime.now().millisecondsSinceEpoch; if (mounted) { FlutterToastr.show(localizations.appExitTips, this.context, @@ -371,13 +371,18 @@ class RequestPageState extends State { startup: proxyServer.configuration.startup, serverLaunch: false, onStart: () async { - //ios端口可能会被系统杀掉 + String host = Platform.isAndroid ? await localIp(readCache: false) : "127.0.0.1"; + int port = proxyServer.port; if (Platform.isIOS) { - await proxyServer.restart(); + await proxyServer.retryBind(); } - Vpn.startVpn(Platform.isAndroid ? await localIp(readCache: false) : "127.0.0.1", proxyServer.port, - proxyServer.configuration); + if (remoteDevice.value.ipProxy == true) { + host = remoteDevice.value.host!; + port = remoteDevice.value.port!; + } + + Vpn.startVpn(host, port, proxyServer.configuration, ipProxy: remoteDevice.value.ipProxy); }, onStop: () => Vpn.stopVpn())), ); diff --git a/lib/ui/mobile/setting/proxy.dart b/lib/ui/mobile/setting/proxy.dart index 13df80c..753ed0e 100644 --- a/lib/ui/mobile/setting/proxy.dart +++ b/lib/ui/mobile/setting/proxy.dart @@ -37,7 +37,6 @@ class _ExternalProxyDialogState extends State { Widget build(BuildContext context) { bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); - return AlertDialog( scrollable: true, title: Text(localizations.externalProxy, style: const TextStyle(fontSize: 15)), diff --git a/lib/ui/mobile/widgets/remote_device.dart b/lib/ui/mobile/widgets/remote_device.dart index 600c353..5665042 100644 --- a/lib/ui/mobile/widgets/remote_device.dart +++ b/lib/ui/mobile/widgets/remote_device.dart @@ -47,6 +47,7 @@ class RemoteModel { final int? port; final String? os; final String? hostname; + final bool? ipProxy; RemoteModel({ required this.connect, @@ -54,6 +55,7 @@ class RemoteModel { this.port, this.os, this.hostname, + this.ipProxy, }); factory RemoteModel.fromJson(Map json) { @@ -63,9 +65,35 @@ class RemoteModel { port: json['port'], os: json['os'], hostname: json['hostname'], + ipProxy: json['ipProxy'], ); } + RemoteModel copyWith({ + bool? connect, + String? host, + int? port, + String? os, + String? hostname, + bool? ipProxy, + }) { + return RemoteModel( + connect: connect ?? this.connect, + host: host ?? this.host, + port: port ?? this.port, + os: os ?? this.os, + hostname: hostname ?? this.hostname, + ipProxy: ipProxy ?? this.ipProxy, + ); + } + + String get identification => '$host:$port'; + + //host和端口是否相等 + bool equals(RemoteModel remoteModel) { + return identification == remoteModel.identification; + } + Map toJson() { return { 'connect': connect, @@ -73,6 +101,7 @@ class RemoteModel { 'port': port, 'os': os, 'hostname': hostname, + 'ipProxy': ipProxy, }; } } @@ -110,13 +139,16 @@ class _RemoteDevicePageState extends State { dense: true, title: Text(localizations.scanCode), onTap: () => connectRemote(context))), - // CustomPopupMenuItem( - // height: 32, - // child: ListTile( - // leading: const Icon(Icons.edit_rounded), - // dense: true, - // title: Text(localizations.inputAddress), - // onTap: () {})), + CustomPopupMenuItem( + height: 32, + child: ListTile( + leading: const Icon(Icons.edit_rounded), + dense: true, + title: Text(localizations.inputAddress), + onTap: () async { + Navigator.maybePop(context); + inputAddress(await localIp()); + })), PopupMenuItem( height: 32, child: ListTile( @@ -154,20 +186,28 @@ class _RemoteDevicePageState extends State { } Widget rows(SharedPreferences prefs) { - var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? []; + var remoteDeviceList = getRemoteDeviceList(prefs); return ListView( - children: remoteDeviceList.map((it) { - var remoteDevice = RemoteModel.fromJson(jsonDecode(it)); - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 5), - title: Text(remoteDevice.hostname ?? ''), - subtitle: Text('${remoteDevice.host}:${remoteDevice.port}'), - trailing: getIcon(remoteDevice.os!), - onTap: () { - doConnect(remoteDevice.host!, remoteDevice.port!); - }, - ); + children: remoteDeviceList.map((remoteDevice) { + return Dismissible( + key: Key(remoteDevice.identification), + onDismissed: (direction) async { + remoteDeviceList.removeWhere((it) => it.equals(remoteDevice)); + await setRemoteDeviceList(prefs, remoteDeviceList); + + setState(() {}); + if (mounted) FlutterToastr.show(localizations.deleteSuccess, context); + }, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 5), + title: Text(remoteDevice.hostname ?? ''), + subtitle: Text('${remoteDevice.host}:${remoteDevice.port}'), + trailing: getIcon(remoteDevice.os!), + onTap: () { + doConnect(remoteDevice.host!, remoteDevice.port!, ipProxy: remoteDevice.ipProxy); + }, + )); }).toList(), ); } @@ -186,6 +226,16 @@ class _RemoteDevicePageState extends State { } } + List getRemoteDeviceList(SharedPreferences prefs) { + var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? []; + return remoteDeviceList.map((it) => RemoteModel.fromJson(jsonDecode(it))).toList(); + } + + Future setRemoteDeviceList(SharedPreferences prefs, Iterable remoteDeviceList) { + var list = remoteDeviceList.map((it) => jsonEncode(it.toJson())).toList(); + return prefs.setStringList('remoteDeviceList', list); + } + ///远程设备状态 Widget remoteDeviceStatus() { if (widget.remoteDevice.value.connect) { @@ -193,6 +243,32 @@ class _RemoteDevicePageState extends State { child: Column( children: [ const Icon(Icons.check_circle_outline_outlined, size: 55, color: Colors.green), + Row( + children: [ + if (Platform.isIOS) Expanded(child: ListTile(title: Text(localizations.ipLayerProxy))), + const SizedBox(height: 6), + SwitchWidget( + value: widget.remoteDevice.value.ipProxy ?? false, + scale: 0.85, + onChanged: (val) async { + widget.remoteDevice.value = widget.remoteDevice.value.copyWith(ipProxy: val); + SharedPreferences.getInstance().then((prefs) { + var remoteDeviceList = getRemoteDeviceList(prefs); + remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value)); + remoteDeviceList.insert(0, widget.remoteDevice.value); + + setRemoteDeviceList(prefs, remoteDeviceList); + }); + + if ((await Vpn.isRunning())) { + print('重启VPN'); + Vpn.restartVpn(widget.remoteDevice.value.host!, widget.remoteDevice.value.port!, + widget.proxyServer.configuration, + ipProxy: val); + } + }), + ], + ), Text('${localizations.connected}:${widget.remoteDevice.value.hostname}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 6), @@ -230,6 +306,59 @@ class _RemoteDevicePageState extends State { ])); } + ///输入地址链接 + inputAddress(var host) { + //输入账号密码连接 + host = host.substring(0, host.contains('.') ? host.lastIndexOf('.') + 1 : host.length); + int? port = 9099; + if (!context.mounted) return; + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(localizations.inputAddress), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + initialValue: host, + decoration: const InputDecoration(hintText: 'Host'), + keyboardType: TextInputType.url, + onChanged: (value) => host = value, + ), + TextFormField( + initialValue: port.toString(), + decoration: const InputDecoration(hintText: 'Port'), + keyboardType: TextInputType.number, + onChanged: (value) { + port = value.isEmpty ? null : int.tryParse(value); + }), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(localizations.cancel)), + TextButton( + onPressed: () async { + if (host.isEmpty || port == null) { + FlutterToastr.show(localizations.cannotBeEmpty, context); + return; + } + + if ((await doConnect(host, port!)) && context.mounted) { + Navigator.pop(context); + } + }, + child: Text(localizations.connected)), + ], + ); + }); + } + ///扫码连接 connectRemote(BuildContext context) async { AppLocalizations localizations = AppLocalizations.of(context)!; @@ -260,34 +389,39 @@ class _RemoteDevicePageState extends State { } } - doConnect(String host, int port) async { + /// + Future doConnect(String host, int port, {bool? ipProxy}) async { try { - var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 3)); + var response = await HttpClients.get("http://$host:$port/ping", timeout: const Duration(milliseconds: 3000)); if (response.bodyAsString == "pong") { widget.remoteDevice.value = RemoteModel( - connect: true, - host: host, - port: port, - os: response.headers.get("os"), - hostname: response.headers.get("hostname")); + connect: true, + host: host, + port: port, + os: response.headers.get("os"), + hostname: response.headers.get("hostname"), + ipProxy: ipProxy, + ); //去重记录5条连接记录 SharedPreferences prefs = await SharedPreferences.getInstance(); - var remoteDeviceList = prefs.getStringList('remoteDeviceList') ?? []; - var value = jsonEncode(widget.remoteDevice.value.toJson()); - remoteDeviceList.remove(value); - remoteDeviceList.insert(0, value); - prefs.setStringList('remoteDeviceList', remoteDeviceList).then((value) { + var remoteDeviceList = getRemoteDeviceList(prefs); + remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value)); + remoteDeviceList.insert(0, widget.remoteDevice.value); + + var list = remoteDeviceList.take(5); + setRemoteDeviceList(prefs, list).whenComplete(() { setState(() {}); }); - if (mounted && Navigator.canPop(context)) { + if (mounted) { FlutterToastr.show( "${localizations.connectSuccess}${Vpn.isVpnStarted ? '' : ', ${localizations.remoteConnectSuccessTips}'}", context, duration: 3); } } + return true; } catch (e) { logger.e(e); if (mounted) { @@ -297,6 +431,7 @@ class _RemoteDevicePageState extends State { return AlertDialog(content: Text(localizations.remoteConnectFail)); }); } + return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index 4c0176b..bb0a666 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: network_proxy description: ProxyPin publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.1.3+13 +version: 1.1.4+14 environment: sdk: '>=3.0.2 <4.0.0' @@ -24,7 +24,7 @@ dependencies: url: https://gitee.com/wanghongenpin/flutter-plugins.git path: packages/desktop_multi_window path_provider: ^2.1.4 - url_launcher: ^6.3.0 + url_launcher: ^6.3.1 proxy_manager: ^0.0.3 qr_flutter: ^4.1.0 easy_permission: ^1.0.0