PSA: Python SSL wrap_socket has a bad default behavior for servers

The SSLContext.wrap_socket method has an optional do_handshake_on_connect argument. When True (the default), it performs the TLS handshake (where the client and server agree on a protocol, exchange certificates, etc) directly after the socket is connected.
This is fine for client sockets (as long as you are prepared to handle SSL errors when calling socket.connect — they are a subclass of OSError so that is not too difficult). But on server sockets, the handshake is performed on the newly connected socket returned by socket.accept, before it’s actually returned.
This causes two problems:

  • If a client fails the TLS handshake, accept will throw a SSLError. You can catch it, but you will not know the address of the client that caused the error, because this address is normally returned by accept and accept threw instead of returning.
  • If a client connects and then proceeds to do nothing, the SSL library will wait indefinitely for the handshake to proceed. Your code stays blocked in accept and you cannot handle more clients while this happens! You may think that you could set a timeout on the listening socket, but this timeout is not transferred to the connected socket.

Once you know that, the solution is simple:

  • Pass do_handshake_on_connect=False when wrapping the socket
  • Once accept returns a socket, set a timeout on it (or set it to non-blocking and use asynchronous I/O), then call do_handshake. If that fails with a SSLError (or a socket error…), you can properly log it since accept returned the address of the client.