Skip to content

Commit 3db5c4a

Browse files
authored
Support for custom protocols in DatagramBootstrap (#2516)
Motivation This patch adds support for custom protocol families to DatagramBootstrap. In most cases this isn't useful, and will fail, but in many OSes it's a recognised system for non-root processes to get a way to send ICMP echo requests. This is worth supporting. Modifications - Expose the `ProtocolSubtype` type publicly. - Add a `protocolSubtype` setter on `DatagramBootstrap` - Write a test that actually does ICMP as non-root Results Users can send ICMP echo requests without root privileges.
1 parent e0d8554 commit 3db5c4a

File tree

4 files changed

+163
-12
lines changed

4 files changed

+163
-12
lines changed

Sources/NIOPosix/BSDSocketAPICommon.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,33 @@ extension NIOBSDSocket.Option {
136136
}
137137

138138
extension NIOBSDSocket {
139-
struct ProtocolSubtype: RawRepresentable, Hashable {
140-
typealias RawValue = CInt
141-
var rawValue: RawValue
142-
143-
init(rawValue: RawValue) {
139+
/// Defines a protocol subtype.
140+
///
141+
/// Protocol subtypes are the third argument passed to the `socket` system call.
142+
/// They aren't necessarily protocols in their own right: for example, ``mptcp``
143+
/// is not. They act to modify the socket type instead: thus, ``mptcp`` acts
144+
/// to modify `SOCK_STREAM` to ask for ``mptcp`` support.
145+
public struct ProtocolSubtype: RawRepresentable, Hashable {
146+
public typealias RawValue = CInt
147+
148+
/// The underlying value of the protocol subtype.
149+
public var rawValue: RawValue
150+
151+
/// Construct a protocol subtype from its underlying value.
152+
public init(rawValue: RawValue) {
144153
self.rawValue = rawValue
145154
}
146155
}
147156
}
148157

149158
extension NIOBSDSocket.ProtocolSubtype {
150-
static let `default` = Self(rawValue: 0)
159+
/// Refers to the "default" protocol subtype for a given socket type.
160+
public static let `default` = Self(rawValue: 0)
161+
151162
/// The protocol subtype for MPTCP.
163+
///
152164
/// - returns: nil if MPTCP is not supported.
153-
static var mptcp: Self? {
165+
public static var mptcp: Self? {
154166
#if os(Linux)
155167
// Defined by the linux kernel, this is IPPROTO_MPTCP.
156168
return .init(rawValue: 262)
@@ -161,7 +173,8 @@ extension NIOBSDSocket.ProtocolSubtype {
161173
}
162174

163175
extension NIOBSDSocket.ProtocolSubtype {
164-
init(_ protocol: NIOIPProtocol) {
176+
/// Construct a protocol subtype from an IP protocol.
177+
public init(_ protocol: NIOIPProtocol) {
165178
self.rawValue = CInt(`protocol`.rawValue)
166179
}
167180
}

Sources/NIOPosix/Bootstrap.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,7 @@ public final class DatagramBootstrap {
14031403
private var channelInitializer: Optional<ChannelInitializerCallback>
14041404
@usableFromInline
14051405
internal var _channelOptions: ChannelOptions.Storage
1406+
private var proto: NIOBSDSocket.ProtocolSubtype = .default
14061407

14071408
/// Create a `DatagramBootstrap` on the `EventLoopGroup` `group`.
14081409
///
@@ -1468,6 +1469,11 @@ public final class DatagramBootstrap {
14681469
return self
14691470
}
14701471

1472+
public func protocolSubtype(_ subtype: NIOBSDSocket.ProtocolSubtype) -> Self {
1473+
self.proto = subtype
1474+
return self
1475+
}
1476+
14711477
#if !os(Windows)
14721478
/// Use the existing bound socket file descriptor.
14731479
///
@@ -1542,6 +1548,7 @@ public final class DatagramBootstrap {
15421548
}
15431549

15441550
private func bind0(_ makeSocketAddress: () throws -> SocketAddress) -> EventLoopFuture<Channel> {
1551+
let subtype = self.proto
15451552
let address: SocketAddress
15461553
do {
15471554
address = try makeSocketAddress()
@@ -1551,7 +1558,7 @@ public final class DatagramBootstrap {
15511558
func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
15521559
return try DatagramChannel(eventLoop: eventLoop,
15531560
protocolFamily: address.protocol,
1554-
protocolSubtype: .default)
1561+
protocolSubtype: subtype)
15551562
}
15561563
return withNewChannel(makeChannel: makeChannel) { _, channel in
15571564
channel.register().flatMap {
@@ -1590,6 +1597,7 @@ public final class DatagramBootstrap {
15901597
}
15911598

15921599
private func connect0(_ makeSocketAddress: () throws -> SocketAddress) -> EventLoopFuture<Channel> {
1600+
let subtype = self.proto
15931601
let address: SocketAddress
15941602
do {
15951603
address = try makeSocketAddress()
@@ -1599,7 +1607,7 @@ public final class DatagramBootstrap {
15991607
func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
16001608
return try DatagramChannel(eventLoop: eventLoop,
16011609
protocolFamily: address.protocol,
1602-
protocolSubtype: .default)
1610+
protocolSubtype: subtype)
16031611
}
16041612
return withNewChannel(makeChannel: makeChannel) { _, channel in
16051613
channel.register().flatMap {
@@ -1839,12 +1847,13 @@ extension DatagramBootstrap {
18391847
postRegisterTransformation: @escaping @Sendable (ChannelInitializerResult, EventLoop) -> EventLoopFuture<PostRegistrationTransformationResult>
18401848
) async throws -> PostRegistrationTransformationResult {
18411849
let address = try makeSocketAddress()
1850+
let subtype = self.proto
18421851

18431852
func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
18441853
return try DatagramChannel(
18451854
eventLoop: eventLoop,
18461855
protocolFamily: address.protocol,
1847-
protocolSubtype: .default
1856+
protocolSubtype: subtype
18481857
)
18491858
}
18501859

@@ -1867,12 +1876,13 @@ extension DatagramBootstrap {
18671876
postRegisterTransformation: @escaping @Sendable (ChannelInitializerResult, EventLoop) -> EventLoopFuture<PostRegistrationTransformationResult>
18681877
) async throws -> PostRegistrationTransformationResult {
18691878
let address = try makeSocketAddress()
1879+
let subtype = self.proto
18701880

18711881
func makeChannel(_ eventLoop: SelectableEventLoop) throws -> DatagramChannel {
18721882
return try DatagramChannel(
18731883
eventLoop: eventLoop,
18741884
protocolFamily: address.protocol,
1875-
protocolSubtype: .default
1885+
protocolSubtype: subtype
18761886
)
18771887
}
18781888

Tests/NIOPosixTests/DatagramChannelTests.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,118 @@ class DatagramChannelTests: XCTestCase {
930930
testEcnAndPacketInfoReceive(address: "::1", vectorRead: true, vectorSend: true, receivePacketInfo: true)
931931
}
932932

933+
func testDoingICMPWithoutRoot() throws {
934+
// This test validates we can send ICMP messages on a datagram socket without having root privilege.
935+
//
936+
// This doesn't always work: ability to do this on Linux is gated behind a sysctl (net.ipv4.ping_group_range)
937+
// which may exclude us. So we have to tolerate this throwing EPERM as well.
938+
939+
final class EchoRequestHandler: ChannelInboundHandler {
940+
typealias InboundIn = AddressedEnvelope<ByteBuffer>
941+
typealias OutboundOut = AddressedEnvelope<ByteBuffer>
942+
943+
let completePromise: EventLoopPromise<ByteBuffer>
944+
945+
init(completePromise: EventLoopPromise<ByteBuffer>) {
946+
self.completePromise = completePromise
947+
}
948+
949+
func channelActive(context: ChannelHandlerContext) {
950+
var buffer = context.channel.allocator.buffer(capacity: 32)
951+
952+
// We're going to write an ICMP echo packet from scratch, like heroes.
953+
// Echo request is type 8, code 0.
954+
// The checksum is tricky: on Linux, the kernel doesn't care what we set, it'll
955+
// calculate it. On macOS, however, we have to calculate it. For both platforms, then,
956+
// we calculate it.
957+
// Identifier is irrelevant.
958+
// Sequence number does matter, but we'll set to 0.
959+
let type = UInt8(8)
960+
let code = UInt8(0)
961+
let fakeChecksum = UInt16(0)
962+
let identifier = UInt16(0)
963+
let sequenceNumber = UInt16(0)
964+
buffer.writeMultipleIntegers(type, code, fakeChecksum, identifier, sequenceNumber)
965+
966+
// Then we write a payload, which will be "hello from NIO".
967+
buffer.writeString("Hello from NIO")
968+
969+
// Now calculate the checksum, and store it back at offset 2.
970+
let checksum = buffer.readableBytesView.computeIPChecksum()
971+
buffer.setInteger(checksum, at: 2)
972+
973+
// Now wrap it into an addressed envelope pointed at localhost.
974+
let envelope = AddressedEnvelope(
975+
remoteAddress: try! SocketAddress(ipAddress: "127.0.0.1", port: 0),
976+
data: buffer
977+
)
978+
979+
context.writeAndFlush(self.wrapOutboundOut(envelope)).cascadeFailure(to: self.completePromise)
980+
}
981+
982+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
983+
let envelope = self.unwrapInboundIn(data)
984+
985+
// Complete with the payload.
986+
self.completePromise.succeed(envelope.data)
987+
}
988+
}
989+
990+
let loop = self.group.next()
991+
let completePromise = loop.makePromise(of: ByteBuffer.self)
992+
do {
993+
let channel = try DatagramBootstrap(group: group)
994+
.protocolSubtype(.init(.icmp))
995+
.channelInitializer { channel in
996+
channel.pipeline.addHandler(EchoRequestHandler(completePromise: completePromise))
997+
}
998+
.bind(host: "127.0.0.1", port: 0)
999+
.wait()
1000+
defer {
1001+
XCTAssertNoThrow(try channel.close().wait())
1002+
}
1003+
1004+
// Let's try to send an ICMP echo request and get a response.
1005+
var response = try completePromise.futureResult.wait()
1006+
1007+
#if canImport(Darwin)
1008+
// Again, a platform difference. On Darwin, this returns a complete IP packet. On Linux, it does not.
1009+
// We assume the Linux platform is the more general approach, but if this test fails on your platform
1010+
// it is _probably_ because it behaves differently. To make this general, we can skip the IPv4 header.
1011+
//
1012+
// To do that, we have to work out how long that header is. That's held in bottom 4 bits of the first
1013+
// byte, which is the IHL field. This is in "number of 32-bit words".
1014+
guard let firstByte = response.getInteger(at: response.readerIndex, as: UInt8.self),
1015+
let _ = response.readSlice(length: Int(firstByte & 0x0F) * 4) else {
1016+
XCTFail("Insufficient bytes for IPv4 header")
1017+
return
1018+
}
1019+
#endif
1020+
1021+
// Now we've got the ICMP packet. Let's parse this.
1022+
guard let header = response.readMultipleIntegers(as: (UInt8, UInt8, UInt16, UInt16, UInt16).self) else {
1023+
XCTFail("Insufficient bytes for ICMP header")
1024+
return
1025+
}
1026+
1027+
// Echo response has type 0, code 0, unpredictable checksum and identifier, same sequence number we sent.
1028+
XCTAssertEqual(header.0 /* type */, 0)
1029+
XCTAssertEqual(header.1 /* code */, 0)
1030+
XCTAssertEqual(header.4 /* sequence number */, 0)
1031+
1032+
// Remaining payload should have been our string.
1033+
XCTAssertEqual(String(buffer: response), "Hello from NIO")
1034+
} catch let error as IOError {
1035+
// Firstly, fail this promise in case it leaks.
1036+
completePromise.fail(error)
1037+
if error.errnoCode == EACCES {
1038+
// Acceptable
1039+
return
1040+
}
1041+
XCTFail("Unexpected IOError: \(error)")
1042+
}
1043+
}
1044+
9331045
func assertSending(
9341046
data: ByteBuffer,
9351047
from sourceChannel: Channel,

Tests/NIOPosixTests/IPv4Header.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,22 @@ extension IPv4Header {
314314
}
315315
}
316316

317+
extension Sequence where Element == UInt8 {
318+
func computeIPChecksum() -> UInt16 {
319+
var sum = UInt16(0)
320+
321+
var iterator = self.makeIterator()
322+
323+
while let nextHigh = iterator.next() {
324+
let nextLow = iterator.next() ?? 0
325+
let next = (UInt16(nextHigh) << 8) | UInt16(nextLow)
326+
sum = onesComplementAdd(lhs: sum, rhs: next)
327+
}
328+
329+
return ~sum
330+
}
331+
}
332+
317333
private func onesComplementAdd<Integer: FixedWidthInteger>(lhs: Integer, rhs: Integer) -> Integer {
318334
var (sum, overflowed) = lhs.addingReportingOverflow(rhs)
319335
if overflowed {

0 commit comments

Comments
 (0)