Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions FlyingSocks/Sources/Socket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ private extension SocketAddress {
}

#if !canImport(WinSDK)
fileprivate extension Socket {
extension Socket {
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0138-unsaferawbufferpointer.md
private static func withControlMessage(
control: UnsafeRawPointer,
Expand Down Expand Up @@ -713,7 +713,7 @@ fileprivate extension Socket {
}
}

static func getPacketInfoControl(
fileprivate static func getPacketInfoControl(
msghdr: msghdr
) -> (UInt32?, sockaddr_storage?) {
var interfaceIndex: UInt32?
Expand Down Expand Up @@ -745,6 +745,8 @@ fileprivate extension Socket {
return (interfaceIndex, interfaceIndex != nil ? localAddress : nil)
}

// Internal (not fileprivate) so SocketTests can assert the control-message
// level/type contract without a live socket.
static func withPacketInfoControl<T>(
family: sa_family_t,
interfaceIndex: UInt32?,
Expand All @@ -756,8 +758,13 @@ fileprivate extension Socket {
let buffer = ManagedBuffer<cmsghdr, in_pktinfo>.create(minimumCapacity: 1) { buffer in
buffer.withUnsafeMutablePointers { header, element in
header.pointee.cmsg_len = ControlMessageHeaderLengthType(MemoryLayout<cmsghdr>.size + MemoryLayout<in_pktinfo>.size)
header.pointee.cmsg_level = SOL_SOCKET
header.pointee.cmsg_type = Socket.ipproto_ip
// An IP_PKTINFO ancillary message is at the IPPROTO_IP
// cmsg level — ip(7): "Pass an IP_PKTINFO ancillary
// message ... sent/retrieved only as a control message
// with a packet using recvmsg(2) or sendmsg(2)". Must
// match getPacketInfoControl above.
header.pointee.cmsg_level = Socket.ipproto_ip
header.pointee.cmsg_type = Socket.ip_pktinfo
element.pointee.ipi_ifindex = IPv4InterfaceIndexType(interfaceIndex ?? 0)
if let address {
var address = address
Expand All @@ -779,8 +786,11 @@ fileprivate extension Socket {
let buffer = ManagedBuffer<cmsghdr, in6_pktinfo>.create(minimumCapacity: 1) { buffer in
buffer.withUnsafeMutablePointers { header, element in
header.pointee.cmsg_len = ControlMessageHeaderLengthType(MemoryLayout<cmsghdr>.size + MemoryLayout<in6_pktinfo>.size)
header.pointee.cmsg_level = SOL_SOCKET
header.pointee.cmsg_type = Socket.ipproto_ipv6
// RFC 3542 §6: "the socket option and cmsghdr level will
// be IPPROTO_IPV6, the type will be IPV6_PKTINFO". Must
// match getPacketInfoControl above.
header.pointee.cmsg_level = Socket.ipproto_ipv6
header.pointee.cmsg_type = Socket.ipv6_pktinfo
element.pointee.ipi6_ifindex = IPv6InterfaceIndexType(interfaceIndex ?? 0)
if let address {
var address = address
Expand Down
83 changes: 83 additions & 0 deletions FlyingSocks/Tests/AsyncSocketTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,80 @@ struct AsyncSocketTests {
}
)
}

@Test
func sendMessage_WithPacketInfo_RoundTripsDatagram() async throws {
// TVT-1064 smoke test: sendmsg(2) accepts the corrected pktinfo
// control message (Linux rejects a malformed level/type with EINVAL).
// The values asserted below are receive-side pktinfo generated by the
// kernel — on loopback they match regardless of whether the sender's
// request was honored, so the exact cmsg level/type contract is
// asserted in SocketTests.withPacketInfoControl_IP4_SetsPktInfoLevelAndType.
// The localAddress argument exercises the source-address branch of
// withPacketInfoControl; whether it is honored is TVT-1132.
let (server, port) = try await AsyncSocket.makeLoopbackDatagramIP4()

async let received: AsyncSocket.Message = server.receive(atMost: 100)

let client = try await AsyncSocket.makeLoopbackDatagramIP4().0
try await client.send(message: AsyncSocket.Message(
peerAddress: sockaddr_in.inet(ip4: "127.0.0.1", port: port),
payload: Data("Fish 🐡".utf8),
localAddress: sockaddr_in.inet(ip4: "127.0.0.1", port: 0)
))

let message = try await received
#expect(try message.payloadString == "Fish 🐡")
#expect(message.interfaceIndex != nil)
let storage = try #require(message.localAddress).makeStorage()
let sin = withUnsafeBytes(of: storage) { $0.load(as: sockaddr_in.self) }
#expect(
try sin.sin_addr.s_addr == sockaddr_in.inet(ip4: "127.0.0.1", port: 0).sin_addr.s_addr
)
}

@Test
func sendMessage_WithPacketInfoIPv6_RoundTripsDatagram() async throws {
// TVT-1064 smoke test: sendmsg(2) accepts the corrected pktinfo
// control message. The values asserted below are receive-side pktinfo
// generated by the kernel — on loopback they match regardless of
// whether the sender's request was honored, so the exact cmsg
// level/type contract is asserted in
// SocketTests.withPacketInfoControl_IP6_SetsPktInfoLevelAndType.
let (server, port) = try await AsyncSocket.makeLoopbackDatagram()

async let received: AsyncSocket.Message = server.receive(atMost: 100)

// Select the outgoing interface via ipi6_ifindex — RFC 3542 §6.1.
// A ::1 source address is not used here: on Darwin, sendmsg(2) then
// fails with EADDRNOTAVAIL (observed during TVT-1064 execution).
#if canImport(Darwin)
let loopbackIndex = if_nametoindex("lo0")
#else
let loopbackIndex = if_nametoindex("lo")
#endif
// if_nametoindex(3): "If the specified interface does not exist, it
// returns 0."
try #require(loopbackIndex != 0)
let client = try await AsyncSocket.makeLoopbackDatagram().0
try await client.send(message: AsyncSocket.Message(
peerAddress: sockaddr_in6.loopback(port: port),
payload: Data("Chips 🍟".utf8),
interfaceIndex: UInt32(loopbackIndex)
))

let message = try await received
#expect(try message.payloadString == "Chips 🍟")
#expect(message.interfaceIndex != nil)
let storage = try #require(message.localAddress).makeStorage()
let sin6 = withUnsafeBytes(of: storage) { $0.load(as: sockaddr_in6.self) }
let loopback = sockaddr_in6.loopback(port: 0).sin6_addr
#expect(
withUnsafeBytes(of: sin6.sin6_addr) { received in
withUnsafeBytes(of: loopback) { received.elementsEqual($0) }
}
)
}
#endif
}

Expand Down Expand Up @@ -365,6 +439,15 @@ extension AsyncSocket {
return (socket, port)
}

static func makeLoopbackDatagramIP4() async throws -> (AsyncSocket, port: UInt16) {
let socket = try await AsyncSocket.make(domain: AF_INET, type: .datagram)
try socket.socket.bind(to: .inet(ip4: "127.0.0.1", port: 0))
guard case let .ip4(_, port: port) = try socket.socket.sockname() else {
fatalError()
}
return (socket, port)
}

#if !canImport(WinSDK)
static func makeDatagramPair() async throws -> (AsyncSocket, AsyncSocket, sockaddr_un) {
let socketPair = try await makePair(pool: .client, type: .datagram)
Expand Down
43 changes: 43 additions & 0 deletions FlyingSocks/Tests/SocketTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,49 @@ struct SocketTests {
try socket.getValue(for: .packetInfoIPv6) == true
)
}

#if !canImport(WinSDK)
@Test
func withPacketInfoControl_IP4_SetsPktInfoLevelAndType() {
// TVT-1064 regression: cmsg_level must identify the originating
// protocol and cmsg_type the protocol-specific option — not
// SOL_SOCKET with a protocol number in the type field. macOS SDK
// <netinet/in.h>: "#define IP_PKTINFO 26 /* get pktinfo on recv
// socket, set src on sent dgram */"; ip(7) on Linux.
let (level, type) = Socket.withPacketInfoControl(
family: sa_family_t(AF_INET),
interfaceIndex: nil,
address: nil
) { header, _ in
(header?.pointee.cmsg_level, header?.pointee.cmsg_type)
}
#expect(level == Int32(IPPROTO_IP))
#expect(type == Int32(IP_PKTINFO))
}

@Test
func withPacketInfoControl_IP6_SetsPktInfoLevelAndType() {
// TVT-1064 regression: RFC 3542 §6 — "the socket option and cmsghdr
// level will be IPPROTO_IPV6, the type will be IPV6_PKTINFO".
// The non-nil address exercises the source-address copy branch.
let (level, type) = Socket.withPacketInfoControl(
family: sa_family_t(AF_INET6),
interfaceIndex: nil,
address: sockaddr_in6.loopback(port: 0)
) { header, _ in
(header?.pointee.cmsg_level, header?.pointee.cmsg_type)
}
#expect(level == Int32(IPPROTO_IPV6))
#if canImport(Darwin)
// macOS SDK <netinet6/in6.h>: "#define IPV6_3542PKTINFO 46
// /* in6_pktinfo; send if, src addr */". IPV6_PKTINFO is gated
// behind __APPLE_USE_RFC_3542, which Swift cannot define.
#expect(type == 46)
#else
#expect(type == Int32(IPV6_PKTINFO))
#endif
}
#endif
}

extension Socket.Flags {
Expand Down
Loading