Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ RxCodeMobile/GoogleService-Info.plist
RxCodeAndroid/app/google-services.json
.env
*.log
android/.idea
64 changes: 64 additions & 0 deletions Packages/Sources/RxCodeSync/Protocol/ICEPayloads.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

/// Direct-path (P2P) signaling payloads.
///
/// These are exchanged between two paired devices **over the relay** (the always-
/// warm signaling channel) to negotiate a direct connection, then consumed
/// entirely inside `PeerConnectionManager`. They never reach the app layer — an
/// older peer that predates this feature decodes them as `Payload.unknown` and
/// ignores them, so no version gating is required.

/// A single reachable address a peer offers for a direct connection.
public struct ICECandidate: Codable, Sendable, Equatable {
public enum Kind: String, Codable, Sendable {
/// Discovered via Bonjour; connect by resolving the service name.
case lanBonjour
/// A raw local interface IP:port (mDNS-blocked subnets). Phase 2.
case lanInterface
/// A mapped public IP:port obtained via NAT-PMP/PCP. Phase 3.
case wanMapped
}

public let kind: Kind
/// IP literal for `.lanInterface`/`.wanMapped`; unused for `.lanBonjour`.
public let host: String
public let port: Int
/// Bonjour service instance name, set only for `.lanBonjour`.
public let bonjourName: String?

public init(kind: Kind, host: String, port: Int, bonjourName: String? = nil) {
self.kind = kind
self.host = host
self.port = port
self.bonjourName = bonjourName
}
}

/// The set of candidates one peer offers for a given probing round.
public struct ICECandidatesPayload: Codable, Sendable {
/// Groups a probing round so stale offers can be ignored.
public let sessionID: UUID
public let candidates: [ICECandidate]
/// What the sender believes its own public IP is (Phase 3; nil until then).
public let observedPublicIP: String?

public init(sessionID: UUID, candidates: [ICECandidate], observedPublicIP: String? = nil) {
self.sessionID = sessionID
self.candidates = candidates
self.observedPublicIP = observedPublicIP
}
}

/// Handshake probe/echo sent **over a candidate direct link itself** to confirm
/// it is truly usable (not a half-open NAT that TCP-connects then black-holes)
/// before the coordinator promotes it to the active path.
public struct ICESelectedPayload: Codable, Sendable {
public let sessionID: UUID
/// `false` = probe from the initiator; `true` = echo reply from the peer.
public let echo: Bool

public init(sessionID: UUID, echo: Bool) {
self.sessionID = sessionID
self.echo = echo
}
}
12 changes: 12 additions & 0 deletions Packages/Sources/RxCodeSync/Protocol/Payload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ public enum Payload: Sendable {
case autopilotResult(AutopilotResultPayload)
case ping(PingPayload)
case pong(PongPayload)
// Direct-path (P2P) signaling — exchanged over the relay, consumed inside
// `PeerConnectionManager`, never surfaced to the app layer. See ICEPayloads.swift.
case iceCandidates(ICECandidatesPayload)
case iceSelected(ICESelectedPayload)
case unknown(type: String)
}

Expand Down Expand Up @@ -139,6 +143,8 @@ public extension Payload {
case .autopilotResult: return "autopilot_result"
case .ping: return "ping"
case .pong: return "pong"
case .iceCandidates: return "ice_candidates"
case .iceSelected: return "ice_selected"
case .unknown(let type): return type
}
}
Expand Down Expand Up @@ -786,6 +792,8 @@ extension Payload: Codable {
case autopilotResult = "autopilot_result"
case ping
case pong
case iceCandidates = "ice_candidates"
case iceSelected = "ice_selected"
}

public init(from decoder: Decoder) throws {
Expand Down Expand Up @@ -859,6 +867,8 @@ extension Payload: Codable {
case .autopilotResult: self = .autopilotResult(try container.decode(AutopilotResultPayload.self, forKey: .data))
case .ping: self = .ping(try container.decode(PingPayload.self, forKey: .data))
case .pong: self = .pong(try container.decode(PongPayload.self, forKey: .data))
case .iceCandidates: self = .iceCandidates(try container.decode(ICECandidatesPayload.self, forKey: .data))
case .iceSelected: self = .iceSelected(try container.decode(ICESelectedPayload.self, forKey: .data))
}
}

Expand Down Expand Up @@ -928,6 +938,8 @@ extension Payload: Codable {
case .autopilotResult(let p): try container.encode(TypeKey.autopilotResult.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .ping(let p): try container.encode(TypeKey.ping.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .pong(let p): try container.encode(TypeKey.pong.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .iceCandidates(let p): try container.encode(TypeKey.iceCandidates.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .iceSelected(let p): try container.encode(TypeKey.iceSelected.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .unknown(let type): try container.encode(type, forKey: .type)
}
}
Expand Down
Loading
Loading