diff options
Diffstat (limited to 'src/libknot/quic/quic.c')
-rw-r--r-- | src/libknot/quic/quic.c | 1294 |
1 files changed, 1294 insertions, 0 deletions
diff --git a/src/libknot/quic/quic.c b/src/libknot/quic/quic.c new file mode 100644 index 0000000..5e447e7 --- /dev/null +++ b/src/libknot/quic/quic.c @@ -0,0 +1,1294 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <fcntl.h> +#include <gnutls/gnutls.h> +#include <gnutls/crypto.h> +#include <gnutls/x509.h> +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> +#include <ngtcp2/ngtcp2_crypto_gnutls.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <time.h> + +#include "libknot/quic/quic.h" + +#include "contrib/macros.h" +#include "contrib/sockaddr.h" +#include "contrib/string.h" +#include "contrib/ucw/lists.h" +#include "libknot/endian.h" +#include "libdnssec/error.h" +#include "libdnssec/random.h" +#include "libknot/attribute.h" +#include "libknot/endian.h" +#include "libknot/error.h" +#include "libknot/wire.h" + +#define SERVER_DEFAULT_SCIDLEN 18 + +#define QUIC_DEFAULT_VERSION "-VERS-ALL:+VERS-TLS1.3" +#define QUIC_DEFAULT_GROUPS "-GROUP-ALL:+GROUP-X25519:+GROUP-SECP256R1:+GROUP-SECP384R1:+GROUP-SECP521R1" +#define QUIC_PRIORITIES "%DISABLE_TLS13_COMPAT_MODE:NORMAL:"QUIC_DEFAULT_VERSION":"QUIC_DEFAULT_GROUPS + +#define QUIC_SEND_VERSION_NEGOTIATION NGTCP2_ERR_VERSION_NEGOTIATION +#define QUIC_SEND_RETRY NGTCP2_ERR_RETRY +#define QUIC_SEND_STATELESS_RESET (-NGTCP2_STATELESS_RESET_TOKENLEN) +#define QUIC_SEND_CONN_CLOSE (-KNOT_QUIC_HANDLE_RET_CLOSE) +#define QUIC_SEND_EXCESSIVE_LOAD (-KNOT_QUIC_ERR_EXCESSIVE_LOAD) + +#define TLS_CALLBACK_ERR (-1) + +const gnutls_datum_t doq_alpn = { + (unsigned char *)"doq", 3 +}; + +typedef struct knot_quic_creds { + gnutls_certificate_credentials_t tls_cert; + gnutls_anti_replay_t tls_anti_replay; + gnutls_datum_t tls_ticket_key; + bool peer; + uint8_t peer_pin_len; + uint8_t peer_pin[]; +} knot_quic_creds_t; + +typedef struct knot_quic_session { + node_t n; + gnutls_datum_t tls_session; + size_t quic_params_len; + uint8_t quic_params[sizeof(ngtcp2_transport_params)]; +} knot_quic_session_t; + +static unsigned addr_len(const struct sockaddr_in6 *ss) +{ + return (ss->sin6_family == AF_INET6 ? + sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)); +} + +_public_ +bool knot_quic_session_available(knot_quic_conn_t *conn) +{ + return conn != NULL && !(conn->flags & KNOT_QUIC_CONN_SESSION_TAKEN) && + (gnutls_session_get_flags(conn->tls_session) & GNUTLS_SFLAGS_SESSION_TICKET); +} + +_public_ +struct knot_quic_session *knot_quic_session_save(knot_quic_conn_t *conn) +{ + if (!knot_quic_session_available(conn)) { + return NULL; + } + + knot_quic_session_t *session = malloc(sizeof(*session)); + if (session == NULL) { + return NULL; + } + + int ret = gnutls_session_get_data2(conn->tls_session, &session->tls_session); + if (ret != GNUTLS_E_SUCCESS) { + free(session); + return NULL; + } + conn->flags |= KNOT_QUIC_CONN_SESSION_TAKEN; + + ngtcp2_ssize ret2 = + ngtcp2_conn_encode_0rtt_transport_params(conn->conn, session->quic_params, + sizeof(session->quic_params)); + if (ret2 < 0) { + free(session); + return NULL; + } + session->quic_params_len = ret2; + + return session; +} + +_public_ +int knot_quic_session_load(knot_quic_conn_t *conn, struct knot_quic_session *session) +{ + if (session == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + if (conn == NULL) { // Just cleanup the session. + goto session_free; + } + + ret = gnutls_session_set_data(conn->tls_session, session->tls_session.data, + session->tls_session.size); + if (ret != GNUTLS_E_SUCCESS) { + ret = KNOT_ERROR; + goto session_free; + } + + ret = ngtcp2_conn_decode_and_set_0rtt_transport_params(conn->conn, session->quic_params, + session->quic_params_len); + if (ret != 0) { + ret = KNOT_ERROR; + } + +session_free: + gnutls_free(session->tls_session.data); + free(session); + return ret; +} + +static int tls_anti_replay_db_add_func(void *dbf, time_t exp_time, + const gnutls_datum_t *key, + const gnutls_datum_t *data) +{ + return 0; +} + +static void tls_session_ticket_key_free(gnutls_datum_t *ticket) +{ + gnutls_memset(ticket->data, 0, ticket->size); + gnutls_free(ticket->data); +} + +static int self_key(gnutls_x509_privkey_t *privkey, const char *key_file) +{ + gnutls_datum_t data = { 0 }; + + int ret = gnutls_x509_privkey_init(privkey); + if (ret != GNUTLS_E_SUCCESS) { + return ret; + } + + int fd = open(key_file, O_RDONLY); + if (fd != -1) { + struct stat stat; + if (fstat(fd, &stat) != 0 || + (data.data = gnutls_malloc(stat.st_size)) == NULL || + read(fd, data.data, stat.st_size) != stat.st_size) { + ret = GNUTLS_E_KEYFILE_ERROR; + goto finish; + } + + data.size = stat.st_size; + ret = gnutls_x509_privkey_import_pkcs8(*privkey, &data, GNUTLS_X509_FMT_PEM, + NULL, GNUTLS_PKCS_PLAIN); + if (ret != GNUTLS_E_SUCCESS) { + goto finish; + } + } else { + ret = gnutls_x509_privkey_generate(*privkey, GNUTLS_PK_EDDSA_ED25519, + GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_ED25519), 0); + if (ret != GNUTLS_E_SUCCESS) { + goto finish; + } + + ret = gnutls_x509_privkey_export2_pkcs8(*privkey, GNUTLS_X509_FMT_PEM, NULL, + GNUTLS_PKCS_PLAIN, &data); + if (ret != GNUTLS_E_SUCCESS || + (fd = open(key_file, O_WRONLY | O_CREAT, 0600)) == -1 || + write(fd, data.data, data.size) != data.size) { + ret = GNUTLS_E_KEYFILE_ERROR; + goto finish; + } + } + +finish: + close(fd); + gnutls_free(data.data); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_privkey_deinit(*privkey); + *privkey = NULL; + } + return ret; +} + +static int self_signed_cert(gnutls_certificate_credentials_t tls_cert, + const char *key_file) +{ + gnutls_x509_privkey_t privkey = NULL; + gnutls_x509_crt_t cert = NULL; + + char *hostname = sockaddr_hostname(); + if (hostname == NULL) { + return GNUTLS_E_MEMORY_ERROR; + } + + int ret; + uint8_t serial[16]; + gnutls_rnd(GNUTLS_RND_NONCE, serial, sizeof(serial)); + // Clear the left-most bit to be a positive number (two's complement form). + serial[0] &= 0x7F; + +#define CHK(cmd) if ((ret = (cmd)) != GNUTLS_E_SUCCESS) { goto finish; } +#define NOW_DAYS(days) (time(NULL) + 24 * 3600 * (days)) + + CHK(self_key(&privkey, key_file)); + + CHK(gnutls_x509_crt_init(&cert)); + CHK(gnutls_x509_crt_set_version(cert, 3)); + CHK(gnutls_x509_crt_set_serial(cert, serial, sizeof(serial))); + CHK(gnutls_x509_crt_set_activation_time(cert, NOW_DAYS(-1))); + CHK(gnutls_x509_crt_set_expiration_time(cert, NOW_DAYS(10 * 365))); + CHK(gnutls_x509_crt_set_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, + hostname, strlen(hostname))); + CHK(gnutls_x509_crt_set_key(cert, privkey)); + CHK(gnutls_x509_crt_sign2(cert, cert, privkey, GNUTLS_DIG_SHA512, 0)); + + ret = gnutls_certificate_set_x509_key(tls_cert, &cert, 1, privkey); + +finish: + free(hostname); + gnutls_x509_crt_deinit(cert); + gnutls_x509_privkey_deinit(privkey); + + return ret; +} + +_public_ +struct knot_quic_creds *knot_quic_init_creds(const char *cert_file, + const char *key_file) +{ + knot_quic_creds_t *creds = calloc(1, sizeof(*creds)); + if (creds == NULL) { + return NULL; + } + + int ret = gnutls_certificate_allocate_credentials(&creds->tls_cert); + if (ret != GNUTLS_E_SUCCESS) { + goto fail; + } + + ret = gnutls_anti_replay_init(&creds->tls_anti_replay); + if (ret != GNUTLS_E_SUCCESS) { + goto fail; + } + gnutls_anti_replay_set_add_function(creds->tls_anti_replay, tls_anti_replay_db_add_func); + gnutls_anti_replay_set_ptr(creds->tls_anti_replay, NULL); + + if (cert_file != NULL) { + ret = gnutls_certificate_set_x509_key_file(creds->tls_cert, + cert_file, key_file, + GNUTLS_X509_FMT_PEM); + } else { + ret = self_signed_cert(creds->tls_cert, key_file); + } + if (ret != GNUTLS_E_SUCCESS) { + goto fail; + } + + ret = gnutls_session_ticket_key_generate(&creds->tls_ticket_key); + if (ret != GNUTLS_E_SUCCESS) { + goto fail; + } + + return creds; +fail: + knot_quic_free_creds(creds); + return NULL; +} + +_public_ +struct knot_quic_creds *knot_quic_init_creds_peer(const struct knot_quic_creds *local_creds, + const uint8_t *peer_pin, + uint8_t peer_pin_len) +{ + knot_quic_creds_t *creds = calloc(1, sizeof(*creds) + peer_pin_len); + if (creds == NULL) { + return NULL; + } + + if (local_creds != NULL) { + creds->peer = true; + creds->tls_cert = local_creds->tls_cert; + } else { + int ret = gnutls_certificate_allocate_credentials(&creds->tls_cert); + if (ret != GNUTLS_E_SUCCESS) { + free(creds); + return NULL; + } + } + + if (peer_pin_len > 0 && peer_pin != NULL) { + memcpy(creds->peer_pin, peer_pin, peer_pin_len); + creds->peer_pin_len = peer_pin_len; + } + + return creds; +} + +_public_ +int knot_quic_creds_cert(struct knot_quic_creds *creds, struct gnutls_x509_crt_int **cert) +{ + if (creds == NULL || cert == NULL) { + return KNOT_EINVAL; + } + + gnutls_x509_crt_t *certs; + unsigned cert_count; + int ret = gnutls_certificate_get_x509_crt(creds->tls_cert, 0, &certs, &cert_count); + if (ret == GNUTLS_E_SUCCESS) { + if (cert_count == 0) { + gnutls_x509_crt_deinit(*certs); + return KNOT_ENOENT; + } + *cert = *certs; + free(certs); + } + return ret; +} + +_public_ +void knot_quic_free_creds(struct knot_quic_creds *creds) +{ + if (creds == NULL) { + return; + } + + if (!creds->peer && creds->tls_cert != NULL) { + gnutls_certificate_free_credentials(creds->tls_cert); + } + gnutls_anti_replay_deinit(creds->tls_anti_replay); + if (creds->tls_ticket_key.data != NULL) { + tls_session_ticket_key_free(&creds->tls_ticket_key); + } + free(creds); +} + +static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) +{ + return ((knot_quic_conn_t *)conn_ref->user_data)->conn; +} + +static int tls_init_conn_session(knot_quic_conn_t *conn, bool server) +{ + if (gnutls_init(&conn->tls_session, (server ? GNUTLS_SERVER : GNUTLS_CLIENT) | + GNUTLS_ENABLE_EARLY_DATA | GNUTLS_NO_AUTO_SEND_TICKET | + GNUTLS_NO_END_OF_EARLY_DATA) != GNUTLS_E_SUCCESS) { + return TLS_CALLBACK_ERR; + } + + gnutls_certificate_send_x509_rdn_sequence(conn->tls_session, 1); + gnutls_certificate_server_set_request(conn->tls_session, GNUTLS_CERT_REQUEST); + + if (gnutls_priority_set_direct(conn->tls_session, QUIC_PRIORITIES, + NULL) != GNUTLS_E_SUCCESS) { + return TLS_CALLBACK_ERR; + } + + if (server && gnutls_session_ticket_enable_server(conn->tls_session, + &conn->quic_table->creds->tls_ticket_key) != GNUTLS_E_SUCCESS) { + return TLS_CALLBACK_ERR; + } + + int ret = ngtcp2_crypto_gnutls_configure_server_session(conn->tls_session); + if (ret != 0) { + return TLS_CALLBACK_ERR; + } + + gnutls_record_set_max_early_data_size(conn->tls_session, 0xffffffffu); + + conn->conn_ref = (nc_conn_ref_placeholder_t) { + .get_conn = get_conn, + .user_data = conn + }; + + _Static_assert(sizeof(nc_conn_ref_placeholder_t) == sizeof(ngtcp2_crypto_conn_ref), "invalid placeholder for conn_ref"); + gnutls_session_set_ptr(conn->tls_session, &conn->conn_ref); + + if (server) { + gnutls_anti_replay_enable(conn->tls_session, conn->quic_table->creds->tls_anti_replay); + + } + if (gnutls_credentials_set(conn->tls_session, GNUTLS_CRD_CERTIFICATE, + conn->quic_table->creds->tls_cert) != GNUTLS_E_SUCCESS) { + return TLS_CALLBACK_ERR; + } + + gnutls_alpn_set_protocols(conn->tls_session, &doq_alpn, 1, GNUTLS_ALPN_MANDATORY); + + ngtcp2_conn_set_tls_native_handle(conn->conn, conn->tls_session); + + return KNOT_EOK; +} + +static uint64_t get_timestamp(void) +{ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + assert(0); + } + + return (uint64_t)ts.tv_sec * NGTCP2_SECONDS + (uint64_t)ts.tv_nsec; +} + +uint64_t quic_conn_get_timeout(knot_quic_conn_t *conn) +{ + return ngtcp2_conn_get_expiry(conn->conn); +} + +bool quic_conn_timeout(knot_quic_conn_t *conn, uint64_t *now) +{ + if (*now == 0) { + *now = get_timestamp(); + } + return *now > quic_conn_get_timeout(conn); +} + +_public_ +int64_t knot_quic_conn_next_timeout(knot_quic_conn_t *conn) +{ + return (((int64_t)quic_conn_get_timeout(conn) - (int64_t)get_timestamp()) / 1000000L); +} + +_public_ +int knot_quic_hanle_expiry(knot_quic_conn_t *conn) +{ + return ngtcp2_conn_handle_expiry(conn->conn, get_timestamp()) == NGTCP2_NO_ERROR ? KNOT_EOK : KNOT_ECONN; +} + +_public_ +uint32_t knot_quic_conn_rtt(knot_quic_conn_t *conn) +{ + ngtcp2_conn_info info = { 0 }; + ngtcp2_conn_get_conn_info(conn->conn, &info); + return info.smoothed_rtt / 1000; // nanosec --> usec +} + +_public_ +uint16_t knot_quic_conn_local_port(knot_quic_conn_t *conn) +{ + const ngtcp2_path *path = ngtcp2_conn_get_path(conn->conn); + return ((const struct sockaddr_in6 *)path->local.addr)->sin6_port; +} + +_public_ +void knot_quic_conn_pin(knot_quic_conn_t *conn, uint8_t *pin, size_t *pin_size, bool local) +{ + if (conn == NULL) { + goto error; + } + + const gnutls_datum_t *data = NULL; + if (local) { + data = gnutls_certificate_get_ours(conn->tls_session); + } else { + unsigned count = 0; + data = gnutls_certificate_get_peers(conn->tls_session, &count); + if (count == 0) { + goto error; + } + } + if (data == NULL) { + goto error; + } + + gnutls_x509_crt_t cert; + int ret = gnutls_x509_crt_init(&cert); + if (ret != GNUTLS_E_SUCCESS) { + goto error; + } + + ret = gnutls_x509_crt_import(cert, data, GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + goto error; + } + + ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256, pin, pin_size); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + goto error; + } + + gnutls_x509_crt_deinit(cert); + + return; +error: + if (pin_size != NULL) { + *pin_size = 0; + } +} + +static void knot_quic_rand_cb(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) +{ + (void)rand_ctx; + dnssec_random_buffer(dest, destlen); +} + +static void init_random_cid(ngtcp2_cid *cid, size_t len) +{ + if (len == 0) { + len = SERVER_DEFAULT_SCIDLEN; + } + + if (dnssec_random_buffer(cid->data, len) != DNSSEC_EOK) { + cid->datalen = 0; + } else { + cid->datalen = len; + } +} + +static bool init_unique_cid(ngtcp2_cid *cid, size_t len, knot_quic_table_t *table) +{ + do { + if (init_random_cid(cid, len), cid->datalen == 0) { + return false; + } + } while (quic_table_lookup(cid, table) != NULL); + return true; +} + +static int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, + uint8_t *token, size_t cidlen, + void *user_data) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + if (!init_unique_cid(cid, cidlen, ctx->quic_table)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + knot_quic_cid_t **addto = quic_table_insert(ctx, cid, ctx->quic_table); + (void)addto; + + if (token != NULL && + ngtcp2_crypto_generate_stateless_reset_token( + token, (uint8_t *)ctx->quic_table->hash_secret, + sizeof(ctx->quic_table->hash_secret), cid) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int remove_connection_id(ngtcp2_conn *conn, const ngtcp2_cid *cid, + void *user_data) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + knot_quic_cid_t **torem = quic_table_lookup2(cid, ctx->quic_table); + if (torem != NULL) { + assert((*torem)->conn == ctx); + quic_table_rem2(torem, ctx->quic_table); + } + + return 0; +} + +static int handshake_completed_cb(ngtcp2_conn *conn, void *user_data) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + assert(!(ctx->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE)); + ctx->flags |= KNOT_QUIC_CONN_HANDSHAKE_DONE; + + if (!ngtcp2_conn_is_server(conn)) { + knot_quic_creds_t *creds = ctx->quic_table->creds; + if (creds->peer_pin_len == 0) { + return 0; + } + uint8_t pin[KNOT_QUIC_PIN_LEN]; + size_t pin_size = sizeof(pin); + knot_quic_conn_pin(ctx, pin, &pin_size, false); + if (pin_size != creds->peer_pin_len || + const_time_memcmp(pin, creds->peer_pin, pin_size) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; + } + + if (gnutls_session_ticket_send(ctx->tls_session, 1, 0) != GNUTLS_E_SUCCESS) { + return TLS_CALLBACK_ERR; + } + + uint8_t token[NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN]; + ngtcp2_path path = *ngtcp2_conn_get_path(ctx->conn); + uint64_t ts = get_timestamp(); + ngtcp2_ssize tokenlen = ngtcp2_crypto_generate_regular_token(token, + (uint8_t *)ctx->quic_table->hash_secret, + sizeof(ctx->quic_table->hash_secret), + path.remote.addr, path.remote.addrlen, ts); + if (tokenlen < 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (ngtcp2_conn_submit_new_token(ctx->conn, token, tokenlen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, + int64_t stream_id, uint64_t offset, + const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) +{ + (void)(stream_user_data); // always NULL + (void)(offset); // QUIC shall ensure that data arrive in-order + + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + int ret = knot_quic_stream_recv_data(ctx, stream_id, data, datalen, + (flags & NGTCP2_STREAM_DATA_FLAG_FIN)); + + return ret == KNOT_EOK ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +static int acked_stream_data_offset_cb(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, + void *user_data, void *stream_user_data) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + + bool keep = !ngtcp2_conn_is_server(conn); // kxdpgun: await incomming reply after query sent&acked + + knot_quic_stream_ack_data(ctx, stream_id, offset + datalen, keep); + + return 0; +} + +static int stream_closed(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, void *stream_user_data) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + // NOTE possible error is stored in (flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET) + + bool keep = !ngtcp2_conn_is_server(conn); // kxdpgun: process incomming reply after recvd&closed + if (!keep) { + knot_quic_conn_stream_free(ctx, stream_id); + } + return 0; +} + +static int recv_stateless_rst(ngtcp2_conn *conn, const ngtcp2_pkt_stateless_reset *sr, + void *user_data) +{ + // NOTE server can't receive stateless resets, only client + + // ngtcp2 verified stateless reset token already + (void)(sr); + + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + assert(ctx->conn == conn); + + knot_quic_table_rem(ctx, ctx->quic_table); + knot_quic_cleanup(&ctx, 1); + + return 0; +} + +static int recv_stream_rst(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, void *stream_user_data) +{ + (void)final_size; + return stream_closed(conn, NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET, + stream_id, app_error_code, user_data, stream_user_data); +} + +static void user_printf(void *user_data, const char *format, ...) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + if (ctx->quic_table->log_cb != NULL) { + char buf[256]; + va_list args; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + ctx->quic_table->log_cb(buf); + } +} + +static void hex_encode(const uint8_t *in, const uint32_t in_len, char *out) +{ + static const char hex[] = "0123456789abcdef"; + + for (uint32_t i = 0; i < in_len; i++) { + out[2 * i] = hex[in[i] / 16]; + out[2 * i + 1] = hex[in[i] % 16]; + } +} + +static void user_qlog(void *user_data, uint32_t flags, const void *data, size_t datalen) +{ + knot_quic_conn_t *ctx = (knot_quic_conn_t *)user_data; + if (ctx->quic_table->qlog_dir != NULL) { + if (ctx->qlog_fd < 0) { + const ngtcp2_cid *cid = ngtcp2_conn_get_client_initial_dcid(ctx->conn); + if (cid->datalen == 0) { + cid = ngtcp2_conn_get_dcid(ctx->conn); + } + unsigned qlog_dir_len = strlen(ctx->quic_table->qlog_dir); + unsigned qlog_name_len = qlog_dir_len + 2 * cid->datalen + 7; + char qlog_name[qlog_name_len]; + memcpy(qlog_name, ctx->quic_table->qlog_dir, qlog_dir_len); + qlog_name[qlog_dir_len] = '/'; + hex_encode(cid->data, cid->datalen, qlog_name + qlog_dir_len + 1); + memcpy(qlog_name + qlog_name_len - 6, ".qlog", 6); + + ctx->qlog_fd = open(qlog_name, O_CREAT | O_WRONLY | O_APPEND, 0666); + } + if (ctx->qlog_fd >= 0) { // othewise silently skip + _unused_ ssize_t unused = write(ctx->qlog_fd, data, datalen); + if (flags & NGTCP2_QLOG_WRITE_FLAG_FIN) { + close(ctx->qlog_fd); + ctx->qlog_fd = -1; + } + } + } +} + +static int conn_new(ngtcp2_conn **pconn, const ngtcp2_path *path, const ngtcp2_cid *scid, + const ngtcp2_cid *dcid, const ngtcp2_cid *odcid, uint32_t version, + uint64_t now, uint64_t idle_timeout_ns, + knot_quic_conn_t *qconn, bool server, bool retry_sent) +{ + knot_quic_table_t *qtable = qconn->quic_table; + + // I. CALLBACKS + const ngtcp2_callbacks callbacks = { + ngtcp2_crypto_client_initial_cb, + ngtcp2_crypto_recv_client_initial_cb, + ngtcp2_crypto_recv_crypto_data_cb, + handshake_completed_cb, + NULL, // recv_version_negotiation not needed on server, nor kxdpgun + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + recv_stream_data, + acked_stream_data_offset_cb, + NULL, // stream_opened + stream_closed, + recv_stateless_rst, + ngtcp2_crypto_recv_retry_cb, + NULL, // extend_max_streams_bidi + NULL, // extend_max_streams_uni + knot_quic_rand_cb, + get_new_connection_id, + remove_connection_id, + ngtcp2_crypto_update_key_cb, + NULL, // path_validation, + NULL, // select_preferred_addr + recv_stream_rst, + NULL, // extend_max_remote_streams_bidi, might be useful to some allocation optimizations? + NULL, // extend_max_remote_streams_uni + NULL, // extend_max_stream_data, + NULL, // dcid_status + NULL, // handshake_confirmed + NULL, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + NULL, // recv_datagram + NULL, // ack_datagram + NULL, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + NULL, // stream_stop_sending + ngtcp2_crypto_version_negotiation_cb, + NULL, // recv_rx_key + NULL // recv_tx_key + }; + + // II. SETTINGS + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.initial_ts = now; + if (qtable->log_cb != NULL) { + settings.log_printf = user_printf; + } + if (qtable->qlog_dir != NULL) { + settings.qlog_write = user_qlog; + } + if (qtable->udp_payload_limit != 0) { + settings.max_tx_udp_payload_size = qtable->udp_payload_limit; + } + + settings.handshake_timeout = idle_timeout_ns; // NOTE setting handshake timeout to idle_timeout for simplicity + settings.no_pmtud = true; + + // III. PARAMS + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + + params.disable_active_migration = true; + params.initial_max_streams_uni = 0; + params.initial_max_streams_bidi = 1024; + params.initial_max_stream_data_bidi_local = NGTCP2_MAX_VARINT; + params.initial_max_stream_data_bidi_remote = 102400; + params.initial_max_data = NGTCP2_MAX_VARINT; + + params.max_idle_timeout = idle_timeout_ns; + // params.stateless_reset_token_present = 1; + // params.active_connection_id_limit = 7; + if (odcid != NULL) { + params.original_dcid = *odcid; + params.original_dcid_present = true; + } + + if (retry_sent) { + assert(scid); + params.retry_scid_present = 1; + params.retry_scid = *scid; + } + if (dnssec_random_buffer(params.stateless_reset_token, NGTCP2_STATELESS_RESET_TOKENLEN) != DNSSEC_EOK) { + return KNOT_ERROR; + } + + if (server) { + return ngtcp2_conn_server_new(pconn, dcid, scid, path, version, &callbacks, + &settings, ¶ms, NULL, qconn); + } else { + return ngtcp2_conn_client_new(pconn, dcid, scid, path, version, &callbacks, + &settings, ¶ms, NULL, qconn); + } +} + +_public_ +int knot_quic_client(knot_quic_table_t *table, struct sockaddr_in6 *dest, + struct sockaddr_in6 *via, const char *server_name, + knot_quic_conn_t **out_conn) +{ + ngtcp2_cid scid = { 0 }, dcid = { 0 }; + uint64_t now = get_timestamp(); + + if (table == NULL || dest == NULL || via == NULL || out_conn == NULL) { + return KNOT_EINVAL; + } + + init_random_cid(&scid, 0); + init_random_cid(&dcid, 0); + + knot_quic_conn_t *conn = quic_table_add(NULL, &dcid, table); + if (conn == NULL) { + return ENOMEM; + } + + ngtcp2_path path; + path.remote.addr = (struct sockaddr *)dest; + path.remote.addrlen = addr_len((const struct sockaddr_in6 *)dest); + path.local.addr = (struct sockaddr *)via; + path.local.addrlen = addr_len((const struct sockaddr_in6 *)via); + + int ret = conn_new(&conn->conn, &path, &dcid, &scid, NULL, NGTCP2_PROTO_VER_V1, now, + 5000000000L, conn, false, false); + if (ret == KNOT_EOK) { + ret = tls_init_conn_session(conn, false); + } + if (ret == KNOT_EOK && server_name != NULL) { + ret = gnutls_server_name_set(conn->tls_session, GNUTLS_NAME_DNS, + server_name, strlen(server_name)); + } + if (ret != KNOT_EOK) { + knot_quic_table_rem(conn, table); + knot_quic_cleanup(&conn, 1); + return ret; + } + + *out_conn = conn; + return KNOT_EOK; +} + +_public_ +int knot_quic_handle(knot_quic_table_t *table, knot_quic_reply_t *reply, + uint64_t idle_timeout, knot_quic_conn_t **out_conn) +{ + *out_conn = NULL; + if (table == NULL || reply == NULL || out_conn == NULL) { + return KNOT_EINVAL; + } + + ngtcp2_version_cid decoded_cids = { 0 }; + ngtcp2_cid scid = { 0 }, dcid = { 0 }, odcid = { 0 }; + uint64_t now = get_timestamp(); + if (reply->in_payload->iov_len < 1) { + reply->handle_ret = KNOT_EOK; + return KNOT_EOK; + } + int ret = ngtcp2_pkt_decode_version_cid(&decoded_cids, + reply->in_payload->iov_base, + reply->in_payload->iov_len, + SERVER_DEFAULT_SCIDLEN); + if (ret == NGTCP2_ERR_VERSION_NEGOTIATION) { + ret = -QUIC_SEND_VERSION_NEGOTIATION; + goto finish; + } else if (ret != NGTCP2_NO_ERROR) { + goto finish; + } + ngtcp2_cid_init(&dcid, decoded_cids.dcid, decoded_cids.dcidlen); + ngtcp2_cid_init(&scid, decoded_cids.scid, decoded_cids.scidlen); + + knot_quic_conn_t *conn = quic_table_lookup(&dcid, table); + + if (decoded_cids.version == 0 /* short header */ && conn == NULL) { + ret = KNOT_EOK; // NOOP + goto finish; + } + + ngtcp2_path path; + path.remote.addr = (struct sockaddr *)reply->ip_rem; + path.remote.addrlen = addr_len((struct sockaddr_in6 *)reply->ip_rem); + path.local.addr = (struct sockaddr *)reply->ip_loc; + path.local.addrlen = addr_len((struct sockaddr_in6 *)reply->ip_loc); + + if (conn == NULL) { + // new conn + + ngtcp2_pkt_hd header = { 0 }; + ret = ngtcp2_accept(&header, reply->in_payload->iov_base, + reply->in_payload->iov_len); + if (ret == NGTCP2_ERR_RETRY) { + ret = -QUIC_SEND_RETRY; + goto finish; + } else if (ret != NGTCP2_NO_ERROR) { // discard packet + ret = KNOT_EOK; + goto finish; + } + + assert(header.type == NGTCP2_PKT_INITIAL); + if (header.tokenlen == 0 && quic_require_retry(table)) { + ret = -QUIC_SEND_RETRY; + goto finish; + } + + if (header.tokenlen > 0) { + ret = ngtcp2_crypto_verify_retry_token( + &odcid, header.token, header.tokenlen, + (const uint8_t *)table->hash_secret, + sizeof(table->hash_secret), header.version, + (const struct sockaddr *)reply->ip_rem, + addr_len((struct sockaddr_in6 *)reply->ip_rem), + &dcid, idle_timeout, now // NOTE setting retry token validity to idle_timeout for simplicity + ); + if (ret != 0) { + ret = KNOT_EOK; + goto finish; + } + } else { + memcpy(&odcid, &dcid, sizeof(odcid)); + } + + // server chooses his CID to his liking + if (!init_unique_cid(&dcid, 0, table)) { + ret = KNOT_ERROR; + goto finish; + } + + conn = quic_table_add(NULL, &dcid, table); + if (conn == NULL) { + ret = KNOT_ENOMEM; + goto finish; + } + + ret = conn_new(&conn->conn, &path, &dcid, &scid, &odcid, decoded_cids.version, + now, idle_timeout, conn, true, header.tokenlen > 0); + if (ret >= 0) { + ret = tls_init_conn_session(conn, true); + } + if (ret < 0) { + knot_quic_table_rem(conn, table); + *out_conn = conn; // we need knot_quic_cleanup() by the caller afterwards + goto finish; + } + } + + ngtcp2_pkt_info pi = { .ecn = reply->ecn, }; + + ret = ngtcp2_conn_read_pkt(conn->conn, &path, &pi, reply->in_payload->iov_base, + reply->in_payload->iov_len, now); + + *out_conn = conn; + if (ret == NGTCP2_ERR_DRAINING) { // received CONNECTION_CLOSE from the counterpart + knot_quic_table_rem(conn, table); + ret = KNOT_EOK; + goto finish; + } else if (ngtcp2_err_is_fatal(ret)) { // connection doomed + if (ret == NGTCP2_ERR_CALLBACK_FAILURE) { + ret = KNOT_EBADCERTKEY; + } else { + ret = KNOT_ECONN; + } + knot_quic_table_rem(conn, table); + goto finish; + } else if (ret != NGTCP2_NO_ERROR) { // non-fatal error, discard packet + ret = KNOT_EOK; + goto finish; + } + + quic_conn_mark_used(conn, table); + + ret = KNOT_EOK; +finish: + reply->handle_ret = ret; + return ret; +} + +static bool stream_exists(knot_quic_conn_t *conn, int64_t stream_id) +{ + // TRICK, we never use stream_user_data + return (ngtcp2_conn_set_stream_user_data(conn->conn, stream_id, NULL) == NGTCP2_NO_ERROR); +} + +static int send_stream(knot_quic_table_t *quic_table, knot_quic_reply_t *rpl, + knot_quic_conn_t *relay, int64_t stream_id, + uint8_t *data, size_t len, bool fin, ngtcp2_ssize *sent) +{ + (void)quic_table; + assert(stream_id >= 0 || (data == NULL && len == 0)); + + while (stream_id >= 0 && !stream_exists(relay, stream_id)) { + int64_t opened = 0; + int ret = ngtcp2_conn_open_bidi_stream(relay->conn, &opened, NULL); + if (ret != KNOT_EOK) { + return ret; + } + assert((bool)(opened == stream_id) == stream_exists(relay, stream_id)); + } + + int ret = rpl->alloc_reply(rpl); + if (ret != KNOT_EOK) { + return ret; + } + + uint32_t fl = ((stream_id >= 0 && fin) ? NGTCP2_WRITE_STREAM_FLAG_FIN : + NGTCP2_WRITE_STREAM_FLAG_NONE); + ngtcp2_vec vec = { .base = data, .len = len }; + ngtcp2_pkt_info pi = { 0 }; + + struct sockaddr_storage path_loc = { 0 }, path_rem = { 0 }; + ngtcp2_path path = { .local = { .addr = (struct sockaddr *)&path_loc, .addrlen = sizeof(path_loc) }, + .remote = { .addr = (struct sockaddr *)&path_rem, .addrlen = sizeof(path_rem) }, + .user_data = NULL }; + bool find_path = (rpl->ip_rem == NULL); + assert(find_path == (bool)(rpl->ip_loc == NULL)); + + ret = ngtcp2_conn_writev_stream(relay->conn, find_path ? &path : NULL, &pi, + rpl->out_payload->iov_base, rpl->out_payload->iov_len, + sent, fl, stream_id, &vec, + (stream_id >= 0 ? 1 : 0), get_timestamp()); + if (ret <= 0) { + rpl->free_reply(rpl); + return ret; + } + if (*sent < 0) { + *sent = 0; + } + + rpl->out_payload->iov_len = ret; + rpl->ecn = pi.ecn; + if (find_path) { + rpl->ip_loc = &path_loc; + rpl->ip_rem = &path_rem; + } + ret = rpl->send_reply(rpl); + if (find_path) { + rpl->ip_loc = NULL; + rpl->ip_rem = NULL; + } + if (ret == KNOT_EOK) { + return 1; + } + return ret; +} + +static int send_special(knot_quic_table_t *quic_table, knot_quic_reply_t *rpl, + knot_quic_conn_t *relay /* only for connection close */) +{ + int ret = rpl->alloc_reply(rpl); + if (ret != KNOT_EOK) { + return ret; + } + + uint64_t now = get_timestamp(); + ngtcp2_version_cid decoded_cids = { 0 }; + ngtcp2_cid scid = { 0 }, dcid = { 0 }; + int dvc_ret = NGTCP2_ERR_FATAL; + + if ((rpl->handle_ret == -QUIC_SEND_VERSION_NEGOTIATION || + rpl->handle_ret == -QUIC_SEND_RETRY) && + rpl->in_payload != NULL && rpl->in_payload->iov_len > 0) { + dvc_ret = ngtcp2_pkt_decode_version_cid( + &decoded_cids, rpl->in_payload->iov_base, + rpl->in_payload->iov_len, SERVER_DEFAULT_SCIDLEN); + } + + uint8_t rnd = 0; + dnssec_random_buffer(&rnd, sizeof(rnd)); + uint32_t supported_quic[1] = { NGTCP2_PROTO_VER_V1 }; + ngtcp2_cid new_dcid; + uint8_t retry_token[NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN]; + uint8_t stateless_reset_token[NGTCP2_STATELESS_RESET_TOKENLEN]; + uint8_t sreset_rand[NGTCP2_MIN_STATELESS_RESET_RANDLEN]; + dnssec_random_buffer(sreset_rand, sizeof(sreset_rand)); + ngtcp2_ccerr ccerr; + ngtcp2_ccerr_default(&ccerr); + ngtcp2_pkt_info pi = { 0 }; + + struct sockaddr_storage path_loc = { 0 }, path_rem = { 0 }; + ngtcp2_path path = { .local = { .addr = (struct sockaddr *)&path_loc, .addrlen = sizeof(path_loc) }, + .remote = { .addr = (struct sockaddr *)&path_rem, .addrlen = sizeof(path_rem) }, + .user_data = NULL }; + bool find_path = (rpl->ip_rem == NULL); + assert(find_path == (bool)(rpl->ip_loc == NULL)); + assert(!find_path || rpl->handle_ret == -QUIC_SEND_EXCESSIVE_LOAD); + + switch (rpl->handle_ret) { + case -QUIC_SEND_VERSION_NEGOTIATION: + if (dvc_ret != NGTCP2_ERR_VERSION_NEGOTIATION) { + rpl->free_reply(rpl); + return KNOT_ERROR; + } + ret = ngtcp2_pkt_write_version_negotiation( + rpl->out_payload->iov_base, rpl->out_payload->iov_len, + rnd, decoded_cids.scid, decoded_cids.scidlen, decoded_cids.dcid, + decoded_cids.dcidlen, supported_quic, + sizeof(supported_quic) / sizeof(*supported_quic) + ); + break; + case -QUIC_SEND_RETRY: + ngtcp2_cid_init(&dcid, decoded_cids.dcid, decoded_cids.dcidlen); + ngtcp2_cid_init(&scid, decoded_cids.scid, decoded_cids.scidlen); + + init_random_cid(&new_dcid, 0); + + ret = ngtcp2_crypto_generate_retry_token( + retry_token, (const uint8_t *)quic_table->hash_secret, + sizeof(quic_table->hash_secret), decoded_cids.version, + (const struct sockaddr *)rpl->ip_rem, sockaddr_len(rpl->ip_rem), + &new_dcid, &dcid, now + ); + + if (ret >= 0) { + ret = ngtcp2_crypto_write_retry( + rpl->out_payload->iov_base, rpl->out_payload->iov_len, + decoded_cids.version, &scid, &new_dcid, &dcid, + retry_token, ret + ); + } + break; + case -QUIC_SEND_STATELESS_RESET: + ret = ngtcp2_pkt_write_stateless_reset( + rpl->out_payload->iov_base, rpl->out_payload->iov_len, + stateless_reset_token, sreset_rand, sizeof(sreset_rand) + ); + break; + case -QUIC_SEND_CONN_CLOSE: + ret = ngtcp2_conn_write_connection_close( + relay->conn, NULL, &pi, rpl->out_payload->iov_base, + rpl->out_payload->iov_len, &ccerr, now + ); + break; + case -QUIC_SEND_EXCESSIVE_LOAD: + ccerr.type = NGTCP2_CCERR_TYPE_APPLICATION; + ccerr.error_code = KNOT_QUIC_ERR_EXCESSIVE_LOAD; + ret = ngtcp2_conn_write_connection_close( + relay->conn, find_path ? &path : NULL, &pi, rpl->out_payload->iov_base, + rpl->out_payload->iov_len, &ccerr, now + ); + break; + default: + ret = KNOT_EINVAL; + break; + } + + if (ret < 0) { + rpl->free_reply(rpl); + } else { + rpl->out_payload->iov_len = ret; + rpl->ecn = pi.ecn; + if (find_path) { + rpl->ip_loc = &path_loc; + rpl->ip_rem = &path_rem; + } + ret = rpl->send_reply(rpl); + if (find_path) { + rpl->ip_loc = NULL; + rpl->ip_rem = NULL; + } + } + return ret; +} + +_public_ +int knot_quic_send(knot_quic_table_t *quic_table, knot_quic_conn_t *conn, + knot_quic_reply_t *reply, unsigned max_msgs, + knot_quic_send_flag_t flags) +{ + if (quic_table == NULL || conn == NULL || reply == NULL) { + return KNOT_EINVAL; + } else if (reply->handle_ret < 0) { + return reply->handle_ret; + } else if (reply->handle_ret > 0) { + return send_special(quic_table, reply, conn); + } else if (conn == NULL) { + return KNOT_EINVAL; + } else if (conn->conn == NULL) { + return KNOT_EOK; + } + + if (!(conn->flags & KNOT_QUIC_CONN_HANDSHAKE_DONE)) { + max_msgs = 1; + } + + unsigned sent_msgs = 0, stream_msgs = 0, ignore_last = ((flags & KNOT_QUIC_SEND_IGNORE_LASTBYTE) ? 1 : 0); + int ret = 1; + for (int64_t si = 0; si < conn->streams_count && sent_msgs < max_msgs; /* NO INCREMENT */) { + int64_t stream_id = 4 * (conn->streams_first + si); + + ngtcp2_ssize sent = 0; + size_t uf = conn->streams[si].unsent_offset; + knot_quic_obuf_t *uo = conn->streams[si].unsent_obuf; + if (uo == NULL) { + si++; + continue; + } + + bool fin = (((node_t *)uo->node.next)->next == NULL) && ignore_last == 0; + ret = send_stream(quic_table, reply, conn, stream_id, + uo->buf + uf, uo->len - uf - ignore_last, + fin, &sent); + if (ret < 0) { + return ret; + } + + sent_msgs++; + stream_msgs++; + if (sent > 0 && ignore_last > 0) { + sent++; + } + if (sent > 0) { + knot_quic_stream_mark_sent(conn, stream_id, sent); + } + + if (stream_msgs >= max_msgs / conn->streams_count) { + stream_msgs = 0; + si++; // if this stream is sending too much, give chance to other streams + } + } + + while (ret == 1) { + ngtcp2_ssize unused = 0; + ret = send_stream(quic_table, reply, conn, -1, NULL, 0, false, &unused); + } + + return ret; +} |