[swift-server-dev] Draft proposal for TLS Service APIs (please review)

Brent Royal-Gordon brent at architechies.com
Wed Apr 5 06:57:50 CDT 2017


> On Apr 4, 2017, at 9:32 AM, Gelareh Taban <gtaban at us.ibm.com> wrote:
> 
> I think perhaps there is misunderstanding about the role of TransportManager (perhaps naming is the problem after all!). This is not a singleton type of manager - rather it is a per connection manager and it "manages" for example the system socket calls. An instance of transport manager handles one connection. An instance of transport manager has one TLS delegate associated with that connection. Therefore, the delegate is associated with only one connection at a time.

Oh, wow! You're right, I completely misunderstood that; I was under the impression that all of the instances in your diagram were long-lived, but it sounds like they're actually used for one connection/request and then thrown away (or perhaps reused in a pool—doesn't matter much either way).

The use of "management" in these names definitely was part of why I misunderstood the proposal, because in Cocoa, a "manager" is almost always long-lived. Often it's a singleton or is frequently used as a singleton (`Foundation.FileManager`, `CoreLocation.CLLocationManager`, `Photos.PHImageManager`.) Sometimes it's associated with a long-lived object like a document (`Foundation.UndoManager`) or a text editing view (`AppKit.NSLayoutManager`). Very occasionally it's short-lived and manages some particular activity (`CoreData.NSMigrationManager`), but even that class is only short-lived because the activity it manages is relatively brief.

Given that the transport manager and `SSLService` are both dedicated to a single connection, I suppose the idea is that the call to `onAccept` or `onConnect` permanently associates the `SSLService` with the `IORef`, an instance that allows it to communicate with the actual socket/connection. When `onReceive` wants to receive data, it should get it from the `IORef`; when `onSend` wants to send data, it should give it to the `IORef`.

One more architectural question: What is the ownership relationship between the application, web server, HTTP management, and transport management layers? That is, is transport management the layer that drives everything else, or is it more of a utility type used by the other layers?

For the moment, I will assume that the transport manager layer is subordinate to the application or web server.

> If we assume the above, then it is not hard to see that once a context is created for a specific TLS connection, and stored by the TLS service object, the context can be passed between the various OpenSSL or Secure Transport calls. 

Yes, I do see that.

With that in mind, I have a question about the commands: Why are `onServerCreate` and `onAccept` separate, and why are `onClientCreate` and `onConnect` separate? I understand that Secure Transport and OpenSSL do those tasks with separate calls, but is there a reason to perform them at different times? I would think that, by the time a `TLSService` has been instantiated, we have already received a connection and want to start reading data from it. (BlueSocket does appear to separate initialization from handshaking, but it also appears to initialize an `SSLService` before it begins listening, so I'm not sure that makes sense for us.) Eliminating `onServerCreate` and `onClientCreate` would therefore simply remove an opportunity for mistakes: it would no longer be possible to accidentally call `onAccept` on a client `TLSService` or `onConnect` on a server `TLSService`, or to forget one of the two initialization setps. (It also means fewer APIs to name. :^) )

As for naming issues, here's what I would suggest.

* * *

I think the best way to think of the transport manager and the IORef is as two parts of a "connection". What you are calling the transport manager—or at least the relevant piece of it for this discussion—is a `Connection` object. A `Connection` object primarily exists to manage a `RawConnection` instance (equivalent to your `IORef`/`TransportManagerDelegate`), which represents an OS-level network connection. `Connection` tracks the `RawConnection`'s lifetime and adds various smart behaviors to it, including, of course, TLS.

I will now briefly sketch `Connection` and `RawConnection`. Keep in mind that these are not exact specifications and aren't being proposed; they're just meant to generally describe the capabilities of these types. If you ask whether `Connection` should be `final` or argue that `RawConnection` should use traditional Unix socket function names, you're sort of missing the point.

	// A ConnectionConfiguration is some object from a higher layer in the application's stack that can 
	// provide configuration info for Connection. Most importantly, it provides the TLSService.
	// In your diagram, the HTTP management, web server, and application layers might all conform.
	protocol ConnectionConfiguration {
		...
		var tlsService: TLSService?
		...
	}

	class Connection {
		// A bit about how I imagine a Connection would usually be created:
		// 
		// For a client connection, convenience initializers (not shown here) would be used to specify 
		// a destination address, protocol, etc. These would create a system-level socket, convert 
		// it into a `RawConnection`, and pass that to `init(toServerVia:configuration:)`.
		// 
		// For a server connection, a separate `Listener` type would listen to a socket, accept 
		// any connections, and call `init(fromClientVia:configuration:)` for them.
		
		// A Connection may be created as a result of connecting to a server as a client.
		// If so, it goes through this initializer.
		init(toServerVia rawConnection: RawConnection, configuration: ConnectionConfiguration) throws
		
		// A Connection may be created as a result of a client connecting to us as a server.
		// If so, it goes through this initializer.
		init(fromClientVia rawConnection: RawConnection, configuration: ConnectionConfiguration) throws
		
		// The Connection will hold on to its RawConnection and to its TLSService, if it has one.
		var rawConnection: RawConnection { get }
		var tlsService: TLSService? { get }
		
		// Synchronously sends data to the other endpoint. The provided data will all be sent before this 
		// method returns, unless an error occurs during sending. If the underlying connection 
		// closes before all data has been sent, this will throw an error. (The error might include 
		// unsent data; that's something to be designed later.)
		func sendBytes(_ data: Data) throws
		
		// Synchronously receives data from the other endpoint. This call will attempt to receive up to the 
		// indicated number of bytes. If the underlying connection closes before data has 
		// been received, this will throw an error. (Again, the error might include any data that 
		// was received; that's something to be designed later.)
		// 
		// Note: I am not deciding whether this is blocking or nonblocking. It shouldn't affect the 
		// implementation of the TLSService either way—the underlying RawConnection will 
		// either block or it won't.
		func receiveBytes(_ count: Int) throws -> Data
		
		// Closes the connection, notifying the other side if possible. Throws if the connection is 
		// already closed or if another error occurs.
		func close() throws
		
		// Indicates whether, to the `Connection`'s knowledge, the underlying connection has closed. 
		// If `true`, all other calls will throw; if `false`, they *may* throw, but it isn't certain.
		var hasClosed: Bool { get {…} }

		// Cleans up if the connection hasn't been closed yet.
		deinit
	}

	// A RawConnection represents whatever low-level OS resource is used to 
	// communicate with the network.
	protocol RawConnection {
		// These calls all just do the underlying thing that the `Connection` call does.
		func sendBytes(_ data: Data) throws
		func receiveBytes(_ count: Int) throws -> Data
		func close() throws
		
		// Some RawConnections may simply wrap an OS file descriptor; if so, this 
		// property will return it so you can access and use it directly.
		// 
		// Note: We may prefer a design where *all* RawConnections are *always* 
		// file descriptors. If so, we would eliminate the Optional on this type, but we'd 
		// still have the above methods. They basically wrap code that both `Connection` 
		// and a `TLSService` that wrapped a library like Secure Transport which didn't 
		// directly do I/O would call.
		var fileDescriptor: Int32? { get }
	}

So, given these rough designs, here's what I propose for `TLSService`. To the extent that names from the above rough design are present in `TLSService`, they would eventually be adjusted to match names in the larger system—so if `RawConnection` turns out to be called `SocketSink` eventually, we would change both the parameter type and the parameter label for the first two methods.

	protocol TLSService {
		// I'll talk about these two method names, and discuss alternatives, in the prose.
		
		/// Called to indicate that the connection has connected to a server, and that it should 
		/// begin negotiating a TLS connection.
		/// 
		/// -Parameter rawConnection: The underlying OS-level connection. The `TLSService` should 
		/// 			negotiate over this raw connection during this call, and then hold onto the raw 
		/// 			connection for future calls to other methods.
		/// -Throws: If the connection closes, TLS negotiation fails, or any other error occurs.
		/// -Precondition: Neither `didConnectToServer` nor `didConnectFromClient` has previously been 
		/// 				called.
		/// 
		/// -Note: This will typically be called from `Connection(toServerVia:configuration:)`.
		func didConnectToServer(via rawConnection: RawConnection) throws
		
		/// Called to indicate that the connection has connected to a client, and that it should 
		/// wait for that client to begin negotiating a TLS connection, and then negotiate as a server.
		/// 
		/// -Parameter rawConnection: The underlying OS-level connection. The `TLSService` should 
		/// 			negotiate over this raw connection during this call, and then hold onto the raw 
		/// 			connection for future calls to other methods.
		/// -Throws: If the connection closes, TLS negotiation fails, or any other error occurs.
		/// -Precondition: Neither `didConnectToServer` nor `didConnectFromClient` has previously been 
		/// 				called.
		/// 
		/// -Note: This will typically be called from `Connection(fromClientVia:configuration:)`.
		func didConnectFromClient(via rawConnection: RawConnection) throws
		
		// The next three calls do not have `will` or `did` prefixes because they are imperative: 
		// they do not merely tell the `TLSService` that something happened—they actually *cause* 
		// it to happen.
		
		/// Called when the connection wants to send the indicated plaintext. This call should 
		/// encipher it, wrap it in TLS framing and protocol data, and send it through the raw 
		/// connection.
		/// 
		/// -Parameter data: The plaintext data to send.
		/// -Throws: If the connection closes, a TLS error occurs, or any other error occurs.
		/// -Precondition: Either `didConnectToServer` or `didConnectFromClient` has been called.
		/// 
		/// -Note: This will typically be called from `Connection.sendBytes(_:)`.
		func sendBytes(_ data: Data) throws

		/// Called when the connection wants to receive data. This call should attempt to receive 
		/// the indicated number of bytes of TLS data, then interpret all TLS protocol messages, 
		/// extract any plaintext from it, and return it.
		/// 
		/// -Parameter count: The number of bytes to attempt to read.
		/// -Throws: If the connection closes, a TLS error occurs, or any other error occurs.
		/// -Precondition: Either `didConnectToServer` or `didConnectFromClient` has been called.
		/// 
		/// -Note: This will typically be called from `Connection.receiveBytes(_:)`.
		func receiveBytes(_ count: Int) throws -> Data
		
		/// Called when the connection wants to close. This call should notify the other side of 
		/// the shutdown through a TLS alert and then close the connection.
		/// 
		/// -Throws: If the connection closes before it finishes, a TLS error occurs, or any other error occurs.
		/// -Precondition: Either `didConnectToServer` or `didConnectFromClient` has been called.
		/// 
		/// -Note: This will typically be called from `Connection.close`.
		func close() throws

		/// Called when the `Connection` that owns this `TLSService` is about to deinitialize itself.
		/// This is a good opportunity to tear down any state associated with the `TLSService`.
		func willDeinitConnection()
	}

A few points to talk about here:

* I am using `Data` here because you mentioned that you're interested in switching to that, but I'm actually pretty agnostic about whether we should use `Data`, `Unsafe(Mutable)RawBufferPointer`, `Unsafe(Mutable)BufferPointer<UInt8>`, or anything else out there. We might even use `Data` on the user-facing `Connection` interfaces, but use `Unsafe(Mutable)RawBufferPointer` in the `TLSService` and `RawConnection` interfaces for speed. My only concern with the buffer pointer design is that, if we try to `receiveBytes(_:)` and after de-TLSing it find that we have more data than will fit in the buffer, I'm not sure how we'd get the excess data back to the caller.

* The first two method names, for the methods used after accepting or connecting.

I chose these names because they read well, describe when they are called clearly, and match the names in the `Connection` sketch. However, they're a little bit...out there. I have two alternative suggestions:

	* Traditional/conservative: `didConnect(via:)` and `didAccept(via:)`.

	* Imperative: `negotiateAsClient(via:)` and `negotiateAsServer(via:)`.

* `sendBytes`, `receiveBytes`, and `close`: These three calls are imperative because, at least in this design, they actually *cause* the thing indicated to happen—`sendBytes` actually sends the data, `receiveBytes` actually goes and gets the data, and `close()` actually causes a disconnect. If this weren't the case, and these calls were more like filters—for instance, `sendBytes` returned data for the caller to send over the connection, instead of actually calling the raw connection and asking it to be sent—then I would suggest `willSendBytes(_:)`, `didReceiveBytes(_:)`, and `willClose()`.

* `willDeinitConnection()` here is defined as happening at connection deinit time, which may be slightly later than would be ideal. However, it seems like the logic necessary to make it happen as soon as possible would be a lot more complicated, so I'm not sure it's worth it to do so.

* And finally, an overall design question: Would we be better off doing this as a sort of generalized filter or layer on the `Connection` object? This is, for instance, how OpenSSL works: it wraps the underlying I/O primitive in a BIO structure, then adds an "object" of sorts to that BIO which transparently applies TLS as you read and write. A similar approach in Swift would probably imagine this protocol not as a TLS-specific hook, but rather as a way to capture *all* operations on a connection and do arbitrary things with them. I'm not necessarily advocating this approach, because it couples the TLS system pretty strongly to our future socket abstraction, but I'm wondering if it was considered or not.

* * *

Anyway, I hope this is helpful. It certainly is long. :^)

-- 
Brent Royal-Gordon
Architechies

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-server-dev/attachments/20170405/e193c593/attachment.html>


More information about the swift-server-dev mailing list