Skip to content

Let the socket protocols adopt and reset sockets, and report soft errors - #338

Merged
PayasR merged 6 commits into
apple:mainfrom
PayasR:payasr/socket-protocol-lifecycle
Nov 14, 2026
Merged

PayasR merged 6 commits into
apple:mainfrom
PayasR:payasr/socket-protocol-lifecycle

Conversation

@PayasR

@PayasR PayasR commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

SocketDatagramProtocol and SocketStreamProtocol can now take a socket that's already connected,
replace their socket with a new one, and tell the protocol above them about errors that don't end
the flow. TCP Fast Open, the source address and the end of a stream connection now behave the way
TCP expects.

Bugs this fixes:

  • The read and write sources of each socket share a descriptor of their own. A socket that
    replaced another used to inherit the old socket's descriptor while the old sources were still
    cancelling, so nothing watched the new socket and the old one stayed open.
  • A stream connection ends at most once. The read and write sides could both report the same
    failure, such as a refused connect, and a write could report the connection as up after it had
    failed.
  • With Fast Open, the connection counts as connected once the handshake finishes, so a cancel
    before then doesn't reset it. A writable event before the handshake starts no longer counts as
    its end, and SO_SNDLOWAT stays put until the socket connects.
  • A failed SO_REUSEADDR or SO_REUSEPORT fails setup instead of leaving a socket that can't share
    its address.
  • The stream protocol's maximumMessageSize no longer traps where Int has 32 bits.

The protocols now:

  • Take a connected socket with adoptConnectedSocket(_:). The adopted socket skips the options
    that only shape the connect, such as TCP_CONNECTIONTIMEOUT, and closes when the protocol is done
    with it, even if setup never runs.
  • Open a new socket from the same parameters with reset(). The new socket connects from a port
    the kernel picks.
  • Report SocketProtocolEvent.softError when a send or receive fails and the flow goes on, and
    softErrorCleared when the next one in the same direction goes through.
  • Report connectedLocalEndpoint and connectedInterfaceIndex once connected, and
    maximumMessageSize, receivesSingleMessage and framesAreMessages.
  • Connect from the local endpoint's address and port, which connectx binds on Darwin. When the
    TCP options reset the local port, the connection comes from port 0 instead of setting
    SO_REUSEPORT.
  • Follow the IP options' local address preference on an IPv6 socket on Darwin, so a flow can
    prefer a temporary or a stable source address.

The stream protocol now:

  • Passes the Fast Open connectx flags, sends the first write with the SYN, and holds further
    writes until the handshake finishes. A read before any write starts the handshake, so a peer
    that speaks first still gets a connection.
  • Sets TCP_NOTSENT_LOWAT for reduced buffering once connected, and sets TCP_MAXSEG again then.
  • Reports a pending SO_ERROR in place of the error from a failed read or write.

The datagram protocol now:

  • Sets multicast loopback for a multicast destination, from the IP options, and SO_BROADCAST
    for a broadcast destination. It attaches UDP metadata to the datagrams it receives unless the
    UDP options turn metadata off.
  • Reports ENOBUFS as flow controlled, then flow allowed once a send goes through.
  • Drops a datagram whose send fails with an ICMP or policy error, and keeps the flow.
  • Delivers the datagrams queued with addInitialInboundData(_:ecn:) before anything it reads, and
    counts them in the transfer snapshot.
  • Limits maximumMessageSize to the path's largest UDP datagram when that's below 9216 bytes, and
    follows the path in updatePath(_:).

SystemSocket gains init(adopting:), init(rawProtocolFamily:sockType:protocolSubType:nonBlocking:),
boundIPAddress() and setSocketOption(level:name:bytes:). connectSocket and System.connectx take a
source address and connectx flags, and socket creation reports the errno the call returned.

SocketDatagramProtocol and SocketStreamProtocol can now take a socket that's already connected,
replace their socket with a new one, and tell the protocol above them about errors that don't end
the flow. TCP Fast Open, the source address and the end of a stream connection now behave the way
TCP expects.

Bugs this fixes:

- The read and write sources of each socket share a descriptor of their own. A socket that
  replaced another used to inherit the old socket's descriptor while the old sources were still
  cancelling, so nothing watched the new socket and the old one stayed open.
- A stream connection ends at most once. The read and write sides could both report the same
  failure, such as a refused connect, and a write could report the connection as up after it had
  failed.
- With Fast Open, the connection counts as connected once the handshake finishes, so a cancel
  before then doesn't reset it. A writable event before the handshake starts no longer counts as
  its end, and SO_SNDLOWAT stays put until the socket connects.
- A failed SO_REUSEADDR or SO_REUSEPORT fails setup instead of leaving a socket that can't share
  its address.
- The stream protocol's maximumMessageSize no longer traps where Int has 32 bits.

The protocols now:

- Take a connected socket with adoptConnectedSocket(_:). The adopted socket skips the options
  that only shape the connect, such as TCP_CONNECTIONTIMEOUT, and closes when the protocol is done
  with it, even if setup never runs.
- Open a new socket from the same parameters with reset(). The new socket connects from a port
  the kernel picks.
- Report SocketProtocolEvent.softError when a send or receive fails and the flow goes on, and
  softErrorCleared when the next one in the same direction goes through.
- Report connectedLocalEndpoint and connectedInterfaceIndex once connected, and
  maximumMessageSize, receivesSingleMessage and framesAreMessages.
- Connect from the local endpoint's address and port, which connectx binds on Darwin. When the
  TCP options reset the local port, the connection comes from port 0 instead of setting
  SO_REUSEPORT.
- Follow the IP options' local address preference on an IPv6 socket on Darwin, so a flow can
  prefer a temporary or a stable source address.

The stream protocol now:

- Passes the Fast Open connectx flags, sends the first write with the SYN, and holds further
  writes until the handshake finishes. A read before any write starts the handshake, so a peer
  that speaks first still gets a connection.
- Sets TCP_NOTSENT_LOWAT for reduced buffering once connected, and sets TCP_MAXSEG again then.
- Reports a pending SO_ERROR in place of the error from a failed read or write.

The datagram protocol now:

- Sets multicast loopback for a multicast destination, from the IP options, and SO_BROADCAST
  for a broadcast destination. It attaches UDP metadata to the datagrams it receives unless the
  UDP options turn metadata off.
- Reports ENOBUFS as flow controlled, then flow allowed once a send goes through.
- Drops a datagram whose send fails with an ICMP or policy error, and keeps the flow.
- Delivers the datagrams queued with addInitialInboundData(_:ecn:) before anything it reads, and
  counts them in the transfer snapshot.
- Limits maximumMessageSize to the path's largest UDP datagram when that's below 9216 bytes, and
  follows the path in updatePath(_:).

SystemSocket gains init(adopting:), init(rawProtocolFamily:sockType:protocolSubType:nonBlocking:),
boundIPAddress() and setSocketOption(level:name:bytes:). connectSocket and System.connectx take a
source address and connectx flags, and socket creation reports the errno the call returned.
@PayasR PayasR added the 🆕 semver/minor Adds new public API. label Nov 12, 2026
PathProperties.scopedInterface returns the override interface when the
override scopes the path, in every build. Network.framework also
returns the direct interface when NECP policy scopes the path.
@PayasR
PayasR force-pushed the payasr/socket-protocol-lifecycle branch from b55396c to aa1a09d Compare November 14, 2026 04:08
@PayasR
PayasR merged commit 81f8125 into apple:main Nov 14, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants