Browse Source

Add bind and connect APIs for VSOCK sockets (#1636)

Motivation:

The VSOCK address family facilitates communication between virtual machines and the host they are running that need a communications channel that is independent of virtual machine network configuration.

While both GRPC has support for building channels using sockets that were created out of band (`withConnectedSocket(_:)` and `withBoundSocket(_:)`, there are no APIs that facilitate the creation of VSOCK-based channels.

https://github.com/apple/swift-nio/pull/2479 adds a `VsockAddress` type and associated bootstrap APIs, but these need corresponding APIs here to be used in GRPC clients and servers.

Modifications:

- Add `ConnectionTarget.vsockAddress` and `BindTarget.vsockAddress `.
- Add `ClientBootstrapProtocol connect(to vsockAddress:)` and `ServerBootstrapProtocol.bind(to vsockAddress:)`.
- Add `Server.bind(vsockAddress: VsockAddress)`.

Signed-off-by: Si Beaumont <beaumont@apple.com>
Si Beaumont 2 years ago
parent
commit
5e3e3f577f

+ 1 - 1
Package.swift

@@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni
 let packageDependencies: [Package.Dependency] = [
   .package(
     url: "https://github.com/apple/swift-nio.git",
-    from: "2.42.0"
+    from: "2.58.0"
   ),
   .package(
     url: "https://github.com/apple/swift-nio-http2.git",

+ 11 - 0
Sources/GRPC/ClientConnection.swift

@@ -23,6 +23,7 @@ import Logging
 import NIOCore
 import NIOHPACK
 import NIOHTTP2
+import NIOPosix
 #if canImport(NIOSSL)
 import NIOSSL
 #endif
@@ -269,6 +270,7 @@ public struct ConnectionTarget: Sendable {
     case unixDomainSocket(String)
     case socketAddress(SocketAddress)
     case connectedSocket(NIOBSDSocket.Handle)
+    case vsockAddress(VsockAddress)
   }
 
   internal var wrapped: Wrapped
@@ -301,6 +303,11 @@ public struct ConnectionTarget: Sendable {
     return ConnectionTarget(.connectedSocket(socket))
   }
 
+  /// A vsock socket.
+  public static func vsockAddress(_ vsockAddress: VsockAddress) -> ConnectionTarget {
+    return ConnectionTarget(.vsockAddress(vsockAddress))
+  }
+
   @usableFromInline
   var host: String {
     switch self.wrapped {
@@ -312,6 +319,8 @@ public struct ConnectionTarget: Sendable {
       return address.host
     case .unixDomainSocket, .socketAddress(.unixDomainSocket), .connectedSocket:
       return "localhost"
+    case let .vsockAddress(address):
+      return "vsock://\(address.cid)"
     }
   }
 }
@@ -552,6 +561,8 @@ extension ClientBootstrapProtocol {
       return self.connect(to: address)
     case let .connectedSocket(socket):
       return self.withConnectedSocket(socket)
+    case let .vsockAddress(address):
+      return self.connect(to: address)
     }
   }
 }

+ 11 - 1
Sources/GRPC/PlatformSupport.swift

@@ -118,6 +118,7 @@ public protocol ClientBootstrapProtocol {
   func connect(host: String, port: Int) -> EventLoopFuture<Channel>
   func connect(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
   func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel>
+  func connect(to vsockAddress: VsockAddress) -> EventLoopFuture<Channel>
 
   func connectTimeout(_ timeout: TimeAmount) -> Self
   func channelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
@@ -144,6 +145,10 @@ extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {
   public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
     preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)")
   }
+
+  public func connect(to vsockAddress: VsockAddress) -> EventLoopFuture<Channel> {
+    preconditionFailure("NIOTSConnectionBootstrap does not support connect(to vsockAddress:)")
+  }
 }
 #endif
 
@@ -154,6 +159,7 @@ public protocol ServerBootstrapProtocol {
   func bind(host: String, port: Int) -> EventLoopFuture<Channel>
   func bind(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
   func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel>
+  func bind(to vsockAddress: VsockAddress) -> EventLoopFuture<Channel>
 
   #if swift(>=5.7)
   @preconcurrency
@@ -189,7 +195,11 @@ extension ServerBootstrap: ServerBootstrapProtocol {}
 @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
 extension NIOTSListenerBootstrap: ServerBootstrapProtocol {
   public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
-    preconditionFailure("NIOTSListenerBootstrap does not support withConnectedSocket(_:)")
+    preconditionFailure("NIOTSListenerBootstrap does not support withBoundSocket(_:)")
+  }
+
+  public func bind(to vsockAddress: VsockAddress) -> EventLoopFuture<Channel> {
+    preconditionFailure("NIOTSListenerBootstrap does not support bind(to vsockAddress:)")
   }
 }
 #endif

+ 3 - 0
Sources/GRPC/Server.swift

@@ -538,6 +538,9 @@ extension ServerBootstrapProtocol {
 
     case let .connectedSocket(socket):
       return self.withBoundSocket(socket)
+
+    case let .vsockAddress(address):
+      return self.bind(to: address)
     }
   }
 }

+ 7 - 0
Sources/GRPC/ServerBuilder.swift

@@ -15,6 +15,7 @@
  */
 import Logging
 import NIOCore
+import NIOPosix
 
 #if canImport(Network)
 import Security
@@ -60,6 +61,12 @@ extension Server {
       self.configuration.tlsConfiguration = self.maybeTLS
       return Server.start(configuration: self.configuration)
     }
+
+    public func bind(vsockAddress: VsockAddress) -> EventLoopFuture<Server> {
+      self.configuration.target = .vsockAddress(vsockAddress)
+      self.configuration.tlsConfiguration = self.maybeTLS
+      return Server.start(configuration: self.configuration)
+    }
   }
 }
 

+ 67 - 0
Tests/GRPCTests/VsockSocketTests.swift

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022, 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 EchoImplementation
+import EchoModel
+import GRPC
+import NIOPosix
+import XCTest
+
+class VsockSocketTests: GRPCTestCase {
+  func testVsockSocket() throws {
+    try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable")
+    let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1)
+    defer {
+      XCTAssertNoThrow(try group.syncShutdownGracefully())
+    }
+
+    // Setup a server.
+    let server = try Server.insecure(group: group)
+      .withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
+      .bind(vsockAddress: .init(cid: .any, port: 31234))
+      .wait()
+    defer {
+      XCTAssertNoThrow(try server.close().wait())
+    }
+
+    let channel = try GRPCChannelPool.with(
+      target: .vsockAddress(.init(cid: .local, port: 31234)),
+      transportSecurity: .plaintext,
+      eventLoopGroup: group
+    )
+    defer {
+      XCTAssertNoThrow(try channel.close().wait())
+    }
+
+    let client = Echo_EchoNIOClient(channel: channel)
+    let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait()
+    XCTAssertEqual(resp.text, "Swift echo get: Hello")
+  }
+
+  private func vsockAvailable() -> Bool {
+    let fd: CInt
+    #if os(Linux)
+    fd = socket(AF_VSOCK, CInt(SOCK_STREAM.rawValue), 0)
+    #elseif canImport(Darwin)
+    fd = socket(AF_VSOCK, SOCK_STREAM, 0)
+    #else
+    fd = -1
+    #endif
+    if fd == -1 { return false }
+    precondition(close(fd) == 0)
+    return true
+  }
+}