From 49356ee87776c8e52f57af70ebe6f337d096c7ee Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 11 Sep 2026 14:37:16 +1000 Subject: [PATCH] Socket: send pktinfo with the source address in the right field withPacketInfoControl built the in_pktinfo or in6_pktinfo in the element storage of a ManagedBuffer, which starts out uninitialised, and: - For IPv4 it wrote the local address to ipi_addr and never set ipi_spec_dst, the field ip(7) says sendmsg(2) takes the source address from. So the source was whatever was in memory, and sendmsg could fail with ENETUNREACH, as sendMessage_WithPacketInfo_RoundTripsDatagram does on some Linux builds. - It read the address by reinterpreting the bytes of the any SocketAddress existential. Those bytes are the address only when the value is held inline: a sockaddr_in6 or sockaddr_storage is boxed, so what was read was the box. Zero the structure, read the address from makeStorage(), and put an IPv4 source in ipi_spec_dst. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE --- FlyingSocks/Sources/Socket.swift | 32 ++++++++++++-------- FlyingSocks/Tests/SocketTests.swift | 46 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/FlyingSocks/Sources/Socket.swift b/FlyingSocks/Sources/Socket.swift index 2e63ff8..b458444 100644 --- a/FlyingSocks/Sources/Socket.swift +++ b/FlyingSocks/Sources/Socket.swift @@ -765,16 +765,20 @@ extension Socket { // match getPacketInfoControl above. header.pointee.cmsg_level = Socket.ipproto_ip header.pointee.cmsg_type = Socket.ip_pktinfo + // ManagedBuffer leaves the element uninitialized: zero it, so that no + // field is sent holding whatever was in memory. + element.initialize(to: in_pktinfo()) element.pointee.ipi_ifindex = IPv4InterfaceIndexType(interfaceIndex ?? 0) + // ip(7): "If IP_PKTINFO is passed to sendmsg(2) and ipi_spec_dst is not + // zero, then it is used as the local source address for the routing + // table lookup". ipi_addr is the destination of a received packet. if let address { - var address = address - withUnsafePointer(to: &address) { - $0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { - element.pointee.ipi_addr = $0.pointee.sin_addr + let storage = address.makeStorage() + if storage.ss_family == sa_family_t(AF_INET) { + element.pointee.ipi_spec_dst = withUnsafeBytes(of: storage) { + $0.load(as: sockaddr_in.self).sin_addr } } - } else { - element.pointee.ipi_addr.s_addr = 0 } return header.pointee @@ -791,16 +795,20 @@ extension Socket { // match getPacketInfoControl above. header.pointee.cmsg_level = Socket.ipproto_ipv6 header.pointee.cmsg_type = Socket.ipv6_pktinfo + // memberwise: Socket+Glibc declares its own in6_pktinfo, which has no + // zero initializer + element.initialize(to: in6_pktinfo(ipi6_addr: in6_addr(), ipi6_ifindex: 0)) element.pointee.ipi6_ifindex = IPv6InterfaceIndexType(interfaceIndex ?? 0) + // RFC 3542 §6.1: on output, ipi6_addr is the source address. Read it + // from the address's storage: a sockaddr_in6 is too large to be held + // inline in an existential, so the existential's own bytes are not it. if let address { - var address = address - withUnsafePointer(to: &address) { - $0.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { - element.pointee.ipi6_addr = $0.pointee.sin6_addr + let storage = address.makeStorage() + if storage.ss_family == sa_family_t(AF_INET6) { + element.pointee.ipi6_addr = withUnsafeBytes(of: storage) { + $0.load(as: sockaddr_in6.self).sin6_addr } } - } else { - element.pointee.ipi6_addr = in6_addr() } return header.pointee diff --git a/FlyingSocks/Tests/SocketTests.swift b/FlyingSocks/Tests/SocketTests.swift index bab2737..2b724e0 100644 --- a/FlyingSocks/Tests/SocketTests.swift +++ b/FlyingSocks/Tests/SocketTests.swift @@ -419,6 +419,52 @@ struct SocketTests { #expect(type == Int32(IPV6_PKTINFO)) #endif } + + @Test + func withPacketInfoControl_IP4_SetsSourceAddressInSpecDst() throws { + // ip(7): sendmsg(2) takes the source address from ipi_spec_dst; ipi_addr is the + // destination of a received packet. The address is also passed as a + // sockaddr_storage, which an existential holds boxed rather than inline. + let loopback = try sockaddr_in.inet(ip4: "127.0.0.1", port: 0) + for address in [loopback as any SocketAddress, loopback.makeStorage()] { + let info = Socket.withPacketInfoControl( + family: sa_family_t(AF_INET), + interfaceIndex: 7, + address: address + ) { header, _ in + UnsafeRawPointer(header!) + .advanced(by: MemoryLayout.size) + .loadUnaligned(as: in_pktinfo.self) + } + #expect(info.ipi_spec_dst.s_addr == loopback.sin_addr.s_addr) + #expect(info.ipi_addr.s_addr == 0) + #expect(info.ipi_ifindex == 7) + } + } + + @Test + func withPacketInfoControl_IP6_SetsSourceAddress() { + // RFC 3542 §6.1: on output, ipi6_addr is the source address. A sockaddr_in6 is too + // large to be held inline in an existential, so this also checks the address is + // read from the value, not from the existential's own storage. + let loopback = sockaddr_in6.loopback(port: 0) + for address in [loopback as any SocketAddress, loopback.makeStorage()] { + let info = Socket.withPacketInfoControl( + family: sa_family_t(AF_INET6), + interfaceIndex: 7, + address: address + ) { header, _ in + UnsafeRawPointer(header!) + .advanced(by: MemoryLayout.size) + .loadUnaligned(as: in6_pktinfo.self) + } + #expect( + withUnsafeBytes(of: info.ipi6_addr) { Array($0) } == + withUnsafeBytes(of: loopback.sin6_addr) { Array($0) } + ) + #expect(info.ipi6_ifindex == 7) + } + } #endif }