mirror of https://github.com/getdnsapi/getdns.git
Implement authenticaiton fallback on a given upstream (needs more work). Also need API option to set auth requirement.
This commit is contained in:
parent
e710286e45
commit
af617e92a7
|
@ -91,6 +91,9 @@ doc: FORCE
|
||||||
example:
|
example:
|
||||||
cd spec/example && $(MAKE) $@
|
cd spec/example && $(MAKE) $@
|
||||||
|
|
||||||
|
test_code:
|
||||||
|
cd src && $(MAKE) $@
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cd src && $(MAKE) $@
|
cd src && $(MAKE) $@
|
||||||
|
|
||||||
|
|
|
@ -107,14 +107,13 @@ AC_CHECK_HEADERS([openssl/err.h],,, [AC_INCLUDES_DEFAULT])
|
||||||
AC_CHECK_HEADERS([openssl/rand.h],,, [AC_INCLUDES_DEFAULT])
|
AC_CHECK_HEADERS([openssl/rand.h],,, [AC_INCLUDES_DEFAULT])
|
||||||
|
|
||||||
dnl TLS v1.2 requires OpenSSL 1.0.1
|
dnl TLS v1.2 requires OpenSSL 1.0.1
|
||||||
AC_CHECK_LIB(ssl, TLSv1_2_client_method,AC_DEFINE([HAVE_LIBTLS1_2], [1],
|
AC_CHECK_LIB(ssl, TLSv1_2_client_method,AC_DEFINE([HAVE_TLS_v1_2], [1],
|
||||||
[Define if you have libssl with tls 1.2]),[AC_MSG_WARN([Cannot find TLSv1_2_client_method in libssl library. TLS will not be available.])])
|
[Define if you have libssl with tls 1.2]),[AC_MSG_WARN([Cannot find TLSv1_2_client_method in libssl library. TLS will not be available.])])
|
||||||
])dnl End of ACX_SSL_CHECKS
|
|
||||||
|
|
||||||
dnl Authentication for TLS requires 1.0.2
|
dnl Native OpenSSL hostname verification requires OpenSSL 1.0.2
|
||||||
AC_CHECK_LIB(ssl, SSL_CTX_get0_param, AC_DEFINE([HAVE_LIBSSL_102], [1],
|
AC_CHECK_LIB(ssl, SSL_CTX_get0_param,AC_DEFINE([HAVE_SSL_HN_AUTH], [1],
|
||||||
[Define if you have libssl 1.0.2 or later]),[AC_MSG_WARN([libssl 1.0.2 or higher is required for TLS authentication. Authenticated TLS will not be available.])])
|
[Define if you have libssl with host name verification]),[AC_MSG_WARN([Cannot find SSL_CTX_get0_param in libssl library. Native TLS hostname verification will not be available, custom code will be used.])])
|
||||||
])dnl End of ACX_SSL_CHECKS
|
])
|
||||||
|
|
||||||
dnl Check for SSL, where SSL is mandatory
|
dnl Check for SSL, where SSL is mandatory
|
||||||
dnl Adds --with-ssl option, searches for openssl and defines HAVE_SSL if found
|
dnl Adds --with-ssl option, searches for openssl and defines HAVE_SSL if found
|
||||||
|
|
|
@ -147,6 +147,8 @@ libgetdns_ext_ev.la: libgetdns.la libev.lo
|
||||||
libgetdns.la: $(GETDNS_OBJ) version.lo context.lo libmini_event.lo $(GLDNS_OBJ) $(COMPAT_OBJ) $(UTIL_OBJ)
|
libgetdns.la: $(GETDNS_OBJ) version.lo context.lo libmini_event.lo $(GLDNS_OBJ) $(COMPAT_OBJ) $(UTIL_OBJ)
|
||||||
$(LIBTOOL) --tag=CC --mode=link $(CC) $(CFLAGS) -o $@ $(GETDNS_OBJ) version.lo context.lo libmini_event.lo $(GLDNS_OBJ) $(COMPAT_OBJ) $(UTIL_OBJ) $(LDFLAGS) -rpath $(libdir) -version-info $(libversion) -no-undefined -export-symbols $(srcdir)/libgetdns.symbols
|
$(LIBTOOL) --tag=CC --mode=link $(CC) $(CFLAGS) -o $@ $(GETDNS_OBJ) version.lo context.lo libmini_event.lo $(GLDNS_OBJ) $(COMPAT_OBJ) $(UTIL_OBJ) $(LDFLAGS) -rpath $(libdir) -version-info $(libversion) -no-undefined -export-symbols $(srcdir)/libgetdns.symbols
|
||||||
|
|
||||||
|
test_code: FORCE
|
||||||
|
cd test && $(MAKE) $@
|
||||||
|
|
||||||
test: FORCE
|
test: FORCE
|
||||||
cd test && $(MAKE) $@
|
cd test && $(MAKE) $@
|
||||||
|
|
|
@ -897,6 +897,8 @@ getdns_context_create_with_extended_memory_functions(
|
||||||
result->edns_maximum_udp_payload_size = -1;
|
result->edns_maximum_udp_payload_size = -1;
|
||||||
if ((r = create_default_dns_transports(result)))
|
if ((r = create_default_dns_transports(result)))
|
||||||
goto error;
|
goto error;
|
||||||
|
result->tls_auth_req = 1;
|
||||||
|
result->tls_auth_fallback_ok = 1;
|
||||||
result->limit_outstanding_queries = 0;
|
result->limit_outstanding_queries = 0;
|
||||||
result->return_dnssec_status = GETDNS_EXTENSION_FALSE;
|
result->return_dnssec_status = GETDNS_EXTENSION_FALSE;
|
||||||
|
|
||||||
|
@ -2176,11 +2178,22 @@ _getdns_context_prepare_for_resolution(struct getdns_context *context,
|
||||||
if (context->resolution_type == GETDNS_RESOLUTION_STUB) {
|
if (context->resolution_type == GETDNS_RESOLUTION_STUB) {
|
||||||
if (tls_is_in_transports_list(context) == 1 &&
|
if (tls_is_in_transports_list(context) == 1 &&
|
||||||
context->tls_ctx == NULL) {
|
context->tls_ctx == NULL) {
|
||||||
#ifdef HAVE_LIBTLS1_2
|
#ifdef HAVE_TLS_v1_2
|
||||||
/* Create client context, use TLS v1.2 only for now */
|
/* Create client context, use TLS v1.2 only for now */
|
||||||
context->tls_ctx = SSL_CTX_new(TLSv1_2_client_method());
|
context->tls_ctx = SSL_CTX_new(TLSv1_2_client_method());
|
||||||
if(context->tls_ctx == NULL)
|
if(context->tls_ctx == NULL)
|
||||||
return GETDNS_RETURN_BAD_CONTEXT;
|
return GETDNS_RETURN_BAD_CONTEXT;
|
||||||
|
/* Be strict and only use the cipher suites recommended in RFC7525 */
|
||||||
|
const char* const PREFERRED_CIPHERS = "EECDH+aRSA+AESGCM:EDH+aRSA+AESGCM";
|
||||||
|
if (!SSL_CTX_set_cipher_list(context->tls_ctx, PREFERRED_CIPHERS))
|
||||||
|
return GETDNS_RETURN_BAD_CONTEXT;
|
||||||
|
if ((tls_only_is_in_transports_list(context) == 1) && context->tls_auth_req)
|
||||||
|
context->tls_auth_fallback_ok = 0;
|
||||||
|
/* TODO: If no auth data provided for any upstream, fail here */
|
||||||
|
else
|
||||||
|
context->tls_auth_fallback_ok = 1;
|
||||||
|
/* By default cert chain will be verified, but note that per
|
||||||
|
connection management of the result and hostname verification is done.*/
|
||||||
SSL_CTX_set_verify(context->tls_ctx, SSL_VERIFY_PEER, _getdns_tls_verify_callback);
|
SSL_CTX_set_verify(context->tls_ctx, SSL_VERIFY_PEER, _getdns_tls_verify_callback);
|
||||||
if (!SSL_CTX_set_default_verify_paths(context->tls_ctx))
|
if (!SSL_CTX_set_default_verify_paths(context->tls_ctx))
|
||||||
return GETDNS_RETURN_BAD_CONTEXT;
|
return GETDNS_RETURN_BAD_CONTEXT;
|
||||||
|
|
|
@ -145,6 +145,9 @@ struct getdns_context {
|
||||||
getdns_upstreams *upstreams;
|
getdns_upstreams *upstreams;
|
||||||
uint16_t limit_outstanding_queries;
|
uint16_t limit_outstanding_queries;
|
||||||
uint32_t dnssec_allowed_skew;
|
uint32_t dnssec_allowed_skew;
|
||||||
|
/*Make this a list*/
|
||||||
|
size_t tls_auth_req;
|
||||||
|
size_t tls_auth_fallback_ok; /*Redundant but convinient*/
|
||||||
|
|
||||||
getdns_transport_list_t *dns_transports;
|
getdns_transport_list_t *dns_transports;
|
||||||
size_t dns_transport_count;
|
size_t dns_transport_count;
|
||||||
|
|
|
@ -104,6 +104,8 @@ network_req_init(getdns_network_req *net_req, getdns_dns_req *owner,
|
||||||
net_req->transport_current = 0;
|
net_req->transport_current = 0;
|
||||||
memcpy(net_req->transports, owner->context->dns_transports,
|
memcpy(net_req->transports, owner->context->dns_transports,
|
||||||
net_req->transport_count * sizeof(getdns_transport_list_t));
|
net_req->transport_count * sizeof(getdns_transport_list_t));
|
||||||
|
net_req->tls_auth_req = owner->context->tls_auth_req;
|
||||||
|
net_req->tls_auth_fallback_ok = owner->context->tls_auth_fallback_ok;
|
||||||
memset(&net_req->event, 0, sizeof(net_req->event));
|
memset(&net_req->event, 0, sizeof(net_req->event));
|
||||||
memset(&net_req->tcp, 0, sizeof(net_req->tcp));
|
memset(&net_req->tcp, 0, sizeof(net_req->tcp));
|
||||||
net_req->query_id = 0;
|
net_req->query_id = 0;
|
||||||
|
|
111
src/stub.c
111
src/stub.c
|
@ -32,6 +32,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <openssl/err.h>
|
#include <openssl/err.h>
|
||||||
|
#include <openssl/conf.h>
|
||||||
#include <openssl/x509v3.h>
|
#include <openssl/x509v3.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
@ -78,7 +79,6 @@ static void stub_timeout_cb(void *userarg);
|
||||||
/* General utility functions */
|
/* General utility functions */
|
||||||
/*****************************/
|
/*****************************/
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
rollover_secret()
|
rollover_secret()
|
||||||
{
|
{
|
||||||
|
@ -822,20 +822,39 @@ tls_failed(getdns_upstream *upstream)
|
||||||
upstream->tls_hs_state == GETDNS_HS_FAILED) ? 1: 0;
|
upstream->tls_hs_state == GETDNS_HS_FAILED) ? 1: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
_getdns_tls_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
|
||||||
|
int err;
|
||||||
|
err = X509_STORE_CTX_get_error(ctx);
|
||||||
|
const char * err_str;
|
||||||
|
err_str = X509_verify_cert_error_string(err);
|
||||||
|
DEBUG_STUB("--- %s, ERROR: %s\n", __FUNCTION__, err_str);
|
||||||
|
/*Always proceed without changing result*/
|
||||||
|
return preverify_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
_getdns_tls_verify_callback_with_fallback(int preverify_ok, X509_STORE_CTX *ctx) {
|
||||||
|
int err;
|
||||||
|
err = X509_STORE_CTX_get_error(ctx);
|
||||||
|
const char * err_str;
|
||||||
|
err_str = X509_verify_cert_error_string(err);
|
||||||
|
DEBUG_STUB("--- %s, ERROR: (%d) \"%s\"\n", __FUNCTION__, err, err_str);
|
||||||
|
/*Proceed if error is hostname mismatch*/
|
||||||
|
if (err == X509_V_ERR_HOSTNAME_MISMATCH)
|
||||||
|
return 1;
|
||||||
|
else
|
||||||
|
return preverify_ok;
|
||||||
|
}
|
||||||
|
|
||||||
static SSL*
|
static SSL*
|
||||||
tls_create_object(getdns_context *context, int fd, const char* auth_name)
|
tls_create_object(getdns_dns_req *dnsreq, int fd, getdns_upstream *upstream)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_LIBSSL_102
|
|
||||||
/* Create SSL instance */
|
/* Create SSL instance */
|
||||||
|
getdns_context *context = dnsreq->context;
|
||||||
if (context->tls_ctx == NULL)
|
if (context->tls_ctx == NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
// if (auth_name[0] == '\0') {
|
|
||||||
// DEBUG_STUB("--- %s, ERROR: No host name provided for authentication\n", __FUNCTION__);
|
|
||||||
// return NULL;
|
|
||||||
// }
|
|
||||||
SSL* ssl = SSL_new(context->tls_ctx);
|
SSL* ssl = SSL_new(context->tls_ctx);
|
||||||
X509_VERIFY_PARAM *param;
|
|
||||||
|
|
||||||
if(!ssl)
|
if(!ssl)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -844,26 +863,47 @@ tls_create_object(getdns_context *context, int fd, const char* auth_name)
|
||||||
SSL_free(ssl);
|
SSL_free(ssl);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
SSL_set_tlsext_host_name(ssl, auth_name);
|
|
||||||
param = SSL_get0_param(ssl);
|
/* NOTE: this code will fallback on a given upstream, without trying
|
||||||
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
|
authentication on other upstreams first. This is non-optimal and is
|
||||||
X509_VERIFY_PARAM_set1_host(param, auth_name, 0);
|
an interim simplification. */
|
||||||
|
|
||||||
|
/* Lack of host name is OK unless only authenticated TLS is specified*/
|
||||||
|
if (upstream->tls_auth_name[0] == '\0') {
|
||||||
|
if (!dnsreq->netreqs[0]->tls_auth_fallback_ok) {
|
||||||
|
DEBUG_STUB("--- %s, ERROR: No host name provided for authentication\n", __FUNCTION__);
|
||||||
|
upstream->tls_hs_state = GETDNS_HS_FAILED;
|
||||||
|
return NULL;
|
||||||
|
} else {
|
||||||
|
SSL_set_verify(ssl, SSL_VERIFY_NONE, NULL);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*Request certificate for the auth_name*/
|
||||||
|
SSL_set_tlsext_host_name(ssl, upstream->tls_auth_name);
|
||||||
|
|
||||||
|
#ifdef HAVE_SSL_HN_AUTH
|
||||||
|
/* Set up native OpenSSL hostname verification*/
|
||||||
|
X509_VERIFY_PARAM *param;
|
||||||
|
param = SSL_get0_param(ssl);
|
||||||
|
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
|
||||||
|
X509_VERIFY_PARAM_set1_host(param, upstream->tls_auth_name, 0);
|
||||||
|
#else
|
||||||
|
/* TODO: Trigger post-handshake custom validation*/
|
||||||
|
if (!dnsreq->netreqs[0]->tls_auth_fallback_ok) {
|
||||||
|
DEBUG_STUB("--- %s, ERROR: Authentication functionality not available\n", __FUNCTION__);
|
||||||
|
upstream->tls_hs_state = GETDNS_HS_FAILED;
|
||||||
|
upstream->tls_auth_failed = 1;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
/* Allow fallback from authenticated TLS if settings permit it (use NONE here?)*/
|
||||||
|
if (dnsreq->netreqs[0]->tls_auth_fallback_ok)
|
||||||
|
SSL_set_verify(ssl, SSL_VERIFY_NONE, _getdns_tls_verify_callback_with_fallback);
|
||||||
|
}
|
||||||
|
|
||||||
SSL_set_connect_state(ssl);
|
SSL_set_connect_state(ssl);
|
||||||
(void) SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
|
(void) SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
|
||||||
return ssl;
|
return ssl;
|
||||||
#else
|
|
||||||
return NULL;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
_getdns_tls_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
|
|
||||||
int err;
|
|
||||||
err = X509_STORE_CTX_get_error(ctx);
|
|
||||||
const char * err_str;
|
|
||||||
err_str = X509_verify_cert_error_string(err);
|
|
||||||
DEBUG_STUB("--- %s, ERROR: %s\n", __FUNCTION__, err_str);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
|
@ -920,6 +960,17 @@ tls_do_handshake(getdns_upstream *upstream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
upstream->tls_hs_state = GETDNS_HS_DONE;
|
upstream->tls_hs_state = GETDNS_HS_DONE;
|
||||||
|
/* TODO: Can we detect the authentication state of the connection here?*/
|
||||||
|
//#ifndef HAVE_SSL_HN_AUTH
|
||||||
|
/*TODO: When OpenSSL 1.0.2 not available, use custom function to validate
|
||||||
|
the hostname.*/
|
||||||
|
// X509* cert = SSL_get_peer_certificate(upstream->tls_obj);
|
||||||
|
// if(cert) { X509_free(cert); } /* Free immediately */
|
||||||
|
// if(NULL == cert) return 0;
|
||||||
|
// if (!verify_cert_hostname(cert, upstream->tls_auth_name))
|
||||||
|
// DEBUG_STUB("--- %s %s\n", __FUNCTION__, "Hostname verification failed: ");
|
||||||
|
// X509_free(cert);
|
||||||
|
//#endif
|
||||||
/* Reset timeout on success*/
|
/* Reset timeout on success*/
|
||||||
GETDNS_CLEAR_EVENT(upstream->loop, &upstream->event);
|
GETDNS_CLEAR_EVENT(upstream->loop, &upstream->event);
|
||||||
upstream->event.read_cb = NULL;
|
upstream->event.read_cb = NULL;
|
||||||
|
@ -1328,9 +1379,9 @@ upstream_read_cb(void *userarg)
|
||||||
if (netreq->owner == upstream->starttls_req) {
|
if (netreq->owner == upstream->starttls_req) {
|
||||||
dnsreq = netreq->owner;
|
dnsreq = netreq->owner;
|
||||||
if (is_starttls_response(netreq)) {
|
if (is_starttls_response(netreq)) {
|
||||||
upstream->tls_obj = tls_create_object(dnsreq->context,
|
upstream->tls_obj = tls_create_object(dnsreq,
|
||||||
upstream->fd,
|
upstream->fd,
|
||||||
upstream->tls_auth_name);
|
upstream);
|
||||||
if (upstream->tls_obj == NULL)
|
if (upstream->tls_obj == NULL)
|
||||||
upstream->tls_hs_state = GETDNS_HS_FAILED;
|
upstream->tls_hs_state = GETDNS_HS_FAILED;
|
||||||
upstream->tls_hs_state = GETDNS_HS_WRITE;
|
upstream->tls_hs_state = GETDNS_HS_WRITE;
|
||||||
|
@ -1570,7 +1621,7 @@ upstream_connect(getdns_upstream *upstream, getdns_transport_list_t transport,
|
||||||
return upstream->fd;
|
return upstream->fd;
|
||||||
fd = tcp_connect(upstream, transport);
|
fd = tcp_connect(upstream, transport);
|
||||||
if (fd == -1) return -1;
|
if (fd == -1) return -1;
|
||||||
upstream->tls_obj = tls_create_object(dnsreq->context, fd, upstream->tls_auth_name);
|
upstream->tls_obj = tls_create_object(dnsreq, fd, upstream);
|
||||||
if (upstream->tls_obj == NULL) {
|
if (upstream->tls_obj == NULL) {
|
||||||
close(fd);
|
close(fd);
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -1633,7 +1684,7 @@ find_upstream_for_netreq(getdns_network_req *netreq)
|
||||||
netreq->transports[i],
|
netreq->transports[i],
|
||||||
&fd);
|
&fd);
|
||||||
if (fd == -1 || !upstream)
|
if (fd == -1 || !upstream)
|
||||||
continue;
|
continue;
|
||||||
netreq->transport_current = i;
|
netreq->transport_current = i;
|
||||||
netreq->upstream = upstream;
|
netreq->upstream = upstream;
|
||||||
return fd;
|
return fd;
|
||||||
|
|
|
@ -157,6 +157,9 @@ nolibldns:
|
||||||
@false
|
@false
|
||||||
|
|
||||||
test: $(NOLIBCHECK) $(NOLIBLDNS) all
|
test: $(NOLIBCHECK) $(NOLIBLDNS) all
|
||||||
|
|
||||||
|
test_code: $(NOLIBCHECK) all
|
||||||
|
|
||||||
(cd $(srcdir)/../.. && find . -type f -executable -and \( -name "*.[ch]" -or -name "*.html" -or -name "*.in" -or -name "*.good" -or -name "*.ac" \) | awk 'BEGIN{e=0}{print("ERROR! Executable bit found on", $$0);e=1}END{exit(e)}')
|
(cd $(srcdir)/../.. && find . -type f -executable -and \( -name "*.[ch]" -or -name "*.html" -or -name "*.in" -or -name "*.good" -or -name "*.ac" \) | awk 'BEGIN{e=0}{print("ERROR! Executable bit found on", $$0);e=1}END{exit(e)}')
|
||||||
./$(CHECK_GETDNS)
|
./$(CHECK_GETDNS)
|
||||||
if test $(have_libevent) = 1 ; then ./$(CHECK_EVENT_PROG) ; fi
|
if test $(have_libevent) = 1 ; then ./$(CHECK_EVENT_PROG) ; fi
|
||||||
|
|
|
@ -862,6 +862,9 @@ getdns_return_t do_the_call(void)
|
||||||
}
|
}
|
||||||
if (r == GETDNS_RETURN_GOOD && !batch_mode)
|
if (r == GETDNS_RETURN_GOOD && !batch_mode)
|
||||||
getdns_context_run(context);
|
getdns_context_run(context);
|
||||||
|
if (r != GETDNS_RETURN_GOOD)
|
||||||
|
fprintf(stderr, "An error occurred: %d '%s'\n", r,
|
||||||
|
getdns_get_errorstr_by_id(r));
|
||||||
} else {
|
} else {
|
||||||
switch (calltype) {
|
switch (calltype) {
|
||||||
case GENERAL:
|
case GENERAL:
|
||||||
|
|
|
@ -196,8 +196,10 @@ typedef struct getdns_network_req
|
||||||
struct getdns_upstream *upstream;
|
struct getdns_upstream *upstream;
|
||||||
int fd;
|
int fd;
|
||||||
getdns_transport_list_t transports[GETDNS_TRANSPORTS_MAX];
|
getdns_transport_list_t transports[GETDNS_TRANSPORTS_MAX];
|
||||||
size_t transport_count;
|
size_t transport_count;
|
||||||
size_t transport_current;
|
size_t transport_current;
|
||||||
|
size_t tls_auth_req;
|
||||||
|
size_t tls_auth_fallback_ok;
|
||||||
getdns_eventloop_event event;
|
getdns_eventloop_event event;
|
||||||
getdns_tcp_state tcp;
|
getdns_tcp_state tcp;
|
||||||
uint16_t query_id;
|
uint16_t query_id;
|
||||||
|
|
Loading…
Reference in New Issue