Pārlūkot izejas kodu

Add a socket address type (#1803)

Motivation:

Name resolvers resolve targets to an address we know how to connect to.
This could be represented as an IPv4, IPv6, UDS or VSOCK address which
can all be wrapped up in a broader 'SocketAddress'.

Modifications:

- Add a SocketAddress type and tests

Results:

Can represent an address to connect to
George Barnett 1 gadu atpakaļ
vecāks
revīzija
e05326be1c

+ 313 - 0
Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift

@@ -0,0 +1,313 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// An address to which a socket may connect or bind.
+public struct SocketAddress: Hashable, Sendable {
+  private enum Value: Hashable, Sendable {
+    case ipv4(IPv4)
+    case ipv6(IPv6)
+    case unix(UnixDomainSocket)
+    case vsock(VirtualSocket)
+  }
+
+  private var value: Value
+  private init(_ value: Value) {
+    self.value = value
+  }
+
+  /// Returns the address as an IPv4 address, if possible.
+  public var ipv4: IPv4? {
+    switch self.value {
+    case .ipv4(let address):
+      return address
+    default:
+      return nil
+    }
+  }
+
+  /// Returns the address as an IPv6 address, if possible.
+  public var ipv6: IPv6? {
+    switch self.value {
+    case .ipv6(let address):
+      return address
+    default:
+      return nil
+    }
+  }
+
+  /// Returns the address as an Unix domain socket address, if possible.
+  public var unixDomainSocket: UnixDomainSocket? {
+    switch self.value {
+    case .unix(let address):
+      return address
+    default:
+      return nil
+    }
+  }
+
+  /// Returns the address as an VSOCK address, if possible.
+  public var virtualSocket: VirtualSocket? {
+    switch self.value {
+    case .vsock(let address):
+      return address
+    default:
+      return nil
+    }
+  }
+}
+
+extension SocketAddress {
+  /// Creates a socket address by wrapping a ``SocketAddress/IPv4-swift.struct``.
+  public static func ipv4(_ address: IPv4) -> Self {
+    return Self(.ipv4(address))
+  }
+
+  /// Creates a socket address by wrapping a ``SocketAddress/IPv6-swift.struct``.
+  public static func ipv6(_ address: IPv6) -> Self {
+    return Self(.ipv6(address))
+  }
+
+  /// Creates a socket address by wrapping a ``SocketAddress/UnixDomainSocket-swift.struct``.
+  public static func unixDomainSocket(_ address: UnixDomainSocket) -> Self {
+    return Self(.unix(address))
+  }
+
+  /// Creates a socket address by wrapping a ``SocketAddress/VirtualSocket-swift.struct``.
+  public static func vsock(_ address: VirtualSocket) -> Self {
+    return Self(.vsock(address))
+  }
+}
+
+extension SocketAddress {
+  /// Creates an IPv4 socket address.
+  public static func ipv4(host: String, port: Int) -> Self {
+    return .ipv4(IPv4(host: host, port: port))
+  }
+
+  /// Creates an IPv6 socket address.
+  public static func ipv6(host: String, port: Int) -> Self {
+    return .ipv6(IPv6(host: host, port: port))
+  }
+  /// Creates a Unix socket address.
+  public static func unixDomainSocket(path: String) -> Self {
+    return .unixDomainSocket(UnixDomainSocket(path: path))
+  }
+
+  /// Create a Virtual Socket ('vsock') address.
+  public static func vsock(contextID: VirtualSocket.ContextID, port: VirtualSocket.Port) -> Self {
+    return .vsock(VirtualSocket(contextID: contextID, port: port))
+  }
+}
+
+extension SocketAddress: CustomStringConvertible {
+  public var description: String {
+    switch self.value {
+    case .ipv4(let address):
+      return String(describing: address)
+    case .ipv6(let address):
+      return String(describing: address)
+    case .unix(let address):
+      return String(describing: address)
+    case .vsock(let address):
+      return String(describing: address)
+    }
+  }
+}
+
+extension SocketAddress {
+  public struct IPv4: Hashable, Sendable {
+    /// The resolved host address.
+    public var host: String
+    /// The port to connect to.
+    public var port: Int
+
+    /// Creates a new IPv4 address.
+    ///
+    /// - Parameters:
+    ///   - host: Resolved host address.
+    ///   - port: Port to connect to.
+    public init(host: String, port: Int) {
+      self.host = host
+      self.port = port
+    }
+  }
+
+  public struct IPv6: Hashable, Sendable {
+    /// The resolved host address.
+    public var host: String
+    /// The port to connect to.
+    public var port: Int
+
+    /// Creates a new IPv6 address.
+    ///
+    /// - Parameters:
+    ///   - host: Resolved host address.
+    ///   - port: Port to connect to.
+    public init(host: String, port: Int) {
+      self.host = host
+      self.port = port
+    }
+  }
+
+  public struct UnixDomainSocket: Hashable, Sendable {
+    /// The path name of the Unix domain socket.
+    public var path: String
+
+    /// Create a new Unix domain socket address.
+    ///
+    /// - Parameter path: The path name of the Unix domain socket.
+    public init(path: String) {
+      self.path = path
+    }
+  }
+
+  public struct VirtualSocket: Hashable, Sendable {
+    /// A context identifier.
+    ///
+    /// Indicates the source or destination which is either a virtual machine or the host.
+    public var contextID: ContextID
+
+    /// The port number.
+    public var port: Port
+
+    /// Create a new VSOCK address.
+    ///
+    /// - Parameters:
+    ///   - contextID: The context ID (or 'cid') of the address.
+    ///   - port: The port number.
+    public init(contextID: ContextID, port: Port) {
+      self.contextID = contextID
+      self.port = port
+    }
+
+    public struct Port: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral {
+      /// The port number.
+      public var rawValue: UInt32
+
+      public init(rawValue: UInt32) {
+        self.rawValue = rawValue
+      }
+
+      public init(integerLiteral value: UInt32) {
+        self.rawValue = value
+      }
+
+      public init(_ value: Int) {
+        self.init(rawValue: UInt32(bitPattern: Int32(truncatingIfNeeded: value)))
+      }
+
+      /// Used to bind to any port number.
+      ///
+      /// This is equal to `VMADDR_PORT_ANY (-1U)`.
+      public static var any: Self {
+        Self(rawValue: UInt32(bitPattern: -1))
+      }
+    }
+
+    public struct ContextID: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral {
+      /// The context identifier.
+      public var rawValue: UInt32
+
+      public init(rawValue: UInt32) {
+        self.rawValue = rawValue
+      }
+
+      public init(integerLiteral value: UInt32) {
+        self.rawValue = value
+      }
+
+      public init(_ value: Int) {
+        self.rawValue = UInt32(bitPattern: Int32(truncatingIfNeeded: value))
+      }
+
+      /// Wildcard, matches any address.
+      ///
+      /// On all platforms, using this value with `bind(2)` means "any address".
+      ///
+      /// On Darwin platforms, the man page states this can be used with `connect(2)`
+      /// to mean "this host".
+      ///
+      /// This is equal to `VMADDR_CID_ANY (-1U)`.
+      public static var any: Self {
+        Self(rawValue: UInt32(bitPattern: -1))
+      }
+
+      /// The address of the hypervisor.
+      ///
+      /// This is equal to `VMADDR_CID_HYPERVISOR (0)`.
+      public static var hypervisor: Self {
+        Self(rawValue: 0)
+      }
+
+      /// The address of the host.
+      ///
+      /// This is equal to `VMADDR_CID_HOST (2)`.
+      public static var host: Self {
+        Self(rawValue: 2)
+      }
+
+      /// The address for local communication (loopback).
+      ///
+      /// This directs packets to the same host that generated them.  This is useful for testing
+      /// applications on a single host and for debugging.
+      ///
+      /// This is equal to `VMADDR_CID_LOCAL (1)` on platforms that define it.
+      ///
+      /// - Warning: `VMADDR_CID_LOCAL (1)` is available from Linux 5.6. Its use is unsupported on
+      /// other platforms.
+      /// - SeeAlso: https://man7.org/linux/man-pages/man7/vsock.7.html
+      public static var local: Self {
+        Self(rawValue: 1)
+      }
+    }
+  }
+}
+
+extension SocketAddress.IPv4: CustomStringConvertible {
+  public var description: String {
+    "[ipv4]\(self.host):\(self.port)"
+  }
+}
+
+extension SocketAddress.IPv6: CustomStringConvertible {
+  public var description: String {
+    "[ipv6]\(self.host):\(self.port)"
+  }
+}
+
+extension SocketAddress.UnixDomainSocket: CustomStringConvertible {
+  public var description: String {
+    "[unix]\(self.path)"
+  }
+}
+
+extension SocketAddress.VirtualSocket: CustomStringConvertible {
+  public var description: String {
+    "[vsock]\(self.contextID):\(self.port)"
+  }
+}
+
+extension SocketAddress.VirtualSocket.ContextID: CustomStringConvertible {
+  public var description: String {
+    self == .any ? "-1" : String(describing: self.rawValue)
+  }
+}
+
+extension SocketAddress.VirtualSocket.Port: CustomStringConvertible {
+  public var description: String {
+    self == .any ? "-1" : String(describing: self.rawValue)
+  }
+}

+ 80 - 0
Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import GRPCHTTP2Core
+import XCTest
+
+final class SocketAddressTests: XCTestCase {
+  func testSocketAddressUnwrapping() {
+    var address: SocketAddress = .ipv4(host: "foo", port: 42)
+    XCTAssertEqual(address.ipv4, SocketAddress.IPv4(host: "foo", port: 42))
+    XCTAssertNil(address.ipv6)
+    XCTAssertNil(address.unixDomainSocket)
+    XCTAssertNil(address.virtualSocket)
+
+    address = .ipv6(host: "bar", port: 42)
+    XCTAssertEqual(address.ipv6, SocketAddress.IPv6(host: "bar", port: 42))
+    XCTAssertNil(address.ipv4)
+    XCTAssertNil(address.unixDomainSocket)
+    XCTAssertNil(address.virtualSocket)
+
+    address = .unixDomainSocket(path: "baz")
+    XCTAssertEqual(address.unixDomainSocket, SocketAddress.UnixDomainSocket(path: "baz"))
+    XCTAssertNil(address.ipv4)
+    XCTAssertNil(address.ipv6)
+    XCTAssertNil(address.virtualSocket)
+
+    address = .vsock(contextID: .any, port: .any)
+    XCTAssertEqual(address.virtualSocket, SocketAddress.VirtualSocket(contextID: .any, port: .any))
+    XCTAssertNil(address.ipv4)
+    XCTAssertNil(address.ipv6)
+    XCTAssertNil(address.unixDomainSocket)
+  }
+
+  func testSocketAddressDescription() {
+    var address: SocketAddress = .ipv4(host: "127.0.0.1", port: 42)
+    XCTAssertDescription(address, "[ipv4]127.0.0.1:42")
+
+    address = .ipv6(host: "::1", port: 42)
+    XCTAssertDescription(address, "[ipv6]::1:42")
+
+    address = .unixDomainSocket(path: "baz")
+    XCTAssertDescription(address, "[unix]baz")
+
+    address = .vsock(contextID: 314, port: 159)
+    XCTAssertDescription(address, "[vsock]314:159")
+    address = .vsock(contextID: .any, port: .any)
+    XCTAssertDescription(address, "[vsock]-1:-1")
+
+  }
+
+  func testSocketAddressSubTypesDescription() {
+    let ipv4 = SocketAddress.IPv4(host: "127.0.0.1", port: 42)
+    XCTAssertDescription(ipv4, "[ipv4]127.0.0.1:42")
+
+    let ipv6 = SocketAddress.IPv6(host: "foo", port: 42)
+    XCTAssertDescription(ipv6, "[ipv6]foo:42")
+
+    let uds = SocketAddress.UnixDomainSocket(path: "baz")
+    XCTAssertDescription(uds, "[unix]baz")
+
+    var vsock = SocketAddress.VirtualSocket(contextID: 314, port: 159)
+    XCTAssertDescription(vsock, "[vsock]314:159")
+    vsock.contextID = .any
+    vsock.port = .any
+    XCTAssertDescription(vsock, "[vsock]-1:-1")
+  }
+}

+ 9 - 0
Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift

@@ -28,3 +28,12 @@ func XCTAssertThrowsError<T, E: Error>(
     errorHandler(error)
   }
 }
+
+func XCTAssertDescription(
+  _ subject: some CustomStringConvertible,
+  _ expected: String,
+  file: StaticString = #filePath,
+  line: UInt = #line
+) {
+  XCTAssertEqual(String(describing: subject), expected, file: file, line: line)
+}