Skip to main content

Apple Platforms

Apple support splits by protocol, and the split matters:

ProtocolBackend on Apple
TCP + TLSNWConnection / NWListener (Network.framework)
UDPNWConnection in UDP mode for connected clients; a dual-stack POSIX socket for servers and multicast
QUIC / HTTP​/3 / WebTransportCloudflare quiche on macOS and iOS, compiled in and reached through cinterop — not Network.framework. tvOS and watchOS have no quiche build, so QUIC throws UnsupportedOperationException there
QUIC on Apple is quiche, not NWProtocolQUIC

This library has no Network.framework-native QUIC backend: every platform with QUIC — Apple included — runs the same Cloudflare quiche engine. Network.framework's role in the QUIC stack is limited to carrying the client's UDP datagrams (see below).

Running one engine everywhere is deliberate: it is what makes connection migration, unreliable datagrams, pluggable congestion control, and per-stream RESET_STREAM/STOP_SENDING behave identically on Apple and everywhere else. NWProtocolQUIC exposes none of those knobs.

Supported Targets​

  • macOS (arm64, x64)
  • iOS (arm64, simulator arm64, simulator x64)
  • tvOS (arm64, simulator arm64, simulator x64)
  • watchOS (arm64, simulator arm64, simulator x64)

TCP and TLS​

TCP connections and listeners are NWConnection and NWListener, reached from Kotlin/Native through a small C shim rather than a Swift library. src/nativeInterop/cinterop/nw_helpers.h bridges Network.framework's Objective-C and dispatch APIs to K/N-safe types (opaque handles, plain callbacks), and the cinterop is wired per-target in build.gradle.kts via configureNWHelpersCinterop().

The Kotlin side lives in src/appleNativeImpl/kotlin/com/ditchoom/socket/:

  • NWClientSocketWrapper — outbound TCP connections
  • NWSocketWrapper — the shared read/write/close machinery
  • NWServerWrapper — NWListener for accepting inbound connections

Received data is materialized from NSData inside the completion callback, so no copy is made on the way to a ReadBuffer.

TLS is handled natively by Network.framework: when SocketOptions carries a non-null TlsConfig, NWProtocolTLS.Options is configured on the connection parameters. Certificate validation therefore goes through the system keychain — see TLS for what that implies for private CAs.

Read timeouts are non-destructive​

Network.framework has no per-receive cancel. Rather than tear the connection down when a read times out, the outstanding nw_helper_tcp_receive is left in flight and its one-shot completion is captured in a socket-held CompletableDeferred; the caller only withTimeouts the await. A timed-out read throws SocketTimeoutException and orphans the receive for the next read() to re-await. The connection is cancelled only on genuine EOF, error, or close().

QUIC​

socket-quic on macOS and iOS is quiche, exactly as on JVM, Android, and Linux. The Kotlin ↔ quiche binding is CinteropQuicheApi (socket-quic-quiche/src/appleMain/), and the Apple targets link the real libquiche.a — the shared Quic*TestSuite conformance suites run against it, not against a stub.

Datagram path​

QUIC needs a UDP socket underneath, and the two roles use different ones:

  • Client — NwUdpDatagramChannel, an NWConnection in UDP mode. Network.framework is used here specifically because it reports path changes, which is what makes QUIC connection migration (RFC 9000 §9) react to a Wi-Fi → cellular switch on iOS instead of stalling.
  • Server — PosixUdpDatagramChannel, a plain dual-stack POSIX socket. A server needs recvfrom-style unconnected receive to accept from arbitrary peers, and it must be dual-stack: Network.framework resolves localhost to ::1, so a v4-only server would never be reached from an NWConnection client on the same machine.

Multicast uses MulticastPosixUdpDatagramChannel. Note that SO_REUSEADDR is applied for multicast only — on Darwin as well as Linux — because for unicast it lets a more-specific bind steal delivery from a wildcard socket.

Certificate verification​

QUIC certificates are verified by quiche's BoringSSL, as on every other platform — not by SecTrust, and not against the keychain. Anchors passed as QuicOptions.trustedCaCertificatesPem are loaded into BoringSSL and must carry CA:TRUE; a missing flag surfaces as an opaque handshake failure. With no anchors, macOS verifies against /etc/ssl/cert.pem and iOS against a bundled Mozilla root set, so MDM-installed roots are not trusted for QUIC. (TCP TLS, by contrast, goes through NWProtocolTLS and the keychain — see above.)

Building​

Apple targets require macOS with Xcode installed. There is no separate Swift build step — the C shim is compiled by the Kotlin cinterop tooling as part of the normal Gradle build:

./gradlew macosArm64Test          # or macosX64Test
./gradlew iosSimulatorArm64Test

A full ./gradlew build on macOS also builds quiche for each Apple target, which is the slow part of a cold build.

Requirements​

  • macOS with Xcode installed
  • Rust toolchain (to build quiche for the Apple targets)