Post

Replies

Boosts

Views

Activity

Reply to NWBrowser + NWListener + NWConnection
Thank you! Would you be able to provide a simple example on how to use NWConnectionGroup with QUIC and peer to peer and star architecture? Im confused on what to put for the with param in the NWConnectionGroup initializer. https://developer.apple.com/documentation/network/nwconnectiongroup/init(with:using:) I believe i will put my NWParameters that contains all my TLS1.3 stuff for the using param Would i use an endpoint discovered in my browser for the with param? let nwcg = NWConnectionGroup(with: endpoint, using: parameters) If that is the case wont an NWConnectionGroup only be created when the device ID of the browsing device is larger than the discovered peer? So the device that gets connected to via its listener wouldn't have a NWConnectionGroup? Also, would I have a NWConnectionGroup for each peer im connected to? I may have a picture of how things would work... As you know i have a concept of a device controller. So if I have three devices connected to each other (not in a star configuration - more server client) there will be a single controller. Device A is the controller Device B gets controlled Device C gets controlled Device A -> Device B Device A -> Device C Device B <-XXXX-> Device C (no connection between device B and C) Since device A is the controller I will only start its NWBrowser and i will add the discovered endpoints to my Endpoints class that conforms to NWGroupDescriptor final class Endpoints: NWGroupDescriptor { var members: [NWEndpoint] init(endpoints: [NWEndpoint]) { self.members = endpoints } } Device B and C i will only start their listeners if device B wants to be come the leader then maybe a sequence of messages could passed between device B and A and this will result in Device A stopping its NWBrowser and starting its NWListener and device B stopping its NWListener and starting its NWBrowser.
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
my connect function private func connect(_ connection: NWConnection, _ deviceID: DeviceID? = nil) { connection.stateUpdateHandler = { [weak self] state in guard let self = self else { return } print("Connection state changed to: \(state)") switch state { case .preparing: print("Connection preparing") case .waiting(let error): print("Connection waiting: \(error)") case .setup: print("Connection setup") case .ready: Task { @MainActor in if let deviceID = deviceID { self.isConnected[deviceID] = true } self.messagingService?.receive(from: connection) } case .failed(let error): print("Connection failed: \(error)") case .cancelled: print("Connection cancelled") Task { @MainActor in if let deviceID = deviceID { self.isConnected[deviceID] = false } } default: break } } connection.start(queue: queue) } and send function func send(_ data: Data) { let dataLength = UInt32(data.count) var content = withUnsafeBytes(of: dataLength.bigEndian) { Data($0) } content.append(data) Task { @MainActor in for connection in connections.values { guard connection.state == .ready else { print("skipping connetion that is not ready") continue } // connection.send(content: content, completion: .contentProcessed { [weak self] error in // guard let self = self else { return } connection.send(content: content, completion: .contentProcessed { error in if let error = error { print("Failed to send message: \(error)") } else { print("Message sent successfully") } }) } } }
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
I have the following data structures: @Published private(set) var endpoints: [DeviceID: NWEndpoint] = [:] @Published private(set) var connections: [ConnectionID: NWConnection] = [:] @Published private(set) var connectionDevice: [ConnectionID: DeviceID] = [:] @Published private(set) var deviceConnection: [DeviceID: ConnectionID] = [:] I changed my code such that the browser will first check if its deviceID is greater than the discovered device ID - if it is then it will try and connect to it. I also changed my code to save the connection in the newConnectionHandler. With all that I am experiencing some odd behavior. I have two physical devices, an iPhone and an iPad. The iPhone's deviceID is greater than the deviceID of the iPad. Due to this the iphone will create a connection to the ipad. Also due to this the ipad will not try and create a connection to the iphone. I am able to see, via printIt(), that the iphone has one item in each data structure. The iPad only has one item in endpoints (this is due to me saving it from the ipad's browser) and one item in connections (the incoming connection from the iphone) the odd behavior is as follows: right after things hit steady state - meaning that the ipad and iphone are done discovering and connection to each other - when i go to send a message from the ipad to the iphone i get the following message on the iphone logs nw_protocol_instance_add_new_flow [C18.1.1.1:1] No listener registered, cannot accept new flow quic_stream_add_new_flow [C18.1.1.1:1] [-1fe5f3879e2c580d] failed to create new stream for received stream id 1 if i then send a message from the iphone to the ipad and then try and send a message from the ipad to the iphone - it works Here is how i add an endpoint from the browser browser.browseResultsChangedHandler = { [weak self] _, changes in guard let self = self else { return } Task { @MainActor in guard let thisDeviceID = self.devicesService?.thisDevice?.id else { print("[browseResultsChangedHandler] cant get device id") return } for change in changes { switch change { case .added(let added): guard let deviceID = self.devicesService?.deviceIDFromEndpoint(added.endpoint) else { print("[browseResultsChangedHandler - .added] could not get device id") continue } guard !self.endpoints.keys.contains(deviceID) else { print("[browseResultsChangedHandler] already added device \(deviceID)") continue } guard deviceID != thisDeviceID else { print("found myself - ignoring") continue } self.addEndpoint(added.endpoint) case .removed(let removed): print("browseResultsChangedHandler: removed peer \(removed)") guard let deviceID = self.devicesService?.deviceIDFromEndpoint(removed.endpoint) else { print("[browseResultsChangedHandler - .removed] could not get deviceID") continue } guard self.endpoints.keys.contains(deviceID) else { print("[browseResultsChangedHandler] endpoint was never added \(deviceID)") continue } self.removeEndpoint(removed.endpoint) default: continue } } } } private func addEndpoint(_ endpoint: NWEndpoint) { guard !endpointExists(endpoint) else { print("endpoint already exists") return } guard let deviceID = devicesService?.deviceIDFromEndpoint(endpoint) else { print("[removeEndpoint] could not get deviceID") return } guard let thisDeviceID = devicesService?.thisDevice?.id else { print("[removeEndpoint] could not get thisDeviceID") return } if thisDeviceID > deviceID { do { let identity = importIdentityFromPKCS12() let parameters = try NWParameters(identity: identity) let connection = NWConnection(to: endpoint, using: parameters) let connectionID = getConnectionID(connection) connections[connectionID] = connection connectionDevice[connectionID] = deviceID deviceConnection[deviceID] = connectionID connect(deviceID: deviceID) } catch { print("Failed to create connection parameters: \(error)") } } endpoints[deviceID] = endpoint devicesService?.addDevice(device: DevicesService.Device(id: deviceID, name: nil, model: nil, camera: nil)) } Here is my new connection handler listener?.newConnectionHandler = { [weak self] connection in guard let self = self else { return } print("New inbound connection received from: \(connection)") Task { @MainActor in let connectionID = self.getConnectionID(connection) self.connections[connectionID] = connection self.connect(connection) } }
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
That’s not expected. You’re using TCP so every connection is bidirectional. I am using QUIC I want to clarify some terminology. An inbound connection is a connection from newConnectionHandler. An outbound connection is a connection I build from a discovered endpoint in browseResultsChangedHandler. Can you confirm this metal model? Assuming the above metal model is correct and quoting you In the star architecture it doesn’t matter which peer started the connection, just that there’s a single connection between each peer. In a star architecture I should only be saving one connection between peers. How I interpret this is I need a deterministic way to make sure Peer A and Peer B don't both try and connect to each other from their respective NWBrowser. In the past I had the following tactic: Get the service name (which is the peerID) of the discovered peer Compare it against mine Only create a connection and try and connect to the peer if my peer ID is greater than the discovered peer ID If my peer ID is not greater than I do nothing and assume that the discovered peer will discover me and try to connect to me For sake of this example let's assume: Device A has an ID of "a" Device B has an ID of "b" "a" < "b" == true Due to the above then Device A will discover Device B but wont try to connect to it. Device B will discover Device A and will create a connection and try to connect to it. Device B will save the connection it created to Device A. Device A will get a new connection in newConnectionHandler and will save it. Since there is no way to identify which peer the new connection came from Device B will need to send its info (device ID and device name) to Device A so it can properly map the device ID to the connection. And what im reading is that both devices should be able to use that single connection to send data back and forth. So Device A should be able to use the connection that come in newConnectionHandler to send data to Device B. Is this correct?
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
Thank you for the info. So me not being able to use an incoming connection (from listener.newConnectionHandler) to send data over it is expected? If I cant send data over it then do I need to keep it so i can call cancel on it if the device saving it goes offline or the app is killed? I guess since I wasn't able to use an incoming connection to send data over i thought it was useless to me. Since you're saying I should be saving the incoming connection - what do I do with it - what is the expected use of it?
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
No. The issue here is that client A and client B can discover and connect to each other simultaneously. You need to find a deterministic way to deduplicate such connections. I touched on this in Moving from Multipeer Connectivity to Network Framework but I’ve expanded that discussion with more specifics. Check out the newly updated Create a peer identifier section. I am still unclear if I should save the connection I get in listener?.newConnectionHandler = { [weak self] connection in Currently I have the following that works: star network (all devices connected to all devices) on each device i start a browser and a listener i save the endpoint found in the browser and i create a connection from said endpoint (i dont start the connection yet - the user needs to tap a button to start the connection to the discovered device --- this may change where i auto connect to all discovered devices) in listener?.newConnectionHandler i do setup the connection i get - i call connection.receive on it So in my case i have Device A which discovered Device B - saved Device B endpoint and created a connection. Device A has saved the endpoint of Device B and the created connection to Device B (all this happened in the NWBrowser) Device B has discovered Device A - saved Device A endpoint and created a connection. Device B has saved the end of Device A and the created connection to Device A (all this happened in the NWBrowser) I think all this is confusing because of the following test i did: i blindly saved every discovered endpoint i blindly saved every connection created from every discovered endpoint i blindly saved every connection from my listener newConnectionHandler In the end, on each device (i only had two devices), i roughly had 6 total connections in my connections array. I had everything setup to be able to send messages back and forth. When i tapped the button on Device A to send a hello world message to all the connections in the connections array Device B ever only received the hello world message twice. Two of the connections in Device A's connections array where connections I created from endpoints i discovered. The rest were connections i saved from my listener. From that I concluded that I cannot send data through connections saved from my listener?.newConnectionHandler. Am I wrong?
Apr ’25
Reply to NWBrowser + NWListener + NWConnection
Thank you for that link! I read it and learned a lot. Areas I can change in my app: generate the peerID each time the app starts put the peerID in txtRecord and not in the service name Some notes about my app (which uses star architecture): In the browser i ignore my own endpoint (i see that the browser discovers its device's listener) In the browser I first check to see if i have a connection for a discovered peer - if not then i create a connection and save it - if i do already have a connection for the discovered peer then i do nothing with it I see in your post that you show code to save the new connection in the listener.newConnectionHandler. Is this in the context of a client / server? Just so I can get a handle on things: If one uses the client / server architecture - the server is the device that has a listener going and the client is the device that has the browser going. The server saves new connections in listener.newConnectionHandler and the client saves the connections it creates with the NWEndpoints it discovers. If one is using the star architecture (all devices connected to all devices) then each device has a listener going and a browser going and ONLY needs to save the connections created from the NWBrowser (ie discovered NWEndpoints). Is that right? In my app there will be basic message / command passing (ie "I'm the new leader"). I can use a basic QUIC connection for that. I also will want to transfer photos and videos. I believe I can again use a QUIC connection for this - with chunking. I want to live stream video from one device to another and for this (based on your article) the advised pattern would be opening a new UDP connection to do the live video stream? Looking at your article right now I'm seeing the section on "Start a stream". Would it be advisable to use a QUIC connection to a peer to start a new stream to then use to stream video data? From your post: If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole. Does that mean a stream is a QUIC connection in a QUIC Connection Group? For my use cases: simple commands / chunked video / photo files live streamed video Would I want to create a QUIC Connection Group and open two connections - one connection to handle #1 and one connection to handle #2? Thank you for your time and help.
Apr ’25
Reply to Using Network Framework + Bonjour + QUIC + TLS
You said this previously If you want to authenticate the device, then each device needs its own identity. In that case, your identity generation code will need to include something device specific into the identity’s certificate so that a peer can tell that remote peer is the device that it’s expecting. If you want to authenticate the user, then you can use a single identity for that. A peer can tell that the remote peer is the same user by checking that the certificate matches the certificate in the digital identity that it’s using. The tricky part of about the latter is that your server has to store the digital identity (so, the certificate and the private key) so that new clients can access it. OTOH, if you authenticate the device then each device retains its own private key and the server only has to issue the certificate and store that. I am getting stuck on this step. I used this online resource to create a certificate for testing https://www.samltool.com/self_signed_certs.php. I tried to base64 encode it and make it a constant in my codebase. I then tried to base64 decode it and then use it to create a certificate. I was able to do that successfully. The connection failed and I think it's because I don't have an identity but when I go to create an identity it's asking for a private key. Between the two options you shared above I would think id want the "authenticate the user" because I don't care about the device - just that the same user is trying to connect. When you say "then you can use a single identity" does that mean I send the client the certificate I created on my backend and then the client uses a private key it creates on its device to then create an identity? Do I send both the private key and certificate used on the backend to the client which is then used to create an identity. If the "authenticate the device" is easier im open to that. The goal is as i shared above "bob's devices cant connect to tom's devices and vice versa". The "something device specific into the identity’s certificate" could be some hash identifier similar to what I shared about TXT_RECORD.
Nov ’24
Reply to Using Network Framework + Bonjour + QUIC + TLS
Can I please have some assistance on how to properly setup an NWParameters extension to accept a base64Encoded public certificate and then use it to correctly secure connections? I have this so far extension NWParameters { convenience init(base64EncodedCert: String) throws { // Create QUIC parameters with the TLS options let quicOptions = NWProtocolQUIC.Options(alpn: ["h3"]) // Convert the base64 string to a string to handle PEM format guard let pemData = Data(base64Encoded: base64EncodedCert), let pemString = String(data: pemData, encoding: .utf8) else { print("Failed to decode initial base64 string") throw CertificateError.invalidBase64String } // Extract the certificate content between the PEM markers let lines = pemString.components(separatedBy: .newlines) let certificateLines = lines.filter { line in !line.contains("BEGIN CERTIFICATE") && !line.contains("END CERTIFICATE") && !line.isEmpty } // Join the lines and create certificate data let certificateString = certificateLines.joined() guard let certificateData = Data(base64Encoded: certificateString) else { print("Failed to decode certificate content") throw CertificateError.invalidBase64String } print("Successfully decoded certificate content, data length: \(certificateData.count)") // Try to create certificate guard let certificate = SecCertificateCreateWithData(nil, certificateData as CFData) else { print("Failed to create certificate from data") throw CertificateError.certificateCreationFailed } sec_protocol_options_set_min_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13) sec_protocol_options_set_max_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13) print("Successfully created certificate") // Get certificate summary for verification let certificateSummary = SecCertificateCopySubjectSummary(certificate) as? String print("Certificate Summary: \(certificateSummary ?? "No summary available")") // Create identity from certificate sec_protocol_options_set_verify_block(quicOptions.securityProtocolOptions, { _, sec_trust, sec_protocol_verify_complete in // Get the certificates from the trust object let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue() // Get the certificate chain guard let certificates = SecTrustCopyCertificateChain(trust) as? [SecCertificate], let peerCertificate = certificates.first else { print("Failed to get peer certificate from chain") sec_protocol_verify_complete(false) return } // Compare the peer's certificate with our pinned certificate let peerCertificateData = SecCertificateCopyData(peerCertificate) as Data let pinnedCertificateData = SecCertificateCopyData(certificate) as Data // Verify the certificates match let certificatesMatch = peerCertificateData == pinnedCertificateData print("Certificate verification result: \(certificatesMatch)") sec_protocol_verify_complete(certificatesMatch) }, DispatchQueue(label: "com.example.certificate.verification")) self.init(quic: quicOptions) } } I am seeing "Certificate Summary: " printed to the console but I am getting the following errors Connection state changed to: failed(-9858: handshake failed) Connection failed: -9858: handshake failed
Nov ’24
Reply to Using Network Framework + Bonjour + QUIC + TLS
I came here for help and advise and I got just that. Thank you. I want to head your input so I'll be attempting to get the basics working with QUIC. In my mind they are as follows: Generate a digital identity per user and store it on my backend Be able to send generated digital identity to ios app after the user authenticates Store digital identity in keychain Use digital identity to setup TLS for QUIC NWListener and NWConnection Use SHA256(user.id + "TXT_RECORD" + user.email + "TXT_RECORD" + user.id) for the TXT Record Be able to validate / verify the certificate chain while in sec_protocol_options_set_verify_block Am I missing anything?
Nov ’24
Reply to Using Network Framework + Bonjour + QUIC + TLS
I would still greatly appreciate a response to my last question so this thread can be complete for me if I need to come back here later. I did some testing with TCP and was able to download a 30 second video from a peer that was 36 feet away using only peer to peer wifi (both devices were NOT on a wifi network) in around 2 seconds. At this point this is sufficient for me to get going. I'm sure I can clean up the code and make it more robust and maybe "faster". I say all this to say I'm going to move forward with two connections per peer. One TCP connection, which will be used for sending commands and downloading content and one UDP connection, which will be used for video streaming. I plan to create both of these connection right when the peer is discovered. On the topic of TCP and UDP. Would the NWBrowser need to be UDP or TCP or does it matter? For example _p2pchat._udp or _p2pchat._tcp? Is using PSK (with both TCP and UDP) enough security to protect against malicious devices trying to connect? Should I still use the bonjourWithTXTRecord(type:domain:)? For the PSK I was planning on making it SHA256(user.id + "PSK" + user.email + "PSK" + user.id). The user.id is something I generate on the backend (appwrite generates it actually). Can I use something similar for the TXT record, like SHA256(user.id + "TXT_RECORD" + user.email + "TXT_RECORD" + user.id)? Thank you for all your time and input - it is really appreciated!
Nov ’24
Reply to Using Network Framework + Bonjour + QUIC + TLS
Get the certificate from the remote peer and check that it matches the certificate from the digital identity you’ve saved. As long as your back end provisions each user with their own unique certificate, that should be sufficient to ensure that users can only talk to themselves. How can I get the certificate from the remote peer if I can't make a connect with the peer until I've verified its certificate? It feels like a chicken and the egg type of thing. Isn't the sec_protocol_options_set_verify_block part of the NWParameters setup process? How would I get the certificate from the remote peer while in that callback?
Nov ’24
Reply to Using Network Framework + Bonjour + QUIC + TLS
After doing research about the benefits QUIC has over TCP I would like to fully move forward with the QUIC implementation. Before I start coding I like to get a clear mental model of what needs to happen and all the moving parts. After reading your response many times I think I understand. To give a bit more background into how I want my app to operate I want to share some expected behavior. Side note - on the Appwrite side (my backend) I am going to limit the user to having a max of 7 auth sessions (including web auth sessions and ios device auth sessions). So if they are authenticated on 2 browsers they would only be allows to run my app on 5 physical ios devices. Example behavior: Bob has my app on 3 of his devices. He has signed in and is authenticated (via the Appwrite swift SDK). Tom has my app on 3 of his devices and he too has signed in and is authenticated. Both Bob and Tom are in the same room. While in the same room Bob and Tom could be on no wifi or they could be on the same wifi or one is on wifi and one is not. The expectation is that Bob cannot discover and connect to Tom's devices and vice versa. Seeing how the the serviceName (ie "_p2pchat._udp") needs to be set prior to the app starting and since it's also set in the Bonjour Info.plist field could, in theory, any device using my app discover one another? Using the TickTackToe example code I was thinking I would use the hash of the user's email concatenated with their userID as the PSK. So I guess Bob and Tom's devices could all be discovered but only Bob's devices could connect to each other and only Tom's devices could connect to each other. I would then only show devices to the user that I've connected to. Now when it comes to using QUIC and digital identify I'm a bit confused about how to achieve the desired behavior (ie Bob cannot discover and connect to Tom's devices and vice versa). My first thought would be when a new user signs up I generate them a digital identity on my backend. When they go to the app and sign in they would request their digital identity and I would then store in UserDefaults to persist it. I would then use that digital identity to setup TLS for QUIC. I know I can use a NWBrowser without TLS - it's just NWListener and NWConnection that require TLS. So going back to the above example. For Bob's devices I would use his downloaded digital identity to listen for connections and use it to create connections. If the identities don't match then the connection cannot be made. Is my thinking on the right track?
Nov ’24