I'm building a network client with Swift (using QUIC). I set everything up properly (I know this because I can successfully connect, send and receive streams). But I'm trying to catch connection errors.
For example if I try to connect to a totally bogus IP address, I would like to display Connecting, then ConnectionFailed
I do the following:
create my NWMultiplexGroup descriptor
set my appropriate NWParameters
create my NWConnectionGroup
set up my handlers (setReceiveHandler, newConnectionHandler) and my state update handler
i call connection.start
When I pass a valid address to a server that is listening for the connection, all is good - in my stateUpdateHandler I get the .ready state, but I don't get any intermediate states, and if I pass it a bogus IP address, I get absolutely no callbacks to my handler
(I would have expected to get .waiting and/or .failed)
I couldn't find any quic options that I'm not doing, and the apple documentation is not helpful
Any suggestions as to what I might be missing?
QUIC
RSS for tagCreate network connections to send and receive data using the QUIC protocol.
Posts under QUIC tag
25 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Hey all here is an example you can try out:
https://github.com/paxsonsa/quic-swift-demo
I am prototype a QUIC base application system with a client and server.
My server is a simple test to experiment with QUIC and Network Framework but I am see some odd behaviour.
Selecting Stream Direction for new streams
In the example below, we are creating a new multiplexed QUIC connection and establish a new stream once the group connection is ready. In some cases, I want to be able to use a different stream kind (uni/bi).
By specifying the options, I get an error in Xcode console like so:
running....
group state: waiting(POSIXErrorCode(rawValue: 50): Network is down)
group state: ready
Connected using QUIC!
nw_endpoint_flow_setup_cloned_protocols [C3 127.0.0.1:4567 in_progress socket-flow (satisfied (Path is satisfied), viable, interface: lo0)] could not find protocol to join in existing protocol stack
nw_endpoint_flow_failed_with_error [C3 127.0.0.1:4567 in_progress socket-flow (satisfied (Path is satisfied), viable, interface: lo0)] failed to clone from flow, moving directly to failed state
Main Connection State: failed(POSIXErrorCode(rawValue: 50): Network is down)
quic_recovery_pto PTO fired after validation
Here is my swift code:
//
// main.swift
// QuicTool
//
// Created by Andrew Paxson on 2024-01-14.
//
import Foundation
import Network
/// Helper function to create a message frame.
func createMessage(version: UInt8, messageType: UInt8, message: String) -> Data {
let messageData = message.data(using: .utf8) ?? Data()
let length = UInt32(messageData.count)
var data = Data()
data.append(version)
data.append(messageType)
// Convert length to 4 bytes and append (big-endian format)
let bigEndianLength = length.bigEndian
data.append(contentsOf: withUnsafeBytes(of: bigEndianLength) { Array($0) })
// Append 2 bytes of padding for 8-byte alignment
data.append(Data(repeating: 0, count: 2))
// Add Message Data.
data.append(messageData)
return data
}
// Queue for QUIC things.
let queue = DispatchQueue(label: "quic", qos: .userInteractive)
// Create Inital Options for the tunnel.
// This is using an insecure connection as this operation is meant to be local network.
let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: .init(integerLiteral: 4567))
let options = NWProtocolQUIC.Options(alpn: ["demo"])
// Set the initial stream to bidirectional.
options.direction = .bidirectional
sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
sec_protocol_verify_complete(true)
}, queue)
let parameters = NWParameters(quic: options)
// 1) Create a new multiplexed connection
let descriptor = NWMultiplexGroup(to: endpoint)
let group = NWConnectionGroup(with: descriptor, using: parameters)
var mainConn: NWConnection? = nil
// Here we are establishing a state handler for when the connection to the
// the server is neogiated and "ready". Once its ready we want to establish a
// stream using the group with the options set.
//
// This is the main location of the issue we are seeing where the stream is
// established and the data is sent but never updated.
group.stateUpdateHandler = { newState in
print("group state: \(newState)")
switch newState {
// Once the tunnel is established, create a new stream with bidirectional parameters.
case .ready:
print("Connected using QUIC!")
// 2) In normal application I may want to open different kinds of streams in providing
// new options. Is there a better way to select the stream kind for subsequent streams?
let options = NWProtocolQUIC.Options(alpn: ["demo"])
options.direction = .bidirectional
// When providing unique options the stream will fail. Removeing the using argument works.
mainConn = group.extract()! // force unwrap
mainConn?.stateUpdateHandler = { state in
print("Main Connection State: \(state)")
switch state {
case .ready:
// Once the connection is ready, lets send some sweet data sauce.
//
// By establishing this new stream and sending data, on the server this causes the inital
// stream with no handle to be open.
let version: UInt8 = 1
let messageType: UInt8 = 1
let message = "hello, I am from the multiplex group ready."
let messageData = createMessage(version: version, messageType: messageType, message: message)
mainConn?.send(content: messageData, isComplete: true, completion: .contentProcessed({ sendError in
if let error = sendError {
print("There was an error sending data: \(error)")
} else {
print("Data was sent successfully from Main Connection.")
}
}))
default:
break
}
}
// Don't forget to start the connection.
mainConn?.start(queue: queue)
default:
break
}
}
// Receive new incoming streams initiated by the remote endpoint
// this is not used for this example.
group.newConnectionHandler = { conn in
print("New Connection: \(conn)")
// Set state update handler on incoming stream
conn.stateUpdateHandler = { newState in
print("newState: \(newState) for \(conn)")
switch newState {
case .ready:
print("got a new stream!")
default:
break
}
}
// Start the incoming stream
conn.start(queue: queue)
}
// Start the group with callback queue
group.start(queue: queue)
print("running....")
// We iterate trying to send data on the new stream we created after the
// connection is established.
while true {
switch mainConn?.state {
case .ready:
// Once the connection is ready, lets send some sweet data sauce.
let version: UInt8 = 1
let messageType: UInt8 = 1
let message = "hello, im from the main loop"
let messageData = createMessage(version: version, messageType: messageType, message: message)
print("Local Stream Send: \(messageData)")
mainConn?.send(content: messageData, completion: .contentProcessed({ sendError in
if let error = sendError {
print("There was an error sending data: \(error)")
}
}))
sleep(1)
default:
continue
}
}
Hello, I noticed that Facetime attempts to use the QUIC protocol during a Facetime session with the following ports 3478 through 3497 (UDP). Can Facetime use the QUIC protocol for the following ports 3478 through 3497 (UDP) because on the internet I have not found anywhere the possibility that QUIC can be used other than ports 80 and 443?
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
My team are now testing http/3 support in our app and backend. It looks strange to me how the behaviour is different between iOS 15.7.3 and 16.x (now 16.6).
It is a default URLSession config and just a regular URLRequest, with no assumesHTTP3Capable flag set. I observe the protocol in use with both collecting URLSessionTaskMetrics and the Network instrument.
On 15.7.3 the 1st request is done using h2, the network subsystem caches the Alt-Svc header (per host I suppose). Several seconds pass and then when I issue the same 2nd request a new connection is established and the protocol is upgraded to h3. This is all fine and as described in the documentation or wwdc videos.
However, when I take the same steps with a device running e.g. iOS 16.5.1 I see that the protocol is never upgraded to h3. Neither a longer timeout nor an app relaunch make any difference.
Interestingly, on my Ventura desktop the same url is also handled differently: h3 by Chrome but always h2 by Safari. In the mobile Safari on iOS 16, I'm also always shown HTTP/2 on e.g. the cloudflare status page for QUIC.
What can be the reason for such behaviour on iOS 16 and Apple platforms in general?
PS. I've tried running the app with CFNETWORK_DIAGNOSTICS but found no useful log messages to clarify problems with QUIC. Is there still a way to better diagnose such a problem?