Skip to content

Handle port scan on SSL server gracefully.#750

Closed
mayfield wants to merge 1 commit intotornadoweb:masterfrom
mayfield:master
Closed

Handle port scan on SSL server gracefully.#750
mayfield wants to merge 1 commit intotornadoweb:masterfrom
mayfield:master

Conversation

@mayfield
Copy link

Port scans and other quickly closing connections will
result in an unwrapped/disconnected socket. Tornado
needs to check the ssl socket's _connected attribute
prior to attempting a ssl handshake in this case.

This patch makes the SSL server behave similarly to its'
non-ssl counterpart.

Port scans and other quickly closing connections will
result in an unwrapped/disconnected socket.  Tornado
needs to check the ssl socket's _connected attribute
prior to attempting a ssl handshake in this case.
@bdarnell
Copy link
Member

socket._connected is not a documented public attribute; is there any way to do this that doesn't rely on implementation details? Can you reproduce this in a test case? What exactly has to happen for the failure to occur (and how exactly does do_handshake fail when this happens)?

@mayfield
Copy link
Author

Hi Ben,

Yeah, I'm not particularly happy with the impl either. Frankly I'm not convinced it shouldn't be fixed in the python ssl module, but that's of little utility for deployments on deprecated python releases.

As for test case, I considered trying to just emulate the behavior of nmap but you'd ultimately being dealing with a race where this ordering is required but not guaranteed...

port scanner    |    tornado
----------------------------
connect (SYN)   |    
                |    accept(ACK)
close(FIN/RST)  |    
                |    getpeername : silent error in ssl
                |    ...
                |    read Attempt : Type Error: sslobj is now None                                                             

The problem I see there is that the accept() and subsequent getpeername() in the SSLSocket constructor would be difficult to separate deterministically such that you could close the connecting socket between the two. It could probably be done with some monkey patching or zeus forbid sys.settrace(). Please advise if you want me to attempt such feats.

Regarding not using the "_connected" attribute. The only other approach that I can see would be testing for _sslobj, which is probably just as offensive as testing _connected; I could also catch the TypeError in the read method but this is probably too big of a net, IMO. The responsible code in SSLSocket.init looks like this..

    # see if it's connected
    try:
        socket.getpeername(self)
    except socket_error, e:
        if e.errno != errno.ENOTCONN:
            raise
        # no, no connection yet
        self._connected = False
        self._sslobj = None
    else:
        # yes, create the SSL object
        self._connected = True
        self._sslobj = _ssl.sslwrap(self._sock, server_side,
                                    keyfile, certfile,
                                    cert_reqs, ssl_version, ca_certs,
                                    ciphers)

@bdarnell
Copy link
Member

Excellent, thanks for the analysis. Does it really get far enough to have a TypeError in read? It looks to me like it would be an AttributeError in do_handshake, but I've only read the code, I haven't tried too hard to reproduce the problem.

What platform are you on? My understanding is that once a socket becomes connected, getpeername is valid until the socket is closed; it won't concurrently revert to its ENOTCONN state. This suggests that we can work around the problem by calling original_connection.getpeername in TCPServer._handle_connection before or after the call to ssl_wrap_socket, and if it throws an exception don't proceed to create the IOStream. I'd prefer to do this test before calling ssl_wrap_socket, if I'm right about when getpeername can change out from under you (although that would imply the equally-surprising result that accept() is returning sockets that are not actually connected)

@bdarnell
Copy link
Member

It looks like I was wrong: getpeername will return ENOTCONN if a previously-connected socket gets an RST (https://code.google.com/p/pyftpdlib/issues/detail?id=100). I can reproduce this now (with nmap -sT), so I've got a fix (which is different for mac and linux)

Related python bug: http://bugs.python.org/issue13721

@bdarnell bdarnell closed this in 0bb4d9b Apr 28, 2013
@mayfield
Copy link
Author

mayfield commented May 1, 2013

Thanks Ben.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants