The short version:
Closing NSStream doesn't completely reset SSL connection as subsequent connection attempts have SSL SessionID from the previously closed session. Session ID only resets after process restart or after ~10 minutes.
The long version:
Our Cocoa app using is NSStream to connect over TCP with TLS 1.2 to the server powered by libevent (libevent.org). The initial connection works fine, both sets of certtificates are validated, events are being delivered normally. When the client decides to close the connection (see "closeWithReason" method below) , it basically just closes both NSInputStream and NSOutputStream objects. What happens next is that any subsequent connection attempt with the same settings/certificates results in SSL Handshake Error -9806 ("connection closed via error"). The issue resolves itself when either process is restarted (client or server), it also fixes itself in about 10 minute time. After that (restart or time period) the connection is established as usual.
Digging with Wireshark revealed that the client tries to establish the connection after breaking it previously, it sends "Client Hello" message but the server doesn't reply with "Server Hello". As it turns out the subsequent connections have SSL Session ID filled with the id from the session that was closed before, therefore server doesn't treat it as a new connection and it all goes weird from there. As it was mentioned above, this Session ID is reset in about 10 minutes time.
What we've tried:
- Using kCFStreamPropertyShouldCloseNativeSocket flag
- Closing the socket on the client (in addition to closing the streams).
- Experimenting with reseting different properties of SSL Session when we closing the connection.
Notes:
The issue is reproducable when the server is using libevent library but it all works fine when server is using OpenSSL. It seems that they treat having SSL Session ID for otherwise new connection differently.
Here is the code snippet for the connection logic though there is nothing out of ordniary there:
@property (strong) NSInputStream *inputStream;
@property (strong) NSOutputStream *outputStream;
- (void)connectToServer
{
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef) self.ip, (uint) self.port, &readStream, &writeStream);
self.inputStream = (NSInputStream *) CFBridgingRelease(readStream);
self.outputStream = (NSOutputStream *) CFBridgingRelease(writeStream);
self.identity = //Valid client certificate
NSDictionary *settings = @{ (NSString *) kCFStreamSSLLevel : @"kCFStreamSocketSecurityLevelTLSv1_2",
(NSString *) kCFStreamSSLCertificates : self.identity,
(NSString *) kCFStreamSSLValidatesCertificateChain : (id) kCFBooleanFalse,
(NSString *) kCFStreamSSLPeerName : (NSNull *) kCFNull,
};
[self.inputStream setProperty:settings forKey:(NSString *) kCFStreamPropertySSLSettings];
[self.inputStream setProperty:(id) kCFBooleanTrue forKey:(NSString *) kCFStreamPropertyShouldCloseNativeSocket];
[self.outputStream setProperty:settings forKey:(NSString *) kCFStreamPropertySSLSettings];
[self.outputStream setProperty:(id) kCFBooleanTrue forKey:(NSString *) kCFStreamPropertyShouldCloseNativeSocket];
[self.inputStream setDelegate:self];
[self.outputStream setDelegate:self];
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.inputStream open];
[self.outputStream open];
}
- (void)closeWithReason:(NSString *)reason
{
//...
[self.inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.inputStream close];
[self.outputStream close];
self.inputStream = nil;
self.outputStream = nil;
}