Skip to content

Add channel options for a vsock socket's local and remote addresses - #3689

Open
VictorDebray wants to merge 4 commits into
apple:mainfrom
VictorDebray:feat/add-remote-vsock-address-channel-option
Open

Add channel options for a vsock socket's local and remote addresses#3689
VictorDebray wants to merge 4 commits into
apple:mainfrom
VictorDebray:feat/add-remote-vsock-address-channel-option

Conversation

@VictorDebray

@VictorDebray VictorDebray commented Aug 3, 2026

Copy link
Copy Markdown

Add channel options to read a vsock socket's local and remote addresses

Motivation

Channel.remoteAddress and Channel.localAddress are always nil on vsock channels.
NIOCore.SocketAddress has no vsock representation. NIOPosix otherwise supports vsock through VsockAddress, ServerBootstrap.bind(to:) and ClientBootstrap.connect(to:).
So a server accepting a vsock connection can't find out which context (VM) connected. A listener bound to VsockAddress/Port/any can't find out which port the kernel gave it.

The peer's context ID is the only identity a vsock connection carries. The kernel supplies it, not the peer. That makes it the natural thing to key authorization on.
ChannelOptions.localVsockContextID already exists for the same reason on the local side. It reports the context ID without the port. A wildcard bind therefore still leaves servers and tests hard-coding a port and racing for it.

Modifications

  • Add ChannelOptions.Types.RemoteVsockAddress and LocalVsockAddress. Both are get-only options with Value == VsockAddress.

  • Add BaseSocket.getRemoteVsockAddress() and getLocalVsockAddress(). On a non-vsock socket these calls fill a different sockaddr. Reinterpreting those bytes would yield a context ID derived from unrelated data, such as an IP address and port. Callers key authorization on the CID, so failing beats returning something meaningless.

  • Service both in SocketChannel.getOption0. LocalVsockAddress is serviced on ServerSocketChannel too, which is the case which needs it. RemoteVsockAddress isn't: a listener has no peer.

  • Tests: both options are rejected on a connected non-vsock socket. Over vsock loopback, a client's peer port is the port the listener bound, and the accepted channel reports the same CID the client saw. A listener bound to Port/any reports a concrete port and a context ID matching getLocalVsockContextID(), and the channel option agrees with the socket. The loopback tests are gated on System.supportsVsockLoopback, matching testGetLocalCID and testEchoVsock.

Result

let peer = try await channel.getOption(.remoteVsockAddress).get()
peer.cid    // the connecting context's CID

let local = try await listener.getOption(.localVsockAddress).get()
local.port  // the port a Port/any bind was given

Exposes an accepted vsock connection's peer address -- and therefore the peer
context ID (CID) -- without changing `NIOCore.SocketAddress`.

`Channel/remoteAddress` returns `SocketAddress?`, which has no vsock representation, so it is always nil for vsock channels. Rather than add a `.vsock` case to that public enum (a source-breaking change for every exhaustive switch over it, in NIO and downstream), this mirrors the pattern NIO already uses for the same problem on the local side: `localVsockContextID` is a get-only `ChannelOption` precisely because `localAddress` cannot report a vsock address either.

The helper validates the address family before trusting the result.
On a non-vsock socket `getpeername` fills a different `sockaddr`, and reinterpreting those bytes would yield a context ID derived from unrelated data such as a peer's IP address and port.
Callers use the CID for trust decisions, so a mismatch throws `SocketAddressError.unsupported` instead.
A socketpair-based test covers this and fails if the guard is removed.

All additive: a new option type, a new static accessor, and a new arm in an internal switch. No existing declaration changes, so no exhaustive switch anywhere needs updating.
Covers the path the option exists for: a live vsock connection over loopback, where `getpeername` really does fill a `sockaddr_vm`.

The client asserts its peer's port is the one the listener bound, which is what would catch a misread `sockaddr_vm.
Wrong field offsets could not happen to reproduce the chosen port. The accepted channel then asserts reading its peer succeeds and reports the same CID the client saw, since both ends of a loopback connection live in the same context.

Gated on `System.supportsVsockLoopback` in the same way as `testGetLocalCID` and `testEchoVsock`, so it skips on hosts without the transport and runs in the `vsock-tests` CI job, which loads `vsock_loopback` and fails on skipped tests.
@VictorDebray VictorDebray changed the title Aadd remote vsock address channel option Add remote vsock address channel option Aug 3, 2026
Mirrors remoteVsockAddress: exposes a vsock socket's local
address (including bound port) since Channel/localAddress
can't represent vsock addresses and LocalVsockContextID
alone can't report the port chosen for Port/any binds.
@VictorDebray VictorDebray changed the title Add remote vsock address channel option Add remote local and remote vsock address channels option Aug 19, 2026
@VictorDebray VictorDebray changed the title Add remote local and remote vsock address channels option Add channel options for a vsock socket's local and remote addresses Aug 20, 2026

extension ChannelOptions {
/// - seealso: `LocalVsockAddress`
public static let localVsockAddress = Types.LocalVsockAddress()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can also be a static computed var

Comment on lines +194 to +200
///
/// `Channel/localAddress` cannot report a vsock address, because `SocketAddress` has no vsock
/// representation. This option exposes the address that `getsockname` reports for the socket
/// instead.
///
/// ``LocalVsockContextID`` reports only the context ID, so it can't tell you which port a
/// listener bound to when it was bound to `VsockAddress/Port/any`. This option reports both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary context


extension ChannelOptions {
/// - seealso: `RemoteVsockAddress`
public static let remoteVsockAddress = Types.RemoteVsockAddress()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a static computed var

Comment on lines +218 to +223
///
/// `Channel/remoteAddress` cannot report a vsock peer, because `SocketAddress` has no vsock
/// representation. This option exposes the peer address that `getpeername` reports for the
/// connection instead, which is where the peer's context ID (CID) comes from.
///
/// Only meaningful on a connected vsock channel; getting it on a listening channel fails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary context

Comment on lines +343 to +345
///
/// ``getLocalVsockContextID()`` reports only the context ID, so it can't say which port a
/// listener bound to when it was bound to `VsockAddress/Port/any`. This reports both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary context


let address = try socket.getLocalVsockAddress()
XCTAssertNotEqual(address.port, .any, "The kernel should have assigned a concrete port")
XCTAssertEqual(address.cid, try socket.getLocalVsockContextID())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove this? We're not testing getLocalVsockContextID here.

Comment on lines +117 to +118
/// A non-vsock socket must not report a vsock local address, for the same reason
/// ``testGetRemoteVsockAddressRejectsNonVsockSocket()`` covers on the peer side.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary context.

Comment on lines +131 to +135
/// A non-vsock socket must not report a vsock peer address.
///
/// `getpeername` on a UDS socket fills a `sockaddr_un`; if those bytes were reinterpreted as a
/// `sockaddr_vm` the option would return a context ID derived from unrelated memory. Callers use
/// the CID to make trust decisions, so this must fail instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More noise.

Comment on lines +158 to +162
/// Both ends of a real vsock connection report the peer's address.
///
/// This is the path the option exists for, so it needs a live connection: it exercises
/// `getpeername` on an `AF_VSOCK` socket and the reinterpretation of the resulting
/// `sockaddr_vm`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More unnecessary context.

Comment on lines +171 to +172
// Read the peer address on the accepted channel. Sending the address rather than the
// channel through the promise keeps this Sendable-clean.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is wrong. Channel is Sendable so there's nothing about sending a channel through a promise that would make this sendable-unclean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants