diff options
Diffstat (limited to 'nts_ke_session.c')
-rw-r--r-- | nts_ke_session.c | 929 |
1 files changed, 929 insertions, 0 deletions
diff --git a/nts_ke_session.c b/nts_ke_session.c new file mode 100644 index 0000000..2ae1e91 --- /dev/null +++ b/nts_ke_session.c @@ -0,0 +1,929 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Miroslav Lichvar 2020-2021 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + NTS-KE session used by server and client + */ + +#include "config.h" + +#include "sysincl.h" + +#include "nts_ke_session.h" + +#include "conf.h" +#include "local.h" +#include "logging.h" +#include "memory.h" +#include "siv.h" +#include "socket.h" +#include "sched.h" +#include "util.h" + +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> + +#define INVALID_SOCK_FD (-8) + +struct RecordHeader { + uint16_t type; + uint16_t body_length; +}; + +struct Message { + int length; + int sent; + int parsed; + int complete; + unsigned char data[NKE_MAX_MESSAGE_LENGTH]; +}; + +typedef enum { + KE_WAIT_CONNECT, + KE_HANDSHAKE, + KE_SEND, + KE_RECEIVE, + KE_SHUTDOWN, + KE_STOPPED, +} KeState; + +struct NKSN_Instance_Record { + int server; + char *server_name; + NKSN_MessageHandler handler; + void *handler_arg; + + KeState state; + int sock_fd; + char *label; + gnutls_session_t tls_session; + SCH_TimeoutID timeout_id; + int retry_factor; + + struct Message message; + int new_message; +}; + +/* ================================================== */ + +static gnutls_priority_t priority_cache; + +static int credentials_counter = 0; + +static int clock_updates = 0; + +/* ================================================== */ + +static void +reset_message(struct Message *message) +{ + message->length = 0; + message->sent = 0; + message->parsed = 0; + message->complete = 0; +} + +/* ================================================== */ + +static int +add_record(struct Message *message, int critical, int type, const void *body, int body_length) +{ + struct RecordHeader header; + + assert(message->length <= sizeof (message->data)); + + if (body_length < 0 || body_length > 0xffff || type < 0 || type > 0x7fff || + message->length + sizeof (header) + body_length > sizeof (message->data)) + return 0; + + header.type = htons(!!critical * NKE_RECORD_CRITICAL_BIT | type); + header.body_length = htons(body_length); + + memcpy(&message->data[message->length], &header, sizeof (header)); + message->length += sizeof (header); + + if (body_length > 0) { + memcpy(&message->data[message->length], body, body_length); + message->length += body_length; + } + + return 1; +} + +/* ================================================== */ + +static void +reset_message_parsing(struct Message *message) +{ + message->parsed = 0; +} + +/* ================================================== */ + +static int +get_record(struct Message *message, int *critical, int *type, int *body_length, + void *body, int buffer_length) +{ + struct RecordHeader header; + int blen, rlen; + + if (message->length < message->parsed + sizeof (header) || + buffer_length < 0) + return 0; + + memcpy(&header, &message->data[message->parsed], sizeof (header)); + + blen = ntohs(header.body_length); + rlen = sizeof (header) + blen; + assert(blen >= 0 && rlen > 0); + + if (message->length < message->parsed + rlen) + return 0; + + if (critical) + *critical = !!(ntohs(header.type) & NKE_RECORD_CRITICAL_BIT); + if (type) + *type = ntohs(header.type) & ~NKE_RECORD_CRITICAL_BIT; + if (body) + memcpy(body, &message->data[message->parsed + sizeof (header)], MIN(buffer_length, blen)); + if (body_length) + *body_length = blen; + + message->parsed += rlen; + + return 1; +} + +/* ================================================== */ + +static int +check_message_format(struct Message *message, int eof) +{ + int critical = 0, type = -1, length = -1, ends = 0; + + reset_message_parsing(message); + message->complete = 0; + + while (get_record(message, &critical, &type, &length, NULL, 0)) { + if (type == NKE_RECORD_END_OF_MESSAGE) { + if (!critical || length != 0 || ends > 0) + return 0; + ends++; + } + } + + /* If the message cannot be fully parsed, but more data may be coming, + consider the format to be ok */ + if (message->length == 0 || message->parsed < message->length) + return !eof; + + if (type != NKE_RECORD_END_OF_MESSAGE) + return !eof; + + message->complete = 1; + + return 1; +} + +/* ================================================== */ + +static gnutls_session_t +create_tls_session(int server_mode, int sock_fd, const char *server_name, + gnutls_certificate_credentials_t credentials, + gnutls_priority_t priority) +{ + unsigned char alpn_name[sizeof (NKE_ALPN_NAME)]; + gnutls_session_t session; + gnutls_datum_t alpn; + unsigned int flags; + int r; + + r = gnutls_init(&session, GNUTLS_NONBLOCK | GNUTLS_NO_TICKETS | + (server_mode ? GNUTLS_SERVER : GNUTLS_CLIENT)); + if (r < 0) { + LOG(LOGS_ERR, "Could not %s TLS session : %s", "create", gnutls_strerror(r)); + return NULL; + } + + if (!server_mode) { + assert(server_name); + + if (!UTI_IsStringIP(server_name)) { + r = gnutls_server_name_set(session, GNUTLS_NAME_DNS, server_name, strlen(server_name)); + if (r < 0) + goto error; + } + + flags = 0; + + if (clock_updates < CNF_GetNoCertTimeCheck()) { + flags |= GNUTLS_VERIFY_DISABLE_TIME_CHECKS | GNUTLS_VERIFY_DISABLE_TRUSTED_TIME_CHECKS; + DEBUG_LOG("Disabled time checks"); + } + + gnutls_session_set_verify_cert(session, server_name, flags); + } + + r = gnutls_priority_set(session, priority); + if (r < 0) + goto error; + + r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, credentials); + if (r < 0) + goto error; + + memcpy(alpn_name, NKE_ALPN_NAME, sizeof (alpn_name)); + alpn.data = alpn_name; + alpn.size = sizeof (alpn_name) - 1; + + r = gnutls_alpn_set_protocols(session, &alpn, 1, 0); + if (r < 0) + goto error; + + gnutls_transport_set_int(session, sock_fd); + + return session; + +error: + LOG(LOGS_ERR, "Could not %s TLS session : %s", "set", gnutls_strerror(r)); + gnutls_deinit(session); + return NULL; +} + +/* ================================================== */ + +static void +stop_session(NKSN_Instance inst) +{ + if (inst->state == KE_STOPPED) + return; + + inst->state = KE_STOPPED; + + SCH_RemoveFileHandler(inst->sock_fd); + SCK_CloseSocket(inst->sock_fd); + inst->sock_fd = INVALID_SOCK_FD; + + Free(inst->label); + inst->label = NULL; + + gnutls_deinit(inst->tls_session); + inst->tls_session = NULL; + + SCH_RemoveTimeout(inst->timeout_id); + inst->timeout_id = 0; +} + +/* ================================================== */ + +static void +session_timeout(void *arg) +{ + NKSN_Instance inst = arg; + + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE session with %s timed out", inst->label); + + inst->timeout_id = 0; + stop_session(inst); +} + +/* ================================================== */ + +static int +check_alpn(NKSN_Instance inst) +{ + gnutls_datum_t alpn; + + if (gnutls_alpn_get_selected_protocol(inst->tls_session, &alpn) < 0 || + alpn.size != sizeof (NKE_ALPN_NAME) - 1 || + memcmp(alpn.data, NKE_ALPN_NAME, sizeof (NKE_ALPN_NAME) - 1) != 0) + return 0; + + return 1; +} + +/* ================================================== */ + +static void +set_input_output(NKSN_Instance inst, int output) +{ + SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_INPUT, !output); + SCH_SetFileHandlerEvent(inst->sock_fd, SCH_FILE_OUTPUT, output); +} + +/* ================================================== */ + +static void +change_state(NKSN_Instance inst, KeState state) +{ + int output; + + switch (state) { + case KE_HANDSHAKE: + output = !inst->server; + break; + case KE_WAIT_CONNECT: + case KE_SEND: + case KE_SHUTDOWN: + output = 1; + break; + case KE_RECEIVE: + output = 0; + break; + default: + assert(0); + } + + set_input_output(inst, output); + + inst->state = state; +} + +/* ================================================== */ + +static int +handle_event(NKSN_Instance inst, int event) +{ + struct Message *message = &inst->message; + int r; + + DEBUG_LOG("Session event %d fd=%d state=%d", event, inst->sock_fd, (int)inst->state); + + switch (inst->state) { + case KE_WAIT_CONNECT: + /* Check if connect() succeeded */ + if (event != SCH_FILE_OUTPUT) + return 0; + + /* Get the socket error */ + if (!SCK_GetIntOption(inst->sock_fd, SOL_SOCKET, SO_ERROR, &r)) + r = EINVAL; + + if (r != 0) { + LOG(LOGS_ERR, "Could not connect to %s : %s", inst->label, strerror(r)); + stop_session(inst); + return 0; + } + + DEBUG_LOG("Connected to %s", inst->label); + + change_state(inst, KE_HANDSHAKE); + return 0; + + case KE_HANDSHAKE: + r = gnutls_handshake(inst->tls_session); + + if (r < 0) { + if (gnutls_error_is_fatal(r)) { + gnutls_datum_t cert_error; + + /* Get a description of verification errors */ + if (r != GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR || + gnutls_certificate_verification_status_print( + gnutls_session_get_verify_cert_status(inst->tls_session), + gnutls_certificate_type_get(inst->tls_session), &cert_error, 0) < 0) + cert_error.data = NULL; + + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, + "TLS handshake with %s failed : %s%s%s", inst->label, gnutls_strerror(r), + cert_error.data ? " " : "", cert_error.data ? (const char *)cert_error.data : ""); + + if (cert_error.data) + gnutls_free(cert_error.data); + + stop_session(inst); + + /* Increase the retry interval if the handshake did not fail due + to the other end closing the connection */ + if (r != GNUTLS_E_PULL_ERROR && r != GNUTLS_E_PREMATURE_TERMINATION) + inst->retry_factor = NKE_RETRY_FACTOR2_TLS; + + return 0; + } + + /* Disable output when the handshake is trying to receive data */ + set_input_output(inst, gnutls_record_get_direction(inst->tls_session)); + return 0; + } + + inst->retry_factor = NKE_RETRY_FACTOR2_TLS; + + if (DEBUG) { + char *description = gnutls_session_get_desc(inst->tls_session); + DEBUG_LOG("Handshake with %s completed %s", + inst->label, description ? description : ""); + gnutls_free(description); + } + + if (!check_alpn(inst)) { + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, "NTS-KE not supported by %s", inst->label); + stop_session(inst); + return 0; + } + + /* Client will send a request to the server */ + change_state(inst, inst->server ? KE_RECEIVE : KE_SEND); + return 0; + + case KE_SEND: + assert(inst->new_message && message->complete); + assert(message->length <= sizeof (message->data) && message->length > message->sent); + + r = gnutls_record_send(inst->tls_session, &message->data[message->sent], + message->length - message->sent); + + if (r < 0) { + if (gnutls_error_is_fatal(r)) { + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, + "Could not send NTS-KE message to %s : %s", inst->label, gnutls_strerror(r)); + stop_session(inst); + } + return 0; + } + + DEBUG_LOG("Sent %d bytes to %s", r, inst->label); + + message->sent += r; + if (message->sent < message->length) + return 0; + + /* Client will receive a response */ + change_state(inst, inst->server ? KE_SHUTDOWN : KE_RECEIVE); + reset_message(&inst->message); + inst->new_message = 0; + return 0; + + case KE_RECEIVE: + do { + if (message->length >= sizeof (message->data)) { + DEBUG_LOG("Message is too long"); + stop_session(inst); + return 0; + } + + r = gnutls_record_recv(inst->tls_session, &message->data[message->length], + sizeof (message->data) - message->length); + + if (r < 0) { + /* Handle a renegotiation request on both client and server as + a protocol error */ + if (gnutls_error_is_fatal(r) || r == GNUTLS_E_REHANDSHAKE) { + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, + "Could not receive NTS-KE message from %s : %s", + inst->label, gnutls_strerror(r)); + stop_session(inst); + } + return 0; + } + + DEBUG_LOG("Received %d bytes from %s", r, inst->label); + + message->length += r; + + } while (gnutls_record_check_pending(inst->tls_session) > 0); + + if (!check_message_format(message, r == 0)) { + LOG(inst->server ? LOGS_DEBUG : LOGS_ERR, + "Received invalid NTS-KE message from %s", inst->label); + stop_session(inst); + return 0; + } + + /* Wait for more data if the message is not complete yet */ + if (!message->complete) + return 0; + + /* Server will send a response to the client */ + change_state(inst, inst->server ? KE_SEND : KE_SHUTDOWN); + + /* Return success to process the received message */ + return 1; + + case KE_SHUTDOWN: + r = gnutls_bye(inst->tls_session, GNUTLS_SHUT_RDWR); + + if (r < 0) { + if (gnutls_error_is_fatal(r)) { + DEBUG_LOG("Shutdown with %s failed : %s", inst->label, gnutls_strerror(r)); + stop_session(inst); + return 0; + } + + /* Disable output when the TLS shutdown is trying to receive data */ + set_input_output(inst, gnutls_record_get_direction(inst->tls_session)); + return 0; + } + + SCK_ShutdownConnection(inst->sock_fd); + stop_session(inst); + + DEBUG_LOG("Shutdown completed"); + return 0; + + default: + assert(0); + return 0; + } +} + +/* ================================================== */ + +static void +read_write_socket(int fd, int event, void *arg) +{ + NKSN_Instance inst = arg; + + if (!handle_event(inst, event)) + return; + + /* A valid message was received. Call the handler to process the message, + and prepare a response if it is a server. */ + + reset_message_parsing(&inst->message); + + if (!(inst->handler)(inst->handler_arg)) { + stop_session(inst); + return; + } +} + +/* ================================================== */ + +static time_t +get_time(time_t *t) +{ + struct timespec now; + + LCL_ReadCookedTime(&now, NULL); + if (t) + *t = now.tv_sec; + + return now.tv_sec; +} + +/* ================================================== */ + +static void +handle_step(struct timespec *raw, struct timespec *cooked, double dfreq, + double doffset, LCL_ChangeType change_type, void *anything) +{ + if (change_type != LCL_ChangeUnknownStep && clock_updates < INT_MAX) + clock_updates++; +} + +/* ================================================== */ + +static int gnutls_initialised = 0; + +static int +init_gnutls(void) +{ + int r; + + if (gnutls_initialised) + return 1; + + r = gnutls_global_init(); + if (r < 0) + LOG_FATAL("Could not initialise %s : %s", "gnutls", gnutls_strerror(r)); + + /* Prepare a priority cache for server and client NTS-KE sessions + (the NTS specification requires TLS1.3 or later) */ + r = gnutls_priority_init2(&priority_cache, + "-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2:-VERS-DTLS-ALL", + NULL, GNUTLS_PRIORITY_INIT_DEF_APPEND); + if (r < 0) { + LOG(LOGS_ERR, "Could not initialise %s : %s", + "priority cache for TLS", gnutls_strerror(r)); + gnutls_global_deinit(); + return 0; + } + + /* Use our clock instead of the system clock in certificate verification */ + gnutls_global_set_time_function(get_time); + + gnutls_initialised = 1; + DEBUG_LOG("Initialised"); + + LCL_AddParameterChangeHandler(handle_step, NULL); + + return 1; +} + +/* ================================================== */ + +static void +deinit_gnutls(void) +{ + if (!gnutls_initialised || credentials_counter > 0) + return; + + LCL_RemoveParameterChangeHandler(handle_step, NULL); + + gnutls_priority_deinit(priority_cache); + gnutls_global_deinit(); + gnutls_initialised = 0; + DEBUG_LOG("Deinitialised"); +} + +/* ================================================== */ + +static NKSN_Credentials +create_credentials(const char **certs, const char **keys, int n_certs_keys, + const char **trusted_certs, uint32_t *trusted_certs_ids, + int n_trusted_certs, uint32_t trusted_cert_set) +{ + gnutls_certificate_credentials_t credentials = NULL; + int i, r; + + if (!init_gnutls()) + return NULL; + + r = gnutls_certificate_allocate_credentials(&credentials); + if (r < 0) + goto error; + + if (certs && keys) { + if (trusted_certs || trusted_certs_ids) + assert(0); + + for (i = 0; i < n_certs_keys; i++) { + if (!UTI_CheckFilePermissions(keys[i], 0771)) + ; + r = gnutls_certificate_set_x509_key_file(credentials, certs[i], keys[i], + GNUTLS_X509_FMT_PEM); + if (r < 0) + goto error; + } + } else { + if (certs || keys || n_certs_keys > 0) + assert(0); + + if (trusted_cert_set == 0 && !CNF_GetNoSystemCert()) { + r = gnutls_certificate_set_x509_system_trust(credentials); + if (r < 0) + goto error; + } + + if (trusted_certs && trusted_certs_ids) { + for (i = 0; i < n_trusted_certs; i++) { + struct stat buf; + + if (trusted_certs_ids[i] != trusted_cert_set) + continue; + + if (stat(trusted_certs[i], &buf) == 0 && S_ISDIR(buf.st_mode)) + r = gnutls_certificate_set_x509_trust_dir(credentials, trusted_certs[i], + GNUTLS_X509_FMT_PEM); + else + r = gnutls_certificate_set_x509_trust_file(credentials, trusted_certs[i], + GNUTLS_X509_FMT_PEM); + if (r < 0) + goto error; + + DEBUG_LOG("Added %d trusted certs from %s", r, trusted_certs[i]); + } + } + } + + credentials_counter++; + + return (NKSN_Credentials)credentials; + +error: + LOG(LOGS_ERR, "Could not set credentials : %s", gnutls_strerror(r)); + if (credentials) + gnutls_certificate_free_credentials(credentials); + deinit_gnutls(); + return NULL; +} + +/* ================================================== */ + +NKSN_Credentials +NKSN_CreateServerCertCredentials(const char **certs, const char **keys, int n_certs_keys) +{ + return create_credentials(certs, keys, n_certs_keys, NULL, NULL, 0, 0); +} + +/* ================================================== */ + +NKSN_Credentials +NKSN_CreateClientCertCredentials(const char **certs, uint32_t *ids, + int n_certs_ids, uint32_t trusted_cert_set) +{ + return create_credentials(NULL, NULL, 0, certs, ids, n_certs_ids, trusted_cert_set); +} + +/* ================================================== */ + +void +NKSN_DestroyCertCredentials(NKSN_Credentials credentials) +{ + gnutls_certificate_free_credentials((gnutls_certificate_credentials_t)credentials); + credentials_counter--; + deinit_gnutls(); +} + +/* ================================================== */ + +NKSN_Instance +NKSN_CreateInstance(int server_mode, const char *server_name, + NKSN_MessageHandler handler, void *handler_arg) +{ + NKSN_Instance inst; + + inst = MallocNew(struct NKSN_Instance_Record); + + inst->server = server_mode; + inst->server_name = server_name ? Strdup(server_name) : NULL; + inst->handler = handler; + inst->handler_arg = handler_arg; + /* Replace a NULL argument with the session itself */ + if (!inst->handler_arg) + inst->handler_arg = inst; + + inst->state = KE_STOPPED; + inst->sock_fd = INVALID_SOCK_FD; + inst->label = NULL; + inst->tls_session = NULL; + inst->timeout_id = 0; + inst->retry_factor = NKE_RETRY_FACTOR2_CONNECT; + + return inst; +} + +/* ================================================== */ + +void +NKSN_DestroyInstance(NKSN_Instance inst) +{ + stop_session(inst); + + Free(inst->server_name); + Free(inst); +} + +/* ================================================== */ + +int +NKSN_StartSession(NKSN_Instance inst, int sock_fd, const char *label, + NKSN_Credentials credentials, double timeout) +{ + assert(inst->state == KE_STOPPED); + + inst->tls_session = create_tls_session(inst->server, sock_fd, inst->server_name, + (gnutls_certificate_credentials_t)credentials, + priority_cache); + if (!inst->tls_session) + return 0; + + inst->sock_fd = sock_fd; + SCH_AddFileHandler(sock_fd, SCH_FILE_INPUT, read_write_socket, inst); + + inst->label = Strdup(label); + inst->timeout_id = SCH_AddTimeoutByDelay(timeout, session_timeout, inst); + inst->retry_factor = NKE_RETRY_FACTOR2_CONNECT; + + reset_message(&inst->message); + inst->new_message = 0; + + change_state(inst, inst->server ? KE_HANDSHAKE : KE_WAIT_CONNECT); + + return 1; +} + +/* ================================================== */ + +void +NKSN_BeginMessage(NKSN_Instance inst) +{ + reset_message(&inst->message); + inst->new_message = 1; +} + +/* ================================================== */ + +int +NKSN_AddRecord(NKSN_Instance inst, int critical, int type, const void *body, int body_length) +{ + assert(inst->new_message && !inst->message.complete); + assert(type != NKE_RECORD_END_OF_MESSAGE); + + return add_record(&inst->message, critical, type, body, body_length); +} + +/* ================================================== */ + +int +NKSN_EndMessage(NKSN_Instance inst) +{ + assert(!inst->message.complete); + + /* Terminate the message */ + if (!add_record(&inst->message, 1, NKE_RECORD_END_OF_MESSAGE, NULL, 0)) + return 0; + + inst->message.complete = 1; + + return 1; +} + +/* ================================================== */ + +int +NKSN_GetRecord(NKSN_Instance inst, int *critical, int *type, int *body_length, + void *body, int buffer_length) +{ + int type2; + + assert(inst->message.complete); + + if (body_length) + *body_length = 0; + + if (!get_record(&inst->message, critical, &type2, body_length, body, buffer_length)) + return 0; + + /* Hide the end-of-message record */ + if (type2 == NKE_RECORD_END_OF_MESSAGE) + return 0; + + if (type) + *type = type2; + + return 1; +} + +/* ================================================== */ + +int +NKSN_GetKeys(NKSN_Instance inst, SIV_Algorithm siv, NKE_Key *c2s, NKE_Key *s2c) +{ + int length = SIV_GetKeyLength(siv); + + if (length <= 0 || length > sizeof (c2s->key) || length > sizeof (s2c->key)) { + DEBUG_LOG("Invalid algorithm"); + return 0; + } + + if (gnutls_prf_rfc5705(inst->tls_session, + sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL, + sizeof (NKE_EXPORTER_CONTEXT_C2S) - 1, NKE_EXPORTER_CONTEXT_C2S, + length, (char *)c2s->key) < 0 || + gnutls_prf_rfc5705(inst->tls_session, + sizeof (NKE_EXPORTER_LABEL) - 1, NKE_EXPORTER_LABEL, + sizeof (NKE_EXPORTER_CONTEXT_S2C) - 1, NKE_EXPORTER_CONTEXT_S2C, + length, (char *)s2c->key) < 0) { + DEBUG_LOG("Could not export key"); + return 0; + } + + c2s->length = length; + s2c->length = length; + + return 1; +} + +/* ================================================== */ + +int +NKSN_IsStopped(NKSN_Instance inst) +{ + return inst->state == KE_STOPPED; +} + +/* ================================================== */ + +void +NKSN_StopSession(NKSN_Instance inst) +{ + stop_session(inst); +} + +/* ================================================== */ + +int +NKSN_GetRetryFactor(NKSN_Instance inst) +{ + return inst->retry_factor; +} |