diff --git a/src/context.c b/src/context.c index 95aed89b..cc87a11d 100644 --- a/src/context.c +++ b/src/context.c @@ -558,6 +558,7 @@ _getdns_upstream_shutdown(getdns_upstream *upstream) upstream->tcp.write_error = 0; upstream->writes_done = 0; upstream->responses_received = 0; + upstream->keepalive_timeout = 0; if (upstream->tls_hs_state != GETDNS_HS_FAILED) { upstream->tls_hs_state = GETDNS_HS_NONE; upstream->tls_auth_failed = 0; @@ -609,6 +610,7 @@ upstream_init(getdns_upstream *upstream, /* How is this upstream doing? */ upstream->writes_done = 0; upstream->responses_received = 0; + upstream->keepalive_timeout = 0; upstream->to_retry = 2; upstream->back_off = 1; @@ -1482,9 +1484,8 @@ getdns_context_set_idle_timeout(struct getdns_context *context, uint64_t timeout { RETURN_IF_NULL(context, GETDNS_RETURN_INVALID_PARAMETER); - if (timeout == 0) { - return GETDNS_RETURN_INVALID_PARAMETER; - } + /* Shuold we enforce maximum based on edns-tcp-keepalive spec? */ + /* 0 should be allowed as that is the default.*/ context->idle_timeout = timeout; diff --git a/src/context.h b/src/context.h index 98068f1f..1c3ac1f0 100644 --- a/src/context.h +++ b/src/context.h @@ -89,6 +89,7 @@ typedef struct getdns_upstream { /* How is this upstream doing? */ size_t writes_done; size_t responses_received; + uint64_t keepalive_timeout; int to_retry; int back_off; diff --git a/src/gldns/rrdef.h b/src/gldns/rrdef.h index 8a14ad36..3f7035cf 100644 --- a/src/gldns/rrdef.h +++ b/src/gldns/rrdef.h @@ -417,7 +417,8 @@ enum gldns_enum_edns_option GLDNS_EDNS_DAU = 5, /* RFC6975 */ GLDNS_EDNS_DHU = 6, /* RFC6975 */ GLDNS_EDNS_N3U = 7, /* RFC6975 */ - GLDNS_EDNS_CLIENT_SUBNET = 8 /* draft-vandergaast-edns-client-subnet */ + GLDNS_EDNS_CLIENT_SUBNET = 8, /* draft-vandergaast-edns-client-subnet */ + GLDNS_EDNS_KEEPALIVE = 11 /* draft-ietf-dnsop-edns-tcp-keepalive*/ }; typedef enum gldns_enum_edns_option gldns_edns_option; diff --git a/src/gldns/wire2str.c b/src/gldns/wire2str.c index fcd9e554..0fa1c4cb 100644 --- a/src/gldns/wire2str.c +++ b/src/gldns/wire2str.c @@ -165,6 +165,7 @@ static gldns_lookup_table gldns_edns_options_data[] = { { 6, "DHU" }, { 7, "N3U" }, { 8, "edns-client-subnet" }, + { 11, "edns-tcp-keepalive"}, { 0, NULL} }; gldns_lookup_table* gldns_edns_options = gldns_edns_options_data; @@ -1833,6 +1834,25 @@ int gldns_wire2str_edns_subnet_print(char** s, size_t* sl, uint8_t* data, return w; } +int gldns_wire2str_edns_keepalive_print(char** s, size_t* sl, uint8_t* data, + size_t len) +{ + int w = 0; + uint16_t timeout; + if(!(len == 0 || len == 2)) { + w += gldns_str_print(s, sl, "malformed keepalive "); + w += print_hex_buf(s, sl, data, len); + return w; + } + if(len == 0 ) { + w += gldns_str_print(s, sl, "no timeout value (only valid for client option) "); + } else { + timeout = gldns_read_uint16(data); + w += gldns_str_print(s, sl, "timeout value in units of 100ms %u", (int)timeout); + } + return w; +} + int gldns_wire2str_edns_option_print(char** s, size_t* sl, uint16_t option_code, uint8_t* optdata, size_t optlen) { @@ -1861,6 +1881,9 @@ int gldns_wire2str_edns_option_print(char** s, size_t* sl, case GLDNS_EDNS_CLIENT_SUBNET: w += gldns_wire2str_edns_subnet_print(s, sl, optdata, optlen); break; + case GLDNS_EDNS_KEEPALIVE: + w += gldns_wire2str_edns_keepalive_print(s, sl, optdata, optlen); + break; default: /* unknown option code */ w += print_hex_buf(s, sl, optdata, optlen); diff --git a/src/request-internal.c b/src/request-internal.c index 8d847ab6..0ffeb13f 100644 --- a/src/request-internal.c +++ b/src/request-internal.c @@ -128,6 +128,7 @@ network_req_init(getdns_network_req *net_req, getdns_dns_req *owner, net_req->edns_maximum_udp_payload_size = edns_maximum_udp_payload_size; net_req->max_udp_payload_size = edns_maximum_udp_payload_size != -1 ? edns_maximum_udp_payload_size : 1432; + net_req->keepalive_sent = 0; net_req->write_queue_tail = NULL; net_req->response_len = 0; net_req->base_query_option_sz = opt_options_size; diff --git a/src/stub.c b/src/stub.c index 02c86a25..c1188589 100644 --- a/src/stub.c +++ b/src/stub.c @@ -42,6 +42,7 @@ #include "gldns/pkthdr.h" #include "gldns/rrdef.h" #include "gldns/str2wire.h" +#include "gldns/wire2str.h" #include "rr-iter.h" #include "context.h" #include "util-internal.h" @@ -54,6 +55,8 @@ /* Don't currently have access to the context whilst doing handshake */ #define TIMEOUT_TLS 2500 +/* Arbritray number of message for EDNS keepalive resend*/ +#define EDNS_KEEPALIVE_RESEND 5 static time_t secret_rollover_time = 0; static uint32_t secret = 0; @@ -75,7 +78,6 @@ static void netreq_upstream_read_cb(void *userarg); static void netreq_upstream_write_cb(void *userarg); static int fallback_on_write(getdns_network_req *netreq); -static void stub_tcp_write_cb(void *userarg); static void stub_timeout_cb(void *userarg); /*****************************/ /* General utility functions */ @@ -145,6 +147,15 @@ attach_edns_client_subnet_private(getdns_network_req *req) 4, NULL); } +static getdns_return_t +attach_edns_keepalive(getdns_network_req *req) +{ + /* Client always sends length 0, omits the timeout */ + return _getdns_network_req_add_upstream_option(req, + GLDNS_EDNS_KEEPALIVE, + 0, NULL); +} + static getdns_return_t attach_edns_cookie(getdns_network_req *req) { @@ -183,9 +194,10 @@ attach_edns_cookie(getdns_network_req *req) } +/* Will find a matching OPT RR, but leaves the caller to validate it*/ static int -match_and_process_server_cookie( - getdns_upstream *upstream, uint8_t *response, size_t response_len) +match_edns_opt_rr(uint16_t code, uint8_t *response, size_t response_len, + uint8_t **position, uint16_t *option_len) { _getdns_rr_iter rr_iter_storage, *rr_iter; uint8_t *pos; @@ -211,7 +223,17 @@ match_and_process_server_cookie( pos = rr_iter->rr_type + 8; - /* OPT found, now search for the cookie option */ +#if defined(STUB_DEBUG) && STUB_DEBUG + char str_spc[8192], *str = str_spc; + size_t str_len = sizeof(str_spc); + uint8_t *data = rr_iter->pos; + size_t data_len = rr_iter->nxt - rr_iter->pos; + (void) gldns_wire2str_rr_scan( + &data, &data_len, &str, &str_len, rr_iter->pkt, rr_iter->pkt_end - rr_iter->pkt); + DEBUG_STUB("OPT RR: %s", str_spc); +#endif + + /* OPT found, now search for the specified option */ if (pos + 2 > rr_iter->nxt) return 1; /* FORMERR */ @@ -224,23 +246,39 @@ match_and_process_server_cookie( opt_len = gldns_read_uint16(pos); pos += 2; if (pos + opt_len > rr_iter->nxt) return 1; /* FORMERR */ - if (opt_code == EDNS_COOKIE_OPCODE) + if (opt_code == code) break; pos += opt_len; /* Skip unknown options */ } - if (pos >= rr_iter->nxt || opt_code != EDNS_COOKIE_OPCODE) + if (pos >= rr_iter->nxt || opt_code != code) return 0; /* Everything OK, just no cookie found. */ + *position = pos; + *option_len = opt_len; + return 2; +} - if (opt_len < 16 || opt_len > 40) +/* TODO: Test combinations of EDNS0 options*/ +static int +match_and_process_server_cookie( + getdns_upstream *upstream, uint8_t *response, size_t response_len) +{ + uint8_t *position = NULL; + uint16_t option_len = 0; + int found = match_edns_opt_rr(EDNS_COOKIE_OPCODE, response, + response_len, &position, &option_len); + if (found != 2) + return found; + + if (option_len < 16 || option_len > 40) return 1; /* FORMERR */ if (!upstream->has_client_cookie) return 1; /* Cookie reply, but we didn't sent one */ - if (memcmp(upstream->client_cookie, pos, 8) != 0) { + if (memcmp(upstream->client_cookie, position, 8) != 0) { if (!upstream->has_prev_client_cookie) return 1; /* Cookie didn't match */ - if (memcmp(upstream->prev_client_cookie, pos, 8) != 0) + if (memcmp(upstream->prev_client_cookie, position, 8) != 0) return 1; /* Previous cookie didn't match either */ upstream->has_server_cookie = 0; @@ -248,11 +286,42 @@ match_and_process_server_cookie( * is for our previous client cookie */ } - pos += 8; - opt_len -= 8; + position += 8; + option_len -= 8; upstream->has_server_cookie = 1; - upstream->server_cookie_len = opt_len; - (void) memcpy(upstream->server_cookie, pos, opt_len); + upstream->server_cookie_len = option_len; + (void) memcpy(upstream->server_cookie, position, option_len); + return 0; +} + +static int +process_keepalive( + getdns_upstream *upstream, getdns_network_req *netreq, + uint8_t *response, size_t response_len) +{ + uint8_t *position = NULL; + uint16_t option_len = 0; + int found = match_edns_opt_rr(GLDNS_EDNS_KEEPALIVE, response, + response_len, &position, &option_len); + if (found != 2) { + if (netreq->keepalive_sent == 1) + /* If no keepalive sent back, then we must use 0 idle timeout + as server does not support it.*/ + upstream->keepalive_timeout = 0; + return found; + } + if (option_len != 2) + return 1; /* FORMERR */ + /* Use server sent value unless the client specified a shorter one. + Convert to ms first (wire value has units of 100ms) */ + uint64_t server_keepalive = ((uint64_t)gldns_read_uint16(position))*100; + if (netreq->owner->context->idle_timeout < server_keepalive) + upstream->keepalive_timeout = netreq->owner->context->idle_timeout; + else { + upstream->keepalive_timeout = server_keepalive; + DEBUG_STUB("*** %s: SERVER KEEPALIVE USED : %d ms\n", + __FUNCTION__, (int)server_keepalive); + } return 0; } @@ -390,7 +459,7 @@ stub_cleanup(getdns_network_req *netreq) netreq->write_queue_tail = NULL; break; } - upstream_reschedule_events(upstream, netreq->owner->context->idle_timeout); + upstream_reschedule_events(upstream, upstream->keepalive_timeout); } static int @@ -446,18 +515,17 @@ _getdns_cancel_stub_request(getdns_network_req *netreq) if (netreq->fd >= 0) close(netreq->fd); } -static void +/* May be needed in future for better UDP error handling?*/ +/*static void stub_erred(getdns_network_req *netreq) { DEBUG_STUB("*** %s\n", __FUNCTION__); stub_next_upstream(netreq); stub_cleanup(netreq); - /* TODO[TLS]: When we get an error (which is probably a timeout) and are - * using to keep connections open should we leave the connection up here? */ if (netreq->fd >= 0) close(netreq->fd); netreq->state = NET_REQ_FINISHED; _getdns_check_dns_req_complete(netreq->owner); -} +}*/ static void stub_timeout_cb(void *userarg) @@ -651,6 +719,14 @@ stub_tcp_write(int fd, getdns_tcp_state *tcp, getdns_network_req *netreq) if (netreq->owner->edns_client_subnet_private) if (attach_edns_client_subnet_private(netreq)) return STUB_OUT_OF_OPTIONS; + if (netreq->upstream->writes_done == 0 && + netreq->owner->context->idle_timeout != 0) { + /* Add the keepalive option to the first query on this connection*/ + DEBUG_STUB("# %s: Requesting keepalive\n", __FUNCTION__); + if (attach_edns_keepalive(netreq)) + return STUB_OUT_OF_OPTIONS; + netreq->keepalive_sent = 1; + } } pkt_len = netreq->response - netreq->query; /* We have an initialized packet buffer. @@ -1098,6 +1174,7 @@ stub_tls_write(getdns_upstream *upstream, getdns_tcp_state *tcp, &netreq->upstream->netreq_by_query_id, &netreq->node)); GLDNS_ID_SET(netreq->query, query_id); + /* TODO: Review if more EDNS0 handling can be centralised.*/ if (netreq->opt) { _getdns_network_req_clear_upstream_options(netreq); /* no limits on the max udp payload size with tcp */ @@ -1108,6 +1185,15 @@ stub_tls_write(getdns_upstream *upstream, getdns_tcp_state *tcp, if (netreq->owner->edns_client_subnet_private) if (attach_edns_client_subnet_private(netreq)) return STUB_OUT_OF_OPTIONS; + if (netreq->upstream->writes_done % EDNS_KEEPALIVE_RESEND == 0 && + netreq->owner->context->idle_timeout != 0) { + /* Add the keepalive option to every nth query on this + connection */ + DEBUG_STUB("# %s: Requesting keepalive\n", __FUNCTION__); + if (attach_edns_keepalive(netreq)) + return STUB_OUT_OF_OPTIONS; + netreq->keepalive_sent = 1; + } if (netreq->owner->tls_query_padding_blocksize > 1) { pkt_len = netreq->response - netreq->query; pkt_len += 4; /* this accounts for the OPTION-CODE and OPTION-LENGTH of the padding */ @@ -1259,76 +1345,6 @@ stub_udp_write_cb(void *userarg) stub_udp_read_cb, NULL, stub_timeout_cb)); } -/**************************/ -/* TCP callback functions*/ -/**************************/ - -static void -stub_tcp_read_cb(void *userarg) -{ - getdns_network_req *netreq = (getdns_network_req *)userarg; - getdns_dns_req *dnsreq = netreq->owner; - int q; - - switch ((q = stub_tcp_read(netreq->fd, &netreq->tcp, - &dnsreq->context->mf))) { - - case STUB_TCP_AGAIN: - return; - - case STUB_TCP_ERROR: - stub_erred(netreq); - return; - - default: - GETDNS_CLEAR_EVENT(dnsreq->loop, &netreq->event); - if (q != netreq->query_id) - return; - if (netreq->owner->edns_cookies && - match_and_process_server_cookie( - netreq->upstream, netreq->tcp.read_buf, - netreq->tcp.read_pos - netreq->tcp.read_buf)) - return; /* Client cookie didn't match? */ - netreq->state = NET_REQ_FINISHED; - netreq->response = netreq->tcp.read_buf; - netreq->response_len = - netreq->tcp.read_pos - netreq->tcp.read_buf; - netreq->tcp.read_buf = NULL; - dnsreq->upstreams->current = 0; - netreq->debug_end_time = _getdns_get_time_as_uintt64(); - stub_cleanup(netreq); - close(netreq->fd); - _getdns_check_dns_req_complete(dnsreq); - } -} - -static void -stub_tcp_write_cb(void *userarg) -{ - getdns_network_req *netreq = (getdns_network_req *)userarg; - getdns_dns_req *dnsreq = netreq->owner; - int q; - netreq->debug_start_time = _getdns_get_time_as_uintt64(); - switch ((q = stub_tcp_write(netreq->fd, &netreq->tcp, netreq))) { - case STUB_TCP_AGAIN: - return; - - case STUB_TCP_ERROR: - stub_erred(netreq); - return; - - default: - netreq->debug_udp = 0; - netreq->query_id = (uint16_t) q; - GETDNS_CLEAR_EVENT(dnsreq->loop, &netreq->event); - GETDNS_SCHEDULE_EVENT( - dnsreq->loop, netreq->fd, dnsreq->context->timeout, - getdns_eventloop_event_init(&netreq->event, netreq, - stub_tcp_read_cb, NULL, stub_timeout_cb)); - return; - } -} - /**************************/ /* Upstream callback functions*/ /**************************/ @@ -1381,6 +1397,18 @@ upstream_read_cb(void *userarg) /* TODO[TLS]: I don't think we should do this for TCP. We should stay * on a working connection until we hit a problem.*/ upstream->upstreams->current = 0; + + /* !THIS CODE NEEDS TESTING!*/ + if (netreq->owner->edns_cookies && + match_and_process_server_cookie( + netreq->upstream, netreq->tcp.read_buf, + netreq->tcp.read_pos - netreq->tcp.read_buf)) + return; /* Client cookie didn't match? */ + + if ((netreq->owner->context->idle_timeout != 0) && + process_keepalive(netreq->upstream, netreq, netreq->response, + netreq->response_len)) + return; netreq->debug_end_time = _getdns_get_time_as_uintt64(); /* This also reschedules events for the upstream*/ @@ -1644,6 +1672,7 @@ find_upstream_for_netreq(getdns_network_req *netreq) continue; netreq->transport_current = i; netreq->upstream = upstream; + netreq->keepalive_sent = 0; return fd; } return -1; @@ -1795,8 +1824,7 @@ _getdns_submit_stub_request(getdns_network_req *netreq) GETDNS_SCHEDULE_EVENT( dnsreq->loop, netreq->fd, dnsreq->context->timeout, getdns_eventloop_event_init(&netreq->event, netreq, - NULL, (transport == GETDNS_TRANSPORT_UDP ? - stub_udp_write_cb: stub_tcp_write_cb), stub_timeout_cb)); + NULL, stub_udp_write_cb, stub_timeout_cb)); return GETDNS_RETURN_GOOD; case GETDNS_TRANSPORT_TLS: diff --git a/src/types-internal.h b/src/types-internal.h index a451f028..c06c2db3 100644 --- a/src/types-internal.h +++ b/src/types-internal.h @@ -223,6 +223,8 @@ typedef struct getdns_network_req int edns_maximum_udp_payload_size; uint16_t max_udp_payload_size; + size_t keepalive_sent; + /* Network requests scheduled to write after me */ struct getdns_network_req *write_queue_tail; diff --git a/src/util-internal.c b/src/util-internal.c index 4649a79c..3141da4a 100644 --- a/src/util-internal.c +++ b/src/util-internal.c @@ -730,6 +730,23 @@ _getdns_create_call_reporting_dict( } getdns_dict_destroy(address_debug); + if (transport != GETDNS_TRANSPORT_UDP) { + /* Report the idle timeout actually used on the connection. Must trim, + maximum used in practice is 6553500ms, but this is stored in a uint64_t.*/ + if (netreq->upstream->keepalive_timeout > UINT32_MAX) { + if (getdns_dict_set_int( netreq_debug, "idle timeout in ms (overflow)", UINT32_MAX)) { + getdns_dict_destroy(netreq_debug); + return NULL; + } + } else{ + uint32_t idle_timeout = netreq->upstream->keepalive_timeout; + if (getdns_dict_set_int( netreq_debug, "idle timeout in ms", idle_timeout)) { + getdns_dict_destroy(netreq_debug); + return NULL; + } + } + } + if (netreq->upstream->transport != GETDNS_TRANSPORT_TLS) return netreq_debug;