]> git.ipfire.org Git - thirdparty/squid.git/blame - src/ssl/PeerConnector.cc
Splice implementation
[thirdparty/squid.git] / src / ssl / PeerConnector.cc
CommitLineData
a23223bf
CT
1/*
2 * DEBUG: section 17 Request Forwarding
3 *
4 */
5
6#include "squid.h"
7#include "acl/FilledChecklist.h"
8#include "base/AsyncCbdataCalls.h"
9#include "CachePeer.h"
10#include "client_side.h"
11#include "comm/Loops.h"
12#include "errorpage.h"
13#include "fde.h"
14#include "globals.h"
15#include "HttpRequest.h"
16#include "neighbors.h"
31855516 17#include "ssl/bio.h"
a23223bf
CT
18#include "ssl/cert_validate_message.h"
19#include "ssl/Config.h"
20#include "ssl/ErrorDetail.h"
21#include "ssl/helper.h"
22#include "ssl/PeerConnector.h"
23#include "ssl/ServerBump.h"
24#include "ssl/support.h"
25#include "SquidConfig.h"
26
27CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeerConnector);
28
29Ssl::PeerConnector::PeerConnector(
30 HttpRequestPointer &aRequest,
31 const Comm::ConnectionPointer &aServerConn,
93ead3fd 32 const Comm::ConnectionPointer &aClientConn,
a23223bf
CT
33 AsyncCall::Pointer &aCallback):
34 AsyncJob("Ssl::PeerConnector"),
35 request(aRequest),
36 serverConn(aServerConn),
93ead3fd 37 clientConn(aClientConn),
a23223bf
CT
38 callback(aCallback)
39{
40 // if this throws, the caller's cb dialer is not our CbDialer
41 Must(dynamic_cast<CbDialer*>(callback->getDialer()));
42}
43
44Ssl::PeerConnector::~PeerConnector()
45{
46 debugs(83, 5, "Peer connector " << this << " gone");
47}
48
49bool Ssl::PeerConnector::doneAll() const
50{
51 return (!callback || callback->canceled()) && AsyncJob::doneAll();
52}
53
54/// Preps connection and SSL state. Calls negotiate().
55void
56Ssl::PeerConnector::start()
57{
58 AsyncJob::start();
59
60 if (prepareSocket()) {
61 initializeSsl();
62 negotiateSsl();
63 }
64}
65
66void
67Ssl::PeerConnector::commCloseHandler(const CommCloseCbParams &params)
68{
69 debugs(83, 5, "FD " << params.fd << ", Ssl::PeerConnector=" << params.data);
70 connectionClosed("Ssl::PeerConnector::commCloseHandler");
71}
72
73void
74Ssl::PeerConnector::connectionClosed(const char *reason)
75{
76 mustStop(reason);
77 callback = NULL;
78}
79
80bool
81Ssl::PeerConnector::prepareSocket()
82{
83 const int fd = serverConnection()->fd;
84 if (!Comm::IsConnOpen(serverConn) || fd_table[serverConn->fd].closing()) {
85 connectionClosed("Ssl::PeerConnector::prepareSocket");
86 return false;
87 }
88
89 // watch for external connection closures
90 typedef CommCbMemFunT<Ssl::PeerConnector, CommCloseCbParams> Dialer;
91 closeHandler = JobCallback(9, 5, Dialer, this, Ssl::PeerConnector::commCloseHandler);
92 comm_add_close_handler(fd, closeHandler);
93 return true;
94}
95
96void
97Ssl::PeerConnector::initializeSsl()
98{
a23223bf
CT
99 SSL_CTX *sslContext = NULL;
100 const CachePeer *peer = serverConnection()->getPeer();
101 const int fd = serverConnection()->fd;
102
103 if (peer) {
104 assert(peer->use_ssl);
105 sslContext = peer->sslContext;
106 } else {
107 sslContext = ::Config.ssl_client.sslContext;
108 }
109
110 assert(sslContext);
111
31855516
CT
112 SSL *ssl = Ssl::CreateClient(sslContext, fd, "server https start");
113 if (!ssl) {
a23223bf
CT
114 ErrorState *anErr = new ErrorState(ERR_SOCKET_FAILURE, Http::scInternalServerError, request.getRaw());
115 anErr->xerrno = errno;
116 debugs(83, DBG_IMPORTANT, "Error allocating SSL handle: " << ERR_error_string(ERR_get_error(), NULL));
117 bail(anErr);
118 return;
119 }
120
a23223bf
CT
121 if (peer) {
122 if (peer->ssldomain)
123 SSL_set_ex_data(ssl, ssl_ex_index_server, peer->ssldomain);
124
125#if NOT_YET
126
127 else if (peer->name)
128 SSL_set_ex_data(ssl, ssl_ex_index_server, peer->name);
129
130#endif
131
132 else
133 SSL_set_ex_data(ssl, ssl_ex_index_server, peer->host);
134
135 if (peer->sslSession)
136 SSL_set_session(ssl, peer->sslSession);
137
31855516
CT
138 } else if (request->clientConnectionManager->sslBumpMode == Ssl::bumpPeekAndSplice) {
139 SSL *clientSsl = fd_table[request->clientConnectionManager->clientConnection->fd].ssl;
140 BIO *b = SSL_get_rbio(clientSsl);
141 Ssl::ClientBio *clnBio = static_cast<Ssl::ClientBio *>(b->ptr);
142 const Ssl::Bio::sslFeatures &features = clnBio->getFeatures();
143 if (features.sslVersion != -1) {
144 SSL_set_ssl_method(ssl, Ssl::method(features.toSquidSSLVersion()));
145 if (!features.serverName.empty())
146 SSL_set_tlsext_host_name(ssl, features.serverName.c_str());
147 if (!features.clientRequestedCiphers.empty())
148 SSL_set_cipher_list(ssl, features.clientRequestedCiphers.c_str());
149#ifdef SSL_OP_NO_COMPRESSION /* XXX: OpenSSL 0.9.8k lacks SSL_OP_NO_COMPRESSION */
150 if (features.compressMethod == 0)
151 SSL_set_options(ssl, SSL_OP_NO_COMPRESSION);
152#endif
153 if (features.sslVersion >= 3) {
154 b = SSL_get_rbio(ssl);
155 Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(b->ptr);
156 srvBio->setClientRandom(features.client_random);
157 srvBio->recordInput(true);
158 }
159 }
a23223bf
CT
160 } else {
161 // While we are peeking at the certificate, we may not know the server
162 // name that the client will request (after interception or CONNECT)
163 // unless it was the CONNECT request with a user-typed address.
164 const char *hostname = request->GetHost();
165 const bool hostnameIsIp = request->GetHostIsNumeric();
166 const bool isConnectRequest = request->clientConnectionManager.valid() &&
167 !request->clientConnectionManager->port->flags.isIntercepted();
168 if (!request->flags.sslPeek || isConnectRequest)
169 SSL_set_ex_data(ssl, ssl_ex_index_server, (void*)hostname);
170
171 // Use SNI TLS extension only when we connect directly
172 // to the origin server and we know the server host name.
173 if (!hostnameIsIp)
174 Ssl::setClientSNI(ssl, hostname);
175 }
176
177 // If CertValidation Helper used do not lookup checklist for errors,
178 // but keep a list of errors to send it to CertValidator
179 if (!Ssl::TheConfig.ssl_crt_validator) {
180 // Create the ACL check list now, while we have access to more info.
181 // The list is used in ssl_verify_cb() and is freed in ssl_free().
182 if (acl_access *acl = ::Config.ssl_client.cert_error) {
183 ACLFilledChecklist *check = new ACLFilledChecklist(acl, request.getRaw(), dash_str);
184 // check->fd(fd); XXX: need client FD here
185 SSL_set_ex_data(ssl, ssl_ex_index_cert_error_check, check);
186 }
187 }
188
189 // store peeked cert to check SQUID_X509_V_ERR_CERT_CHANGE
190 X509 *peeked_cert;
191 if (request->clientConnectionManager.valid() &&
192 request->clientConnectionManager->serverBump() &&
193 (peeked_cert = request->clientConnectionManager->serverBump()->serverCert.get())) {
194 CRYPTO_add(&(peeked_cert->references),1,CRYPTO_LOCK_X509);
195 SSL_set_ex_data(ssl, ssl_ex_index_ssl_peeked_cert, peeked_cert);
196 }
a23223bf
CT
197}
198
199void
200Ssl::PeerConnector::negotiateSsl()
201{
202 if (!Comm::IsConnOpen(serverConnection()) || fd_table[serverConnection()->fd].closing())
203 return;
204
205 const int fd = serverConnection()->fd;
206 SSL *ssl = fd_table[fd].ssl;
207 const int result = SSL_connect(ssl);
208 if (result <= 0) {
209 handleNegotiateError(result);
210 return; // we might be gone by now
211 }
212
213 if (request->clientConnectionManager.valid()) {
214 // remember the server certificate from the ErrorDetail object
215 if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) {
216 serverBump->serverCert.reset(SSL_get_peer_certificate(ssl));
217
218 // remember validation errors, if any
219 if (Ssl::CertErrors *errs = static_cast<Ssl::CertErrors *>(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors)))
220 serverBump->sslErrors = cbdataReference(errs);
221 }
222 }
223
224 if (serverConnection()->getPeer() && !SSL_session_reused(ssl)) {
225 if (serverConnection()->getPeer()->sslSession)
226 SSL_SESSION_free(serverConnection()->getPeer()->sslSession);
227
228 serverConnection()->getPeer()->sslSession = SSL_get1_session(ssl);
229 }
230
231 if (Ssl::TheConfig.ssl_crt_validator) {
232 Ssl::CertValidationRequest validationRequest;
233 // WARNING: Currently we do not use any locking for any of the
234 // members of the Ssl::CertValidationRequest class. In this code the
235 // Ssl::CertValidationRequest object used only to pass data to
236 // Ssl::CertValidationHelper::submit method.
237 validationRequest.ssl = ssl;
238 validationRequest.domainName = request->GetHost();
239 if (Ssl::CertErrors *errs = static_cast<Ssl::CertErrors *>(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors)))
240 // validationRequest disappears on return so no need to cbdataReference
241 validationRequest.errors = errs;
242 else
243 validationRequest.errors = NULL;
244 try {
245 debugs(83, 5, "Sending SSL certificate for validation to ssl_crtvd.");
246 Ssl::CertValidationHelper::GetInstance()->sslSubmit(validationRequest, sslCrtvdHandleReplyWrapper, this);
247 return;
248 } catch (const std::exception &e) {
249 debugs(83, DBG_IMPORTANT, "ERROR: Failed to compose ssl_crtvd " <<
250 "request for " << validationRequest.domainName <<
251 " certificate: " << e.what() << "; will now block to " <<
252 "validate that certificate.");
253 // fall through to do blocking in-process generation.
254 ErrorState *anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw());
255 bail(anErr);
256 if (serverConnection()->getPeer()) {
257 peerConnectFailed(serverConnection()->getPeer());
258 }
259 serverConn->close();
260 return;
261 }
262 }
263
264 callBack();
265}
266
93ead3fd 267void switchToTunnel(HttpRequest *request, int *status_ptr, Comm::ConnectionPointer & clientConn, Comm::ConnectionPointer &srvConn);
31855516
CT
268void
269Ssl::PeerConnector::checkForPeekAndSplice()
270{
271 SSL *ssl = fd_table[serverConn->fd].ssl;
272 BIO *b = SSL_get_rbio(ssl);
273 Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(b->ptr);
274 debugs(83,5, "Will check for peek and splice on fd " << serverConn->fd);
93ead3fd 275 const bool splice = true;
31855516
CT
276 if (!splice) {
277 //Allow write, proceed with the connection
278 srvBio->holdWrite(false);
279 srvBio->recordInput(false);
280 Comm::SetSelect(serverConn->fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0);
281 debugs(83,5, "Retry the fwdNegotiateSSL on fd " << serverConn->fd);
93ead3fd
CT
282 } else {
283 static int status_code = 0;
284 debugs(83,5, "Revert to tunnel fd " << clientConn->fd << " with fd " << serverConn->fd);
285 switchToTunnel(request.getRaw(), &status_code, clientConn, serverConn);
31855516
CT
286 }
287}
288
a23223bf
CT
289void
290Ssl::PeerConnector::sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &validationResponse)
291{
292 Ssl::PeerConnector *connector = (Ssl::PeerConnector *)(data);
293 connector->sslCrtvdHandleReply(validationResponse);
294}
295
296void
297Ssl::PeerConnector::sslCrtvdHandleReply(Ssl::CertValidationResponse const &validationResponse)
298{
299 Ssl::CertErrors *errs = NULL;
300 Ssl::ErrorDetail *errDetails = NULL;
301 bool validatorFailed = false;
302 if (!Comm::IsConnOpen(serverConnection())) {
303 return;
304 }
305
306 debugs(83,5, request->GetHost() << " cert validation result: " << validationResponse.resultCode);
307
308 if (validationResponse.resultCode == HelperReply::Error)
309 errs = sslCrtvdCheckForErrors(validationResponse, errDetails);
310 else if (validationResponse.resultCode != HelperReply::Okay)
311 validatorFailed = true;
312
313 if (!errDetails && !validatorFailed) {
314 callBack();
315 return;
316 }
317
318 ErrorState *anErr = NULL;
319 if (validatorFailed) {
320 anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw());
321 } else {
322
323 // Check the list error with
324 if (errDetails && request->clientConnectionManager.valid()) {
325 // remember the server certificate from the ErrorDetail object
326 if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) {
327 // remember validation errors, if any
328 if (errs) {
329 if (serverBump->sslErrors)
330 cbdataReferenceDone(serverBump->sslErrors);
331 serverBump->sslErrors = cbdataReference(errs);
332 }
333 }
334 }
335
336 anErr = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scServiceUnavailable, request.getRaw());
337 anErr->detail = errDetails;
338 /*anErr->xerrno= Should preserved*/
339 }
340
341 bail(anErr);
342 if (serverConnection()->getPeer()) {
343 peerConnectFailed(serverConnection()->getPeer());
344 }
345 serverConn->close();
346 return;
347}
348
349/// Checks errors in the cert. validator response against sslproxy_cert_error.
350/// The first honored error, if any, is returned via errDetails parameter.
351/// The method returns all seen errors except SSL_ERROR_NONE as Ssl::CertErrors.
352Ssl::CertErrors *
353Ssl::PeerConnector::sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &resp, Ssl::ErrorDetail *& errDetails)
354{
355 Ssl::CertErrors *errs = NULL;
356
357 ACLFilledChecklist *check = NULL;
358 if (acl_access *acl = ::Config.ssl_client.cert_error)
359 check = new ACLFilledChecklist(acl, request.getRaw(), dash_str);
360
361 SSL *ssl = fd_table[serverConnection()->fd].ssl;
362 typedef Ssl::CertValidationResponse::RecvdErrors::const_iterator SVCRECI;
363 for (SVCRECI i = resp.errors.begin(); i != resp.errors.end(); ++i) {
364 debugs(83, 7, "Error item: " << i->error_no << " " << i->error_reason);
365
366 assert(i->error_no != SSL_ERROR_NONE);
367
368 if (!errDetails) {
369 bool allowed = false;
370 if (check) {
371 check->sslErrors = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get()));
372 if (check->fastCheck() == ACCESS_ALLOWED)
373 allowed = true;
374 }
375 // else the Config.ssl_client.cert_error access list is not defined
376 // and the first error will cause the error page
377
378 if (allowed) {
379 debugs(83, 3, "bypassing SSL error " << i->error_no << " in " << "buffer");
380 } else {
381 debugs(83, 5, "confirming SSL error " << i->error_no);
382 X509 *brokenCert = i->cert.get();
383 Ssl::X509_Pointer peerCert(SSL_get_peer_certificate(ssl));
384 const char *aReason = i->error_reason.empty() ? NULL : i->error_reason.c_str();
385 errDetails = new Ssl::ErrorDetail(i->error_no, peerCert.get(), brokenCert, aReason);
386 }
387 if (check) {
388 delete check->sslErrors;
389 check->sslErrors = NULL;
390 }
391 }
392
393 if (!errs)
394 errs = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get()));
395 else
396 errs->push_back_unique(Ssl::CertError(i->error_no, i->cert.get()));
397 }
398 if (check)
399 delete check;
400
401 return errs;
402}
403
404/// A wrapper for Comm::SetSelect() notifications.
405void
406Ssl::PeerConnector::NegotiateSsl(int, void *data)
407{
408 PeerConnector *pc = static_cast<PeerConnector*>(data);
409 // Use job calls to add done() checks and other job logic/protections.
410 CallJobHere(83, 7, pc, Ssl::PeerConnector, negotiateSsl);
411}
412
413void
414Ssl::PeerConnector::handleNegotiateError(const int ret)
415{
416 const int fd = serverConnection()->fd;
417 unsigned long ssl_lib_error = SSL_ERROR_NONE;
418 SSL *ssl = fd_table[fd].ssl;
419 int ssl_error = SSL_get_error(ssl, ret);
31855516
CT
420 BIO *b = SSL_get_rbio(ssl);
421 Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(b->ptr);
a23223bf
CT
422
423#ifdef EPROTO
424 int sysErrNo = EPROTO;
425#else
426 int sysErrNo = EACCES;
427#endif
428
429 switch (ssl_error) {
430
431 case SSL_ERROR_WANT_READ:
432 Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, this, 0);
433 return;
434
435 case SSL_ERROR_WANT_WRITE:
31855516
CT
436 if (request->clientConnectionManager->sslBumpMode == Ssl::bumpPeekAndSplice && srvBio->holdWrite()) {
437 debugs(81, DBG_IMPORTANT, "hold write on SSL connection on FD " << fd);
438 checkForPeekAndSplice();
439 return;
440 }
a23223bf
CT
441 Comm::SetSelect(fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0);
442 return;
443
444 case SSL_ERROR_SSL:
445 case SSL_ERROR_SYSCALL:
446 ssl_lib_error = ERR_get_error();
447
448 // store/report errno when ssl_error is SSL_ERROR_SYSCALL, ssl_lib_error is 0, and ret is -1
449 if (ssl_error == SSL_ERROR_SYSCALL && ret == -1 && ssl_lib_error == 0)
450 sysErrNo = errno;
451
452 debugs(83, DBG_IMPORTANT, "Error negotiating SSL on FD " << fd <<
453 ": " << ERR_error_string(ssl_lib_error, NULL) << " (" <<
454 ssl_error << "/" << ret << "/" << errno << ")");
455
456 break; // proceed to the general error handling code
457
458 default:
459 break; // no special error handling for all other errors
460 }
461
462 ErrorState *const anErr = ErrorState::NewForwarding(ERR_SECURE_CONNECT_FAIL, request.getRaw());
463 anErr->xerrno = sysErrNo;
464
465 Ssl::ErrorDetail *errFromFailure = (Ssl::ErrorDetail *)SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail);
466 if (errFromFailure != NULL) {
467 // The errFromFailure is attached to the ssl object
468 // and will be released when ssl object destroyed.
469 // Copy errFromFailure to a new Ssl::ErrorDetail object
470 anErr->detail = new Ssl::ErrorDetail(*errFromFailure);
471 } else {
472 // server_cert can be NULL here
473 X509 *server_cert = SSL_get_peer_certificate(ssl);
474 anErr->detail = new Ssl::ErrorDetail(SQUID_ERR_SSL_HANDSHAKE, server_cert, NULL);
475 X509_free(server_cert);
476 }
477
478 if (ssl_lib_error != SSL_ERROR_NONE)
479 anErr->detail->setLibError(ssl_lib_error);
480
481 if (request->clientConnectionManager.valid()) {
482 // remember the server certificate from the ErrorDetail object
483 if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) {
484 serverBump->serverCert.resetAndLock(anErr->detail->peerCert());
485
486 // remember validation errors, if any
487 if (Ssl::CertErrors *errs = static_cast<Ssl::CertErrors*>(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors)))
488 serverBump->sslErrors = cbdataReference(errs);
489 }
490
491 // For intercepted connections, set the host name to the server
492 // certificate CN. Otherwise, we just hope that CONNECT is using
493 // a user-entered address (a host name or a user-entered IP).
494 const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted();
495 if (request->flags.sslPeek && !isConnectRequest) {
496 if (X509 *srvX509 = anErr->detail->peerCert()) {
497 if (const char *name = Ssl::CommonHostName(srvX509)) {
498 request->SetHost(name);
499 debugs(83, 3, HERE << "reset request host: " << name);
500 }
501 }
502 }
503 }
504
505 bail(anErr);
506}
507
508void
509Ssl::PeerConnector::bail(ErrorState *error)
510{
511 Must(error); // or the recepient will not know there was a problem
512
513 // XXX: forward.cc calls peerConnectSucceeded() after an OK TCP connect but
514 // we call peerConnectFailed() if SSL failed afterwards. Is that OK?
515 // It is not clear whether we should call peerConnectSucceeded/Failed()
516 // based on TCP results, SSL results, or both. And the code is probably not
517 // consistent in this aspect across tunnelling and forwarding modules.
518 if (CachePeer *p = serverConnection()->getPeer())
519 peerConnectFailed(p);
520
521 Must(callback != NULL);
522 CbDialer *dialer = dynamic_cast<CbDialer*>(callback->getDialer());
523 Must(dialer);
524 dialer->answer().error = error;
525
526 callBack();
527 // Our job is done. The callabck recepient will probably close the failed
528 // peer connection and try another peer or go direct (if possible). We
529 // can close the connection ourselves (our error notification would reach
530 // the recepient before the fd-closure notification), but we would rather
531 // minimize the number of fd-closure notifications and let the recepient
532 // manage the TCP state of the connection.
533}
534
535void
536Ssl::PeerConnector::callBack()
537{
538 AsyncCall::Pointer cb = callback;
539 // Do this now so that if we throw below, swanSong() assert that we _tried_
540 // to call back holds.
541 callback = NULL; // this should make done() true
542
543 // remove close handler
544 comm_remove_close_handler(serverConnection()->fd, closeHandler);
545
546 CbDialer *dialer = dynamic_cast<CbDialer*>(cb->getDialer());
547 Must(dialer);
548 dialer->answer().conn = serverConnection();
549 ScheduleCallHere(cb);
550}
551
552
553void
554Ssl::PeerConnector::swanSong()
555{
556 // XXX: unregister fd-closure monitoring and CommSetSelect interest, if any
557 AsyncJob::swanSong();
558 assert(!callback); // paranoid: we have not left the caller waiting
559}
560
561const char *
562Ssl::PeerConnector::status() const
563{
564 static MemBuf buf;
565 buf.reset();
566
567 // TODO: redesign AsyncJob::status() API to avoid this
568 // id and stop reason reporting duplication.
569 buf.append(" [", 2);
570 if (stopReason != NULL) {
571 buf.Printf("Stopped, reason:");
572 buf.Printf("%s",stopReason);
573 }
574 if (serverConn != NULL)
575 buf.Printf(" FD %d", serverConn->fd);
576 buf.Printf(" %s%u]", id.Prefix, id.value);
577 buf.terminate();
578
579 return buf.content();
580}
581
582/* PeerConnectorAnswer */
583
584Ssl::PeerConnectorAnswer::~PeerConnectorAnswer()
585{
586 delete error.get();
587}
588
589std::ostream &
590operator <<(std::ostream &os, const Ssl::PeerConnectorAnswer &answer)
591{
592 return os << answer.conn << ", " << answer.error;
593}