From 4225e45a33855710437aaf154cb7b16eed0feac4 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Thu, 5 Sep 2024 23:35:01 -0700 Subject: [PATCH 01/11] reduce memory footprint for icmp ping handler --- Package.swift | 4 +-- Sources/LCLPing/ICMP/ICMPHandler.swift | 31 +++++++++---------- .../HTTPTracingHandlerTests.swift | 1 + 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Package.swift b/Package.swift index 4608db7..1a26e82 100644 --- a/Package.swift +++ b/Package.swift @@ -16,10 +16,10 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.72.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.25.0"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4") + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/LCLPing/ICMP/ICMPHandler.swift b/Sources/LCLPing/ICMP/ICMPHandler.swift index 9dc329c..6e9060b 100644 --- a/Sources/LCLPing/ICMP/ICMPHandler.swift +++ b/Sources/LCLPing/ICMP/ICMPHandler.swift @@ -12,6 +12,7 @@ import Foundation import NIOCore +import Collections extension ICMPPingClient { @@ -139,11 +140,11 @@ final class ICMPHandler: PingHandler { // sequence number to ICMP request private var seqToRequest: [Int: ICMPPingClient.ICMPHeader] - // sequence number to an optional ICMP response - private var seqToResponse: [Int: ICMPPingClient.ICMPHeader?] + // a bit set that contains the response sequence number seen so far + private var seen: BitSet - // a set that contains the response sequence number received by the handler - private var responseSeqNumSet: Set + // a bit set that contains the response sequence number received by the handler + private var hasResponses: BitSet // a list of `PingResponse` private var result: [PingResponse] @@ -154,10 +155,10 @@ final class ICMPHandler: PingHandler { init(totalCount: Int, promise: EventLoopPromise<[PingResponse]>) { self.totalCount = totalCount self.seqToRequest = [:] - self.seqToResponse = [:] - self.responseSeqNumSet = Set() self.result = [] self.icmpPingPromise = promise + self.seen = BitSet(reservingCapacity: self.totalCount) + self.hasResponses = BitSet(reservingCapacity: self.totalCount) } func handleRead(response: ICMPPingClient.ICMPHeader) { @@ -176,17 +177,16 @@ final class ICMPHandler: PingHandler { return } - if self.responseSeqNumSet.contains(sequenceNum) { - let pingResponse: PingResponse = self.seqToResponse[sequenceNum] == - nil ? .timeout(sequenceNum) : .duplicated(sequenceNum) - logger.debug("[\(#fileID)][\(#line)][\(#function)]:: response for #\(sequenceNum) is \(self.seqToResponse[sequenceNum] == nil ? "timeout" : "duplicate")") + if self.seen.contains(sequenceNum) { + let pingResponse: PingResponse = self.hasResponses.contains(sequenceNum) ? .duplicated(sequenceNum) : .timeout(sequenceNum) + logger.debug("[\(#fileID)][\(#line)][\(#function)]:: response for #\(sequenceNum) is \(self.hasResponses.contains(sequenceNum) ? "timeout" : "duplicate")") result.append(pingResponse) shouldCloseHandler() return } - self.seqToResponse[sequenceNum] = response - self.responseSeqNumSet.insert(sequenceNum) + self.hasResponses.insert(sequenceNum) + self.seen.insert(sequenceNum) switch (type, code) { case (ICMPPingClient.ICMPType.echoReply.rawValue, 0): @@ -334,9 +334,9 @@ final class ICMPHandler: PingHandler { } func handleTimeout(sequenceNumber: Int) { - if !self.responseSeqNumSet.contains(sequenceNumber) { + if !self.seen.contains(sequenceNumber) { logger.debug("[\(#fileID)][\(#line)][\(#function)]: #\(sequenceNumber) timed out") - self.responseSeqNumSet.insert(sequenceNumber) + self.seen.insert(sequenceNumber) self.result.append(.timeout(sequenceNumber)) shouldCloseHandler() } @@ -353,12 +353,11 @@ final class ICMPHandler: PingHandler { func reset() { self.seqToRequest.removeAll() - self.seqToResponse.removeAll() self.result.removeAll() } func shouldCloseHandler(shouldForceClose: Bool = false) { - if self.responseSeqNumSet.count == self.totalCount || shouldForceClose { + if self.seen.count == self.totalCount || shouldForceClose { logger.debug("[\(#fileID)][\(#line)][\(#function)]: should close icmp handler") self.icmpPingPromise.succeed(self.result) } diff --git a/Tests/HTTPChannelTests/HTTPTracingHandlerTests.swift b/Tests/HTTPChannelTests/HTTPTracingHandlerTests.swift index 3abd50f..b58c0b2 100644 --- a/Tests/HTTPChannelTests/HTTPTracingHandlerTests.swift +++ b/Tests/HTTPChannelTests/HTTPTracingHandlerTests.swift @@ -68,6 +68,7 @@ final class HTTPTracingHandlerTests: XCTestCase { XCTAssertEqual(request.method, HTTPMethod.GET) XCTAssertEqual(request.uri, config.url.uri) XCTAssertEqual(request.headers, config.httpHeaders) + promise.succeed(.ok(2, 0, 0)) default: XCTFail("Should receive a head and end. But received head = \(String(describing: head)), end = \(String(describing: end))") } From 46bf972af7ea9c5d7668720b79d59044da68da6f Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Thu, 26 Sep 2024 23:40:34 -0700 Subject: [PATCH 02/11] allow icmp to bind to a specific interface --- Sources/LCLPing/ICMP/ICMPPingClient.swift | 92 +++++++++++++++++---- Sources/LCLPing/Models/Errors+LCLPing.swift | 6 ++ 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 7295843..f66e25c 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -78,23 +78,31 @@ public final class ICMPPingClient: Pingable { } } } - - for cnt in 0.. Self { + .init(level: .ipv6, name: name) + } +} diff --git a/Sources/LCLPing/Models/Errors+LCLPing.swift b/Sources/LCLPing/Models/Errors+LCLPing.swift index 0ba0545..2370018 100644 --- a/Sources/LCLPing/Models/Errors+LCLPing.swift +++ b/Sources/LCLPing/Models/Errors+LCLPing.swift @@ -52,6 +52,8 @@ public enum PingError: Error { case icmpPointerIndicatesError case icmpMissingARequiredOption case icmpBadLength + case icmpDestinationNotMulticast + case icmpUnixDomainSocketForMulticast case httpMissingHost case httpMissingSchema @@ -165,6 +167,10 @@ extension PingError: CustomStringConvertible { return "Missing HTTP response." case .httpInvalidURLSessionTask(let id): return "URLSession Task \(id) is invalid." + case .icmpDestinationNotMulticast: + return "Destination address is not a multicast address." + case .icmpUnixDomainSocketForMulticast: + return "Cannot bind a unix domain soket as a multicast socket." } } From 07366b27bf3d286a54578b4907cc5fba24d28210 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Thu, 26 Sep 2024 23:55:51 -0700 Subject: [PATCH 03/11] fix type for linux platform --- Sources/LCLPing/ICMP/ICMPPingClient.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index f66e25c..2538219 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -181,13 +181,13 @@ public final class ICMPPingClient: Pingable { #if canImport(Darwin) return channel.setOption(.ipOption(.ip_bound_if), value: CInt(device.interfaceIndex)) #elseif canImport(Glibc) - return channel.setOption(.ipOption(.so_bindtodevice), value: CInt(device.interfaceIndex)) + return channel.setOption(.ipOption(.so_bindtodevice), value: device.interfaceIndex) #endif case .v6: #if canImport(Darwin) return channel.setOption(.ipv6Option(.ip_bound_if), value: CInt(device.interfaceIndex)) #elseif canImport(Glibc) - return channel.setOption(.ipv6Option(.so_bindtodevice), value: CInt(device.interfaceIndex)) + return channel.setOption(.ipv6Option(.so_bindtodevice), value: device.interfaceIndex) #endif case .unixDomainSocket: self.stateLock.withLock { From 6db84a424c81d4c3ef3c412755c1ed3c689d6131 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Thu, 26 Sep 2024 23:56:30 -0700 Subject: [PATCH 04/11] bump swiftnio version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 1a26e82..71b6c30 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/apple/swift-nio.git", from: "2.72.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.73.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.25.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1") From d2a7b7f8fc0201d921317d96777d6b1730ff4395 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Sun, 29 Sep 2024 22:02:59 -0700 Subject: [PATCH 05/11] allow http ping to bind to the device --- Sources/LCLPing/HTTP/HTTPPingClient.swift | 26 ++++++++++++++++--- Sources/LCLPing/HTTP/NIOHTTPClient.swift | 24 +++++++++++++++++ Sources/LCLPing/ICMP/ICMPPingClient.swift | 18 ++----------- Sources/LCLPing/Models/Errors+LCLPing.swift | 9 ++++--- .../Utilities/LCLPing+ChannelOption.swift | 20 ++++++++++++++ .../Utilities/LCLPing+SocketOption.swift | 22 ++++++++++++++++ 6 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 Sources/LCLPing/Utilities/LCLPing+ChannelOption.swift create mode 100644 Sources/LCLPing/Utilities/LCLPing+SocketOption.swift diff --git a/Sources/LCLPing/HTTP/HTTPPingClient.swift b/Sources/LCLPing/HTTP/HTTPPingClient.swift index a01d7a4..b71981f 100644 --- a/Sources/LCLPing/HTTP/HTTPPingClient.swift +++ b/Sources/LCLPing/HTTP/HTTPPingClient.swift @@ -178,6 +178,9 @@ extension HTTPPingClient { /// The DNS-resolved address according to the URL endpoint public let resolvedAddress: SocketAddress + + /// The outgoing device associated with the given interface name + public var device: NIONetworkDevice? /// Initialize a HTTP Ping Client `Configuration`. /// @@ -190,6 +193,7 @@ extension HTTPPingClient { /// - useServerTimimg: Indicate whether the HTTP Ping Client should take `ServerTiming` attribute /// from the reponse header. /// - useURLSession: Indicate whether the HTTP Ping Client should use native URLSession implementation. + /// - outgoingInterfaceName: the interface name for which the outbound data will be sent to. /// /// - Throws: /// - httpMissingHost: if URL does not include any host information. @@ -201,7 +205,8 @@ extension HTTPPingClient { connectionTimeout: TimeAmount = .seconds(5), headers: [String: String] = Configuration.defaultHeaders, useServerTiming: Bool = false, - useURLSession: Bool = false) throws { + useURLSession: Bool = false, + outgoingInterfaceName: String? = nil) throws { self.url = url self.count = count self.readTimeout = readTimeout @@ -244,6 +249,17 @@ extension HTTPPingClient { // NOTE: URLSession is not fully supported in swift-corelibs-foundation self.useURLSession = false #endif + + for device in try System.enumerateDevices() { + if device.name == outgoingInterfaceName, let address = device.address { + switch (address.protocol, self.resolvedAddress.protocol) { + case (.inet, .inet), (.inet6, .inet6): + self.device = device + default: + continue + } + } + } } /// Initialize a HTTP Ping Client `Configuration`. @@ -257,6 +273,7 @@ extension HTTPPingClient { /// - useServerTimimg: Indicate whether the HTTP Ping Client should take `ServerTiming` attribute /// from the reponse header. /// - useURLSession: Indicate whether the HTTP Ping Client should use native URLSession implementation. + /// - outgoingInterfaceName: the interface name for which the outbound data will be sent to /// /// - Throws: /// - httpMissingHost: if URL does not include any host information. @@ -268,7 +285,8 @@ extension HTTPPingClient { connectionTimeout: TimeAmount = .seconds(5), headers: [String: String] = Configuration.defaultHeaders, useServerTiming: Bool = false, - useURLSession: Bool = false) throws { + useURLSession: Bool = false, + outgoingIntefaceName: String? = nil) throws { guard let urlObj = URL(string: url) else { throw PingError.invalidURL(url) } @@ -279,8 +297,8 @@ extension HTTPPingClient { connectionTimeout: connectionTimeout, headers: headers, useServerTiming: useServerTiming, - useURLSession: useURLSession - ) + useURLSession: useURLSession, + outgoingInterfaceName: outgoingIntefaceName) } /// Initialize a HTTP Ping Client `Configuration`. diff --git a/Sources/LCLPing/HTTP/NIOHTTPClient.swift b/Sources/LCLPing/HTTP/NIOHTTPClient.swift index 054d236..1f25629 100644 --- a/Sources/LCLPing/HTTP/NIOHTTPClient.swift +++ b/Sources/LCLPing/HTTP/NIOHTTPClient.swift @@ -198,6 +198,30 @@ final class NIOHTTPClient: Pingable { } catch { return channel.eventLoop.makeFailedFuture(error) } + + if let device = self.configuration.device { + switch device.address { + case .v4: + #if canImport(Darwin) + return channel.setOption(.ipOption(.ip_bound_if), value: CInt(device.interfaceIndex)) + #elseif canImport(Glibc) + return channel.setOption(.ipOption(.so_bindtodevice), value: device.interfaceIndex) + #endif + case .v6: + #if canImport(Darwin) + return channel.setOption(.ipv6Option(.ip_bound_if), value: CInt(device.interfaceIndex)) + #elseif canImport(Glibc) + return channel.setOption(.ipv6Option(.so_bindtodevice), value: device.interfaceIndex) + #endif + case .unixDomainSocket: + self.stateLock.withLock { + self.state = .error + } + return channel.eventLoop.makeFailedFuture(PingError.httpBindToUnixDomainSocket) + default: + () + } + } return channel.eventLoop.makeSucceededVoidFuture() } diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 2538219..79b1db4 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -193,7 +193,7 @@ public final class ICMPPingClient: Pingable { self.stateLock.withLock { self.state = .error } - return channel.eventLoop.makeFailedFuture(PingError.icmpUnixDomainSocketForMulticast) + return channel.eventLoop.makeFailedFuture(PingError.icmpBindToUnixDomainSocket) default: () } @@ -238,7 +238,7 @@ extension ICMPPingClient { /// The resolved socket address public let resolvedAddress: SocketAddress - /// The multicast outgoing device associated with the given interface name + /// The outgoing device associated with the given interface name public var device: NIONetworkDevice? public init(endpoint: EndpointTarget, @@ -280,17 +280,3 @@ extension ICMPPingClient { case ipv6(String, Int?) } } - -extension NIOBSDSocket.Option { - #if canImport(Darwin) - public static let ip_bound_if: NIOBSDSocket.Option = Self(rawValue: IP_BOUND_IF) - #elseif canImport(Glibc) - public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE) - #endif -} - -extension ChannelOption where Self == ChannelOptions.Types.SocketOption { - public static func ipv6Option(_ name: NIOBSDSocket.Option) -> Self { - .init(level: .ipv6, name: name) - } -} diff --git a/Sources/LCLPing/Models/Errors+LCLPing.swift b/Sources/LCLPing/Models/Errors+LCLPing.swift index 2370018..e33924d 100644 --- a/Sources/LCLPing/Models/Errors+LCLPing.swift +++ b/Sources/LCLPing/Models/Errors+LCLPing.swift @@ -53,7 +53,7 @@ public enum PingError: Error { case icmpMissingARequiredOption case icmpBadLength case icmpDestinationNotMulticast - case icmpUnixDomainSocketForMulticast + case icmpBindToUnixDomainSocket case httpMissingHost case httpMissingSchema @@ -61,6 +61,7 @@ public enum PingError: Error { case httpInvalidHandlerState case httpMissingResponse case httpInvalidURLSessionTask(Int) + case httpBindToUnixDomainSocket case invalidHexFormat @@ -169,8 +170,10 @@ extension PingError: CustomStringConvertible { return "URLSession Task \(id) is invalid." case .icmpDestinationNotMulticast: return "Destination address is not a multicast address." - case .icmpUnixDomainSocketForMulticast: - return "Cannot bind a unix domain soket as a multicast socket." + case .icmpBindToUnixDomainSocket: + return "Cannot bind to a unix domain socket device." + case .httpBindToUnixDomainSocket: + return "Cannot bind to a unix domain socket device." } } diff --git a/Sources/LCLPing/Utilities/LCLPing+ChannelOption.swift b/Sources/LCLPing/Utilities/LCLPing+ChannelOption.swift new file mode 100644 index 0000000..fd65e42 --- /dev/null +++ b/Sources/LCLPing/Utilities/LCLPing+ChannelOption.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the LCL open source project +// +// Copyright (c) 2021-2024 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import NIOCore + +extension ChannelOption where Self == ChannelOptions.Types.SocketOption { + public static func ipv6Option(_ name: NIOBSDSocket.Option) -> Self { + .init(level: .ipv6, name: name) + } +} diff --git a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift new file mode 100644 index 0000000..0b47ef9 --- /dev/null +++ b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift @@ -0,0 +1,22 @@ +// +// This source file is part of the LCL open source project +// +// Copyright (c) 2021-2024 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import NIOCore + +extension NIOBSDSocket.Option { + #if canImport(Darwin) + public static let ip_bound_if: NIOBSDSocket.Option = Self(rawValue: IP_BOUND_IF) + #elseif canImport(Glibc) + public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE) + #endif +} From 10d3417babfebd1d9d3e3f13915cb6045558b809 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Mon, 30 Sep 2024 00:37:06 -0700 Subject: [PATCH 06/11] add ipv6_bound_if option and fix comments --- Sources/LCLPing/HTTP/HTTPPingClient.swift | 3 +++ Sources/LCLPing/HTTP/NIOHTTPClient.swift | 16 ++++++---------- Sources/LCLPing/ICMP/ICMPPingClient.swift | 17 ++++++++--------- .../Utilities/LCLPing+SocketOption.swift | 1 + 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Sources/LCLPing/HTTP/HTTPPingClient.swift b/Sources/LCLPing/HTTP/HTTPPingClient.swift index b71981f..409da9e 100644 --- a/Sources/LCLPing/HTTP/HTTPPingClient.swift +++ b/Sources/LCLPing/HTTP/HTTPPingClient.swift @@ -259,6 +259,9 @@ extension HTTPPingClient { continue } } + if self.device != nil { + break + } } } diff --git a/Sources/LCLPing/HTTP/NIOHTTPClient.swift b/Sources/LCLPing/HTTP/NIOHTTPClient.swift index 1f25629..f350ba8 100644 --- a/Sources/LCLPing/HTTP/NIOHTTPClient.swift +++ b/Sources/LCLPing/HTTP/NIOHTTPClient.swift @@ -200,27 +200,23 @@ final class NIOHTTPClient: Pingable { } if let device = self.configuration.device { + #if canImport(Darwin) switch device.address { case .v4: - #if canImport(Darwin) return channel.setOption(.ipOption(.ip_bound_if), value: CInt(device.interfaceIndex)) - #elseif canImport(Glibc) - return channel.setOption(.ipOption(.so_bindtodevice), value: device.interfaceIndex) - #endif case .v6: - #if canImport(Darwin) - return channel.setOption(.ipv6Option(.ip_bound_if), value: CInt(device.interfaceIndex)) - #elseif canImport(Glibc) - return channel.setOption(.ipv6Option(.so_bindtodevice), value: device.interfaceIndex) - #endif + return channel.setOption(.ipv6Option(.ipv6_bound_if), value: CInt(device.interfaceIndex)) case .unixDomainSocket: self.stateLock.withLock { self.state = .error } - return channel.eventLoop.makeFailedFuture(PingError.httpBindToUnixDomainSocket) + return channel.eventLoop.makeFailedFuture(PingError.icmpBindToUnixDomainSocket) default: () } + #elseif canImport(Glibc) + return channel.setOption(.socketOption(.so_bindtodevice), value: device.interfaceIndex) + #endif } return channel.eventLoop.makeSucceededVoidFuture() diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 79b1db4..0cdef56 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -176,19 +176,12 @@ public final class ICMPPingClient: Pingable { } if let device = self.configuration.device { + #if canImport(Darwin) switch device.address { case .v4: - #if canImport(Darwin) return channel.setOption(.ipOption(.ip_bound_if), value: CInt(device.interfaceIndex)) - #elseif canImport(Glibc) - return channel.setOption(.ipOption(.so_bindtodevice), value: device.interfaceIndex) - #endif case .v6: - #if canImport(Darwin) - return channel.setOption(.ipv6Option(.ip_bound_if), value: CInt(device.interfaceIndex)) - #elseif canImport(Glibc) - return channel.setOption(.ipv6Option(.so_bindtodevice), value: device.interfaceIndex) - #endif + return channel.setOption(.ipv6Option(.ipv6_bound_if), value: CInt(device.interfaceIndex)) case .unixDomainSocket: self.stateLock.withLock { self.state = .error @@ -197,6 +190,9 @@ public final class ICMPPingClient: Pingable { default: () } + #elseif canImport(Glibc) + return channel.setOption(.socketOption(.so_bindtodevice), value: device.interfaceIndex) + #endif } return channel.eventLoop.makeSucceededVoidFuture() @@ -270,6 +266,9 @@ extension ICMPPingClient { continue } } + if self.device != nil { + break + } } } } diff --git a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift index 0b47ef9..17d2083 100644 --- a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift +++ b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift @@ -16,6 +16,7 @@ import NIOCore extension NIOBSDSocket.Option { #if canImport(Darwin) public static let ip_bound_if: NIOBSDSocket.Option = Self(rawValue: IP_BOUND_IF) + public static let ipv6_bound_if: NIOBSDSocket.Option = Self(rawValue: IPV6_BOUND_IF) #elseif canImport(Glibc) public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE) #endif From a1f798105ebe79d3b0dac3ff75a90a0576d84d76 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Mon, 30 Sep 2024 10:58:01 -0700 Subject: [PATCH 07/11] fix comment --- Sources/LCLPing/HTTP/HTTPPingClient.swift | 12 ++++++------ Sources/LCLPing/ICMP/ICMPPingClient.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/LCLPing/HTTP/HTTPPingClient.swift b/Sources/LCLPing/HTTP/HTTPPingClient.swift index 409da9e..86bdd47 100644 --- a/Sources/LCLPing/HTTP/HTTPPingClient.swift +++ b/Sources/LCLPing/HTTP/HTTPPingClient.swift @@ -193,7 +193,7 @@ extension HTTPPingClient { /// - useServerTimimg: Indicate whether the HTTP Ping Client should take `ServerTiming` attribute /// from the reponse header. /// - useURLSession: Indicate whether the HTTP Ping Client should use native URLSession implementation. - /// - outgoingInterfaceName: the interface name for which the outbound data will be sent to. + /// - deviceName: the interface name for which the outbound data will be sent to. /// /// - Throws: /// - httpMissingHost: if URL does not include any host information. @@ -206,7 +206,7 @@ extension HTTPPingClient { headers: [String: String] = Configuration.defaultHeaders, useServerTiming: Bool = false, useURLSession: Bool = false, - outgoingInterfaceName: String? = nil) throws { + deviceName: String? = nil) throws { self.url = url self.count = count self.readTimeout = readTimeout @@ -251,7 +251,7 @@ extension HTTPPingClient { #endif for device in try System.enumerateDevices() { - if device.name == outgoingInterfaceName, let address = device.address { + if device.name == deviceName, let address = device.address { switch (address.protocol, self.resolvedAddress.protocol) { case (.inet, .inet), (.inet6, .inet6): self.device = device @@ -276,7 +276,7 @@ extension HTTPPingClient { /// - useServerTimimg: Indicate whether the HTTP Ping Client should take `ServerTiming` attribute /// from the reponse header. /// - useURLSession: Indicate whether the HTTP Ping Client should use native URLSession implementation. - /// - outgoingInterfaceName: the interface name for which the outbound data will be sent to + /// - deviceName: the interface name for which the outbound data will be sent to /// /// - Throws: /// - httpMissingHost: if URL does not include any host information. @@ -289,7 +289,7 @@ extension HTTPPingClient { headers: [String: String] = Configuration.defaultHeaders, useServerTiming: Bool = false, useURLSession: Bool = false, - outgoingIntefaceName: String? = nil) throws { + deviceName: String? = nil) throws { guard let urlObj = URL(string: url) else { throw PingError.invalidURL(url) } @@ -301,7 +301,7 @@ extension HTTPPingClient { headers: headers, useServerTiming: useServerTiming, useURLSession: useURLSession, - outgoingInterfaceName: outgoingIntefaceName) + deviceName: deviceName) } /// Initialize a HTTP Ping Client `Configuration`. diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 0cdef56..5163bd7 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -242,7 +242,7 @@ extension ICMPPingClient { interval: TimeAmount = .seconds(1), timeToLive: UInt8 = 64, timeout: TimeAmount = .seconds(1), - outgoingIntefaceName: String? = nil + deviceName: String? = nil ) throws { self.endpoint = endpoint self.count = count @@ -257,7 +257,7 @@ extension ICMPPingClient { } for device in try System.enumerateDevices() { - if device.name == outgoingIntefaceName, let address = device.address { + if device.name == deviceName, let address = device.address { switch (address.protocol, self.endpoint) { case (.inet, .ipv4), (.inet6, .ipv6): logger.info("device selcted is \(device)") From 74e4a795e35645caca517a128d230776ec09268f Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Tue, 1 Oct 2024 00:55:32 -0700 Subject: [PATCH 08/11] fix bind to device on linux --- Sources/LCLPing/HTTP/NIOHTTPClient.swift | 2 +- Sources/LCLPing/ICMP/ICMPPingClient.swift | 2 +- .../LCLPing/Utilities/LCLPing+SocketOption.swift | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/LCLPing/HTTP/NIOHTTPClient.swift b/Sources/LCLPing/HTTP/NIOHTTPClient.swift index f350ba8..c6fa3eb 100644 --- a/Sources/LCLPing/HTTP/NIOHTTPClient.swift +++ b/Sources/LCLPing/HTTP/NIOHTTPClient.swift @@ -215,7 +215,7 @@ final class NIOHTTPClient: Pingable { () } #elseif canImport(Glibc) - return channel.setOption(.socketOption(.so_bindtodevice), value: device.interfaceIndex) + return (channel as! SocketOptionProvider).setBindToDevice(device.name) #endif } diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 5163bd7..7898297 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -191,7 +191,7 @@ public final class ICMPPingClient: Pingable { () } #elseif canImport(Glibc) - return channel.setOption(.socketOption(.so_bindtodevice), value: device.interfaceIndex) + return (channel as! SocketOptionProvider).setBindToDevice(device.name) #endif } diff --git a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift index 17d2083..2e654be 100644 --- a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift +++ b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift @@ -21,3 +21,17 @@ extension NIOBSDSocket.Option { public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE) #endif } + +extension SocketOptionProvider { + #if canImport(Glibc) + /// Sets the socket option SO_BINDTODEVICE to `value`. + /// + /// - parameters: + /// - value: The value to set SO_BINDTODEVICE to. + /// - returns: An `EventLoopFuture` that fires when the option has been set, + /// or if an error has occurred. + public func setBindToDevice(_ value: String) -> EventLoopFuture { + self.unsafeSetSocketOption(level: .socket, name: .so_bindtodevice, value: value) + } + #endif +} From ac64fa4f8b0b85b8b854b1c7dad4be6b8409f520 Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Tue, 1 Oct 2024 23:43:31 -0700 Subject: [PATCH 09/11] make device object immutable; support musl platform --- Sources/LCLPing/HTTP/HTTPPingClient.swift | 2 +- Sources/LCLPing/HTTP/NIOHTTPClient.swift | 2 +- Sources/LCLPing/ICMP/ICMPPingClient.swift | 4 ++-- Sources/LCLPing/Utilities/LCLPing+SocketOption.swift | 12 ++++++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/LCLPing/HTTP/HTTPPingClient.swift b/Sources/LCLPing/HTTP/HTTPPingClient.swift index 86bdd47..57f4a44 100644 --- a/Sources/LCLPing/HTTP/HTTPPingClient.swift +++ b/Sources/LCLPing/HTTP/HTTPPingClient.swift @@ -180,7 +180,7 @@ extension HTTPPingClient { public let resolvedAddress: SocketAddress /// The outgoing device associated with the given interface name - public var device: NIONetworkDevice? + public private(set) var device: NIONetworkDevice? /// Initialize a HTTP Ping Client `Configuration`. /// diff --git a/Sources/LCLPing/HTTP/NIOHTTPClient.swift b/Sources/LCLPing/HTTP/NIOHTTPClient.swift index c6fa3eb..f30fc58 100644 --- a/Sources/LCLPing/HTTP/NIOHTTPClient.swift +++ b/Sources/LCLPing/HTTP/NIOHTTPClient.swift @@ -214,7 +214,7 @@ final class NIOHTTPClient: Pingable { default: () } - #elseif canImport(Glibc) + #elseif canImport(Glibc) || canImport(Musl) return (channel as! SocketOptionProvider).setBindToDevice(device.name) #endif } diff --git a/Sources/LCLPing/ICMP/ICMPPingClient.swift b/Sources/LCLPing/ICMP/ICMPPingClient.swift index 7898297..513475d 100644 --- a/Sources/LCLPing/ICMP/ICMPPingClient.swift +++ b/Sources/LCLPing/ICMP/ICMPPingClient.swift @@ -190,7 +190,7 @@ public final class ICMPPingClient: Pingable { default: () } - #elseif canImport(Glibc) + #elseif canImport(Glibc) || canImport(Musl) return (channel as! SocketOptionProvider).setBindToDevice(device.name) #endif } @@ -235,7 +235,7 @@ extension ICMPPingClient { public let resolvedAddress: SocketAddress /// The outgoing device associated with the given interface name - public var device: NIONetworkDevice? + public private(set) var device: NIONetworkDevice? public init(endpoint: EndpointTarget, count: Int = 10, diff --git a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift index 2e654be..158948a 100644 --- a/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift +++ b/Sources/LCLPing/Utilities/LCLPing+SocketOption.swift @@ -12,18 +12,26 @@ import Foundation import NIOCore +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif extension NIOBSDSocket.Option { #if canImport(Darwin) public static let ip_bound_if: NIOBSDSocket.Option = Self(rawValue: IP_BOUND_IF) public static let ipv6_bound_if: NIOBSDSocket.Option = Self(rawValue: IPV6_BOUND_IF) - #elseif canImport(Glibc) + #elseif canImport(Glibc) || canImport(Musl) public static let so_bindtodevice = Self(rawValue: SO_BINDTODEVICE) #endif } extension SocketOptionProvider { - #if canImport(Glibc) + #if canImport(Glibc) || canImport(Musl) + /// Sets the socket option SO_BINDTODEVICE to `value`. /// /// - parameters: From db042d097d7cffb97d8ac91854f0d21385a6319b Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Wed, 9 Apr 2025 23:15:30 -0700 Subject: [PATCH 10/11] bump swiftnio to 2.81.0 --- Package.swift | 2 +- Sources/LCLPing/Configuration.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 71b6c30..2055e39 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/apple/swift-nio.git", from: "2.73.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.25.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1") diff --git a/Sources/LCLPing/Configuration.swift b/Sources/LCLPing/Configuration.swift index 46d031a..1175244 100644 --- a/Sources/LCLPing/Configuration.swift +++ b/Sources/LCLPing/Configuration.swift @@ -13,7 +13,7 @@ import Foundation /// Type of reachability test that `LCLPing` supports -public enum PingType { +public enum PingType: String, CaseIterable { /// Perform reachability ping test through the ICMP protocol. case icmp From f4f3068da5d04431de52a667bab0c2ee493fe3dc Mon Sep 17 00:00:00 2001 From: Zhennan Zhou Date: Wed, 9 Apr 2025 23:38:44 -0700 Subject: [PATCH 11/11] add caseIterable and identifiable to PingType --- Sources/LCLPing/Configuration.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/LCLPing/Configuration.swift b/Sources/LCLPing/Configuration.swift index 1175244..5246e92 100644 --- a/Sources/LCLPing/Configuration.swift +++ b/Sources/LCLPing/Configuration.swift @@ -13,7 +13,8 @@ import Foundation /// Type of reachability test that `LCLPing` supports -public enum PingType: String, CaseIterable { +public enum PingType: String, CaseIterable, Identifiable { + public var id: Self { self } /// Perform reachability ping test through the ICMP protocol. case icmp