205 lines
7.6 KiB
Swift
205 lines
7.6 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|