montana/iOS/Apps/Montana/Montana/Network/MontanaConnection.swift

205 lines
7.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MontanaConnection.swift
// Montana
//
// iOS Network transport через Apple Network.framework.
// Spec section "Сетевой уровень Connection lifecycle" Step 1-6:
// TCP TLS 1.3 IBT proof ProtocolMessage envelope exchange
//
// Note: Apple Network.framework предоставляет TLS 1.3 native
// (без libp2p / Noise). IBT proof application-layer custom protocol
// поверх TLS 1.3.
//
import Foundation
import Network
import os
@available(iOS 13.0, macOS 10.15, *)
public final class MontanaConnection {
public enum State {
case idle
case tcpConnecting
case tlsHandshake
case ibtHandshake
case ready
case failed(Error)
}
public enum ConnectionError: Error {
case connectionFailed(String)
case ibtFailed(String)
case sendFailed(String)
case receiveFailed(String)
case envelopeDecode(WireDecodeError)
}
private let logger = Logger(subsystem: "network.montana.junona", category: "MontanaConnection")
private let serverNodeId: Data
private let host: NWEndpoint.Host
private let port: NWEndpoint.Port
private var connection: NWConnection?
private(set) public var state: State = .idle
public init(host: String, port: UInt16, serverNodeId: Data) {
precondition(serverNodeId.count == 32, "serverNodeId must be 32 bytes")
self.host = NWEndpoint.Host(host)
self.port = NWEndpoint.Port(integerLiteral: port)
self.serverNodeId = serverNodeId
}
public func connect(
clientSecretKey: Data,
currentWindow: UInt64,
completion: @escaping (Result<Void, ConnectionError>) -> Void
) {
let tlsOptions = NWProtocolTLS.Options()
// Spec section "Обфускация транспорта Шифрование": TLS 1.3
// на :443. Apple Network.framework обеспечивает TLS 1.3 при
// available system support (iOS 12.2+).
sec_protocol_options_set_min_tls_protocol_version(
tlsOptions.securityProtocolOptions,
.TLSv13
)
let params = NWParameters(tls: tlsOptions, tcp: NWProtocolTCP.Options())
let conn = NWConnection(host: host, port: port, using: params)
connection = conn
state = .tcpConnecting
conn.stateUpdateHandler = { [weak self] newState in
guard let self = self else { return }
switch newState {
case .ready:
self.state = .ibtHandshake
self.logger.info("[MontanaConnection] TCP+TLS ready, sending IBT proof")
self.sendIbtProof(
secretKey: clientSecretKey,
currentWindow: currentWindow,
completion: completion
)
case .failed(let err):
self.state = .failed(err)
self.logger.error("[MontanaConnection] failed: \(err.localizedDescription)")
completion(.failure(.connectionFailed(err.localizedDescription)))
default:
break
}
}
conn.start(queue: .global(qos: .userInitiated))
}
private func sendIbtProof(
secretKey: Data,
currentWindow: UInt64,
completion: @escaping (Result<Void, ConnectionError>) -> Void
) {
do {
let proof = try IBTProof.online(
secretKey: secretKey,
serverNodeId: serverNodeId,
windowIndex: currentWindow
)
// Wrap proof в ProtocolMessage envelope (msg_type custom для
// IBT здесь используем reserved тип; spec может уточнить).
// Альтернатива: send raw proof bytes как opening protocol frame.
// Для MVP: отправляем proof как payload в Ping envelope с
// request_id = window_index (correlation для server verify).
let envelope = MontanaEnvelope.encode(
msgType: 0xF0, // Ping repurposed для opening IBT
requestId: currentWindow,
payload: proof
)
send(data: envelope) { result in
switch result {
case .success:
self.state = .ready
self.logger.info("[MontanaConnection] IBT proof sent, state=ready")
completion(.success(()))
case .failure(let e):
completion(.failure(e))
}
}
} catch {
completion(.failure(.ibtFailed("\(error)")))
}
}
public func send(data: Data, completion: @escaping (Result<Void, ConnectionError>) -> Void) {
guard let conn = connection else {
completion(.failure(.sendFailed("no connection")))
return
}
conn.send(content: data, completion: .contentProcessed { err in
if let err = err {
completion(.failure(.sendFailed(err.localizedDescription)))
} else {
completion(.success(()))
}
})
}
public func sendRequest(
msgType: UInt8,
requestId: UInt64,
payload: Data,
completion: @escaping (Result<Void, ConnectionError>) -> Void
) {
let env = MontanaEnvelope.encode(msgType: msgType, requestId: requestId, payload: payload)
send(data: env, completion: completion)
}
public func receiveEnvelope(
completion: @escaping (Result<(msgType: UInt8, requestId: UInt64, payload: Data), ConnectionError>) -> Void
) {
guard let conn = connection else {
completion(.failure(.receiveFailed("no connection")))
return
}
conn.receive(minimumIncompleteLength: 14, maximumLength: 14) { [weak self] header, _, _, error in
if let error = error {
completion(.failure(.receiveFailed(error.localizedDescription)))
return
}
guard let header = header, header.count == 14 else {
completion(.failure(.receiveFailed("incomplete header")))
return
}
let msgType = header[0]
let requestId = header[2..<10].withUnsafeBytes { $0.load(as: UInt64.self).littleEndian }
let payloadLength = Int(header[10..<14].withUnsafeBytes { $0.load(as: UInt32.self).littleEndian })
// Backpressure rule B5 reject до allocation если over MAX
if payloadLength > Int(MontanaEnvelope.maxPayloadBytes) {
completion(.failure(.envelopeDecode(.payloadLengthMismatch)))
return
}
if payloadLength == 0 {
completion(.success((msgType, requestId, Data())))
return
}
conn.receive(
minimumIncompleteLength: payloadLength,
maximumLength: payloadLength
) { payload, _, _, payloadError in
if let payloadError = payloadError {
completion(.failure(.receiveFailed(payloadError.localizedDescription)))
return
}
guard let payload = payload, payload.count == payloadLength else {
completion(.failure(.receiveFailed("incomplete payload")))
return
}
_ = self
completion(.success((msgType, requestId, payload)))
}
}
}
public func close() {
connection?.cancel()
connection = nil
state = .idle
}
}