import Foundation import Network import os private let lnlLog = Logger(subsystem: "dev.smoll.KDChat", category: "LiteNetLib") // MARK: - Constants private enum LNL { static let protocolId: Int32 = 13 static let maxSequence: UInt16 = 32768 static let halfMaxSequence: Int = 16384 static let windowSize: Int = 64 static let channeledHeaderSize: Int = 4 static let reliableOrderedChannelId: UInt8 = 2 // channel 0, ReliableOrdered (0*4+2) static let pingIntervalMs: Int = 1000 static let disconnectTimeoutMs: Int = 5000 static let resendBaseMs: Double = 25 static let resendRttMultiplier: Double = 2.1 static let updateIntervalMs: Int = 50 // .NET epoch offset: ticks between 0001-01-01 and 1970-01-01 static let dotNetEpochOffset: Double = 62_135_596_800 static var dotNetTicks: Int64 { Int64((Date.now.timeIntervalSince1970 + dotNetEpochOffset) * 10_000_000) } } // MARK: - Packet Property private enum PacketProperty: UInt8 { case unreliable = 0 case channeled = 1 case ack = 2 case ping = 3 case pong = 4 case connectRequest = 5 case connectAccept = 6 case disconnect = 7 case unconnectedMessage = 8 case mtuCheck = 9 case mtuOk = 10 case broadcast = 11 case merged = 12 case shutdownOk = 13 case peerNotFound = 14 case invalidProtocol = 15 case natMessage = 16 case empty = 17 } // MARK: - Connection State private enum ConnectionState { case disconnected case connecting case connected case shutdownRequested } // MARK: - Pending Packet private struct PendingPacket { var data: Data var timestamp: UInt64 var isSent: Bool } // MARK: - Fragment Reassembly private struct IncomingFragment { let totalParts: Int let channelId: UInt8 var parts: [Data?] var receivedCount: Int init(totalParts: Int, channelId: UInt8) { self.totalParts = totalParts self.channelId = channelId self.parts = Array(repeating: nil, count: totalParts) self.receivedCount = 0 } } // MARK: - Client Event extension LNLClient { enum Event: Sendable { case connected case disconnected(String?) case received(Data) } } // MARK: - LNLClient /// Minimal LiteNetLib client supporting ReliableOrdered delivery over UDP. actor LNLClient { // NWConnection requires a DispatchQueue for its callback-based API private let nwQueue = DispatchQueue(label: "com.kdchat.litenetlib.nw") private var connection: NWConnection? private var updateTask: Task? // Event stream for consumers let events: AsyncStream private let eventContinuation: AsyncStream.Continuation // Connection state private var state: ConnectionState = .disconnected private var connectionTime: Int64 = 0 private var connectionNumber: UInt8 = 0 private var remotePeerId: Int32 = 0 private var localPeerId: Int32 = Int32.random(in: 1...Int32.max) private var connectKey: String = "" private var connectAttempts: Int = 0 private let maxConnectAttempts: Int = 10 private var lastConnectSendTime: UInt64 = 0 // Reliable ordered channel - send side private var localSequence: UInt16 = 0 private var localWindowStart: UInt16 = 0 private var pendingPackets: [PendingPacket?] private var outgoingQueue: [Data] = [] // Reliable ordered channel - receive side private var remoteSequence: UInt16 = 0 private var remoteWindowStart: UInt16 = 0 private var receivedPackets: [Data?] private var ackBitfield: [UInt8] private var mustSendAcks: Bool = false // Fragment reassembly private var incomingFragments: [UInt16: IncomingFragment] = [:] // Ping / keepalive private var pingSequence: UInt16 = 0 private var lastPingSentTime: UInt64 = 0 private var lastPacketReceivedTime: UInt64 = 0 private var avgRtt: Double = 50 private var rttSum: Double = 0 private var rttCount: Int = 0 private var pingSentTimestamp: UInt64 = 0 // Resend delay (computed from RTT) private var resendDelay: Double { LNL.resendBaseMs + avgRtt * LNL.resendRttMultiplier } // Send stats private var totalBytesSent: Int = 0 private var totalPacketsSent: Int = 0 init() { let ws = LNL.windowSize self.pendingPackets = Array(repeating: nil, count: ws) self.receivedPackets = Array(repeating: nil, count: ws) let ackSize = (ws - 1) / 8 + 2 self.ackBitfield = Array(repeating: 0, count: ackSize) let (stream, continuation) = AsyncStream.makeStream(of: Event.self) self.events = stream self.eventContinuation = continuation } deinit { eventContinuation.finish() } // MARK: - Public API func connect(host: String, port: UInt16, key: String) { guard state == .disconnected else { lnlLog.warning("connect() called but state is not disconnected") return } lnlLog.info("Connecting to \(host):\(port) with key '\(key)'") connectKey = key connectionTime = LNL.dotNetTicks connectAttempts = 0 state = .connecting let nwHost = NWEndpoint.Host(host) guard let nwPort = NWEndpoint.Port(rawValue: port) else { lnlLog.error("Invalid port: \(port)") return } let conn = NWConnection(host: nwHost, port: nwPort, using: .udp) connection = conn conn.stateUpdateHandler = { [weak self] newState in Task { [weak self] in await self?.handleNWState(newState) } } conn.pathUpdateHandler = { path in lnlLog.info("Path status: \(String(describing: path.status)), isExpensive: \(path.isExpensive), isConstrained: \(path.isConstrained)") } conn.start(queue: nwQueue) } func send(data: Data) { guard state == .connected else { return } outgoingQueue.append(data) } func disconnect() { performDisconnect(reason: "Client disconnected") } // MARK: - NWConnection State private func handleNWState(_ newState: NWConnection.State) { lnlLog.info("NWConnection state: \(String(describing: newState))") switch newState { case .ready: if let path = connection?.currentPath { lnlLog.info("UDP socket ready. Local: \(String(describing: path.localEndpoint)), remote: \(String(describing: path.remoteEndpoint))") for iface in path.availableInterfaces { lnlLog.info("Interface: \(iface.name) type: \(String(describing: iface.type))") } } startReceiving() startUpdateLoop() sendConnectRequest() case .failed(let error): lnlLog.error("NWConnection failed: \(error.localizedDescription)") performDisconnect(reason: "Connection failed: \(error.localizedDescription)") case .cancelled: lnlLog.info("NWConnection cancelled") performDisconnect(reason: nil) default: break } } // MARK: - Receive Loop private func startReceiving() { lnlLog.debug("Waiting for incoming UDP data...") connection?.receiveMessage { [weak self] data, _, _, error in Task { [weak self] in await self?.handleReceiveCallback(data: data, error: error) } } } private func handleReceiveCallback(data: Data?, error: NWError?) { if let error { lnlLog.error("Receive error: \(error.localizedDescription)") } if let data, !data.isEmpty { lnlLog.info("Received \(data.count) bytes: \(data.prefix(20).map { String(format: "%02x", $0) }.joined(separator: " "))\(data.count > 20 ? "..." : "")") lastPacketReceivedTime = currentMs() handleRawPacket(data) } else if data == nil && error == nil { lnlLog.warning("receiveMessage returned nil data with no error") } if error == nil && state != .disconnected { startReceiving() } else if state == .disconnected { lnlLog.info("Not re-registering receive (disconnected)") } } // MARK: - Update Loop private func startUpdateLoop() { updateTask = Task { [weak self] in while !Task.isCancelled { await self?.update() try? await Task.sleep(for: .milliseconds(LNL.updateIntervalMs)) } } } private func update() { let now = currentMs() switch state { case .connecting: if now >= lastConnectSendTime, now - lastConnectSendTime > 500 { connectAttempts += 1 lnlLog.info("ConnectRequest attempt \(self.connectAttempts)/\(self.maxConnectAttempts)") if connectAttempts > maxConnectAttempts { lnlLog.error("Connection timed out after \(self.maxConnectAttempts) attempts") performDisconnect(reason: "Connection timed out") return } sendConnectRequest() } case .connected: if now >= lastPacketReceivedTime, now - lastPacketReceivedTime > UInt64(LNL.disconnectTimeoutMs) { performDisconnect(reason: "Connection timed out") return } flushOutgoingQueue() retransmitPending(now: now) if mustSendAcks { sendAck() mustSendAcks = false } if now >= lastPingSentTime, now - lastPingSentTime > UInt64(LNL.pingIntervalMs) { sendPing() } case .shutdownRequested, .disconnected: break } } // MARK: - Packet Handling private func handleRawPacket(_ data: Data) { guard !data.isEmpty else { return } let byte0 = data[data.startIndex] let property = byte0 & 0x1f let connNum = (byte0 >> 5) & 0x03 guard let prop = PacketProperty(rawValue: property) else { lnlLog.debug("Unknown packet property: \(property)") return } lnlLog.debug("Received packet: \(String(describing: prop)), \(data.count) bytes") if state == .connected && prop != .connectAccept && prop != .connectRequest { if connNum != connectionNumber { return } } switch prop { case .connectAccept: handleConnectAccept(data) case .channeled: handleChanneled(data) case .ack: handleAck(data) case .ping: handlePing(data) case .pong: handlePong(data) case .disconnect: handleServerDisconnect(data) case .merged: handleMerged(data) case .shutdownOk: performDisconnect(reason: nil) default: break } } // MARK: - Connect Handshake private func sendConnectRequest() { lnlLog.debug("Sending ConnectRequest") var w = DataWriter() w.writeByte(PacketProperty.connectRequest.rawValue) w.writeInt32LE(LNL.protocolId) w.writeInt64LE(connectionTime) w.writeInt32LE(localPeerId) // Dummy IPv4 sockaddr (16 bytes, .NET SocketAddress format) w.writeByte(16) var addr = Data(count: 16) addr[0] = 2 // AF_INET w.writeData(addr) // Connection key: ushort16 LE (byteCount + 1) + UTF-8 let keyBytes = Array(connectKey.utf8) w.writeUInt16LE(UInt16(keyBytes.count + 1)) w.writeData(Data(keyBytes)) lastConnectSendTime = currentMs() let packet = w.data lnlLog.info("ConnectRequest packet (\(packet.count) bytes): \(packet.map { String(format: "%02x", $0) }.joined(separator: " "))") sendRaw(packet) } private func handleConnectAccept(_ data: Data) { guard state == .connecting else { lnlLog.warning("ConnectAccept received but not in connecting state") return } guard data.count >= 15 else { lnlLog.error("ConnectAccept too short: \(data.count) bytes") return } var r = DataReader(data: data, offset: 1) guard let echoedTime = r.readInt64LE(), echoedTime == connectionTime else { lnlLog.error("ConnectAccept connectionTime mismatch") return } connectionNumber = r.readByte() ?? 0 _ = r.readByte() // isReused remotePeerId = r.readInt32LE() ?? 0 lnlLog.info("Connected! connNum=\(self.connectionNumber), remotePeerId=\(self.remotePeerId)") state = .connected lastPacketReceivedTime = currentMs() lastPingSentTime = currentMs() eventContinuation.yield(.connected) } // MARK: - Channeled (ReliableOrdered) private func handleChanneled(_ data: Data) { guard data.count >= LNL.channeledHeaderSize else { return } var r = DataReader(data: data, offset: 1) guard let seq = r.readUInt16LE() else { return } guard let channelId = r.readByte() else { return } guard channelId == LNL.reliableOrderedChannelId else { return } // Pass the full packet (including fragment header if present) through // the reliable ordered channel for sequencing and ACKs. // Delivery/reassembly happens in deliverOrderedPacket. let fullPayload = Data(data[data.startIndex ..< data.endIndex]) processReliableOrdered(sequence: seq, data: fullPayload) } /// Called when a reliable ordered packet is delivered in-sequence. /// Handles fragment reassembly before yielding to the consumer. private func deliverOrderedPacket(_ data: Data) { guard data.count >= LNL.channeledHeaderSize else { return } let isFragmented = (data[data.startIndex] & 0x80) != 0 if isFragmented { guard data.count >= 10 else { return } var r = DataReader(data: data, offset: 4) // skip channeled header guard let fragmentId = r.readUInt16LE() else { return } guard let fragmentPart = r.readUInt16LE() else { return } guard let fragmentsTotal = r.readUInt16LE() else { return } guard fragmentsTotal > 0, fragmentPart < fragmentsTotal else { return } let channelId = data[data.startIndex + 3] let fragmentData = Data(data[data.startIndex + 10 ..< data.endIndex]) if incomingFragments[fragmentId] == nil { incomingFragments[fragmentId] = IncomingFragment( totalParts: Int(fragmentsTotal), channelId: channelId ) } guard var fragment = incomingFragments[fragmentId], fragment.channelId == channelId, Int(fragmentPart) < fragment.totalParts, fragment.parts[Int(fragmentPart)] == nil else { return } fragment.parts[Int(fragmentPart)] = fragmentData fragment.receivedCount += 1 incomingFragments[fragmentId] = fragment if fragment.receivedCount == fragment.totalParts { var assembled = Data() for part in fragment.parts { if let part { assembled.append(part) } } incomingFragments.removeValue(forKey: fragmentId) lnlLog.info("Reassembled fragmented packet: \(fragment.totalParts) parts, \(assembled.count) bytes") eventContinuation.yield(.received(assembled)) } } else { let userData = Data(data[data.startIndex + LNL.channeledHeaderSize ..< data.endIndex]) eventContinuation.yield(.received(userData)) } } private func processReliableOrdered(sequence seq: UInt16, data: Data) { // Reject already-delivered sequences let relateToDelivered = relativeSequence(seq, to: remoteSequence) if relateToDelivered < 0 { return } let relate = relativeSequence(seq, to: remoteWindowStart) if relate >= LNL.windowSize { let diff = relate - LNL.windowSize + 1 for _ in 0..= LNL.windowSize { break } let userData = outgoingQueue.removeFirst() let packet = buildChanneledPacket(sequence: localSequence, data: userData) let idx = Int(localSequence) % LNL.windowSize pendingPackets[idx] = PendingPacket(data: packet, timestamp: currentMs(), isSent: true) localSequence = (localSequence &+ 1) % LNL.maxSequence sendRaw(packet) } } private func retransmitPending(now: UInt64) { var seq = localWindowStart while seq != localSequence { let idx = Int(seq) % LNL.windowSize if let pending = pendingPackets[idx] { if now >= pending.timestamp, Double(now - pending.timestamp) >= resendDelay { pendingPackets[idx]?.timestamp = now sendRaw(pending.data) } } seq = (seq &+ 1) % LNL.maxSequence } } private func buildChanneledPacket(sequence: UInt16, data: Data) -> Data { var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.channeled.rawValue) w.writeUInt16LE(sequence) w.writeByte(LNL.reliableOrderedChannelId) w.writeData(data) return w.data } // MARK: - ACK private func sendAck() { var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.ack.rawValue) w.writeUInt16LE(remoteWindowStart) w.writeByte(LNL.reliableOrderedChannelId) w.writeData(Data(ackBitfield)) sendRaw(w.data) } private func handleAck(_ data: Data) { guard data.count >= LNL.channeledHeaderSize else { return } var r = DataReader(data: data, offset: 1) guard let ackWindowStart = r.readUInt16LE() else { return } guard let channelId = r.readByte(), channelId == LNL.reliableOrderedChannelId else { return } let bitfieldStart = data.startIndex + LNL.channeledHeaderSize let bitfieldData = data[bitfieldStart...] var seq = localWindowStart while seq != localSequence { let relToAckStart = relativeSequence(seq, to: ackWindowStart) if relToAckStart < 0 { break } if relToAckStart >= LNL.windowSize { break } let byteIdx = relToAckStart / 8 let bitIdx = relToAckStart % 8 if byteIdx < bitfieldData.count { let acked = (bitfieldData[bitfieldStart + byteIdx] & (1 << bitIdx)) != 0 if acked { let idx = Int(seq) % LNL.windowSize pendingPackets[idx] = nil if seq == localWindowStart { localWindowStart = (localWindowStart &+ 1) % LNL.maxSequence } } } seq = (seq &+ 1) % LNL.maxSequence } while localWindowStart != localSequence { let idx = Int(localWindowStart) % LNL.windowSize if pendingPackets[idx] != nil { break } localWindowStart = (localWindowStart &+ 1) % LNL.maxSequence } } // MARK: - Ping / Pong private func sendPing() { var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.ping.rawValue) w.writeUInt16LE(pingSequence) pingSequence = (pingSequence &+ 1) % LNL.maxSequence pingSentTimestamp = currentMs() lastPingSentTime = pingSentTimestamp sendRaw(w.data) } private func handlePing(_ data: Data) { guard data.count >= 3 else { return } var r = DataReader(data: data, offset: 1) guard let seq = r.readUInt16LE() else { return } var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.pong.rawValue) w.writeUInt16LE(seq) w.writeInt64LE(LNL.dotNetTicks) sendRaw(w.data) } private func handlePong(_ data: Data) { guard data.count >= 3 else { return } let now = currentMs() guard now >= pingSentTimestamp else { return } let elapsed = Double(now - pingSentTimestamp) rttSum += elapsed rttCount += 1 avgRtt = rttSum / Double(rttCount) if rttCount > 10 { rttSum = avgRtt rttCount = 1 } } // MARK: - Disconnect private func handleServerDisconnect(_ data: Data) { guard data.count >= 9 else { return } var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.shutdownOk.rawValue) sendRaw(w.data) performDisconnect(reason: "Server disconnected") } private func handleMerged(_ data: Data) { var offset = 1 while offset + 2 <= data.count { let sizeBytes = data[data.startIndex + offset ..< data.startIndex + offset + 2] let size = Int(UInt16(littleEndian: sizeBytes.withUnsafeBytes { $0.loadUnaligned(as: UInt16.self) })) offset += 2 guard offset + size <= data.count else { break } let subPacket = Data(data[data.startIndex + offset ..< data.startIndex + offset + size]) offset += size handleRawPacket(subPacket) } } private func performDisconnect(reason: String?) { guard state != .disconnected else { return } lnlLog.info("Disconnecting: \(reason ?? "no reason")") if state == .connected { var w = DataWriter() w.writeByte((connectionNumber << 5) | PacketProperty.disconnect.rawValue) w.writeInt64LE(connectionTime) sendRaw(w.data) } state = .disconnected updateTask?.cancel() updateTask = nil connection?.cancel() connection = nil // Reset channel state localSequence = 0 localWindowStart = 0 remoteSequence = 0 remoteWindowStart = 0 outgoingQueue.removeAll() for i in 0.. UInt64 { UInt64(DispatchTime.now().uptimeNanoseconds / 1_000_000) } private func relativeSequence(_ number: UInt16, to expected: UInt16) -> Int { let n = Int(number) let e = Int(expected) let max = Int(LNL.maxSequence) return (n - e + max + LNL.halfMaxSequence) % max - LNL.halfMaxSequence } } // MARK: - Binary Helpers private struct DataWriter { var data = Data() mutating func writeByte(_ byte: UInt8) { data.append(byte) } mutating func writeUInt16LE(_ value: UInt16) { withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) } } mutating func writeInt32LE(_ value: Int32) { withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) } } mutating func writeInt64LE(_ value: Int64) { withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) } } mutating func writeData(_ d: Data) { data.append(d) } } private struct DataReader { let data: Data var offset: Int mutating func readByte() -> UInt8? { let idx = data.startIndex + offset guard idx < data.endIndex else { return nil } offset += 1 return data[idx] } mutating func readUInt16LE() -> UInt16? { let start = data.startIndex + offset guard start + 2 <= data.endIndex else { return nil } offset += 2 return data[start.. Int32? { let start = data.startIndex + offset guard start + 4 <= data.endIndex else { return nil } offset += 4 return data[start.. Int64? { let start = data.startIndex + offset guard start + 8 <= data.endIndex else { return nil } offset += 8 return data[start..