diff --git a/lib/isc/netmgr/http.c b/lib/isc/netmgr/http.c index cbd657185b..5fb367953f 100644 --- a/lib/isc/netmgr/http.c +++ b/lib/isc/netmgr/http.c @@ -85,6 +85,37 @@ #define INITIAL_DNS_MESSAGE_BUFFER_SIZE (512) +/* + * The value should be small enough to not allow a server to open too + * many streams at once. It should not be too small either because + * the incoming data will be split into too many chunks with each of + * them processed asynchronously. + */ +#define INCOMING_DATA_CHUNK_SIZE (256) + +/* + * Often processing a chunk does not change the number of streams. In + * that case we can process more than once, but we still should have a + * hard limit on that. + */ +#define INCOMING_DATA_MAX_CHUNKS_AT_ONCE (4) + +/* + * These constants define the grace period to help detect flooding clients. + * + * The first one defines how much data can be processed before opening + * a first stream and received at least some useful (=DNS) data. + * + * The second one defines how much data from a client we read before + * trying to drop a clients who sends not enough useful data. + * + * The third constant defines how many streams we agree to process + * before checking if there was at least one DNS request received. + */ +#define INCOMING_DATA_INITIAL_STREAM_SIZE (1536) +#define INCOMING_DATA_GRACE_SIZE (MAX_ALLOWED_DATA_IN_HEADERS) +#define MAX_STREAMS_BEFORE_FIRST_REQUEST (50) + typedef struct isc_nm_http_response_status { size_t code; size_t content_length; @@ -143,6 +174,7 @@ struct isc_nm_http_session { ISC_LIST(http_cstream_t) cstreams; ISC_LIST(isc_nmsocket_h2_t) sstreams; size_t nsstreams; + uint64_t total_opened_sstreams; isc_nmhandle_t *handle; isc_nmhandle_t *client_httphandle; @@ -155,6 +187,18 @@ struct isc_nm_http_session { isc__nm_http_pending_callbacks_t pending_write_callbacks; isc_buffer_t *pending_write_data; + + /* + * The statistical values below are for usage on server-side + * only. They are meant to detect clients that are taking too many + * resources from the server. + */ + uint64_t received; /* How many requests have been received. */ + uint64_t submitted; /* How many responses were submitted to send */ + uint64_t processed; /* How many responses were processed. */ + + uint64_t processed_incoming_data; + uint64_t processed_useful_data; /* DNS data */ }; typedef enum isc_http_error_responses { @@ -177,6 +221,7 @@ typedef struct isc_http_send_req { void *cbarg; isc_buffer_t *pending_write_data; isc__nm_http_pending_callbacks_t pending_write_callbacks; + uint64_t submitted; } isc_http_send_req_t; #define HTTP_ENDPOINTS_MAGIC ISC_MAGIC('H', 'T', 'E', 'P') @@ -189,10 +234,26 @@ static bool http_send_outgoing(isc_nm_http_session_t *session, isc_nmhandle_t *httphandle, isc_nm_cb_t cb, void *cbarg); +static void +http_log_flooding_peer(isc_nm_http_session_t *session); + +static bool +http_is_flooding_peer(isc_nm_http_session_t *session); + +static ssize_t +http_process_input_data(isc_nm_http_session_t *session, + isc_buffer_t *input_data); + +static inline bool +http_too_many_active_streams(isc_nm_http_session_t *session); + static void http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle, isc_nm_cb_t send_cb, void *send_cbarg); +static void +http_do_bio_async(isc_nm_http_session_t *session); + static void failed_httpstream_read_cb(isc_nmsocket_t *sock, isc_result_t result, isc_nm_http_session_t *session); @@ -499,9 +560,20 @@ finish_http_session(isc_nm_http_session_t *session) { if (!session->closed) { session->closed = true; session->reading = false; + isc_nm_read_stop(session->handle); + isc__nmsocket_timer_stop(session->handle->sock); isc_nmhandle_close(session->handle); } + /* + * Free any unprocessed incoming data in order to not process + * it during indirect calls to http_do_bio() that might happen + * when calling the failed callbacks. + */ + if (session->buf != NULL) { + isc_buffer_free(&session->buf); + } + if (session->client) { client_call_failed_read_cb(ISC_R_UNEXPECTED, session); } else { @@ -575,6 +647,7 @@ on_server_data_chunk_recv_callback(int32_t stream_id, const uint8_t *data, if (new_bufsize <= MAX_DNS_MESSAGE_SIZE && new_bufsize <= h2->content_length) { + session->processed_useful_data += len; isc_buffer_putmem(&h2->rbuf, data, len); break; } @@ -623,6 +696,9 @@ call_unlink_cstream_readcb(http_cstream_t *cstream, isc_buffer_usedregion(cstream->rbuf, &read_data); cstream->read_cb(session->client_httphandle, result, &read_data, cstream->read_cbarg); + if (result == ISC_R_SUCCESS) { + isc__nmsocket_timer_restart(session->handle->sock); + } put_http_cstream(session->mctx, cstream); } @@ -664,6 +740,9 @@ on_server_stream_close_callback(int32_t stream_id, ISC_LIST_UNLINK(session->sstreams, sock->h2, link); session->nsstreams--; + if (sock->h2->request_received) { + session->submitted++; + } /* * By making a call to isc__nmsocket_prep_destroy(), we ensure that @@ -975,6 +1054,181 @@ client_submit_request(isc_nm_http_session_t *session, http_cstream_t *stream) { return ISC_R_SUCCESS; } +static ssize_t +http_process_input_data(isc_nm_http_session_t *session, + isc_buffer_t *input_data) { + ssize_t readlen = 0; + ssize_t processed = 0; + isc_region_t chunk = { 0 }; + size_t before, after; + size_t i; + + REQUIRE(VALID_HTTP2_SESSION(session)); + REQUIRE(input_data != NULL); + + if (!http_session_active(session)) { + return 0; + } + + /* + * For clients that initiate request themselves just process + * everything. + */ + if (session->client) { + isc_buffer_remainingregion(input_data, &chunk); + if (chunk.length == 0) { + return 0; + } + + readlen = nghttp2_session_mem_recv(session->ngsession, + chunk.base, chunk.length); + + if (readlen >= 0) { + isc_buffer_forward(input_data, readlen); + session->processed_incoming_data += readlen; + } + + return readlen; + } + + /* + * If no streams are created during processing, we might process + * more than one chunk at a time. Still we should not overdo that + * to avoid processing too much data at once as such behaviour is + * known for trashing the memory allocator at times. + */ + for (before = after = session->nsstreams, i = 0; + after <= before && i < INCOMING_DATA_MAX_CHUNKS_AT_ONCE; + after = session->nsstreams, i++) + { + const uint64_t active_streams = + (session->received - session->processed); + + /* + * If there are non completed send requests in flight -let's + * not process any incoming data, as it could lead to piling + * up too much send data in send buffers. With many clients + * connected it can lead to excessive memory consumption on + * the server instance. + */ + if (session->sending > 0) { + break; + } + + /* + * If we have reached the maximum number of streams used, we + * might stop processing for now, as nghttp2 will happily + * consume as much data as possible. + */ + if (session->nsstreams >= session->max_concurrent_streams && + active_streams > 0) + { + break; + } + + if (http_too_many_active_streams(session)) { + break; + } + + isc_buffer_remainingregion(input_data, &chunk); + if (chunk.length == 0) { + break; + } + + chunk.length = ISC_MIN(chunk.length, INCOMING_DATA_CHUNK_SIZE); + + readlen = nghttp2_session_mem_recv(session->ngsession, + chunk.base, chunk.length); + + if (readlen >= 0) { + isc_buffer_forward(input_data, readlen); + session->processed_incoming_data += readlen; + processed += readlen; + } else { + isc_buffer_clear(input_data); + return readlen; + } + } + + return processed; +} + +static void +http_log_flooding_peer(isc_nm_http_session_t *session) { + const int log_level = ISC_LOG_DEBUG(1); + if (session->handle != NULL && isc_log_wouldlog(isc_lctx, log_level)) { + char client_sabuf[ISC_SOCKADDR_FORMATSIZE]; + char local_sabuf[ISC_SOCKADDR_FORMATSIZE]; + + isc_sockaddr_format(&session->handle->sock->peer, client_sabuf, + sizeof(client_sabuf)); + isc_sockaddr_format(&session->handle->sock->iface, local_sabuf, + sizeof(local_sabuf)); + isc__nmsocket_log(session->handle->sock, log_level, + "Dropping a flooding HTTP/2 peer " + "%s (on %s) - processed: %" PRIu64 + " bytes, of them useful: %" PRIu64 "", + client_sabuf, local_sabuf, + session->processed_incoming_data, + session->processed_useful_data); + } +} + +static bool +http_is_flooding_peer(isc_nm_http_session_t *session) { + if (session->client) { + return false; + } + + /* + * A flooding client can try to open a lot of streams before + * submitting a request. Let's drop such clients. + */ + if (session->received == 0 && + session->total_opened_sstreams > MAX_STREAMS_BEFORE_FIRST_REQUEST) + { + return true; + } + + /* + * We have processed enough data to open at least one stream and + * get some useful data. + */ + if (session->processed_incoming_data > + INCOMING_DATA_INITIAL_STREAM_SIZE && + (session->total_opened_sstreams == 0 || + session->processed_useful_data == 0)) + { + return true; + } + + if (session->processed_incoming_data < INCOMING_DATA_GRACE_SIZE) { + return false; + } + + /* + * The overhead of DoH per DNS message can be minimum 160-180 + * bytes. We should allow more for extra information that can be + * included in headers, so let's use 256 bytes. Minimum DNS + * message size is 12 bytes. So, (256+12)/12=22. Even that can be + * too restricting for some edge cases, but should be good enough + * for any practical purposes. Not to mention that HTTP/2 may + * include legitimate data that is completely useless for DNS + * purposes... + * + * Anyway, at that point we should have processed enough requests + * for such clients (if any). + */ + if (session->processed_useful_data == 0 || + (session->processed_incoming_data / + session->processed_useful_data) > 22) + { + return true; + } + + return false; +} + /* * Read callback from TLS socket. */ @@ -984,6 +1238,7 @@ http_readcb(isc_nmhandle_t *handle ISC_ATTR_UNUSED, isc_result_t result, isc_nm_http_session_t *session = (isc_nm_http_session_t *)data; isc_nm_http_session_t *tmpsess = NULL; ssize_t readlen; + isc_buffer_t input; REQUIRE(VALID_HTTP2_SESSION(session)); @@ -1001,11 +1256,17 @@ http_readcb(isc_nmhandle_t *handle ISC_ATTR_UNUSED, isc_result_t result, goto done; } - readlen = nghttp2_session_mem_recv(session->ngsession, region->base, - region->length); + isc_buffer_init(&input, region->base, region->length); + isc_buffer_add(&input, region->length); + + readlen = http_process_input_data(session, &input); if (readlen < 0) { failed_read_cb(ISC_R_UNEXPECTED, session); goto done; + } else if (http_is_flooding_peer(session)) { + http_log_flooding_peer(session); + failed_read_cb(ISC_R_RANGE, session); + goto done; } if ((size_t)readlen < region->length) { @@ -1017,11 +1278,12 @@ http_readcb(isc_nmhandle_t *handle ISC_ATTR_UNUSED, isc_result_t result, isc_buffer_putmem(session->buf, region->base + readlen, unread_size); isc_nm_read_stop(session->handle); + http_do_bio_async(session); + } else { + /* We might have something to receive or send, do IO */ + http_do_bio(session, NULL, NULL, NULL); } - /* We might have something to receive or send, do IO */ - http_do_bio(session, NULL, NULL, NULL); - done: isc__nm_httpsession_detach(&tmpsess); } @@ -1059,14 +1321,18 @@ http_writecb(isc_nmhandle_t *handle, isc_result_t result, void *arg) { } isc_buffer_free(&req->pending_write_data); + session->processed += req->submitted; isc_mem_put(session->mctx, req, sizeof(*req)); session->sending--; - http_do_bio(session, NULL, NULL, NULL); - isc_nmhandle_detach(&transphandle); - if (result != ISC_R_SUCCESS && session->sending == 0) { + + if (result == ISC_R_SUCCESS) { + http_do_bio(session, NULL, NULL, NULL); + } else { finish_http_session(session); } + isc_nmhandle_detach(&transphandle); + isc__nm_httpsession_detach(&session); } @@ -1227,7 +1493,9 @@ http_send_outgoing(isc_nm_http_session_t *session, isc_nmhandle_t *httphandle, *send = (isc_http_send_req_t){ .pending_write_data = session->pending_write_data, .cb = cb, - .cbarg = cbarg }; + .cbarg = cbarg, + .submitted = session->submitted }; + session->submitted = 0; session->pending_write_data = NULL; move_pending_send_callbacks(session, send); @@ -1249,6 +1517,28 @@ nothing_to_send: return false; } +static inline bool +http_too_many_active_streams(isc_nm_http_session_t *session) { + const uint64_t active_streams = session->received - session->processed; + const uint64_t max_active_streams = + ISC_MIN(ISC_NETMGR_MAX_STREAM_CLIENTS_PER_CONN, + session->max_concurrent_streams); + + if (session->client) { + return false; + } + + /* + * Do not process incoming data if there are too many active DNS + * clients (streams) per connection. + */ + if (active_streams >= max_active_streams) { + return true; + } + + return false; +} + static void http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle, isc_nm_cb_t send_cb, void *send_cbarg) { @@ -1264,42 +1554,86 @@ http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle, finish_http_session(session); } return; - } else if (nghttp2_session_want_read(session->ngsession) == 0 && - nghttp2_session_want_write(session->ngsession) == 0 && - session->pending_write_data == NULL) - { - session->closing = true; + } + + if (send_cb != NULL) { + INSIST(VALID_NMHANDLE(send_httphandle)); + (void)http_send_outgoing(session, send_httphandle, send_cb, + send_cbarg); + return; + } + + INSIST(send_httphandle == NULL); + INSIST(send_cb == NULL); + INSIST(send_cbarg == NULL); + + if (session->pending_write_data != NULL && session->sending == 0) { + (void)http_send_outgoing(session, NULL, NULL, NULL); return; } if (nghttp2_session_want_read(session->ngsession) != 0) { if (!session->reading) { - /* We have not yet started - * reading from this handle */ + /* We have not yet started reading from this handle */ + isc__nmsocket_timer_start(session->handle->sock); isc_nm_read(session->handle, http_readcb, session); session->reading = true; } else if (session->buf != NULL) { size_t remaining = isc_buffer_remaininglength(session->buf); - /* Leftover data in the - * buffer, use it */ - size_t readlen = nghttp2_session_mem_recv( - session->ngsession, - isc_buffer_current(session->buf), remaining); + /* Leftover data in the buffer, use it */ + size_t remaining_after = 0; + ssize_t readlen = 0; + isc_nm_http_session_t *tmpsess = NULL; + + /* + * Let's ensure that HTTP/2 session and its associated + * data will not go "out of scope" too early. + */ + isc__nm_httpsession_attach(session, &tmpsess); + + readlen = http_process_input_data(session, + session->buf); + + remaining_after = + isc_buffer_remaininglength(session->buf); - if (readlen == remaining) { + if (readlen < 0) { + failed_read_cb(ISC_R_UNEXPECTED, session); + } else if (http_is_flooding_peer(session)) { + http_log_flooding_peer(session); + failed_read_cb(ISC_R_RANGE, session); + } else if ((size_t)readlen == remaining) { isc_buffer_free(&session->buf); + http_do_bio(session, NULL, NULL, NULL); + } else if (remaining_after > 0 && + remaining_after < remaining) + { + /* + * We have processed a part of the data, now + * let's delay processing of whatever is left + * here. We want it to be an async operation so + * that we will: + * + * a) let other things run; + * b) have finer grained control over how much + * data is processed at once, because nghttp2 + * would happily consume as much data we pass to + * it and that could overwhelm the server. + */ + http_do_bio_async(session); } else { - isc_buffer_forward(session->buf, readlen); + (void)http_send_outgoing(session, NULL, NULL, + NULL); } - http_do_bio(session, send_httphandle, send_cb, - send_cbarg); + isc__nm_httpsession_detach(&tmpsess); return; } else { - /* Resume reading, it's - * idempotent, wait for more + /* + * Resume reading, it's idempotent, wait for more */ + isc__nmsocket_timer_start(session->handle->sock); isc_nm_read(session->handle, http_readcb, session); } } else { @@ -1307,20 +1641,54 @@ http_do_bio(isc_nm_http_session_t *session, isc_nmhandle_t *send_httphandle, isc_nm_read_stop(session->handle); } - if (send_cb != NULL) { - INSIST(VALID_NMHANDLE(send_httphandle)); - (void)http_send_outgoing(session, send_httphandle, send_cb, - send_cbarg); - } else { - INSIST(send_httphandle == NULL); - INSIST(send_cb == NULL); - INSIST(send_cbarg == NULL); - (void)http_send_outgoing(session, NULL, NULL, NULL); + /* we might have some data to send after processing */ + (void)http_send_outgoing(session, NULL, NULL, NULL); + + if (nghttp2_session_want_read(session->ngsession) == 0 && + nghttp2_session_want_write(session->ngsession) == 0 && + session->pending_write_data == NULL) + { + session->closing = true; + isc_nm_read_stop(session->handle); + if (session->sending == 0) { + finish_http_session(session); + } } return; } +static void +http_do_bio_async_cb(void *arg) { + isc_nm_http_session_t *session = arg; + + REQUIRE(VALID_HTTP2_SESSION(session)); + + if (session->handle != NULL && + !isc__nmsocket_closing(session->handle->sock)) + { + http_do_bio(session, NULL, NULL, NULL); + } + + isc__nm_httpsession_detach(&session); +} + +static void +http_do_bio_async(isc_nm_http_session_t *session) { + isc_nm_http_session_t *tmpsess = NULL; + + REQUIRE(VALID_HTTP2_SESSION(session)); + + if (session->handle == NULL || + isc__nmsocket_closing(session->handle->sock)) + { + return; + } + isc__nm_httpsession_attach(session, &tmpsess); + isc_async_run(session->handle->sock->worker->loop, http_do_bio_async_cb, + tmpsess); +} + static isc_result_t get_http_cstream(isc_nmsocket_t *sock, http_cstream_t **streamp) { http_cstream_t *cstream = sock->h2->connect.cstream; @@ -1442,6 +1810,7 @@ transport_connect_cb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) { } http_transpost_tcp_nodelay(handle); + isc__nmhandle_set_manual_timer(session->handle, true); http_call_connect_cb(http_sock, session, result); @@ -1723,6 +2092,7 @@ server_on_begin_headers_callback(nghttp2_session *ngsession, session->nsstreams++; isc__nm_httpsession_attach(session, &socket->h2->session); ISC_LIST_APPEND(session->sstreams, socket->h2, link); + session->total_opened_sstreams++; nghttp2_session_set_stream_user_data(ngsession, frame->hd.stream_id, socket); @@ -1782,6 +2152,8 @@ server_handle_path_header(isc_nmsocket_t *socket, const uint8_t *value, socket->worker->mctx, dns_value, dns_value_len, &socket->h2->query_data_len); + socket->h2->session->processed_useful_data += + dns_value_len; } else { socket->h2->query_too_large = true; return ISC_HTTP_ERROR_PAYLOAD_TOO_LARGE; @@ -2090,6 +2462,12 @@ server_call_cb(isc_nmsocket_t *socket, const isc_result_t result, handle = isc__nmhandle_get(socket, NULL, NULL); if (result != ISC_R_SUCCESS) { data = NULL; + } else if (socket->h2->session->handle != NULL) { + isc__nmsocket_timer_restart(socket->h2->session->handle->sock); + } + if (result == ISC_R_SUCCESS) { + socket->h2->request_received = true; + socket->h2->session->received++; } socket->h2->cb(handle, result, data, socket->h2->cbarg); isc_nmhandle_detach(&handle); @@ -2106,6 +2484,12 @@ isc__nm_http_bad_request(isc_nmhandle_t *handle) { REQUIRE(!sock->client); REQUIRE(VALID_HTTP2_SESSION(sock->h2->session)); + if (sock->h2->response_submitted || + !http_session_active(sock->h2->session)) + { + return; + } + (void)server_send_error_response(ISC_HTTP_ERROR_BAD_REQUEST, sock->h2->session->ngsession, sock); } @@ -2492,6 +2876,8 @@ httplisten_acceptcb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) { isc__nmsocket_attach(httpserver, &session->serversocket); server_send_connection_header(session); + isc__nmhandle_set_manual_timer(session->handle, true); + /* TODO H2 */ http_do_bio(session, NULL, NULL, NULL); return ISC_R_SUCCESS; diff --git a/lib/isc/netmgr/netmgr-int.h b/lib/isc/netmgr/netmgr-int.h index c7f721b2a0..1a36a5c342 100644 --- a/lib/isc/netmgr/netmgr-int.h +++ b/lib/isc/netmgr/netmgr-int.h @@ -474,6 +474,7 @@ typedef struct isc_nmsocket_h2 { isc_nm_http_endpoints_t *peer_endpoints; + bool request_received; bool response_submitted; struct { char *uri; diff --git a/lib/isc/netmgr/proxystream.c b/lib/isc/netmgr/proxystream.c index ab1eb5c7fe..2943b65520 100644 --- a/lib/isc/netmgr/proxystream.c +++ b/lib/isc/netmgr/proxystream.c @@ -121,6 +121,7 @@ proxystream_on_header_data_cb(const isc_result_t result, * the case of TCP it is disabled by default */ proxystream_read_stop(sock); + isc__nmsocket_timer_stop(sock); isc__nmhandle_set_manual_timer(sock->outerhandle, false); sock->proxy.header_processed = true; @@ -773,6 +774,7 @@ isc__nm_proxystream_close(isc_nmsocket_t *sock) { * external references, we can close everything. */ proxystream_read_stop(sock); + isc__nmsocket_timer_stop(sock); if (sock->outerhandle != NULL) { sock->reading = false; isc_nm_read_stop(sock->outerhandle); diff --git a/lib/isc/netmgr/streamdns.c b/lib/isc/netmgr/streamdns.c index d2cd1325f3..15d14f3438 100644 --- a/lib/isc/netmgr/streamdns.c +++ b/lib/isc/netmgr/streamdns.c @@ -1008,6 +1008,7 @@ streamdns_close_direct(isc_nmsocket_t *sock) { if (sock->outerhandle != NULL) { sock->streamdns.reading = false; + isc__nmsocket_timer_stop(sock); isc_nm_read_stop(sock->outerhandle); isc_nmhandle_close(sock->outerhandle); isc_nmhandle_detach(&sock->outerhandle); diff --git a/lib/isc/netmgr/tcp.c b/lib/isc/netmgr/tcp.c index 863aa26f9b..f191434fab 100644 --- a/lib/isc/netmgr/tcp.c +++ b/lib/isc/netmgr/tcp.c @@ -722,7 +722,9 @@ isc__nm_tcp_read_stop(isc_nmhandle_t *handle) { isc_nmsocket_t *sock = handle->sock; - isc__nmsocket_timer_stop(sock); + if (!sock->manual_read_timer) { + isc__nmsocket_timer_stop(sock); + } isc__nm_stop_reading(sock); sock->reading = false; diff --git a/lib/isc/netmgr/tlsstream.c b/lib/isc/netmgr/tlsstream.c index c045ebe96d..61621e1ebb 100644 --- a/lib/isc/netmgr/tlsstream.c +++ b/lib/isc/netmgr/tlsstream.c @@ -465,6 +465,7 @@ tls_try_handshake(isc_nmsocket_t *sock, isc_result_t *presult) { isc__nmsocket_log_tls_session_reuse(sock, sock->tlsstream.tls); tlshandle = isc__nmhandle_get(sock, &sock->peer, &sock->iface); + isc__nmsocket_timer_stop(sock); tls_read_stop(sock); if (isc__nm_closing(sock->worker)) { @@ -1146,6 +1147,10 @@ isc__nm_tls_read_stop(isc_nmhandle_t *handle) { handle->sock->reading = false; + if (!handle->sock->manual_read_timer) { + isc__nmsocket_timer_stop(handle->sock); + } + tls_read_stop(handle->sock); } @@ -1166,6 +1171,7 @@ isc__nm_tls_close(isc_nmsocket_t *sock) { */ tls_read_stop(sock); if (sock->outerhandle != NULL) { + isc__nmsocket_timer_stop(sock); isc_nm_read_stop(sock->outerhandle); isc_nmhandle_close(sock->outerhandle); isc_nmhandle_detach(&sock->outerhandle);