From bab5ad623c3715aab25300831959cb2402ff42ab Mon Sep 17 00:00:00 2001 From: George Thessalonikefs Date: Tue, 8 Aug 2023 15:19:56 +0200 Subject: [PATCH] - For #762: Introduce stat counters for downstream DNS Cookies per thread and total: num.queries_cookie_valid, num.queries_cookie_client, num.queries.cookie_invalid. --- daemon/remote.c | 6 ++ daemon/stats.c | 16 ++++ daemon/stats.h | 7 ++ daemon/worker.c | 3 + doc/unbound-control.8.in | 23 ++++- libunbound/unbound.h | 6 ++ smallapp/unbound-control.c | 6 ++ testcode/unitmain.c | 16 ++-- testdata/stat_values.tdir/stat_values.pre | 1 + testdata/stat_values.tdir/stat_values.test | 91 +++++++++++++++++++ .../stat_values_downstream_cookies.conf | 32 +++++++ util/data/msgparse.c | 33 ++++--- util/data/msgparse.h | 2 + util/edns.c | 22 +++-- util/edns.h | 16 +++- 15 files changed, 247 insertions(+), 33 deletions(-) create mode 100644 testdata/stat_values.tdir/stat_values_downstream_cookies.conf diff --git a/daemon/remote.c b/daemon/remote.c index c7bfa4e12..4990fc8e9 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -672,6 +672,12 @@ print_stats(RES* ssl, const char* nm, struct ub_stats_info* s) (unsigned long)s->svr.num_queries)) return 0; if(!ssl_printf(ssl, "%s.num.queries_ip_ratelimited"SQ"%lu\n", nm, (unsigned long)s->svr.num_queries_ip_ratelimited)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_valid"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_valid)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_client"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_client)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_invalid"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_invalid)) return 0; if(!ssl_printf(ssl, "%s.num.cachehits"SQ"%lu\n", nm, (unsigned long)(s->svr.num_queries - s->svr.num_queries_missed_cache))) return 0; diff --git a/daemon/stats.c b/daemon/stats.c index fabbd9f60..4855bf1c1 100644 --- a/daemon/stats.c +++ b/daemon/stats.c @@ -435,6 +435,9 @@ void server_stats_add(struct ub_stats_info* total, struct ub_stats_info* a) { total->svr.num_queries += a->svr.num_queries; total->svr.num_queries_ip_ratelimited += a->svr.num_queries_ip_ratelimited; + total->svr.num_queries_cookie_valid += a->svr.num_queries_cookie_valid; + total->svr.num_queries_cookie_client += a->svr.num_queries_cookie_client; + total->svr.num_queries_cookie_invalid += a->svr.num_queries_cookie_invalid; total->svr.num_queries_missed_cache += a->svr.num_queries_missed_cache; total->svr.num_queries_prefetch += a->svr.num_queries_prefetch; total->svr.num_queries_timed_out += a->svr.num_queries_timed_out; @@ -568,3 +571,16 @@ void server_stats_insrcode(struct ub_server_stats* stats, sldns_buffer* buf) stats->ans_rcode_nodata ++; } } + +void server_stats_downstream_cookie(struct ub_server_stats* stats, + struct edns_data* edns) +{ + if(!(edns->edns_present && edns->cookie_present)) return; + if(edns->cookie_valid) { + stats->num_queries_cookie_valid++; + } else if(edns->cookie_client) { + stats->num_queries_cookie_client++; + } else { + stats->num_queries_cookie_invalid++; + } +} diff --git a/daemon/stats.h b/daemon/stats.h index 4e5e6cf8a..47bb20d7f 100644 --- a/daemon/stats.h +++ b/daemon/stats.h @@ -126,4 +126,11 @@ void server_stats_insquery(struct ub_server_stats* stats, struct comm_point* c, */ void server_stats_insrcode(struct ub_server_stats* stats, struct sldns_buffer* buf); +/** + * Add DNS Cookie stats for this query + * @param stats: the stats + * @param edns: edns record + */ +void server_stats_downstream_cookie(struct ub_server_stats* stats, + struct edns_data* edns); #endif /* DAEMON_STATS_H */ diff --git a/daemon/worker.c b/daemon/worker.c index cdd636f81..8c6fa3b9a 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -1602,6 +1602,9 @@ worker_handle_request(struct comm_point* c, void* arg, int error, } } + /* Get stats for cookies */ + server_stats_downstream_cookie(&worker->stats, &edns); + /* If the IP rate limiting check was postponed, check now. */ if(!pre_edns_ip_ratelimit) { /* NOTE: we always check the repinfo->client_address. diff --git a/doc/unbound-control.8.in b/doc/unbound-control.8.in index acbc89abe..7823de3aa 100644 --- a/doc/unbound-control.8.in +++ b/doc/unbound-control.8.in @@ -369,6 +369,15 @@ number of queries received by thread .I threadX.num.queries_ip_ratelimited number of queries rate limited by thread .TP +.I threadX.num.queries_cookie_valid +number of queries with a valid DNS Cookie by thread +.TP +.I threadX.num.queries_cookie_client +number of queries with a client part only DNS Cookie by thread +.TP +.I threadX.num.queries_cookie_invalid +number of queries with an invalid DNS Cookie by thread +.TP .I threadX.num.cachehits number of queries that were successfully answered using a cache lookup .TP @@ -446,6 +455,18 @@ buffers are full. .I total.num.queries summed over threads. .TP +.I total.num.queries_ip_ratelimited +summed over threads. +.TP +.I total.num.queries_cookie_valid +summed over threads. +.TP +.I total.num.queries_cookie_client +summed over threads. +.TP +.I total.num.queries_cookie_invalid +summed over threads. +.TP .I total.num.cachehits summed over threads. .TP @@ -611,7 +632,7 @@ ratelimiting. .TP .I num.query.dnscrypt.shared_secret.cachemiss The number of dnscrypt queries that did not find a shared secret in the cache. -The can be use to compute the shared secret hitrate. +This can be used to compute the shared secret hitrate. .TP .I num.query.dnscrypt.replay The number of dnscrypt queries that found a nonce hit in the nonce cache and diff --git a/libunbound/unbound.h b/libunbound/unbound.h index 97be66a88..bb8e8acf0 100644 --- a/libunbound/unbound.h +++ b/libunbound/unbound.h @@ -695,6 +695,12 @@ struct ub_server_stats { long long num_queries; /** number of queries that have been dropped/ratelimited by ip. */ long long num_queries_ip_ratelimited; + /** number of queries with a valid DNS Cookie. */ + long long num_queries_cookie_valid; + /** number of queries with only the client part of the DNS Cookie. */ + long long num_queries_cookie_client; + /** number of queries with invalid DNS Cookie. */ + long long num_queries_cookie_invalid; /** number of queries that had a cache-miss. */ long long num_queries_missed_cache; /** number of prefetch queries - cachehits with prefetch */ diff --git a/smallapp/unbound-control.c b/smallapp/unbound-control.c index 891ce23ac..c4f730061 100644 --- a/smallapp/unbound-control.c +++ b/smallapp/unbound-control.c @@ -204,6 +204,12 @@ static void pr_stats(const char* nm, struct ub_stats_info* s) PR_UL_NM("num.queries", s->svr.num_queries); PR_UL_NM("num.queries_ip_ratelimited", s->svr.num_queries_ip_ratelimited); + PR_UL_NM("num.queries_cookie_valid", + s->svr.num_queries_cookie_valid); + PR_UL_NM("num.queries_cookie_client", + s->svr.num_queries_cookie_client); + PR_UL_NM("num.queries_cookie_invalid", + s->svr.num_queries_cookie_invalid); PR_UL_NM("num.cachehits", s->svr.num_queries - s->svr.num_queries_missed_cache); PR_UL_NM("num.cachemiss", s->svr.num_queries_missed_cache); diff --git a/testcode/unitmain.c b/testcode/unitmain.c index f7eec421e..647cbca3b 100644 --- a/testcode/unitmain.c +++ b/testcode/unitmain.c @@ -557,7 +557,7 @@ edns_cookie_invalid_version(void) memcpy(buf + 16, "\306\063\144\144", 4); unit_assert(edns_cookie_server_validate(client_cookie, sizeof(client_cookie), server_secret, sizeof(server_secret), 1, - buf, timestamp) == 0); + buf, timestamp) == COOKIE_STATUS_INVALID); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } @@ -587,7 +587,7 @@ edns_cookie_invalid_hash(void) memcpy(buf + 16, "\313\000\161\313", 4); unit_assert(edns_cookie_server_validate(client_cookie, sizeof(client_cookie), server_secret, sizeof(server_secret), 1, - buf, timestamp) == 0); + buf, timestamp) == COOKIE_STATUS_INVALID); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } @@ -620,13 +620,13 @@ edns_cookie_rfc9018_a3_better(void) memcpy(buf + 16, "\313\000\161\313", 4); unit_assert(edns_cookie_server_validate(client_cookie, sizeof(client_cookie), server_secret, sizeof(server_secret), 1, - buf, timestamp) == -1); + buf, timestamp) == COOKIE_STATUS_VALID_RENEW); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } -/* Complete hash-valid client cookie; more than 60 minutes old; needs a - * refreshed server cookie. */ +/* Complete hash-valid client cookie; more than 60 minutes old (expired); + * needs a refreshed server cookie. */ static void edns_cookie_rfc9018_a3(void) { @@ -651,7 +651,7 @@ edns_cookie_rfc9018_a3(void) memcpy(buf + 16, "\313\000\161\313", 4); unit_assert(edns_cookie_server_validate(client_cookie, sizeof(client_cookie), server_secret, sizeof(server_secret), 1, - buf, timestamp) == 0); + buf, timestamp) == COOKIE_STATUS_EXPIRED); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } @@ -682,7 +682,7 @@ edns_cookie_rfc9018_a2(void) memcpy(buf + 16, "\306\063\144\144", 4); unit_assert(edns_cookie_server_validate(client_cookie, sizeof(client_cookie), server_secret, sizeof(server_secret), 1, - buf, timestamp) == -1); + buf, timestamp) == COOKIE_STATUS_VALID_RENEW); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } @@ -711,7 +711,7 @@ edns_cookie_rfc9018_a1(void) sizeof(client_cookie), /* these will not be used; it will return invalid * because of the size. */ - NULL, 0, 1, NULL, 0) == 0); + NULL, 0, 1, NULL, 0) == COOKIE_STATUS_CLIENT_ONLY); edns_cookie_server_write(buf, server_secret, 1, timestamp); unit_assert(memcmp(server_cookie, buf, 24) == 0); } diff --git a/testdata/stat_values.tdir/stat_values.pre b/testdata/stat_values.tdir/stat_values.pre index ad1166a06..7b6eefdfa 100644 --- a/testdata/stat_values.tdir/stat_values.pre +++ b/testdata/stat_values.tdir/stat_values.pre @@ -37,6 +37,7 @@ echo "FWD_EXPIRED_PID=$FWD_EXPIRED_PID" >> .tpkg.var.test # make config file sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' -e 's/@EXPIREDPORT\@/'$FWD_EXPIRED_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values.conf > ub.conf sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' -e 's/@EXPIREDPORT\@/'$FWD_EXPIRED_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values_cachedb.conf > ub_cachedb.conf +sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values_downstream_cookies.conf > ub_downstream_cookies.conf # start unbound in the background $PRE/unbound -d -c ub.conf >unbound.log 2>&1 & UNBOUND_PID=$! diff --git a/testdata/stat_values.tdir/stat_values.test b/testdata/stat_values.tdir/stat_values.test index c9ed66d82..8366ba88b 100644 --- a/testdata/stat_values.tdir/stat_values.test +++ b/testdata/stat_values.tdir/stat_values.test @@ -414,6 +414,97 @@ rrset.cache.count=3 infra.cache.count=2" +# Bring the downstream DNS Cookies configured Unbound up +kill_pid $UNBOUND_PID # kill current Unbound +$PRE/unbound -d -c ub_downstream_cookies.conf >unbound.log 2>&1 & +UNBOUND_PID=$! +echo "UNBOUND_PID=$UNBOUND_PID" >> .tpkg.var.test +wait_unbound_up unbound.log + +echo +echo "[ Get a DNS Cookie. ]" +echo "> dig www.local.zone +tcp +ednsopt=10:0102030405060708" +dig @127.0.0.1 -p $UNBOUND_PORT +tcp +ednsopt=10:0102030405060708 +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + echo "OK" +else + end 1 +fi +# Save valid cookie +valid_cookie=`grep "COOKIE: " outfile | cut -d ' ' -f 3` +invalid_cookie=`echo $valid_cookie | tr '0' '4'` +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_client=1 +total.num.cachehits=1 +num.query.type.A=1 +num.query.class.IN=1 +num.query.opcode.QUERY=1 +num.query.flags.RD=1 +num.query.flags.AD=1 +num.query.edns.present=1 +num.query.tcp=1 +num.answer.rcode.NOERROR=1" + +echo +echo "[ Present the valid DNS Cookie. ]" +echo "> dig www.local.zone +ednsopt=10:valid_cookie" +dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$valid_cookie +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + echo "OK" +else + end 1 +fi +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_valid=1 +total.num.cachehits=1 +num.query.type.A=1 +num.query.class.IN=1 +num.query.opcode.QUERY=1 +num.query.flags.RD=1 +num.query.flags.AD=1 +num.query.edns.present=1 +num.answer.rcode.NOERROR=1" + +echo +echo "[ Present an invalid DNS Cookie. ]" +echo "> dig www.local.zone +ednsopt=10:invalid_cookie" +dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$invalid_cookie +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + end 1 +else + echo "OK" +fi +# A lot of stats are missing since BADCOOKIE error response is before +# those stat calculations. +# BADCOOKIE is an extended error code; we record YXRRSET below. +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_invalid=1 +total.num.cachehits=1 +num.answer.rcode.YXRRSET=1" + +echo +echo "[ Present no DNS Cookie. ]" +echo "> dig www.local.zone +ignore" +dig @127.0.0.1 -p $UNBOUND_PORT +ignore +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + end 1 +else + echo "OK" +fi +# A lot of stats are missing since REFUSED error response because of no DNS +# Cookie is before those stat calculations. +check_stats "\ +total.num.queries=1 +total.num.cachehits=1 +num.answer.rcode.REFUSED=1" + if test x$USE_CACHEDB = "x1"; then # Bring the cachedb configured Unbound up diff --git a/testdata/stat_values.tdir/stat_values_downstream_cookies.conf b/testdata/stat_values.tdir/stat_values_downstream_cookies.conf new file mode 100644 index 000000000..21e78829f --- /dev/null +++ b/testdata/stat_values.tdir/stat_values_downstream_cookies.conf @@ -0,0 +1,32 @@ +server: + verbosity: 5 + module-config: "iterator" + num-threads: 1 + interface: 127.0.0.1 + port: @PORT@ + use-syslog: no + directory: "" + pidfile: "unbound.pid" + chroot: "" + username: "" + extended-statistics: yes + identity: "stat_values" + outbound-msg-retry: 0 + root-key-sentinel: no + trust-anchor-signaling: no + + local-zone: local.zone static + local-data: "www.local.zone A 192.0.2.1" + + answer-cookie: yes + access-control: 127.0.0.1 allow_cookie + +remote-control: + control-enable: yes + control-interface: 127.0.0.1 + # control-interface: ::1 + control-port: @CONTROL_PORT@ + server-key-file: "unbound_server.key" + server-cert-file: "unbound_server.pem" + control-key-file: "unbound_control.key" + control-cert-file: "unbound_control.pem" diff --git a/util/data/msgparse.c b/util/data/msgparse.c index 6aa853c15..8cef37a17 100644 --- a/util/data/msgparse.c +++ b/util/data/msgparse.c @@ -971,7 +971,7 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, uint16_t opt_code = sldns_read_uint16(rdata_ptr); uint16_t opt_len = sldns_read_uint16(rdata_ptr+2); uint8_t server_cookie[40]; - int cookie_is_valid; + enum edns_cookie_val_status cookie_val_status; int cookie_is_v4 = 1; rdata_ptr += 4; @@ -1064,12 +1064,14 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, &((struct sockaddr_in6*)&repinfo->remote_addr)->sin6_addr, 16); } - cookie_is_valid = edns_cookie_server_validate( + cookie_val_status = edns_cookie_server_validate( rdata_ptr, opt_len, cfg->cookie_secret, cfg->cookie_secret_len, cookie_is_v4, server_cookie, now); - if(cookie_is_valid != 0) edns->cookie_valid = 1; - if(cookie_is_valid == 1) { + switch(cookie_val_status) { + case COOKIE_STATUS_VALID: + case COOKIE_STATUS_VALID_RENEW: + edns->cookie_valid = 1; /* Reuse cookie */ if(!edns_opt_list_append( &edns->opt_list_out, LDNS_EDNS_COOKIE, @@ -1081,13 +1083,22 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, * options. Done! */ break; - } - edns_cookie_server_write(server_cookie, - cfg->cookie_secret, cookie_is_v4, now); - if(!edns_opt_list_append(&edns->opt_list_out, - LDNS_EDNS_COOKIE, 24, server_cookie, region)) { - log_err("out of memory"); - return LDNS_RCODE_SERVFAIL; + case COOKIE_STATUS_CLIENT_ONLY: + edns->cookie_client = 1; + /* fallthrough */ + case COOKIE_STATUS_FUTURE: + case COOKIE_STATUS_EXPIRED: + case COOKIE_STATUS_INVALID: + default: + edns_cookie_server_write(server_cookie, + cfg->cookie_secret, cookie_is_v4, now); + if(!edns_opt_list_append(&edns->opt_list_out, + LDNS_EDNS_COOKIE, 24, server_cookie, + region)) { + log_err("out of memory"); + return LDNS_RCODE_SERVFAIL; + } + break; } break; default: diff --git a/util/data/msgparse.h b/util/data/msgparse.h index 74493b76e..b7dc235d6 100644 --- a/util/data/msgparse.h +++ b/util/data/msgparse.h @@ -243,6 +243,8 @@ struct edns_data { unsigned int cookie_present : 1; /** if the cookie validated */ unsigned int cookie_valid : 1; + /** if the cookie holds only the client part */ + unsigned int cookie_client : 1; }; /** diff --git a/util/edns.c b/util/edns.c index d4db29a65..2b4047f0b 100644 --- a/util/edns.c +++ b/util/edns.c @@ -153,7 +153,7 @@ edns_cookie_server_write(uint8_t* buf, const uint8_t* secret, int v4, memcpy(buf + 16, hash, 8); } -int +enum edns_cookie_val_status edns_cookie_server_validate(const uint8_t* cookie, size_t cookie_len, const uint8_t* secret, size_t secret_len, int v4, const uint8_t* hash_input, uint32_t now) @@ -162,26 +162,28 @@ edns_cookie_server_validate(const uint8_t* cookie, size_t cookie_len, uint32_t timestamp; uint32_t subt_1982 = 0; /* Initialize for the compiler; unused value */ int comp_1982; - if(cookie_len != 24 || /* RFC9018 cookies are 24 bytes long */ - secret_len != 16 || /* RFC9018 cookies have 16 byte secrets */ - cookie[8] != 1) /* RFC9018 cookies are cookie version 1 */ - return 0; + if(cookie_len != 24) + /* RFC9018 cookies are 24 bytes long */ + return COOKIE_STATUS_CLIENT_ONLY; + if(secret_len != 16 || /* RFC9018 cookies have 16 byte secrets */ + cookie[8] != 1) /* RFC9018 cookies are cookie version 1 */ + return COOKIE_STATUS_INVALID; timestamp = sldns_read_uint32(cookie + 12); if((comp_1982 = compare_1982(now, timestamp)) > 0 && (subt_1982 = subtract_1982(timestamp, now)) > 3600) /* Cookie is older than 1 hour (see RFC9018 Section 4.3.) */ - return 0; + return COOKIE_STATUS_EXPIRED; if(comp_1982 <= 0 && subtract_1982(now, timestamp) > 300) /* Cookie time is more than 5 minutes in the future. * (see RFC9018 Section 4.3.) */ - return 0; + return COOKIE_STATUS_FUTURE; if(memcmp(edns_cookie_server_hash(hash_input, secret, v4, hash), cookie + 16, 8) != 0) /* Hashes do not match */ - return 0; + return COOKIE_STATUS_INVALID; if(comp_1982 > 0 && subt_1982 > 1800) /* Valid cookie but older than 30 minutes, so create a new one * anyway */ - return -1; - return 1; + return COOKIE_STATUS_VALID_RENEW; + return COOKIE_STATUS_VALID; } diff --git a/util/edns.h b/util/edns.h index 190e69634..5da0ecb29 100644 --- a/util/edns.h +++ b/util/edns.h @@ -75,6 +75,15 @@ struct edns_string_addr { size_t string_len; }; +enum edns_cookie_val_status { + COOKIE_STATUS_CLIENT_ONLY = -3, + COOKIE_STATUS_FUTURE = -2, + COOKIE_STATUS_EXPIRED = -1, + COOKIE_STATUS_INVALID = 0, + COOKIE_STATUS_VALID = 1, + COOKIE_STATUS_VALID_RENEW = 2, +}; + /** * Create structure to hold EDNS strings * @return: newly created edns_strings, NULL on alloc failure. @@ -149,10 +158,11 @@ void edns_cookie_server_write(uint8_t* buf, const uint8_t* secret, int v4, * @param hash_input: pointer to the hash input for validation. It needs to be: * Client Cookie | Version | Reserved | Timestamp | Client-IP * @param now: the current time. - * return 1 if valid, -1 if valid but a new one SHOULD be generated, else 0. + * return edns_cookie_val_status with the cookie validation status i.e., + * <=0 for invalid, else valid. */ -int edns_cookie_server_validate(const uint8_t* cookie, size_t cookie_len, - const uint8_t* secret, size_t secret_len, int v4, +enum edns_cookie_val_status edns_cookie_server_validate(const uint8_t* cookie, + size_t cookie_len, const uint8_t* secret, size_t secret_len, int v4, const uint8_t* hash_input, uint32_t now); #endif