/* Copyright (C) 2023 CZ.NIC, z.s.p.o. 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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_REGULAR_TOKEN_TIMEOUT (24 * 3600 * 1000000000LLU) #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) { if (header.token[0] == NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY) { 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 ); } else { ret = ngtcp2_crypto_verify_regular_token( header.token, header.tokenlen, (const uint8_t *)table->hash_secret, sizeof(table->hash_secret), (const struct sockaddr *)reply->ip_rem, addr_len((struct sockaddr_in6 *)reply->ip_rem), QUIC_REGULAR_TOKEN_TIMEOUT, now ); } 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; }