I want to use a QUIC stream with Swift's NWProtocolQUIC

I would like to use NWProtocolQUIC in Swift's Network.framework to prepare multiple QUIC Streams and send different data to the server for each.

class QuicConnection {
    var acceptConnection: NWConnection?
    var openConnection: NWConnection?
    var acceptConnectionState: NWConnection.State?
    var openConnectionState: NWConnection.State?
    var receiveFromAcceptConnection: String?
    
    static func makeNWParameters() -> NWParameters {
        let options = NWProtocolQUIC.Options(alpn: ["echo"])
        options.direction = .bidirectional
        let securityProtocolOptions: sec_protocol_options_t = options.securityProtocolOptions
        sec_protocol_options_set_verify_block(securityProtocolOptions,
                                              { (_: sec_protocol_metadata_t,
                                                 _: sec_trust_t,
                                                 complete: @escaping sec_protocol_verify_complete_t) in
            complete(true)
        }, DispatchQueue.main)
        return NWParameters(quic: options)
    }

    let group: NWConnectionGroup

    init() {
        print("init")
        let parameters = Self.makeNWParameters()
        let descriptor = NWMultiplexGroup(to: .hostPort(host: "192.168.0.20", port: 4242))
        group = NWConnectionGroup(with: descriptor, using: parameters)

        //subscribe()
        group.stateUpdateHandler = { (state: NWConnectionGroup.State) in
            print("state: \(state)")
            switch state {
            case .ready:
                print("quic connected!")
            default:
                break
            }
        }
        
        group.newConnectionHandler = { [weak self] (connection: NWConnection) in
            print("new connection: \(connection)")
            self?.acceptConnection = connection
            self?.acceptConnection?.stateUpdateHandler = { [weak self] (state: NWConnection.State) in
                self?.acceptConnectionState = state
            }
            self?.subscribeAcceptConnection()
            self?.acceptConnection?.start(queue: DispatchQueue.main)
        }
        
        group.start(queue: DispatchQueue.main)
    }
    
    func createStream() {
        //guard let group else { return }

        let options = NWProtocolQUIC.Options()
        options.direction = .bidirectional
        let securityProtocolOptions: sec_protocol_options_t = options.securityProtocolOptions
        sec_protocol_options_set_verify_block(securityProtocolOptions,
                                              { (_: sec_protocol_metadata_t,
                                                 _: sec_trust_t,
                                                 complete: @escaping sec_protocol_verify_complete_t) in
            complete(true) // Insecure !!!
        }, DispatchQueue.main)
        openConnection = NWConnection(from: group)

        openConnectionState = openConnection?.state

        openConnection?.stateUpdateHandler = { [weak self] (state: NWConnection.State) in
            self?.openConnectionState = state
            print("state: \(state)")
            switch state {
            case .ready:
                print("stream connected!")
                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    self?.send(message: "marker1")
                }
            default:
                break
            }
        }

        openConnection?.start(queue: DispatchQueue.main)
    }
    
    func send(message: String) {
        print("send start")
        let completion: NWConnection.SendCompletion = .contentProcessed { (error: Error?) in
            if let error = error {
                print("send error: \(error)")
            } else {
                print("send successful")
            }
        }
        openConnection?.send(content: message.data(using: .utf8)!,
                         contentContext: .defaultMessage,
                         isComplete: true,
                        completion: completion)
        
        print("message: \(message)")
    }
}

When the app starts, it calls the init function of this QuicConnection class to create an instance and build the QUIC tunnel." quic connected" log appears, and when a specific action is performed in the app, the createStream function is called to put up a stream." stream connected" log is displayed, but when I then try to send data using the send function, the "send successful" message is displayed, but there is no output on the server side indicating that the message was received from the client. However, when I try to send data using the send function after that, I get a "send successful" message.

I don't think there is a problem on the server side, because I can communicate well when only NWConnection is used without NQConnectionGroup. The server is using quic-go.

I would like to borrow knowledge from those who have handled QUIC streams in Swift.

Below are Apple's announcement and official documents that I referred to. I wrote the code referring to these, but even though I can connect the QUIC tunnels, I can not send data by setting up individual streams.

https://developer.apple.com/videos/play/wwdc2021/10094/

https://developer.apple.com/documentation/network/nwprotocolquic

Post not yet marked as solved Up vote post of takt Down vote post of takt
826 views

Replies

What you are describing seems like it should work, but if this is what is sending your first application data packet on your stream then I would get rid of this and try it by setting up some user initiated way to trigger send.

print("stream connected!")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    self?.send(message: "marker1")
}

You may be capturing self in a strange state here. Next, if the above does not work, please either take a packet trace or capture the logs of your stream setting up and sending the data. Then post that here with any sensitive information redacted.

Did you end up finding a solution for this?

@takt so I was running into something similar and I am still chasing it done but I have some notes worth sharing:

While you mentioned it's probably not the server side I have seen an issue (or think so) where the NWConnectionGroup is establishing an initial stream. When the connection is established to the server , you are generating a new stream and sending data which will, on the server side, trigger the stream to be "opened".

What I found was if your server side code is something like this (sudo code for readability):

conn = endpoint.accept_connection()
stream = conn.accept_new_stream()
...
let bytes = stream.recv()

You might find that your server is accepting a stream but you don't get the bytes.

AFAICT, this is because there is an initial stream being establish silently by the NWConnectionGroup which you cannot get a handle to. When you create your new stream on your client, and send data this causes the hidden stream to be opened as well per the QUIC spec:

A stream ID that is used out of order results in all streams of that type with lower-numbered stream IDs also being opened.

https://www.rfc-editor.org/rfc/rfc9000.html#name-stream-types-and-identifier

This means you have a handle to the initial hidden stream and the server will accept data from your newly open stream.

From my testing you need to make sure that your server is accepting streams multiple times:

conn = endpoint.accept_connection()
_ = conn.accept_new_stream()
stream = conn.accept_new_stream()
...

and then I found I want able to process the incoming data.

Reference this example repo and issue where I dug into this will the Rust implementation of QUIC and Network.framework https://github.com/paxsonsa/quic-swift-demo https://github.com/quinn-rs/quinn/issues/1742

Cheers!

  • and by “make sure that your server is accepting streams multiple times” I mean your server should make sure it is accepting multiple open stream events NOT that you need to accept each stream twice.

Add a Comment