diff options
Diffstat (limited to 'src')
154 files changed, 29760 insertions, 7758 deletions
diff --git a/src/ipcpd/CMakeLists.txt b/src/ipcpd/CMakeLists.txt index 609da54a..a84e5369 100644 --- a/src/ipcpd/CMakeLists.txt +++ b/src/ipcpd/CMakeLists.txt @@ -4,7 +4,6 @@ # Common sources shared by all IPCPs (absolute paths for subdirectories) set(IPCP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ipcp.c - ${CMAKE_CURRENT_SOURCE_DIR}/shim-data.c ) set(COMMON_SOURCES @@ -24,7 +23,3 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in" add_subdirectory(local) add_subdirectory(broadcast) add_subdirectory(unicast) -if(HAVE_ETH) - add_subdirectory(eth) -endif() -add_subdirectory(udp) diff --git a/src/ipcpd/broadcast/main.c b/src/ipcpd/broadcast/main.c index d18cac82..22b34a76 100644 --- a/src/ipcpd/broadcast/main.c +++ b/src/ipcpd/broadcast/main.c @@ -37,6 +37,7 @@ #include <ouroboros/logs.h> #include <ouroboros/notifier.h> #include <ouroboros/np1_flow.h> +#include <ouroboros/qos.h> #include <ouroboros/random.h> #include <ouroboros/rib.h> #include <ouroboros/time.h> @@ -100,12 +101,14 @@ static void stop_components(void) enroll_stop(); } -static int broadcast_ipcp_enroll(const char * dst, - struct layer_info * info) +static int broadcast_ipcp_enroll(const char * dst, + const struct poa_addr * addr, + struct layer_info * info) { struct ipcp_config * conf; - struct conn conn; - uint8_t id[ENROLL_ID_LEN]; + struct conn conn; + uint8_t id[ENROLL_ID_LEN]; + qosspec_t qs = qos_msg; if (random_buffer(id, ENROLL_ID_LEN) < 0) { log_err("Failed to generate enrollment ID."); @@ -114,7 +117,7 @@ static int broadcast_ipcp_enroll(const char * dst, log_info_id(id, "Requesting enrollment."); - if (connmgr_alloc(COMPID_ENROLL, dst, NULL, &conn) < 0) { + if (connmgr_alloc(COMPID_ENROLL, dst, &qs, addr, &conn) < 0) { log_err_id(id, "Failed to get connection."); goto fail_id; } diff --git a/src/ipcpd/common/connmgr.c b/src/ipcpd/common/connmgr.c index 48ad79ba..e0ad80cb 100644 --- a/src/ipcpd/common/connmgr.c +++ b/src/ipcpd/common/connmgr.c @@ -26,10 +26,12 @@ #include <ouroboros/dev.h> #include <ouroboros/errno.h> #include <ouroboros/fccntl.h> +#include <ouroboros/ipcp-dev.h> #include <ouroboros/list.h> #include <ouroboros/logs.h> #include <ouroboros/notifier.h> #include <ouroboros/pthread.h> +#include <ouroboros/qos.h> #include "connmgr.h" #include "ipcp.h" @@ -38,6 +40,10 @@ #include <stdlib.h> #include <string.h> +#define CONNMGR_ETH_PROBE_TIMEO 20 /* ms, one query attempt */ +#define CONNMGR_ETH_RETRY_TIMEO 1500 /* ms, the remaining tries */ +#define CONNMGR_DHT_TIMEO 1000 /* ms, bounded lower-layer */ + struct conn_el { struct list_head next; struct conn conn; @@ -59,6 +65,14 @@ struct { pthread_t acceptor; } connmgr; +static bool is_eth_query(const struct poa_addr * addr) +{ + static const uint8_t zero[POA_MAC_SIZE] = { 0 }; + + return addr->type == POA_ETH && + memcmp(addr->eth.dst.mac, zero, POA_MAC_SIZE) == 0; +} + static int get_id_by_name(const char * name) { enum comp_id i; @@ -120,10 +134,10 @@ static int add_comp_conn(enum comp_id id, return 0; } +/* qs is also an in-parameter, and flow_accept writes it back. */ static void * flow_acceptor(void * o) { int fd; - qosspec_t qs; struct conn_info rcv_info; struct conn_info fail_info; struct timespec timeo = TIMESPEC_INIT_MS(CONNMGR_RCV_TIMEOUT); @@ -134,7 +148,8 @@ static void * flow_acceptor(void * o) memset(&fail_info, 0, sizeof(fail_info)); while (true) { - int id; + qosspec_t qs = qos_raw; + int id; fd = flow_accept(&qs, NULL); if (fd < 0) { @@ -322,9 +337,10 @@ void connmgr_comp_fini(enum comp_id id) memset(&connmgr.comps[id].info, 0, sizeof(connmgr.comps[id].info)); } -int connmgr_ipcp_connect(const char * dst, - const char * component, - qosspec_t qs) +int connmgr_ipcp_connect(const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr) { struct conn_el * ce; int id; @@ -333,6 +349,11 @@ int connmgr_ipcp_connect(const char * dst, assert(dst); assert(component); + if (qs.service == SVC_STREAM) { + log_err("No stream service on component flows."); + return -ENOTSUP; + } + ce = malloc(sizeof(*ce)); if (ce == NULL) { log_err("Out of memory."); @@ -347,7 +368,7 @@ int connmgr_ipcp_connect(const char * dst, pthread_cleanup_push(free, ce); - ret = connmgr_alloc(id, dst, &qs, &ce->conn); + ret = connmgr_alloc(id, dst, &qs, addr, &ce->conn); pthread_cleanup_pop(false); @@ -413,10 +434,46 @@ int connmgr_ipcp_disconnect(const char * dst, return 0; } -int connmgr_alloc(enum comp_id id, - const char * dst, - qosspec_t * qs, - struct conn * conn) +/* + * Without an address, a peer may be on the wire or reachable through + * the layer below. A PoA query is cheap and creates no flow, so it + * goes first; the layer below gets a bounded try before the query + * retries, and the last try is unbounded. + */ +static int alloc_any(const char * dst, + qosspec_t * qs) +{ + struct timespec probe = TIMESPEC_INIT_MS(CONNMGR_ETH_PROBE_TIMEO); + struct timespec retry = TIMESPEC_INIT_MS(CONNMGR_ETH_RETRY_TIMEO); + struct timespec below = TIMESPEC_INIT_MS(CONNMGR_DHT_TIMEO); + struct poa_addr addr; + int fd; + + if (poa_query(dst, &probe, &addr) == 0) { + fd = poa_flow_alloc(dst, &addr, qs, NULL); + if (fd >= 0) + return fd; + } + + fd = flow_alloc(dst, qs, &below); + if (fd >= 0) + return fd; + + if (poa_query(dst, &retry, &addr) == 0) { + fd = poa_flow_alloc(dst, &addr, qs, NULL); + if (fd >= 0) + return fd; + } + + return flow_alloc(dst, qs, NULL); +} + +/* A literal peer address bypasses the layer below. */ +int connmgr_alloc(enum comp_id id, + const char * dst, + qosspec_t * qs, + const struct poa_addr * addr, + struct conn * conn) { struct comp * comp; int fd; @@ -427,7 +484,26 @@ int connmgr_alloc(enum comp_id id, comp = connmgr.comps + id; - fd = flow_alloc(dst, qs, NULL); + if (addr != NULL) + fd = poa_flow_alloc(dst, addr, qs, NULL); + else + fd = alloc_any(dst, qs); + + if (fd == -EPERM && addr != NULL) { + log_err("No PoA attached to reach %s.", dst); + goto fail_alloc; + } + + if (fd == -EINVAL && addr != NULL) { + log_err("More than one PoA could reach %s", dst); + goto fail_alloc; + } + + if (fd == -ETIMEDOUT && addr != NULL && is_eth_query(addr)) { + log_err("No answer to name query for %s.", dst); + goto fail_alloc; + } + if (fd < 0) { log_err("Failed to allocate flow to %s.", dst); goto fail_alloc; diff --git a/src/ipcpd/common/connmgr.h b/src/ipcpd/common/connmgr.h index f48ecd1b..86f80fb0 100644 --- a/src/ipcpd/common/connmgr.h +++ b/src/ipcpd/common/connmgr.h @@ -24,6 +24,7 @@ #define OUROBOROS_IPCPD_COMMON_CONNMGR_H #include <ouroboros/cep.h> +#include <ouroboros/ipcp.h> #include <ouroboros/qos.h> #include "comp.h" @@ -53,17 +54,19 @@ int connmgr_comp_init(enum comp_id id, void connmgr_comp_fini(enum comp_id id); -int connmgr_ipcp_connect(const char * dst, - const char * component, - qosspec_t qs); +int connmgr_ipcp_connect(const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr); int connmgr_ipcp_disconnect(const char * dst, const char * component); -int connmgr_alloc(enum comp_id id, - const char * dst, - qosspec_t * qs, - struct conn * conn); +int connmgr_alloc(enum comp_id id, + const char * dst, + qosspec_t * qs, + const struct poa_addr * addr, + struct conn * conn); int connmgr_dealloc(enum comp_id id, struct conn * conn); diff --git a/src/ipcpd/config.h.in b/src/ipcpd/config.h.in index 7edec526..55b0120c 100644 --- a/src/ipcpd/config.h.in +++ b/src/ipcpd/config.h.in @@ -25,6 +25,7 @@ #define SYS_MAX_FLOWS @SYS_MAX_FLOWS@ #define PROC_RES_FDS @PROC_RES_FDS@ #define PROC_MAX_FLOWS @PROC_MAX_FLOWS@ +#define POA_MAX_POAS @POA_MAX_POAS@ #define SOCKET_TIMEOUT @SOCKET_TIMEOUT@ #define CONNECT_TIMEOUT @CONNECT_TIMEOUT@ @@ -48,11 +49,12 @@ #define IPCP_UNICAST_MPL @IPCP_UNICAST_MPL@ #define IPCP_UNICAST_MTU @IPCP_UNICAST_MTU@ #define CONNMGR_RCV_TIMEOUT @CONNMGR_RCV_TIMEOUT@ +#define CA_BUCKETS @IPCP_CA_BUCKETS@ +#cmakedefine IPCP_CA_PER_FLOW #cmakedefine DISABLE_CORE_LOCK #cmakedefine BUILD_CONTAINER #cmakedefine IPCP_FLOW_STATS -#cmakedefine IPCP_ETH_FLOW_STATS #cmakedefine IPCP_DEBUG_LOCAL #ifdef CONFIG_OUROBOROS_DEBUG #cmakedefine DEBUG_PROTO_DHT @@ -60,29 +62,6 @@ #cmakedefine DEBUG_PROTO_LS #endif -/* udp */ -#cmakedefine HAVE_DDNS -#define NSUPDATE_EXEC "@NSUPDATE_EXECUTABLE@" -#define NSLOOKUP_EXEC "@NSLOOKUP_EXECUTABLE@" -#define IPCP_UDP_RD_THR @IPCP_UDP_RD_THR@ -#define IPCP_UDP_WR_THR @IPCP_UDP_WR_THR@ -#define IPCP_UDP_MPL @IPCP_UDP_MPL@ -#define IPCP_UDP4_MTU @IPCP_UDP4_MTU@ -#define IPCP_UDP6_MTU @IPCP_UDP6_MTU@ - -/* eth */ -#cmakedefine HAVE_NETMAP -#cmakedefine HAVE_BPF -#cmakedefine HAVE_RAW_SOCKETS -#cmakedefine IPCP_ETH_QDISC_BYPASS -#define IPCP_ETH_RD_THR @IPCP_ETH_RD_THR@ -#define IPCP_ETH_WR_THR @IPCP_ETH_WR_THR@ -#define IPCP_ETH_LO_MTU @IPCP_ETH_LO_MTU@ -#define IPCP_ETH_MGMT_FRAME_SIZE @IPCP_ETH_MGMT_FRAME_SIZE@ -#define IPCP_ETH_MPL @IPCP_ETH_MPL@ -#define IPCP_ETH_SNDBUF @IPCP_ETH_SNDBUF@ -#define IPCP_ETH_RCVBUF @IPCP_ETH_RCVBUF@ - /* local */ #define IPCP_LOCAL_MPL @IPCP_LOCAL_MPL@ #define IPCP_LOCAL_MTU @IPCP_LOCAL_MTU@ diff --git a/src/ipcpd/eth/CMakeLists.txt b/src/ipcpd/eth/CMakeLists.txt deleted file mode 100644 index 5a36352d..00000000 --- a/src/ipcpd/eth/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Ethernet IPCPs build configuration (LLC and DIX) -# HAVE_ETH detection is in cmake/dependencies.cmake - -add_executable(${IPCP_ETH_LLC_TARGET} llc.c ${IPCP_SOURCES}) -add_executable(${IPCP_ETH_DIX_TARGET} dix.c ${IPCP_SOURCES}) - -foreach(target ${IPCP_ETH_LLC_TARGET} ${IPCP_ETH_DIX_TARGET}) - target_include_directories(${target} PRIVATE ${IPCP_INCLUDE_DIRS}) - if(HAVE_BPF AND NOT APPLE) - target_include_directories(${target} PRIVATE ${BPF_C_INCLUDE_DIR}) - endif() - if(HAVE_NETMAP AND NOT APPLE) - target_compile_options(${target} PRIVATE -std=c99) - target_include_directories(${target} PRIVATE ${NETMAP_C_INCLUDE_DIR}) - endif() - target_link_libraries(${target} PRIVATE ouroboros-dev) - ouroboros_target_debug_definitions(${target}) -endforeach() - -install(TARGETS ${IPCP_ETH_LLC_TARGET} ${IPCP_ETH_DIX_TARGET} - RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}) diff --git a/src/ipcpd/eth/eth.c b/src/ipcpd/eth/eth.c deleted file mode 100644 index d6f476f2..00000000 --- a/src/ipcpd/eth/eth.c +++ /dev/null @@ -1,2359 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * IPC processes over Ethernet - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#if !defined(BUILD_ETH_DIX) && !defined(BUILD_ETH_LLC) -#error Define BUILD_ETH_DIX or BUILD_ETH_LLC to build an Ethernet IPCP -#endif - -#if defined(__APPLE__) -#define _BSD_SOURCE -#define _DARWIN_C_SOURCE -#elif defined(__FreeBSD__) -#define __BSD_VISIBLE 1 -#elif defined (__linux__) || defined (__CYGWIN__) -#define _DEFAULT_SOURCE -#else -#define _POSIX_C_SOURCE 200112L -#endif - -#include "config.h" - -#include <ouroboros/atomics.h> -#include <ouroboros/endian.h> -#include <ouroboros/hash.h> -#include <ouroboros/errno.h> -#include <ouroboros/list.h> -#include <ouroboros/utils.h> -#include <ouroboros/bitmap.h> -#include <ouroboros/crc8.h> -#include <ouroboros/dev.h> -#include <ouroboros/ipcp-dev.h> -#include <ouroboros/fqueue.h> -#include <ouroboros/logs.h> -#include <ouroboros/time.h> -#include <ouroboros/fccntl.h> -#include <ouroboros/pthread.h> -#include <ouroboros/rib.h> - -#ifndef IPCP_ETH_FLOW_STATS -#undef FETCH_ADD_RELAXED -#define FETCH_ADD_RELAXED(p, v) ((void) 0) -#undef FETCH_SUB_RELAXED -#define FETCH_SUB_RELAXED(p, v) ((void) 0) -#endif - -#include "ipcp.h" -#include "np1.h" -#include "shim-data.h" - -#include <signal.h> -#include <stdlib.h> -#include <fcntl.h> -#include <unistd.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <sys/ioctl.h> - -#include <net/if.h> -#include <netinet/in.h> - -#ifdef __linux__ -#include <linux/if_packet.h> -#include <linux/if_ether.h> -#include <linux/netlink.h> -#include <linux/rtnetlink.h> -#endif - -#ifdef __FreeBSD__ -#include <net/if_dl.h> -#include <netinet/if_ether.h> -#include <ifaddrs.h> -#endif - -#ifdef __APPLE__ -#include <net/if_dl.h> -#include <ifaddrs.h> -#endif - -#include <poll.h> -#include <sys/mman.h> - -#if defined(HAVE_NETMAP) - #define NETMAP_WITH_LIBS - #include <net/netmap_user.h> -#elif defined(HAVE_BPF) - #define BPF_DEV_MAX 256 - #define BPF_BLEN sysconf(_SC_PAGESIZE) - #include <net/bpf.h> -#endif - -#define MAC_FMT "%02x:%02x:%02x:%02x:%02x:%02x" -#define MAC_VAL(a) \ - (uint8_t)(a)[0], (uint8_t)(a)[1], (uint8_t)(a)[2], \ - (uint8_t)(a)[3], (uint8_t)(a)[4], (uint8_t)(a)[5] - - -#ifndef ETH_MAX_MTU /* In if_ether.h as of Linux 4.10. */ - #define ETH_MAX_MTU 0xFFFFU -#endif /* ETH_MAX_MTU */ -#ifdef BUILD_ETH_DIX - #define ETH_MTU eth_data.mtu - #define ETH_MTU_MAX ETH_MAX_MTU -#else - #define ETH_MTU eth_data.mtu - #define ETH_MTU_MAX 1500 -#endif /* BUILD_ETH_DIX */ - -#define ETH_TYPE_LENGTH_SIZE sizeof(uint16_t) -#define ETH_HEADER_SIZE (2 * MAC_SIZE + ETH_TYPE_LENGTH_SIZE) - -#if defined(BUILD_ETH_DIX) -#define THIS_TYPE IPCP_ETH_DIX -#define MGMT_EID 0 -#define DIX_EID_SIZE sizeof(uint16_t) -#define DIX_LENGTH_SIZE sizeof(uint16_t) -#define DIX_HCS_SIZE CRC8_HASH_LEN -#define DIX_HEADER_SIZE (DIX_EID_SIZE + DIX_LENGTH_SIZE + DIX_HCS_SIZE) -#define ETH_HEADER_TOT_SIZE (ETH_HEADER_SIZE + DIX_HEADER_SIZE) -#define MAX_EIDS (1 << (8 * DIX_EID_SIZE)) -#define ETH_MAX_PACKET_SIZE (ETH_MTU - DIX_HEADER_SIZE) -#define ETH_FRAME_SIZE (ETH_HEADER_SIZE + ETH_MTU_MAX) -#elif defined(BUILD_ETH_LLC) -#define THIS_TYPE IPCP_ETH_LLC -#define MGMT_SAP 0x01 -#define LLC_FIELDS_SIZE 3 -#define LLC_HCS_SIZE CRC8_HASH_LEN -#define LLC_HEADER_SIZE (LLC_FIELDS_SIZE + LLC_HCS_SIZE) -#define ETH_HEADER_TOT_SIZE (ETH_HEADER_SIZE + LLC_HEADER_SIZE) -#define MAX_SAPS 64 -#define ETH_MAX_PACKET_SIZE (ETH_MTU - LLC_HEADER_SIZE) -#define ETH_FRAME_SIZE (ETH_HEADER_SIZE + ETH_MTU_MAX) -#endif - -#define NAME_QUERY_TIMEO 1900 /* ms total budget */ -#define NAME_QUERY_RETRIES 3 /* retransmits, 4 attempts total */ -#define MGMT_TIMEO 100 /* ms */ -#define MGMT_FRAME_SIZE IPCP_ETH_MGMT_FRAME_SIZE -#define ETH_RIB_PATH "eth" - -#define FLOW_REQ 0 -#define FLOW_REPLY 1 -#define NAME_QUERY_REQ 2 -#define NAME_QUERY_REPLY 3 - -struct mgmt_msg { -#if defined(BUILD_ETH_DIX) - uint16_t seid; - uint16_t deid; -#elif defined(BUILD_ETH_LLC) - uint8_t ssap; - uint8_t dsap; - /* QoS here for alignment */ - uint8_t code; - uint8_t availability; -#endif - /* QoS parameters from spec, aligned */ - uint32_t loss; - uint64_t bandwidth; - uint32_t ber; - uint32_t max_gap; - uint32_t delay; - uint32_t timeout; - int32_t response; - uint8_t service; -#if defined (BUILD_ETH_DIX) - uint8_t code; - uint8_t availability; -#endif -} __attribute__((packed)); - -struct eth_frame { - uint8_t dst_hwaddr[MAC_SIZE]; - uint8_t src_hwaddr[MAC_SIZE]; -#if defined(BUILD_ETH_DIX) - uint16_t ethertype; - uint16_t eid; - uint16_t length; -#elif defined(BUILD_ETH_LLC) - uint16_t length; - uint8_t dsap; - uint8_t ssap; - uint8_t cf; -#endif - uint8_t hcs; - uint8_t payload; -} __attribute__((packed)); - -struct ef { -#if defined(BUILD_ETH_DIX) - int32_t r_eid; -#elif defined(BUILD_ETH_LLC) - int8_t sap; - int8_t r_sap; -#endif - uint8_t r_addr[MAC_SIZE]; -#ifdef IPCP_ETH_FLOW_STATS - struct { - time_t stamp; - size_t p_rcv; - size_t b_rcv; - size_t p_dlv_f; - size_t p_snd; - size_t b_snd; - size_t p_snd_f; - } stat; -#endif -}; - -struct mgmt_frame { - struct list_head next; - uint8_t r_addr[MAC_SIZE]; - uint8_t buf[MGMT_FRAME_SIZE]; - size_t len; -}; - -struct { - struct shim_data * shim_data; - - int mtu; -#ifdef __linux__ - int if_idx; -#endif -#if defined(HAVE_NETMAP) - struct nm_desc * nmd; - uint8_t hw_addr[MAC_SIZE]; - struct pollfd poll_in; - struct pollfd poll_out; -#elif defined(HAVE_BPF) - int bpf; - uint8_t hw_addr[MAC_SIZE]; -#elif defined(HAVE_RAW_SOCKETS) - int s_fd; - struct sockaddr_ll device; -#endif /* HAVE_NETMAP */ -#if defined (BUILD_ETH_DIX) - uint16_t ethertype; -#elif defined(BUILD_ETH_LLC) - struct bmp * saps; - int * ef_to_fd; -#endif - struct ef * fd_to_ef; - fset_t * np1_flows; - pthread_rwlock_t flows_lock; -#ifdef IPCP_ETH_FLOW_STATS - struct { - size_t n_flows; - size_t n_rcv; - size_t n_snd; - size_t n_mgmt_rcv; - size_t n_mgmt_snd; - size_t n_bad_id; - size_t n_dlv_f; - size_t n_buf_f; - size_t n_rcv_f; - size_t n_snd_f; - size_t kern_rcv; - size_t kern_drp; - } stat; -#endif - - pthread_t packet_writer[IPCP_ETH_WR_THR]; - pthread_t packet_reader[IPCP_ETH_RD_THR]; - -#ifdef __linux__ - pthread_t if_monitor; -#endif - - /* Handle mgmt frames in a different thread */ - pthread_t mgmt_handler; - pthread_mutex_t mgmt_lock; - pthread_cond_t mgmt_cond; - struct list_head mgmt_frames; -} eth_data; - -static int eth_data_init(void) -{ - int i; - int ret = -ENOMEM; - pthread_condattr_t cattr; - - eth_data.fd_to_ef = - malloc(sizeof(*eth_data.fd_to_ef) * SYS_MAX_FLOWS); - if (eth_data.fd_to_ef == NULL) - goto fail_fd_to_ef; - -#ifdef BUILD_ETH_LLC - eth_data.ef_to_fd = - malloc(sizeof(*eth_data.ef_to_fd) * MAX_SAPS); - if (eth_data.ef_to_fd == NULL) - goto fail_ef_to_fd; - - for (i = 0; i < MAX_SAPS; ++i) - eth_data.ef_to_fd[i] = -1; - - eth_data.saps = bmp_create(MAX_SAPS, 2); - if (eth_data.saps == NULL) - goto fail_saps; -#endif - eth_data.np1_flows = fset_create(); - if (eth_data.np1_flows == NULL) - goto fail_np1_flows; - - for (i = 0; i < SYS_MAX_FLOWS; ++i) { -#if defined(BUILD_ETH_DIX) - eth_data.fd_to_ef[i].r_eid = -1; -#elif defined(BUILD_ETH_LLC) - eth_data.fd_to_ef[i].sap = -1; - eth_data.fd_to_ef[i].r_sap = -1; -#endif - memset(ð_data.fd_to_ef[i].r_addr, 0, MAC_SIZE); -#ifdef IPCP_ETH_FLOW_STATS - memset(ð_data.fd_to_ef[i].stat, 0, - sizeof(eth_data.fd_to_ef[i].stat)); -#endif - } -#ifdef IPCP_ETH_FLOW_STATS - memset(ð_data.stat, 0, sizeof(eth_data.stat)); -#endif - - eth_data.shim_data = shim_data_create(); - if (eth_data.shim_data == NULL) - goto fail_shim_data; - - ret = -1; - - if (pthread_rwlock_init(ð_data.flows_lock, NULL)) - goto fail_flows_lock; - - if (pthread_mutex_init(ð_data.mgmt_lock, NULL)) - goto fail_mgmt_lock; - - if (pthread_condattr_init(&cattr)) - goto fail_condattr; - -#ifndef __APPLE__ - pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); -#endif - - if (pthread_cond_init(ð_data.mgmt_cond, &cattr)) - goto fail_mgmt_cond; - - pthread_condattr_destroy(&cattr); - - list_head_init(ð_data.mgmt_frames); - - return 0; - - fail_mgmt_cond: - pthread_condattr_destroy(&cattr); - fail_condattr: - pthread_mutex_destroy(ð_data.mgmt_lock); - fail_mgmt_lock: - pthread_rwlock_destroy(ð_data.flows_lock); - fail_flows_lock: - shim_data_destroy(eth_data.shim_data); - fail_shim_data: - fset_destroy(eth_data.np1_flows); - fail_np1_flows: -#ifdef BUILD_ETH_LLC - bmp_destroy(eth_data.saps); - fail_saps: - free(eth_data.ef_to_fd); - fail_ef_to_fd: -#endif - free(eth_data.fd_to_ef); - fail_fd_to_ef: - return ret; -} - -static void eth_data_fini(void) -{ -#if defined(HAVE_NETMAP) - nm_close(eth_data.nmd); -#elif defined(HAVE_BPF) - close(eth_data.bpf); -#elif defined(HAVE_RAW_SOCKETS) - close(eth_data.s_fd); -#endif - pthread_cond_destroy(ð_data.mgmt_cond); - pthread_mutex_destroy(ð_data.mgmt_lock); - pthread_rwlock_destroy(ð_data.flows_lock); - shim_data_destroy(eth_data.shim_data); - fset_destroy(eth_data.np1_flows); -#ifdef BUILD_ETH_LLC - bmp_destroy(eth_data.saps); - free(eth_data.ef_to_fd); -#endif - free(eth_data.fd_to_ef); -} - -#ifdef IPCP_ETH_FLOW_STATS -static int eth_rib_read(const char * path, - char * buf, - size_t len) -{ - struct ef * flow; - int fd; - char tmstr[RIB_TM_STRLEN]; - struct tm * tm; - time_t stamp; - char * entry; - - entry = strstr(path, RIB_SEPARATOR) + 1; - assert(entry); - - if (len < 2048) - return 0; - - buf[0] = '\0'; - - if (strcmp(entry, "summary") == 0) { - int n; -#if defined(HAVE_RAW_SOCKETS) - int rcvbuf = 0; - int sndbuf = 0; - int queued = 0; - socklen_t optlen = sizeof(rcvbuf); -# if defined(__linux__) - struct tpacket_stats tp_stats; - socklen_t tp_len = sizeof(tp_stats); -# endif - - getsockopt(eth_data.s_fd, SOL_SOCKET, - SO_RCVBUF, &rcvbuf, &optlen); - optlen = sizeof(sndbuf); - getsockopt(eth_data.s_fd, SOL_SOCKET, - SO_SNDBUF, &sndbuf, &optlen); - ioctl(eth_data.s_fd, FIONREAD, &queued); -# if defined(__linux__) - if (getsockopt(eth_data.s_fd, SOL_PACKET, - PACKET_STATISTICS, - &tp_stats, &tp_len) == 0) { - FETCH_ADD_RELAXED(ð_data.stat.kern_rcv, - tp_stats.tp_packets); - FETCH_ADD_RELAXED(ð_data.stat.kern_drp, - tp_stats.tp_drops); - } -# endif -#endif - n = sprintf(buf, - "Active flows: %20zu\n" - "Total frames received: %20zu\n" - "Total frames sent: %20zu\n" - "Management frames received: %20zu\n" - "Management frames sent: %20zu\n" - "Bad EID/SAP frames: %20zu\n" - "Delivery (N+1) failures: %20zu\n" - "Buffer alloc failures: %20zu\n" - "Frame read failures: %20zu\n" - "Frame send failures: %20zu\n", - LOAD_RELAXED(ð_data.stat.n_flows), - LOAD_RELAXED(ð_data.stat.n_rcv), - LOAD_RELAXED(ð_data.stat.n_snd), - LOAD_RELAXED(ð_data.stat.n_mgmt_rcv), - LOAD_RELAXED(ð_data.stat.n_mgmt_snd), - LOAD_RELAXED(ð_data.stat.n_bad_id), - LOAD_RELAXED(ð_data.stat.n_dlv_f), - LOAD_RELAXED(ð_data.stat.n_buf_f), - LOAD_RELAXED(ð_data.stat.n_rcv_f), - LOAD_RELAXED(ð_data.stat.n_snd_f)); -#if defined(HAVE_RAW_SOCKETS) - n += sprintf(buf + n, - "Socket rcvbuf (bytes): %20d\n" - "Socket sndbuf (bytes): %20d\n" - "Socket queued (bytes): %20d\n", - rcvbuf, sndbuf, queued); -# if defined(__linux__) - n += sprintf(buf + n, - "Kernel frames received: %20zu\n" - "Kernel frames dropped: %20zu\n", - LOAD_RELAXED(ð_data.stat.kern_rcv), - LOAD_RELAXED(ð_data.stat.kern_drp)); -# endif -#endif - return n; - } - - fd = atoi(entry); - - if (fd < 0 || fd >= SYS_MAX_FLOWS) - return -1; - - flow = ð_data.fd_to_ef[fd]; - - pthread_rwlock_rdlock(ð_data.flows_lock); - - stamp = flow->stat.stamp; - if (stamp == 0) { - pthread_rwlock_unlock(ð_data.flows_lock); - return 0; - } - - pthread_rwlock_unlock(ð_data.flows_lock); - - tm = gmtime(&stamp); - strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); - - sprintf(buf, - "Flow established at: %20s\n" - "Sent (packets): %20zu\n" - "Sent (bytes): %20zu\n" - "Send failed (packets): %20zu\n" - "Received (packets): %20zu\n" - "Received (bytes): %20zu\n" - "Delivery (N+1) failures: %20zu\n", - tmstr, - LOAD_RELAXED(&flow->stat.p_snd), - LOAD_RELAXED(&flow->stat.b_snd), - LOAD_RELAXED(&flow->stat.p_snd_f), - LOAD_RELAXED(&flow->stat.p_rcv), - LOAD_RELAXED(&flow->stat.b_rcv), - LOAD_RELAXED(&flow->stat.p_dlv_f)); - - return strlen(buf); -} - -static int eth_rib_readdir(char *** buf) -{ - char entry[RIB_PATH_LEN + 1]; - size_t i; - int idx = 0; - int n_entries; - - pthread_rwlock_rdlock(ð_data.flows_lock); - - n_entries = (int) LOAD_RELAXED(ð_data.stat.n_flows) + 1; - - *buf = malloc(sizeof(**buf) * n_entries); - if (*buf == NULL) - goto fail_entries; - - (*buf)[idx] = malloc(strlen("summary") + 1); - if ((*buf)[idx] == NULL) - goto fail_entry; - - strcpy((*buf)[idx++], "summary"); - - for (i = 0; i < SYS_MAX_FLOWS && idx < n_entries; ++i) { - if (eth_data.fd_to_ef[i].stat.stamp == 0) - continue; - - sprintf(entry, "%zu", i); - - (*buf)[idx] = malloc(strlen(entry) + 1); - if ((*buf)[idx] == NULL) - goto fail_entry; - - strcpy((*buf)[idx++], entry); - } - - pthread_rwlock_unlock(ð_data.flows_lock); - - return idx; - - fail_entry: - while (idx-- > 0) - free((*buf)[idx]); - free(*buf); - fail_entries: - pthread_rwlock_unlock(ð_data.flows_lock); - return -ENOMEM; -} - -static int eth_rib_getattr(const char * path, - struct rib_attr * attr) -{ - int fd; - char * entry; - struct ef * flow; - - entry = strstr(path, RIB_SEPARATOR) + 1; - assert(entry); - - if (strcmp(entry, "summary") == 0) { - attr->size = 2048; - attr->mtime = 0; - return 0; - } - - fd = atoi(entry); - - if (fd < 0 || fd >= SYS_MAX_FLOWS) { - attr->size = 0; - attr->mtime = 0; - return 0; - } - - flow = ð_data.fd_to_ef[fd]; - - pthread_rwlock_rdlock(ð_data.flows_lock); - - if (flow->stat.stamp != 0) { - attr->size = 2048; - attr->mtime = flow->stat.stamp; - } else { - attr->size = 0; - attr->mtime = 0; - } - - pthread_rwlock_unlock(ð_data.flows_lock); - - return 0; -} - -static struct rib_ops eth_r_ops = { - .read = eth_rib_read, - .readdir = eth_rib_readdir, - .getattr = eth_rib_getattr -}; -#endif /* IPCP_ETH_FLOW_STATS */ - -#ifdef BUILD_ETH_LLC -static uint8_t reverse_bits(uint8_t b) -{ - b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; - b = (b & 0xCC) >> 2 | (b & 0x33) << 2; - b = (b & 0xAA) >> 1 | (b & 0x55) << 1; - - return b; -} -#endif - -/* Pass a buffer that contains space for the header. */ -static int eth_ipcp_send_frame(const uint8_t * dst_addr, -#if defined(BUILD_ETH_DIX) - uint16_t deid, -#elif defined(BUILD_ETH_LLC) - uint8_t dsap, - uint8_t ssap, -#endif - const uint8_t * frame, - size_t len) -{ - uint32_t frame_len = 0; -#ifdef BUILD_ETH_LLC - uint8_t cf = 0x03; -#endif - struct eth_frame * e_frame; -#ifdef HAVE_RAW_SOCKETS - fd_set fds; - - FD_ZERO(&fds); -#endif - - assert(frame); - - if (len > (size_t) ETH_MAX_PACKET_SIZE) - return -1; - - e_frame = (struct eth_frame *) frame; - - memcpy(e_frame->dst_hwaddr, dst_addr, MAC_SIZE); - memcpy(e_frame->src_hwaddr, -#if defined(HAVE_NETMAP) || defined(HAVE_BPF) - eth_data.hw_addr, -#elif defined(HAVE_RAW_SOCKETS) - eth_data.device.sll_addr, -#endif /* HAVE_NETMAP */ - MAC_SIZE); -#if defined(BUILD_ETH_DIX) - e_frame->ethertype = eth_data.ethertype; - e_frame->eid = htons(deid); - e_frame->length = htons(len); - mem_hash(HASH_CRC8, &e_frame->hcs, - (uint8_t *) &e_frame->eid, - DIX_EID_SIZE + DIX_LENGTH_SIZE); - frame_len = ETH_HEADER_TOT_SIZE + len; -#elif defined(BUILD_ETH_LLC) - e_frame->length = htons(LLC_HEADER_SIZE + len); - e_frame->dsap = dsap; - e_frame->ssap = ssap; - e_frame->cf = cf; - mem_hash(HASH_CRC8, &e_frame->hcs, - (uint8_t *) &e_frame->dsap, - LLC_FIELDS_SIZE); - frame_len = ETH_HEADER_TOT_SIZE + len; -#endif - -#if defined(HAVE_NETMAP) - if (poll(ð_data.poll_out, 1, -1) < 0) - return -1; - - if (nm_inject(eth_data.nmd, frame, frame_len) != (int) frame_len) { - log_dbg("Failed to send message."); - return -1; - } -#elif defined(HAVE_BPF) - if (write(eth_data.bpf, frame, frame_len) < 0) { - log_dbg("Failed to send message."); - return -1; - } - -#elif defined(HAVE_RAW_SOCKETS) - FD_SET(eth_data.s_fd, &fds); - if (select(eth_data.s_fd + 1, NULL, &fds, NULL, NULL) < 0) { - log_dbg("Select() failed: %s.", strerror(errno)); - return -1; - } - assert(FD_ISSET(eth_data.s_fd, &fds)); - - if (sendto(eth_data.s_fd, frame, frame_len, 0, - (struct sockaddr *) ð_data.device, - sizeof(eth_data.device)) <= 0) { - log_dbg("Failed to send message: %s.", strerror(errno)); - return -1; - } -#endif /* HAVE_NETMAP */ - - FETCH_ADD_RELAXED(ð_data.stat.n_snd, 1); - - return 0; -} - -static int eth_ipcp_alloc(const uint8_t * dst_addr, -#if defined(BUILD_ETH_DIX) - uint16_t eid, -#elif defined(BUILD_ETH_LLC) - uint8_t ssap, -#endif - const uint8_t * hash, - qosspec_t qs, - const buffer_t * data) -{ - uint8_t * buf; - struct mgmt_msg * msg; - size_t len; - int ret; - - len = sizeof(*msg) + ipcp_dir_hash_len(); - - buf = malloc(len + ETH_HEADER_TOT_SIZE + data->len); - if (buf == NULL) - return -1; - - memset(buf, 0, len + ETH_HEADER_TOT_SIZE + data->len); - - msg = (struct mgmt_msg *) (buf + ETH_HEADER_TOT_SIZE); - msg->code = FLOW_REQ; -#if defined(BUILD_ETH_DIX) - msg->seid = htons(eid); -#elif defined(BUILD_ETH_LLC) - msg->ssap = ssap; -#endif - - msg->delay = hton32(qs.delay); - msg->bandwidth = hton64(qs.bandwidth); - msg->availability = qs.availability; - msg->loss = hton32(qs.loss); - msg->ber = hton32(qs.ber); - msg->service = qs.service; - msg->max_gap = hton32(qs.max_gap); - msg->timeout = hton32(qs.timeout); - - memcpy(msg + 1, hash, ipcp_dir_hash_len()); - if (data->len > 0) - memcpy(buf + len + ETH_HEADER_TOT_SIZE, data->data, data->len); - - ret = eth_ipcp_send_frame(dst_addr, -#if defined(BUILD_ETH_DIX) - MGMT_EID, -#elif defined(BUILD_ETH_LLC) - reverse_bits(MGMT_SAP), - reverse_bits(MGMT_SAP), -#endif - buf, len + data->len); - free(buf); - - if (ret == 0) - FETCH_ADD_RELAXED(ð_data.stat.n_mgmt_snd, 1); - - return ret; -} - -static int eth_ipcp_alloc_resp(uint8_t * dst_addr, -#if defined(BUILD_ETH_DIX) - uint16_t seid, - uint16_t deid, -#elif defined(BUILD_ETH_LLC) - uint8_t ssap, - uint8_t dsap, -#endif - int response, - const buffer_t * data) -{ - struct mgmt_msg * msg; - uint8_t * buf; - - buf = malloc(sizeof(*msg) + ETH_HEADER_TOT_SIZE + data->len); - if (buf == NULL) - return -1; - - memset(buf, 0, sizeof(*msg) + ETH_HEADER_TOT_SIZE + data->len); - - msg = (struct mgmt_msg *) (buf + ETH_HEADER_TOT_SIZE); - - msg->code = FLOW_REPLY; -#if defined(BUILD_ETH_DIX) - msg->seid = htons(seid); - msg->deid = htons(deid); -#elif defined(BUILD_ETH_LLC) - msg->ssap = ssap; - msg->dsap = dsap; -#endif - msg->response = hton32(response); - - if (data->len > 0) - memcpy(msg + 1, data->data, data->len); - - if (eth_ipcp_send_frame(dst_addr, -#if defined(BUILD_ETH_DIX) - MGMT_EID, -#elif defined(BUILD_ETH_LLC) - reverse_bits(MGMT_SAP), - reverse_bits(MGMT_SAP), -#endif - buf, sizeof(*msg) + data->len)) { - free(buf); - return -1; - } - - FETCH_ADD_RELAXED(ð_data.stat.n_mgmt_snd, 1); - - free(buf); - - return 0; -} - -static int eth_ipcp_req(uint8_t * r_addr, -#if defined(BUILD_ETH_DIX) - uint16_t r_eid, -#elif defined(BUILD_ETH_LLC) - uint8_t r_sap, -#endif - const uint8_t * dst, - qosspec_t qs, - const buffer_t * data) -{ - int fd; - - fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_ETH_MPL, - ETH_MAX_PACKET_SIZE, data); - if (fd < 0) { - log_err("Could not get new flow from IRMd."); - return -1; - } - - pthread_rwlock_wrlock(ð_data.flows_lock); -#if defined(BUILD_ETH_DIX) - eth_data.fd_to_ef[fd].r_eid = r_eid; -#elif defined(BUILD_ETH_LLC) - eth_data.fd_to_ef[fd].r_sap = r_sap; -#endif - memcpy(eth_data.fd_to_ef[fd].r_addr, r_addr, MAC_SIZE); - - pthread_rwlock_unlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - log_dbg("New flow request, fd %d, remote endpoint %d.", fd, r_eid); -#elif defined(BUILD_ETH_LLC) - log_dbg("New flow request, fd %d, remote SAP %d.", fd, r_sap); -#endif - return 0; -} - -static int eth_ipcp_alloc_reply(uint8_t * r_addr, -#if defined(BUILD_ETH_DIX) - uint16_t seid, - uint16_t deid, -#elif defined(BUILD_ETH_LLC) - uint8_t ssap, - int dsap, -#endif - int response, - const buffer_t * data) -{ - int ret = 0; - int fd = -1; - time_t mpl = IPCP_ETH_MPL; - - pthread_rwlock_wrlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - fd = deid; -#elif defined(BUILD_ETH_LLC) - fd = eth_data.ef_to_fd[dsap]; -#endif - if (fd < 0) { - pthread_rwlock_unlock(ð_data.flows_lock); - log_err("No flow found with that SAP."); - return -1; /* -EFLOWNOTFOUND */ - } - - if (response) { -#ifdef BUILD_ETH_LLC - bmp_release(eth_data.saps, eth_data.fd_to_ef[fd].sap); -#endif - } else { -#if defined(BUILD_ETH_DIX) - eth_data.fd_to_ef[fd].r_eid = seid; -#elif defined(BUILD_ETH_LLC) - eth_data.fd_to_ef[fd].r_sap = ssap; -#endif - memcpy(eth_data.fd_to_ef[fd].r_addr, r_addr, MAC_SIZE); - } - - pthread_rwlock_unlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - log_dbg("Flow reply, fd %d, src eid %d, dst eid %d.", fd, seid, deid); -#elif defined(BUILD_ETH_LLC) - log_dbg("Flow reply, fd %d, SSAP %d, DSAP %d.", fd, ssap, dsap); -#endif - if ((ret = ipcp_flow_alloc_reply(fd, response, mpl, - ETH_MAX_PACKET_SIZE, data)) < 0) { - log_err("Failed to reply to flow allocation."); - return -1; - } - - return ret; -} - -static int eth_ipcp_name_query_req(const uint8_t * hash, - uint8_t * r_addr) -{ - uint8_t * buf; - struct mgmt_msg * msg; - size_t len; - - if (shim_data_reg_has(eth_data.shim_data, hash)) { - len = sizeof(*msg) + ipcp_dir_hash_len(); - - buf = malloc(len + ETH_HEADER_TOT_SIZE); - if (buf == NULL) - return -1; - - memset(buf, 0, len + ETH_HEADER_TOT_SIZE); - - msg = (struct mgmt_msg *) (buf + ETH_HEADER_TOT_SIZE); - msg->code = NAME_QUERY_REPLY; - - memcpy(msg + 1, hash, ipcp_dir_hash_len()); - - if (eth_ipcp_send_frame(r_addr, -#if defined(BUILD_ETH_DIX) - MGMT_EID, -#elif defined(BUILD_ETH_LLC) - reverse_bits(MGMT_SAP), - reverse_bits(MGMT_SAP), -#endif - buf, len)) { - log_err("Failed to send management frame."); - free(buf); - return -1; - } - - FETCH_ADD_RELAXED(ð_data.stat.n_mgmt_snd, 1); - - free(buf); - } - - return 0; -} - -static int eth_ipcp_name_query_reply(const uint8_t * hash, - uint8_t * r_addr) -{ - struct addr addr; - - memcpy(&addr.mac, r_addr, MAC_SIZE); - - shim_data_dir_add_entry(eth_data.shim_data, hash, addr); - - shim_data_dir_query_respond(eth_data.shim_data, hash); - - return 0; -} - -static int eth_ipcp_mgmt_frame(const uint8_t * buf, - size_t len, - uint8_t * r_addr) -{ - struct mgmt_msg * msg; - size_t msg_len; - qosspec_t qs; - buffer_t data; - - if (len < sizeof(*msg)) - return -1; - - msg = (struct mgmt_msg *) buf; - - switch (msg->code) { - case FLOW_REQ: - msg_len = sizeof(*msg) + ipcp_dir_hash_len(); - - if (len < msg_len) - return -1; - - qs.delay = ntoh32(msg->delay); - qs.bandwidth = ntoh64(msg->bandwidth); - qs.availability = msg->availability; - qs.loss = ntoh32(msg->loss); - qs.ber = ntoh32(msg->ber); - qs.service = msg->service; - qs.max_gap = ntoh32(msg->max_gap); - qs.timeout = ntoh32(msg->timeout); - - data.data = (uint8_t *) buf + msg_len; - data.len = len - msg_len; - - if (shim_data_reg_has(eth_data.shim_data, - buf + sizeof(*msg))) { - eth_ipcp_req(r_addr, -#if defined(BUILD_ETH_DIX) - ntohs(msg->seid), -#elif defined(BUILD_ETH_LLC) - msg->ssap, -#endif - buf + sizeof(*msg), - qs, - &data); - } - break; - case FLOW_REPLY: - data.data = (uint8_t *) buf + sizeof(*msg); - data.len = len - sizeof(*msg); - - eth_ipcp_alloc_reply(r_addr, -#if defined(BUILD_ETH_DIX) - ntohs(msg->seid), - ntohs(msg->deid), -#elif defined(BUILD_ETH_LLC) - msg->ssap, - msg->dsap, -#endif - ntoh32(msg->response), - &data); - break; - case NAME_QUERY_REQ: - if (len < sizeof(*msg) + ipcp_dir_hash_len()) - return -1; - eth_ipcp_name_query_req(buf + sizeof(*msg), r_addr); - break; - case NAME_QUERY_REPLY: - if (len < sizeof(*msg) + ipcp_dir_hash_len()) - return -1; - eth_ipcp_name_query_reply(buf + sizeof(*msg), r_addr); - break; - default: - log_err("Unknown message received %d.", msg->code); - return -1; - } - - return 0; -} - -static void * eth_ipcp_mgmt_handler(void * o) -{ - (void) o; - - pthread_cleanup_push(__cleanup_mutex_unlock, ð_data.mgmt_lock); - - while (true) { - int ret = 0; - struct timespec timeout = TIMESPEC_INIT_MS(MGMT_TIMEO); - struct timespec abstime; - struct mgmt_frame * frame = NULL; - - clock_gettime(PTHREAD_COND_CLOCK, &abstime); - ts_add(&abstime, &timeout, &abstime); - - pthread_mutex_lock(ð_data.mgmt_lock); - - while (list_is_empty(ð_data.mgmt_frames) && - ret != -ETIMEDOUT) - ret = -pthread_cond_timedwait(ð_data.mgmt_cond, - ð_data.mgmt_lock, - &abstime); - if (ret != -ETIMEDOUT) - frame = list_first_entry((ð_data.mgmt_frames), - struct mgmt_frame, next); - if (frame != NULL) - list_del(&frame->next); - - pthread_mutex_unlock(ð_data.mgmt_lock); - - if (frame == NULL) - continue; - - eth_ipcp_mgmt_frame(frame->buf, frame->len, frame->r_addr); - - free(frame); - } - - pthread_cleanup_pop(false); - - return (void *) 0; -} - -static void * eth_ipcp_packet_reader(void * o) -{ - uint8_t br_addr[MAC_SIZE]; -#if defined(BUILD_ETH_DIX) - uint16_t deid; -#elif defined(BUILD_ETH_LLC) - uint8_t dsap; - uint8_t ssap; -#endif - uint16_t length; - int fd; - uint8_t * buf; -#if defined(HAVE_NETMAP) - struct nm_pkthdr hdr; -#else - struct ssm_pk_buff * spb; - fd_set fds; - int frame_len; -#endif -#if defined(HAVE_RAW_SOCKETS) - struct sockaddr_ll src; - socklen_t slen; -#endif - size_t eth_len; - uint8_t hcs; - struct eth_frame * e_frame; - struct mgmt_frame * frame; - - (void) o; - - ipcp_lock_to_core(); - - memset(br_addr, 0xff, MAC_SIZE * sizeof(uint8_t)); - - while (true) { -#if defined(HAVE_NETMAP) - if (poll(ð_data.poll_in, 1, -1) < 0) - continue; - if (eth_data.poll_in.revents == 0) /* TIMED OUT */ - continue; - - buf = nm_nextpkt(eth_data.nmd, &hdr); - if (buf == NULL) { - log_dbg("Bad read from netmap device."); - continue; - } -#else - FD_ZERO(&fds); - #if defined(HAVE_BPF) - FD_SET(eth_data.bpf, &fds); - if (select(eth_data.bpf + 1, &fds, NULL, NULL, NULL)) - continue; - assert(FD_ISSET(eth_data.bpf, &fds)); - if (ipcp_spb_reserve(&spb, BPF_LEN)) - continue; - buf = ssm_pk_buff_head(spb); - frame_len = read(eth_data.bpf, buf, BPF_BLEN); - #elif defined(HAVE_RAW_SOCKETS) - FD_SET(eth_data.s_fd, &fds); - if (select(eth_data.s_fd + 1, &fds, NULL, NULL, NULL) < 0) - continue; - assert(FD_ISSET(eth_data.s_fd, &fds)); - if (ipcp_spb_reserve(&spb, ETH_MTU)) { - FETCH_ADD_RELAXED(ð_data.stat.n_buf_f, 1); - continue; - } - buf = ssm_pk_buff_push(spb, ETH_HEADER_TOT_SIZE); - if (buf == NULL) { - log_dbg("Failed to allocate header."); - ipcp_spb_release(spb); - FETCH_ADD_RELAXED(ð_data.stat.n_buf_f, 1); - continue; - } - slen = sizeof(src); - /* MSG_DONTWAIT: RD_THR>1 race-loser bails with EAGAIN. */ - frame_len = recvfrom(eth_data.s_fd, buf, - ETH_MTU + ETH_HEADER_TOT_SIZE, - MSG_DONTWAIT, - (struct sockaddr *) &src, &slen); - #endif - if (frame_len == 0) { - ipcp_spb_release(spb); - continue; /* Spurious */ - } - - if (frame_len < 0) { - ipcp_spb_release(spb); - - if (errno == EAGAIN || errno == EWOULDBLOCK) - continue; - - log_dbg("Failed to rcv frame: %s.", strerror(errno)); - FETCH_ADD_RELAXED(ð_data.stat.n_rcv_f, 1); - continue; - } -#endif - -#if defined(HAVE_NETMAP) - eth_len = hdr.len; -#elif defined(HAVE_BPF) - eth_len = ((struct bpf_hdr *) buf)->bh_caplen; -#else - eth_len = (size_t) frame_len; -#endif - /* Defense in depth: reject before parsing dereferences. */ - if (eth_len < ETH_HEADER_TOT_SIZE) - goto fail_frame; - -#if defined(HAVE_RAW_SOCKETS) - /* Drop our own egress. */ - if (src.sll_pkttype == PACKET_OUTGOING) - goto fail_frame; -#endif - -#if defined(HAVE_BPF) && !defined(HAVE_NETMAP) - e_frame = (struct eth_frame *) - (buf + ((struct bpf_hdr *) buf)->bh_hdrlen); -#else - e_frame = (struct eth_frame *) buf; -#endif - assert(e_frame->dst_hwaddr); - -#if !defined(HAVE_BPF) - #if defined(HAVE_NETMAP) - if (memcmp(eth_data.hw_addr, - #elif defined(HAVE_RAW_SOCKETS) - if (memcmp(eth_data.device.sll_addr, - #endif /* HAVE_NETMAP */ - e_frame->dst_hwaddr, - MAC_SIZE) && - memcmp(br_addr, e_frame->dst_hwaddr, MAC_SIZE)) { - FETCH_ADD_RELAXED(ð_data.stat.n_bad_id, 1); - goto fail_frame; - } -#endif - length = ntohs(e_frame->length); -#if defined(BUILD_ETH_DIX) - if (e_frame->ethertype != eth_data.ethertype) - goto fail_frame; - - if (length > ETH_MTU) - goto fail_frame; - - deid = ntohs(e_frame->eid); -#elif defined (BUILD_ETH_LLC) - if (length > 0x05FF) /* DIX */ - goto fail_frame; - - if (length < LLC_HEADER_SIZE || length > ETH_MTU) - goto fail_frame; - - length -= LLC_HEADER_SIZE; - - dsap = reverse_bits(e_frame->dsap); - ssap = reverse_bits(e_frame->ssap); -#endif - - if (eth_len < ETH_HEADER_TOT_SIZE + (size_t) length) - goto fail_frame; - -#if defined(BUILD_ETH_DIX) - mem_hash(HASH_CRC8, &hcs, - (uint8_t *) &e_frame->eid, - DIX_EID_SIZE + DIX_LENGTH_SIZE); -#elif defined(BUILD_ETH_LLC) - mem_hash(HASH_CRC8, &hcs, - (uint8_t *) &e_frame->dsap, - LLC_FIELDS_SIZE); -#endif - if (hcs != e_frame->hcs) - goto fail_frame; - -#if defined(BUILD_ETH_DIX) - if (deid == MGMT_EID) { -#elif defined (BUILD_ETH_LLC) - if (ssap == MGMT_SAP && dsap == MGMT_SAP) { -#endif - ipcp_spb_release(spb); /* No need for the N+1 buffer. */ - - if (length > MGMT_FRAME_SIZE) { - log_warn("Management frame size %u exceeds %u.", - length, MGMT_FRAME_SIZE); - continue; - } - - frame = malloc(sizeof(*frame)); - if (frame == NULL) { - log_err("Failed to allocate frame."); - continue; - } - - memcpy(frame->buf, &e_frame->payload, length); - memcpy(frame->r_addr, e_frame->src_hwaddr, MAC_SIZE); - frame->len = length; - - pthread_mutex_lock(ð_data.mgmt_lock); - list_add(&frame->next, ð_data.mgmt_frames); - pthread_cond_signal(ð_data.mgmt_cond); - pthread_mutex_unlock(ð_data.mgmt_lock); - FETCH_ADD_RELAXED(ð_data.stat.n_rcv, 1); - FETCH_ADD_RELAXED(ð_data.stat.n_mgmt_rcv, 1); - } else { - pthread_rwlock_rdlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - fd = deid; -#elif defined(BUILD_ETH_LLC) - fd = eth_data.ef_to_fd[dsap]; -#endif - if (fd < 0) { - pthread_rwlock_unlock(ð_data.flows_lock); - FETCH_ADD_RELAXED(ð_data.stat.n_bad_id, 1); - goto fail_frame; - } - -#ifdef BUILD_ETH_LLC - if (eth_data.fd_to_ef[fd].r_sap != ssap - || memcmp(eth_data.fd_to_ef[fd].r_addr, - e_frame->src_hwaddr, MAC_SIZE)) { - pthread_rwlock_unlock(ð_data.flows_lock); - FETCH_ADD_RELAXED(ð_data.stat.n_bad_id, 1); - goto fail_frame; - } -#endif - FETCH_ADD_RELAXED(ð_data.fd_to_ef[fd].stat.p_rcv, 1); - FETCH_ADD_RELAXED(ð_data.fd_to_ef[fd].stat.b_rcv, - length); - FETCH_ADD_RELAXED(ð_data.stat.n_rcv, 1); - pthread_rwlock_unlock(ð_data.flows_lock); - -#ifndef HAVE_NETMAP - ssm_pk_buff_pop(spb, ETH_HEADER_TOT_SIZE); - ssm_pk_buff_truncate(spb, length); -#else - if (ipcp_spb_reserve(&spb, length)) - continue; - - buf = ssm_pk_buff_head(spb); - memcpy(buf, &e_frame->payload, length); -#endif - if (np1_flow_write(fd, spb, NP1_GET_POOL(fd)) < 0) { - ipcp_spb_release(spb); - FETCH_ADD_RELAXED( - ð_data.fd_to_ef[fd].stat.p_dlv_f, - 1); - FETCH_ADD_RELAXED(ð_data.stat.n_dlv_f, 1); - } - - continue; - fail_frame: -#ifndef HAVE_NETMAP - ipcp_spb_release(spb); -#endif - } - } - - return (void *) 0; -} - -static void cleanup_writer(void * o) -{ - fqueue_destroy((fqueue_t *) o); -} - -static void * eth_ipcp_packet_writer(void * o) -{ - int fd; - struct ssm_pk_buff * spb; - size_t len; -#if defined(BUILD_ETH_DIX) - uint16_t deid; -#elif defined(BUILD_ETH_LLC) - uint8_t dsap; - uint8_t ssap; -#endif - uint8_t r_addr[MAC_SIZE]; - - fqueue_t * fq; - - fq = fqueue_create(); - if (fq == NULL) - return (void *) -1; - - (void) o; - - ipcp_lock_to_core(); - - pthread_cleanup_push(cleanup_writer, fq); - - while (true) { - fevent(eth_data.np1_flows, fq, NULL); - while ((fd = fqueue_next(fq)) >= 0) { - if (fqueue_type(fq) != FLOW_PKT) - continue; - - if (np1_flow_read(fd, &spb, NP1_GET_POOL(fd))) { - log_dbg("Bad read from fd %d.", fd); - continue; - } - - len = ssm_pk_buff_len(spb); - - if (ssm_pk_buff_push(spb, ETH_HEADER_TOT_SIZE) - == NULL) { - log_dbg("Failed to allocate header."); - ipcp_spb_release(spb); - FETCH_ADD_RELAXED(ð_data.stat.n_buf_f, 1); - continue; - } - - pthread_rwlock_rdlock(ð_data.flows_lock); -#if defined(BUILD_ETH_DIX) - deid = eth_data.fd_to_ef[fd].r_eid; -#elif defined(BUILD_ETH_LLC) - dsap = reverse_bits(eth_data.fd_to_ef[fd].r_sap); - ssap = reverse_bits(eth_data.fd_to_ef[fd].sap); -#endif - memcpy(r_addr, - eth_data.fd_to_ef[fd].r_addr, - MAC_SIZE); - - pthread_rwlock_unlock(ð_data.flows_lock); - - if (eth_ipcp_send_frame(r_addr, -#if defined(BUILD_ETH_DIX) - deid, -#elif defined(BUILD_ETH_LLC) - dsap, ssap, -#endif - ssm_pk_buff_head(spb), - len)) { - log_dbg("Failed to send frame."); - FETCH_ADD_RELAXED( - ð_data.fd_to_ef[fd].stat.p_snd_f, - 1); - FETCH_ADD_RELAXED(ð_data.stat.n_snd_f, 1); - } else { - FETCH_ADD_RELAXED( - ð_data.fd_to_ef[fd].stat.p_snd, - 1); - FETCH_ADD_RELAXED( - ð_data.fd_to_ef[fd].stat.b_snd, - len); - } - ipcp_spb_release(spb); - } - } - - pthread_cleanup_pop(true); - - return (void *) 1; -} - -#ifdef __linux__ -static int open_netlink_socket(void) -{ - struct sockaddr_nl sa; - int fd; - - memset(&sa, 0, sizeof(sa)); - sa.nl_family = AF_NETLINK; - sa.nl_pid = getpid(); - sa.nl_groups = RTMGRP_LINK; - - fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (fd < 0) - return -1; - - if (bind(fd, (struct sockaddr *) &sa, sizeof(sa))) { - close(fd); - return -1; - } - - return fd; -} - -static void change_flows_state(bool up) -{ - int i; - uint32_t flags; - - pthread_rwlock_rdlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - for (i = 0; i < SYS_MAX_FLOWS; ++i) - if (eth_data.fd_to_ef[i].r_eid != -1) { - fccntl(i, FLOWGFLAGS, &flags); - if (up) - fccntl(i, FLOWSFLAGS, flags & ~FLOWFDOWN); - else - fccntl(i, FLOWSFLAGS, flags | FLOWFDOWN); - } -#elif defined(BUILD_ETH_LLC) - for (i = 0; i < MAX_SAPS; i++) - if (eth_data.ef_to_fd[i] != -1) { - fccntl(eth_data.ef_to_fd[i], FLOWGFLAGS, &flags); - if (up) - fccntl(eth_data.ef_to_fd[i], - FLOWSFLAGS, flags & ~FLOWFDOWN); - else - fccntl(eth_data.ef_to_fd[i], - FLOWSFLAGS, flags | FLOWFDOWN); - } -#endif - - pthread_rwlock_unlock(ð_data.flows_lock); -} - -static void * eth_ipcp_if_monitor(void * o) -{ - int fd; - int status; - char buf[4096]; - struct iovec iov = {buf, sizeof(buf)}; - struct sockaddr_nl snl; - struct msghdr msg = {(void *) &snl, sizeof(snl), - &iov, 1, NULL, 0, 0}; - struct nlmsghdr * h; - struct ifinfomsg * ifi; - - (void ) o; - - fd = open_netlink_socket(); - if (fd < 0) { - log_err("Failed to open socket."); - return (void *) -1; - } - - pthread_cleanup_push(__cleanup_close_ptr, &fd); - - while (true) { - status = recvmsg(fd, &msg, 0); - if (status < 0) - continue; - - for (h = (struct nlmsghdr *) buf; - NLMSG_OK(h, (unsigned int) status); - h = NLMSG_NEXT(h, status)) { - - /* Finish reading */ - if (h->nlmsg_type == NLMSG_DONE) - break; - - /* Message is some kind of error */ - if (h->nlmsg_type == NLMSG_ERROR) - continue; - - /* Only interested in link up/down */ - if (h->nlmsg_type != RTM_NEWLINK) - continue; - - ifi = NLMSG_DATA(h); - - /* Not our interface */ - if (ifi->ifi_index != eth_data.if_idx) - continue; - - if (ifi->ifi_flags & IFF_UP) { - change_flows_state(true); - log_dbg("Interface up."); - } else { - change_flows_state(false); - log_dbg("Interface down."); - } - } - } - - pthread_cleanup_pop(true); - - return (void *) 0; -} -#endif - -#if defined (HAVE_BPF) && !defined(HAVE_NETMAP) -static int open_bpf_device(void) -{ - char dev[32]; - size_t i = 0; - - for (i = 0; i < BPF_DEV_MAX; i++) { - int fd = -1; - - snprintf(dev, sizeof(dev), "/dev/bpf%zu", i); - - fd = open(dev, O_RDWR); - if (fd > -1) - return fd; - } - - return -1; -} -#endif - -#if defined(__FreeBSD__) || defined(__APPLE__) -static int ifr_hwaddr_from_ifaddrs(struct ifreq * ifr) -{ - struct ifaddrs * ifaddr; - struct ifaddrs * ifa; - int idx; - - if (getifaddrs(&ifaddr) < 0) { - log_err("Could not get interfaces."); - goto fail_ifaddrs; - } - - for (ifa = ifaddr, idx = 0; ifa != NULL; ifa = ifa->ifa_next, ++idx) { - if (strcmp(ifa->ifa_name, ifr->ifr_name) == 0) - break; - } - - if (ifa == NULL) { - log_err("Interface not found."); - goto fail_ifa; - } - - memcpy(&ifr->ifr_addr, ifa->ifa_addr, sizeof(*ifa->ifa_addr)); - - log_dbg("Interface %s hwaddr " MAC_FMT ".", ifr->ifr_name, - MAC_VAL(ifr->ifr_addr.sa_data)); - - freeifaddrs(ifaddr); - - return 0; - fail_ifa: - freeifaddrs(ifaddr); - fail_ifaddrs: - return -1; - -} -#elif defined(__linux__) -static int ifr_hwaddr_from_socket(struct ifreq * ifr) -{ - int skfd; - - skfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (skfd < 0) { - log_err("Failed to open socket."); - goto fail_socket; - } - - if (ioctl(skfd, SIOCGIFHWADDR, ifr)) { - log_err("Failed to get hwaddr."); - goto fail_ifr; - } - - log_dbg("Interface %s hwaddr " MAC_FMT ".", ifr->ifr_name, - MAC_VAL(ifr->ifr_hwaddr.sa_data)); - - close(skfd); - - return 0; - - fail_ifr: - close(skfd); - fail_socket: - return -1; -} -#endif - -static int eth_ifr_hwaddr(struct ifreq * ifr) -{ -#if defined(__FreeBSD__) || defined(__APPLE__) - return ifr_hwaddr_from_ifaddrs(ifr); -#elif defined(__linux__) - return ifr_hwaddr_from_socket(ifr); -#else - return -1; -#endif -} - -static int eth_ifr_mtu(struct ifreq * ifr) -{ - int skfd; - - skfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (skfd < 0) { - log_err("Failed to open socket."); - goto fail_socket; - } - - if (ioctl(skfd, SIOCGIFMTU, ifr) < 0) { - log_err("Failed to get MTU."); - goto fail_mtu; - } - close(skfd); - - return 0; - - fail_mtu: - close(skfd); - fail_socket: - return -1; -} - -static int eth_set_mtu(struct ifreq * ifr) -{ - if (eth_ifr_mtu(ifr) < 0) { - log_err("Failed to get interface MTU."); - return -1; - } - - log_dbg("Device MTU is %d.", ifr->ifr_mtu); - - eth_data.mtu = MIN((int) ETH_MTU_MAX, ifr->ifr_mtu); - if (memcmp(ifr->ifr_name, "lo", 2) == 0 && - eth_data.mtu > IPCP_ETH_LO_MTU) { - log_dbg("Using loopback interface. MTU restricted to %d.", - IPCP_ETH_LO_MTU); - eth_data.mtu = IPCP_ETH_LO_MTU; - } - - log_dbg("Layer MTU is %d.", eth_data.mtu); - - return 0; -} -#if defined(HAVE_NETMAP) -static int eth_init_nmd(struct ifreq * ifr) -{ - strcpy(ifn, "netmap:"); - strcat(ifn, ifr->ifr_name); - - eth_data.nmd = nm_open(ifn, NULL, 0, NULL); - if (eth_data.nmd == NULL) { - log_err("Failed to open netmap device."); - goto fail_nmd; - } - - memset(ð_data.poll_in, 0, sizeof(eth_data.poll_in)); - memset(ð_data.poll_out, 0, sizeof(eth_data.poll_out)); - - eth_data.poll_in.fd = NETMAP_FD(eth_data.nmd); - eth_data.poll_in.events = POLLIN; - eth_data.poll_out.fd = NETMAP_FD(eth_data.nmd); - eth_data.poll_out.events = POLLOUT; - - log_info("Using netmap device."); - - return 0; - fail_nmd: - return -1; -} -#elif defined (HAVE_BPF) -static int eth_init_bpf(struct ifreq * ifr) -{ - int enable = 1; - int disable = 0; - int blen; - - eth_data.bpf = open_bpf_device(); - if (eth_data.bpf < 0) { - log_err("Failed to open bpf device."); - goto fail_bpf; - } - - ioctl(eth_data.bpf, BIOCGBLEN, &blen); - if (BPF_BLEN < blen) { - log_err("BPF buffer too small (is: %ld must be: %d).", - BPF_BLEN, blen); - goto fail_device; - } - - if (ioctl(eth_data.bpf, BIOCSETIF, ifr) < 0) { - log_err("Failed to set interface."); - goto fail_device; - } - - if (ioctl(eth_data.bpf, BIOCSHDRCMPLT, &enable) < 0) { - log_err("Failed to set BIOCSHDRCMPLT."); - goto fail_device; - } - - if (ioctl(eth_data.bpf, BIOCSSEESENT, &disable) < 0) { - log_err("Failed to set BIOCSSEESENT."); - goto fail_device; - } - - if (ioctl(eth_data.bpf, BIOCIMMEDIATE, &enable) < 0) { - log_err("Failed to set BIOCIMMEDIATE."); - goto fail_device; - } - - log_info("Using Berkeley Packet Filter."); - - return 0; - - fail_device: - close(eth_data.bpf); - fail_bpf: - return -1; -} -#elif defined(HAVE_RAW_SOCKETS) -#define SOCKOPT() -static int eth_init_raw_socket(struct ifreq * ifr) -{ - int idx; - int sndbuf; - int rcvbuf; -#if defined(IPCP_ETH_QDISC_BYPASS) - int qdisc_bypass = 1; -#endif /* ENABLE_QDISC_BYPASS */ - - idx = if_nametoindex(ifr->ifr_name); - if (idx == 0) { - log_err("Failed to retrieve interface index."); - return -1; - } - - memset(&(eth_data.device), 0, sizeof(eth_data.device)); - eth_data.device.sll_ifindex = idx; - eth_data.device.sll_family = AF_PACKET; - memcpy(eth_data.device.sll_addr, ifr->ifr_hwaddr.sa_data, MAC_SIZE); - eth_data.device.sll_halen = MAC_SIZE; - eth_data.device.sll_protocol = htons(ETH_P_ALL); -#if defined (BUILD_ETH_DIX) - eth_data.s_fd = socket(AF_PACKET, SOCK_RAW, eth_data.ethertype); -#elif defined (BUILD_ETH_LLC) - eth_data.s_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_802_2)); -#endif - if (eth_data.s_fd < 0) { - log_err("Failed to create socket."); - goto fail_socket; - } - -#if defined(IPCP_ETH_QDISC_BYPASS) - if (setsockopt(eth_data.s_fd, SOL_PACKET, PACKET_QDISC_BYPASS, - &qdisc_bypass, sizeof(qdisc_bypass))) { - log_info("Qdisc bypass not supported."); - } -#endif - - sndbuf = IPCP_ETH_SNDBUF; - if (sndbuf > 0 && setsockopt(eth_data.s_fd, SOL_SOCKET, SO_SNDBUF, - &sndbuf, sizeof(sndbuf))) { - log_info("Failed to set SO_SNDBUF to %d.", sndbuf); - } - - rcvbuf = IPCP_ETH_RCVBUF; - if (rcvbuf > 0 && setsockopt(eth_data.s_fd, SOL_SOCKET, SO_RCVBUF, - &rcvbuf, sizeof(rcvbuf))) { - log_info("Failed to set SO_RCVBUF to %d.", rcvbuf); - } - - if (bind(eth_data.s_fd, (struct sockaddr *) ð_data.device, - sizeof(eth_data.device)) < 0) { - log_err("Failed to bind socket to interface."); - goto fail_device; - } -#ifdef __linux__ - eth_data.if_idx = idx; -#endif - log_info("Using raw socket device."); - - return 0; - fail_device: - close(eth_data.s_fd); - fail_socket: - return -1; -} -#endif - -static int eth_ipcp_bootstrap(struct ipcp_config * conf) -{ - struct ifreq ifr; - int i; -#if defined(HAVE_NETMAP) - char ifn[IFNAMSIZ]; -#endif /* HAVE_NETMAP */ - - assert(conf); - assert(conf->type == THIS_TYPE); - - memset(&ifr, 0, sizeof(ifr)); - strcpy(ifr.ifr_name, conf->eth.dev); - - if (strlen(conf->eth.dev) >= IFNAMSIZ) { - log_err("Invalid device name: %s.", conf->eth.dev); - return -1; - } -#ifdef BUILD_ETH_DIX - if (conf->eth.ethertype < 0x0600 || conf->eth.ethertype == 0xFFFF) { - log_err("Invalid Ethertype: %d.", conf->eth.ethertype); - return -1; - } - eth_data.ethertype = htons(conf->eth.ethertype); -#endif - if (eth_set_mtu(&ifr) < 0) { - log_err("Failed to set MTU."); - return -1; - } - - if (eth_ifr_hwaddr(&ifr) < 0) { - log_err("Failed to get hardware addr."); - return -1; - } -#if defined(HAVE_NETMAP) || defined(HAVE_BPF) - memcpy(eth_data.hw_addr, LLADDR((struct sockaddr_dl *) &ifr.ifr_addr), - MAC_SIZE); -#endif -#if defined(HAVE_NETMAP) - if (eth_init_nmd(&ifr) < 0) { - log_err("Failed to initialize netmap device."); - return -1; - } -#elif defined(HAVE_BPF) /* !HAVE_NETMAP */ - if (eth_init_bpf(&ifr) < 0) { - log_err("Failed to initialize BPF device."); - return -1; - } -#elif defined(HAVE_RAW_SOCKETS) - if (eth_init_raw_socket(&ifr) < 0) { - log_err("Failed to initialize raw socket device."); - return -1; - } -#endif /* HAVE_NETMAP */ -#ifdef IPCP_ETH_FLOW_STATS - if (rib_reg(ETH_RIB_PATH, ð_r_ops)) { - log_err("Failed to register RIB."); - goto fail_rib_reg; - } -#endif -#if defined(__linux__) - if (pthread_create(ð_data.if_monitor, NULL, - eth_ipcp_if_monitor, NULL)) { - log_err("Failed to create monitor thread: %s.", - strerror(errno)); - goto fail_monitor; - } -#endif - if (pthread_create(ð_data.mgmt_handler, NULL, - eth_ipcp_mgmt_handler, NULL)) { - log_err("Failed to create mgmt handler thread: %s.", - strerror(errno)); - goto fail_mgmt_handler; - } - - for (i = 0; i < IPCP_ETH_RD_THR; i++) { - if (pthread_create(ð_data.packet_reader[i], NULL, - eth_ipcp_packet_reader, NULL)) { - log_err("Failed to create packet reader thread: %s", - strerror(errno)); - goto fail_packet_reader; - } - } - - for (i = 0; i < IPCP_ETH_WR_THR; i++) { - if (pthread_create(ð_data.packet_writer[i], NULL, - eth_ipcp_packet_writer, NULL)) { - log_err("Failed to create packet writer thread: %s", - strerror(errno)); - goto fail_packet_writer; - } - } - -#if defined(BUILD_ETH_DIX) - log_dbg("Bootstrapped IPCP over DIX Ethernet with pid %d " - "and Ethertype 0x%X.", getpid(), conf->eth.ethertype); -#elif defined(BUILD_ETH_LLC) - log_dbg("Bootstrapped IPCP over Ethernet with LLC with pid %d.", - getpid()); -#endif - return 0; - - fail_packet_writer: - while (i-- > 0) { - pthread_cancel(eth_data.packet_writer[i]); - pthread_join(eth_data.packet_writer[i], NULL); - } - i = IPCP_ETH_RD_THR; - fail_packet_reader: - while (i-- > 0) { - pthread_cancel(eth_data.packet_reader[i]); - pthread_join(eth_data.packet_reader[i], NULL); - } - pthread_cancel(eth_data.mgmt_handler); - pthread_join(eth_data.mgmt_handler, NULL); - fail_mgmt_handler: -#if defined(__linux__) - pthread_cancel(eth_data.if_monitor); - pthread_join(eth_data.if_monitor, NULL); -#endif -#if defined(__linux__) - fail_monitor: -#endif -#ifdef IPCP_ETH_FLOW_STATS - rib_unreg(ETH_RIB_PATH); - fail_rib_reg: -#endif -#if defined(HAVE_NETMAP) - nm_close(eth_data.nmd); -#elif defined(HAVE_BPF) - close(eth_data.bpf); -#elif defined(HAVE_RAW_SOCKETS) - close(eth_data.s_fd); -#endif - return -1; -} - -static int eth_ipcp_reg(const uint8_t * hash) -{ - if (shim_data_reg_add_entry(eth_data.shim_data, hash)) { - log_err("Failed to add " HASH_FMT32 " to local registry.", - HASH_VAL32(hash)); - return -1; - } - - return 0; -} - -static int eth_ipcp_unreg(const uint8_t * hash) -{ - shim_data_reg_del_entry(eth_data.shim_data, hash); - - return 0; -} - -static int eth_ipcp_query(const uint8_t * hash) -{ - uint8_t r_addr[MAC_SIZE]; - struct timespec timeout; - struct dir_query * query; - int ret; - int attempt; - uint8_t * buf; - struct mgmt_msg * msg; - size_t len; - long per_ms; - - if (shim_data_dir_has(eth_data.shim_data, hash)) - return 0; - - len = sizeof(*msg) + ipcp_dir_hash_len(); - - buf = malloc(len + ETH_HEADER_TOT_SIZE); - if (buf == NULL) - return -1; - - memset(buf, 0, len + ETH_HEADER_TOT_SIZE); - - msg = (struct mgmt_msg *) (buf + ETH_HEADER_TOT_SIZE); - msg->code = NAME_QUERY_REQ; - - memcpy(msg + 1, hash, ipcp_dir_hash_len()); - - memset(r_addr, 0xff, MAC_SIZE); - - per_ms = NAME_QUERY_TIMEO / (NAME_QUERY_RETRIES + 1); - - ret = -1; - for (attempt = 0; attempt <= NAME_QUERY_RETRIES; ++attempt) { - query = shim_data_dir_query_create(eth_data.shim_data, hash); - if (query == NULL) { - ret = -1; - break; - } - - if (eth_ipcp_send_frame(r_addr, -#if defined(BUILD_ETH_DIX) - MGMT_EID, -#elif defined(BUILD_ETH_LLC) - reverse_bits(MGMT_SAP), - reverse_bits(MGMT_SAP), -#endif - buf, len)) { - log_err("Failed to send management frame."); - shim_data_dir_query_destroy(eth_data.shim_data, - query); - ret = -1; - break; - } - - FETCH_ADD_RELAXED(ð_data.stat.n_mgmt_snd, 1); - - timeout.tv_sec = per_ms / 1000; - timeout.tv_nsec = (per_ms % 1000) * 1000000L; - - ret = shim_data_dir_query_wait(query, &timeout); - - shim_data_dir_query_destroy(eth_data.shim_data, query); - - if (ret != -ETIMEDOUT) - break; - } - - free(buf); - - return ret; -} - -static int eth_ipcp_flow_alloc(int fd, - const uint8_t * hash, - qosspec_t qs, - const buffer_t * data) -{ -#ifdef BUILD_ETH_LLC - uint8_t ssap = 0; -#endif - uint8_t r_addr[MAC_SIZE]; - struct addr addr; - - assert(hash); - - if (!shim_data_dir_has(eth_data.shim_data, hash)) { - log_err("Destination "HASH_FMT32 "unreachable.", - HASH_VAL32(hash)); - return -1; - } - - addr = shim_data_dir_get_addr(eth_data.shim_data, hash); - memcpy(r_addr, &addr.mac, MAC_SIZE); - -#ifdef BUILD_ETH_LLC - pthread_rwlock_wrlock(ð_data.flows_lock); - ssap = bmp_allocate(eth_data.saps); - if (!bmp_is_id_valid(eth_data.saps, ssap)) { - pthread_rwlock_unlock(ð_data.flows_lock); - log_err("Failed to allocate SSAP."); - return -1; - } - - eth_data.fd_to_ef[fd].sap = ssap; - eth_data.ef_to_fd[ssap] = fd; - pthread_rwlock_unlock(ð_data.flows_lock); -#endif - - if (eth_ipcp_alloc(r_addr, -#if defined(BUILD_ETH_DIX) - fd, -#elif defined(BUILD_ETH_LLC) - ssap, -#endif - hash, - qs, - data) < 0) { -#ifdef BUILD_ETH_LLC - pthread_rwlock_wrlock(ð_data.flows_lock); - bmp_release(eth_data.saps, eth_data.fd_to_ef[fd].sap); - eth_data.fd_to_ef[fd].sap = -1; - eth_data.ef_to_fd[ssap] = -1; - pthread_rwlock_unlock(ð_data.flows_lock); - log_err("Failed to allocate with peer."); -#endif - return -1; - } - - fset_add(eth_data.np1_flows, fd); -#ifdef IPCP_ETH_FLOW_STATS - pthread_rwlock_wrlock(ð_data.flows_lock); - memset(ð_data.fd_to_ef[fd].stat, 0, - sizeof(eth_data.fd_to_ef[fd].stat)); - eth_data.fd_to_ef[fd].stat.stamp = time(NULL); - FETCH_ADD_RELAXED(ð_data.stat.n_flows, 1); - pthread_rwlock_unlock(ð_data.flows_lock); -#endif -#if defined(BUILD_ETH_LLC) - log_dbg("Assigned SAP %d for fd %d.", ssap, fd); -#endif - return 0; -} - -static int eth_ipcp_flow_alloc_resp(int fd, - int response, - const buffer_t * data) -{ -#if defined(BUILD_ETH_DIX) - uint16_t r_eid; -#elif defined(BUILD_ETH_LLC) - uint8_t ssap; - uint8_t r_sap; -#endif - uint8_t r_addr[MAC_SIZE]; - - if (ipcp_wait_flow_resp(fd) < 0) { - log_err("Failed to wait for flow response."); - return -1; - } - - pthread_rwlock_wrlock(ð_data.flows_lock); -#if defined(BUILD_ETH_DIX) - r_eid = eth_data.fd_to_ef[fd].r_eid; -#elif defined(BUILD_ETH_LLC) - ssap = bmp_allocate(eth_data.saps); - if (!bmp_is_id_valid(eth_data.saps, ssap)) { - pthread_rwlock_unlock(ð_data.flows_lock); - log_err("Failed to allocate SSAP."); - return -1; - } - - eth_data.fd_to_ef[fd].sap = ssap; - r_sap = eth_data.fd_to_ef[fd].r_sap; - eth_data.ef_to_fd[ssap] = fd; -#endif - memcpy(r_addr, eth_data.fd_to_ef[fd].r_addr, MAC_SIZE); - - pthread_rwlock_unlock(ð_data.flows_lock); - - if (eth_ipcp_alloc_resp(r_addr, -#if defined(BUILD_ETH_DIX) - fd, r_eid, -#elif defined(BUILD_ETH_LLC) - ssap, r_sap, -#endif - response, - data) < 0) { -#ifdef BUILD_ETH_LLC - pthread_rwlock_wrlock(ð_data.flows_lock); - bmp_release(eth_data.saps, eth_data.fd_to_ef[fd].sap); - pthread_rwlock_unlock(ð_data.flows_lock); -#endif - log_err("Failed to respond to peer."); - return -1; - } - - fset_add(eth_data.np1_flows, fd); -#ifdef IPCP_ETH_FLOW_STATS - pthread_rwlock_wrlock(ð_data.flows_lock); - memset(ð_data.fd_to_ef[fd].stat, 0, - sizeof(eth_data.fd_to_ef[fd].stat)); - eth_data.fd_to_ef[fd].stat.stamp = time(NULL); - FETCH_ADD_RELAXED(ð_data.stat.n_flows, 1); - pthread_rwlock_unlock(ð_data.flows_lock); -#endif -#if defined(BUILD_ETH_LLC) - log_dbg("Assigned SAP %d for fd %d.", ssap, fd); -#endif - return 0; -} - -static int eth_ipcp_flow_dealloc(int fd) -{ -#ifdef BUILD_ETH_LLC - uint8_t sap; -#endif - ipcp_flow_fini(fd); - - fset_del(eth_data.np1_flows, fd); - - pthread_rwlock_wrlock(ð_data.flows_lock); - -#if defined(BUILD_ETH_DIX) - eth_data.fd_to_ef[fd].r_eid = -1; -#elif defined BUILD_ETH_LLC - sap = eth_data.fd_to_ef[fd].sap; - bmp_release(eth_data.saps, sap); - eth_data.fd_to_ef[fd].sap = -1; - eth_data.fd_to_ef[fd].r_sap = -1; - eth_data.ef_to_fd[sap] = -1; -#endif - memset(ð_data.fd_to_ef[fd].r_addr, 0, MAC_SIZE); - -#ifdef IPCP_ETH_FLOW_STATS - memset(ð_data.fd_to_ef[fd].stat, 0, - sizeof(eth_data.fd_to_ef[fd].stat)); - FETCH_SUB_RELAXED(ð_data.stat.n_flows, 1); -#endif - - pthread_rwlock_unlock(ð_data.flows_lock); - - ipcp_flow_dealloc(fd); - - return 0; -} - -static struct ipcp_ops eth_ops = { - .ipcp_bootstrap = eth_ipcp_bootstrap, - .ipcp_enroll = NULL, - .ipcp_connect = NULL, - .ipcp_disconnect = NULL, - .ipcp_reg = eth_ipcp_reg, - .ipcp_unreg = eth_ipcp_unreg, - .ipcp_query = eth_ipcp_query, - .ipcp_flow_alloc = eth_ipcp_flow_alloc, - .ipcp_flow_join = NULL, - .ipcp_flow_alloc_resp = eth_ipcp_flow_alloc_resp, - .ipcp_flow_dealloc = eth_ipcp_flow_dealloc -}; - -int main(int argc, - char * argv[]) -{ - int i; - - if (eth_data_init() < 0) { -#if defined(BUILD_ETH_DIX) - log_err("Failed to init eth-llc data."); -#elif defined(BUILD_ETH_LLC) - log_err("Failed to init eth-dix data."); -#endif - goto fail_data_init; - } - - if (ipcp_init(argc, argv, ð_ops, THIS_TYPE) < 0) { - log_err("Failed to initialize IPCP."); - goto fail_init; - } - - if (ipcp_start() < 0) { - log_err("Failed to start IPCP."); - goto fail_start; - } - - ipcp_sigwait(); - - if (ipcp_get_state() == IPCP_SHUTDOWN) { - for (i = 0; i < IPCP_ETH_WR_THR; ++i) - pthread_cancel(eth_data.packet_writer[i]); - for (i = 0; i < IPCP_ETH_RD_THR; ++i) - pthread_cancel(eth_data.packet_reader[i]); - - pthread_cancel(eth_data.mgmt_handler); -#ifdef __linux__ - pthread_cancel(eth_data.if_monitor); -#endif - for (i = 0; i < IPCP_ETH_WR_THR; ++i) - pthread_join(eth_data.packet_writer[i], NULL); - for (i = 0; i < IPCP_ETH_RD_THR; ++i) - pthread_join(eth_data.packet_reader[i], NULL); - - pthread_join(eth_data.mgmt_handler, NULL); -#ifdef __linux__ - pthread_join(eth_data.if_monitor, NULL); -#endif -#ifdef IPCP_ETH_FLOW_STATS - rib_unreg(ETH_RIB_PATH); -#endif - } - - ipcp_stop(); - - ipcp_fini(); - - eth_data_fini(); - - exit(EXIT_SUCCESS); - - fail_start: - ipcp_fini(); - fail_init: - eth_data_fini(); - fail_data_init: - exit(EXIT_FAILURE); -} diff --git a/src/ipcpd/ipcp.c b/src/ipcpd/ipcp.c index 1052a686..b25b7d04 100644 --- a/src/ipcpd/ipcp.c +++ b/src/ipcpd/ipcp.c @@ -36,6 +36,7 @@ #define OUROBOROS_PREFIX "ipcpd/ipcp" #define IPCP_INFO "info" #define ALLOC_TIMEOUT 50 /* ms */ +#define HAS_POA (ipcpd.type != IPCP_LOCAL) #include <ouroboros/bitmap.h> #include <ouroboros/dev.h> @@ -54,6 +55,7 @@ #include "ipcp.h" #include "np1.h" +#include <arpa/inet.h> #include <signal.h> #include <string.h> #include <sys/socket.h> @@ -69,14 +71,14 @@ #define CLOCK_REALTIME_COARSE CLOCK_REALTIME #endif +/* Fits "eth <dev> 0x<type>", the longest PoA rendering. */ +/* Matches src/tools/irm/irm_utils.h; keep in sync. */ +#define POA_STRLEN (DEV_NAME_SIZE + 11) + static char * ipcp_type_str[] = { "local", "unicast", - "broadcast", - "eth-llc", - "eth-dix", - "udp4", - "udp6" + "broadcast" }; static char * dir_hash_str[] = { @@ -237,14 +239,6 @@ static int ipcp_rib_read(const char * path, strcpy(buf, "unicast\n"); else if (ipcpd.type == IPCP_BROADCAST) strcpy(buf, "broadcast\n"); - else if (ipcpd.type == IPCP_ETH_LLC) - strcpy(buf, "eth-llc\n"); - else if (ipcpd.type == IPCP_ETH_DIX) - strcpy(buf, "eth-dix\n"); - else if (ipcpd.type == IPCP_UDP4) - strcpy(buf, "udp4\n"); - else if (ipcpd.type == IPCP_UDP6) - strcpy(buf, "udp6\n"); else strcpy(buf, "bug\n"); } @@ -316,8 +310,7 @@ static void * acceptloop(void * o) (void) o; - while (ipcp_get_state() != IPCP_SHUTDOWN && - ipcp_get_state() != IPCP_INIT) { + while (ipcp_get_state() != IPCP_SHUTDOWN) { struct cmd * cmd; csockfd = accept(ipcpd.sockfd, 0, 0); @@ -471,11 +464,6 @@ static void do_bootstrap(ipcp_config_msg_t * conf_msg, conf = ipcp_config_msg_to_s(conf_msg); switch(conf.type) { /* FIXED algorithms */ - case IPCP_UDP4: - /* FALLTHRU */ - case IPCP_UDP6: - conf.layer_info.dir_hash_algo = (enum pol_dir_hash) HASH_MD5; - break; case IPCP_BROADCAST: conf.layer_info.dir_hash_algo = DIR_HASH_SHA3_256; break; @@ -493,6 +481,8 @@ static void do_bootstrap(ipcp_config_msg_t * conf_msg, strcpy(ipcpd.layer_name, info->name); ipcpd.dir_hash_algo = (enum hash_algo) info->dir_hash_algo; + if (poa_set_layer(info->name) < 0) + log_warn("Failed to set layer name for PoA queries."); ret_msg->layer_info = layer_info_s_to_msg(info); ipcp_set_state(IPCP_OPERATIONAL); @@ -503,10 +493,13 @@ static void do_bootstrap(ipcp_config_msg_t * conf_msg, ipcp_dir_hash_len()); } -static void do_enroll(const char * dst, - ipcp_msg_t * ret_msg) +static void do_enroll(const char * dst, + const poa_addr_msg_t * peer, + ipcp_msg_t * ret_msg) { struct layer_info info; + struct poa_addr addr; + struct poa_addr * pa = NULL; log_info("Enrolling with %s...", dst); @@ -524,7 +517,16 @@ static void do_enroll(const char * dst, return; } - ret_msg->result = ipcpd.ops->ipcp_enroll(dst, &info); + if (peer != NULL) { + addr = poa_addr_msg_to_s(peer); + if (addr.type == POA_INVALID || addr.type == POA_UDP) { + ret_msg->result = -EINVAL; + return; + } + pa = &addr; + } + + ret_msg->result = ipcpd.ops->ipcp_enroll(dst, pa, &info); if (ret_msg->result < 0) { log_err("Failed to bootstrap IPCP."); return; @@ -532,6 +534,8 @@ static void do_enroll(const char * dst, strcpy(ipcpd.layer_name, info.name); ipcpd.dir_hash_algo = (enum hash_algo) info.dir_hash_algo; + if (poa_set_layer(info.name) < 0) + log_warn("Failed to set layer name for PoA queries."); ret_msg->layer_info = layer_info_s_to_msg(&info); ipcp_set_state(IPCP_OPERATIONAL); @@ -542,11 +546,136 @@ static void do_enroll(const char * dst, ipcp_dir_hash_len()); } -static void do_connect(const char * dst, - const char * comp, - qosspec_t qs, - ipcp_msg_t * ret_msg) +/* Bounded so one oversized reply cannot be built; 64 is generous. */ +#define POA_LIST_MAX 64 + +static void do_list_poas(ipcp_msg_t * ret_msg) { + struct poa_spec eps[POA_LIST_MAX]; + ssize_t n; + ssize_t i; + + if (ipcpd.type != IPCP_UNICAST && ipcpd.type != IPCP_BROADCAST) { + ret_msg->result = -ENOTSUP; + return; + } + + n = poa_list(eps, POA_LIST_MAX); + if (n < 0) { + ret_msg->result = (int) n; + return; + } + + if (n > POA_LIST_MAX) { + log_warn("Listing %d of %zd PoAs.", POA_LIST_MAX, n); + + n = POA_LIST_MAX; + } + + if (n == 0) { + ret_msg->result = 0; + return; + } + + ret_msg->poas = malloc(n * sizeof(*ret_msg->poas)); + if (ret_msg->poas == NULL) { + ret_msg->result = -ENOMEM; + return; + } + + for (i = 0; i < n; i++) { + ret_msg->poas[i] = poa_spec_s_to_msg(&eps[i]); + if (ret_msg->poas[i] == NULL) { + ret_msg->result = -ENOMEM; + return; + } + ret_msg->n_poas++; + } + + ret_msg->result = 0; +} + +/* Names a PoA the way "irm ipcp poa list" prints it. */ +/* Matches src/tools/irm/irm_utils.c; keep in sync. */ +static void poa_spec_str(const struct poa_spec * poa, + char * buf, + size_t len) +{ + char addr[INET6_ADDRSTRLEN]; + + switch (poa->type) { + case POA_UDP4: + if (inet_ntop(AF_INET, &poa->udp4.ip_addr, + addr, sizeof(addr)) == NULL) + break; + + snprintf(buf, len, "udp4 %s:%u", addr, poa->udp4.port); + return; + case POA_UDP6: + if (inet_ntop(AF_INET6, &poa->udp6.ip_addr, + addr, sizeof(addr)) == NULL) + break; + + snprintf(buf, len, "udp6 [%s]:%u", addr, poa->udp6.port); + return; + case POA_ETH: + snprintf(buf, len, "eth %s 0x%04X", poa->eth.dev, + poa->eth.ethertype); + return; + default: + break; + } + + snprintf(buf, len, "(unknown)"); +} + +static void do_attach(poa_spec_msg_t * msg, + ipcp_msg_t * ret_msg) +{ + struct poa_spec poa; + char str[POA_STRLEN + 1]; + + poa = poa_spec_msg_to_s(msg); + + poa_spec_str(&poa, str, sizeof(str)); + + ret_msg->result = poa_attach(&poa); + if (ret_msg->result < 0) { + log_err("Failed to attach %s.", str); + return; + } + + log_info("Attached %s.", str); +} + +static void do_detach(poa_spec_msg_t * msg, + ipcp_msg_t * ret_msg) +{ + struct poa_spec poa; + char str[POA_STRLEN + 1]; + + poa = poa_spec_msg_to_s(msg); + + poa_spec_str(&poa, str, sizeof(str)); + + ret_msg->result = poa_detach(&poa); + if (ret_msg->result < 0) { + log_err("Failed to detach %s.", str); + return; + } + + log_info("Detached %s.", str); +} + +static void do_connect(const char * dst, + const char * comp, + qosspec_t qs, + const poa_addr_msg_t * peer, + ipcp_msg_t * ret_msg) +{ + struct poa_addr addr; + struct poa_addr * pa = NULL; + log_info("Connecting %s to %s...", comp, dst); if (ipcpd.ops->ipcp_connect == NULL) { @@ -555,7 +684,16 @@ static void do_connect(const char * dst, return; } - ret_msg->result = ipcpd.ops->ipcp_connect(dst, comp, qs); + if (peer != NULL) { + addr = poa_addr_msg_to_s(peer); + if (addr.type == POA_INVALID || addr.type == POA_UDP) { + ret_msg->result = -EINVAL; + return; + } + pa = &addr; + } + + ret_msg->result = ipcpd.ops->ipcp_connect(dst, comp, qs, pa); log_info("Finished connecting."); } @@ -734,9 +872,14 @@ static void do_flow_join(pid_t pid, log_info("Finished joining layer " HASH_FMT32 ".", HASH_VAL32(dst)); } +/* + * The IRMd says whether the flow is on a PoA, as it may not be known + * here yet. PoA flows answer without the IPCP type's flow machinery. + */ static void do_flow_alloc_resp(int resp, int flow_id, uid_t uid, + bool is_poa, const buffer_t * data, ipcp_msg_t * ret_msg) { @@ -745,13 +888,6 @@ static void do_flow_alloc_resp(int resp, log_info("Responding %d to alloc on flow_id %d.", resp, flow_id); - if (ipcpd.ops->ipcp_flow_alloc_resp == NULL) { - log_err("Failed to respond on flow %d: operation unsupported.", - flow_id); - ret_msg->result = -ENOTSUP; - return; - } - if (ipcp_get_state() != IPCP_OPERATIONAL) { log_err("Failed to respond to flow %d:" "IPCP in state <%s>, need <%s>.", @@ -762,6 +898,20 @@ static void do_flow_alloc_resp(int resp, return; } + if (is_poa) { + ret_msg->result = poa_flow_alloc_resp(flow_id, resp, data); + log_info("Finished responding %d on PoA flow %d.", + ret_msg->result, flow_id); + return; + } + + if (ipcpd.ops->ipcp_flow_alloc_resp == NULL) { + log_err("Failed to respond on flow %d: operation unsupported.", + flow_id); + ret_msg->result = -ENOTSUP; + return; + } + fd = np1_flow_resp(flow_id, resp); if (fd < 0) { log_warn("Flow_id %d is not known.", flow_id); @@ -788,18 +938,13 @@ static void do_flow_alloc_resp(int resp, static void do_flow_dealloc(int flow_id, int timeo_sec, + bool is_poa, ipcp_msg_t * ret_msg) { int fd; log_info("Deallocating flow %d.", flow_id); - if (ipcpd.ops->ipcp_flow_dealloc == NULL) { - log_err("Failed to dealloc: operation unsupported."); - ret_msg->result = -ENOTSUP; - return; - } - if (ipcp_get_state() != IPCP_OPERATIONAL) { log_err("Failed to enroll: IPCP in state <%s>, need <%s>.", ipcp_state_str[ipcp_get_state()], @@ -808,6 +953,20 @@ static void do_flow_dealloc(int flow_id, return; } + if (is_poa) { + ret_msg->result = poa_flow_dealloc(flow_id); + + log_info("Finished deallocating PoA flow %d.", flow_id); + return; + } + + if (ipcpd.ops->ipcp_flow_dealloc == NULL) { + log_err("Failed to dealloc: operation unsupported."); + + ret_msg->result = -ENOTSUP; + return; + } + fd = np1_flow_dealloc(flow_id, timeo_sec); if (fd < 0) { log_warn("Could not deallocate flow_id %d.", flow_id); @@ -820,6 +979,40 @@ static void do_flow_dealloc(int flow_id, log_info("Finished deallocating flow %d.", flow_id); } +static void do_flow_update(int flow_id, + const buffer_t * data, + bool is_poa, + ipcp_msg_t * ret_msg) +{ + int fd; + + if (ipcp_get_state() != IPCP_OPERATIONAL) { + ret_msg->result = -EIPCPSTATE; + return; + } + + if (is_poa) { + ret_msg->result = poa_flow_update(flow_id, data); + return; + } + + if (ipcpd.ops->ipcp_flow_update == NULL) { + log_err("Failed to update flow: operation unsupported."); + + ret_msg->result = -ENOTSUP; + return; + } + + fd = np1_flow_fd(flow_id); + if (fd < 0) { + log_warn("Flow update for unknown flow_id %d.", flow_id); + ret_msg->result = -1; + return; + } + + ret_msg->result = ipcpd.ops->ipcp_flow_update(fd, data); +} + static void * mainloop(void * o) { int sfd; @@ -854,6 +1047,7 @@ static void * mainloop(void * o) free(cmd); if (msg == NULL) { + log_err("Failed to unpack command message."); close(sfd); continue; } @@ -870,11 +1064,23 @@ static void * mainloop(void * o) do_bootstrap(msg->conf, &ret_msg); break; case IPCP_MSG_CODE__IPCP_ENROLL: - do_enroll(msg->dst, &ret_msg); + do_enroll(msg->dst, msg->peer, &ret_msg); + break; + case IPCP_MSG_CODE__IPCP_ATTACH: + assert(HAS_POA); + do_attach(msg->poa, &ret_msg); + break; + case IPCP_MSG_CODE__IPCP_DETACH: + assert(HAS_POA); + do_detach(msg->poa, &ret_msg); + break; + case IPCP_MSG_CODE__IPCP_LIST_POAS: + do_list_poas(&ret_msg); break; case IPCP_MSG_CODE__IPCP_CONNECT: qs = qos_spec_msg_to_s(msg->qosspec); - do_connect(msg->dst, msg->comp, qs, &ret_msg); + do_connect(msg->dst, msg->comp, qs, msg->peer, + &ret_msg); break; case IPCP_MSG_CODE__IPCP_DISCONNECT: do_disconnect(msg->dst, msg->comp, &ret_msg); @@ -913,10 +1119,20 @@ static void * mainloop(void * o) data.len = msg->pk.len; data.data = msg->pk.data; do_flow_alloc_resp(msg->response, msg->flow_id, - msg->uid, &data, &ret_msg); + msg->uid, msg->is_poa, + &data, &ret_msg); break; case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC: - do_flow_dealloc(msg->flow_id, msg->timeo_sec, &ret_msg); + do_flow_dealloc(msg->flow_id, msg->timeo_sec, + msg->is_poa, &ret_msg); + break; + case IPCP_MSG_CODE__IPCP_FLOW_UPDATE: + assert(msg->pk.len > 0 ? msg->pk.data != NULL + : msg->pk.data == NULL); + data.len = msg->pk.len; + data.data = msg->pk.data; + do_flow_update(msg->flow_id, &data, msg->is_poa, + &ret_msg); break; default: ret_msg.result = -1; @@ -954,7 +1170,7 @@ static void * mainloop(void * o) if (write(sfd, buffer.data, buffer.len) == -1) log_warn("Failed to send reply message"); - pthread_cleanup_pop(true); /* close sfd */ + pthread_cleanup_pop(true); /* close sfd */ pthread_cleanup_pop(true); /* free buffer.data */ tpm_end_work(ipcpd.tpm); @@ -1067,6 +1283,11 @@ int ipcp_init(int argc, goto fail_rib_reg; } + if (poa_init(ipcpd.name) < 0) { + log_err("Failed to initialize PoAs."); + goto fail_poa_init; + } + list_head_init(&ipcpd.cmds); ipcpd.tpm = tpm_create(IPCP_MIN_THREADS, IPCP_ADD_THREADS, @@ -1090,6 +1311,8 @@ int ipcp_init(int argc, return 0; fail_tpm_create: + poa_fini(); + fail_poa_init: rib_unreg(IPCP_INFO); fail_rib_reg: rib_fini(); @@ -1115,6 +1338,7 @@ int ipcp_init(int argc, return -1; } +/* Enrolment runs over a PoA, so poa_start() precedes any RPC. */ int ipcp_start(void) { sigset_t sigset; @@ -1135,6 +1359,11 @@ int ipcp_start(void) ipcp_set_state(IPCP_BOOT); + if (poa_start() < 0) { + log_err("Failed to start PoAs."); + goto fail_poa_start; + } + if (tpm_start(ipcpd.tpm)) { log_err("Failed to start threadpool manager."); goto fail_tpm_start; @@ -1158,6 +1387,8 @@ int ipcp_start(void) fail_acceptor: tpm_stop(ipcpd.tpm); fail_tpm_start: + poa_stop(); + fail_poa_start: tpm_destroy(ipcpd.tpm); ipcp_set_state(IPCP_INIT); ipcp_create_r(&info); @@ -1229,6 +1460,8 @@ void ipcp_stop(void) tpm_stop(ipcpd.tpm); + poa_stop(); + ipcp_set_state(IPCP_INIT); } @@ -1237,6 +1470,8 @@ void ipcp_fini(void) tpm_destroy(ipcpd.tpm); + poa_fini(); + rib_unreg(IPCP_INFO); rib_fini(); diff --git a/src/ipcpd/ipcp.h b/src/ipcpd/ipcp.h index 0adcc694..e0aab291 100644 --- a/src/ipcpd/ipcp.h +++ b/src/ipcpd/ipcp.h @@ -36,15 +36,18 @@ #define ipcp_dir_hash_strlen() (ipcp_dir_hash_len() * 2) +/* Attach or release one flow PoA on a unicast or broadcast IPCP. */ struct ipcp_ops { int (* ipcp_bootstrap)(struct ipcp_config * conf); - int (* ipcp_enroll)(const char * dst, - struct layer_info * info); + int (* ipcp_enroll)(const char * dst, + const struct poa_addr * addr, + struct layer_info * info); - int (* ipcp_connect)(const char * dst, - const char * component, - qosspec_t qs); + int (* ipcp_connect)(const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr); int (* ipcp_disconnect)(const char * dst, const char * component); @@ -68,6 +71,9 @@ struct ipcp_ops { const buffer_t * data); int (* ipcp_flow_dealloc)(int fd); + + int (* ipcp_flow_update)(int fd, + const buffer_t * data); }; int ipcp_init(int argc, diff --git a/src/ipcpd/local/CMakeLists.txt b/src/ipcpd/local/CMakeLists.txt index 91f300a3..af433d01 100644 --- a/src/ipcpd/local/CMakeLists.txt +++ b/src/ipcpd/local/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(${IPCP_LOCAL_TARGET} main.c + reg.c ${IPCP_SOURCES} ) diff --git a/src/ipcpd/local/main.c b/src/ipcpd/local/main.c index eb9836f2..69eac8a6 100644 --- a/src/ipcpd/local/main.c +++ b/src/ipcpd/local/main.c @@ -38,10 +38,11 @@ #include <ouroboros/ipcp.h> #include <ouroboros/ipcp-dev.h> #include <ouroboros/local-dev.h> +#include <ouroboros/np1_flow.h> #include "ipcp.h" #include "np1.h" -#include "shim-data.h" +#include "reg.h" #include <string.h> #include <stdlib.h> @@ -52,14 +53,14 @@ #define THIS_TYPE IPCP_LOCAL struct { - struct shim_data * shim_data; + struct reg * reg; - int in_out[SYS_MAX_FLOWS]; - fset_t * flows; - fqueue_t * fq; + int in_out[SYS_MAX_FLOWS]; + fset_t * flows; + fqueue_t * fq; - pthread_rwlock_t lock; - pthread_t packet_loop; + pthread_rwlock_t lock; + pthread_t packet_loop; } local_data; static int local_data_init(void) @@ -76,9 +77,9 @@ static int local_data_init(void) if (local_data.fq == NULL) goto fail_fqueue; - local_data.shim_data = shim_data_create(); - if (local_data.shim_data == NULL) - goto fail_shim_data; + local_data.reg = reg_create(); + if (local_data.reg == NULL) + goto fail_reg; if (pthread_rwlock_init(&local_data.lock, NULL) < 0) goto fail_rwlock_init; @@ -86,8 +87,8 @@ static int local_data_init(void) return 0; fail_rwlock_init: - shim_data_destroy(local_data.shim_data); - fail_shim_data: + reg_destroy(local_data.reg); + fail_reg: fqueue_destroy(local_data.fq); fail_fqueue: fset_destroy(local_data.flows); @@ -97,7 +98,7 @@ static int local_data_init(void) static void local_data_fini(void){ pthread_rwlock_destroy(&local_data.lock); - shim_data_destroy(local_data.shim_data); + reg_destroy(local_data.reg); fqueue_destroy(local_data.fq); fset_destroy(local_data.flows); } @@ -165,7 +166,7 @@ static int local_ipcp_bootstrap(struct ipcp_config * conf) static int local_ipcp_reg(const uint8_t * hash) { - if (shim_data_reg_add_entry(local_data.shim_data, hash)) { + if (reg_add(local_data.reg, hash) < 0) { log_err("Failed to add " HASH_FMT32 " to local registry.", HASH_VAL32(hash)); return -1; @@ -176,7 +177,7 @@ static int local_ipcp_reg(const uint8_t * hash) static int local_ipcp_unreg(const uint8_t * hash) { - shim_data_reg_del_entry(local_data.shim_data, hash); + reg_del(local_data.reg, hash); log_info("Unregistered " HASH_FMT32 ".", HASH_VAL32(hash)); @@ -187,7 +188,7 @@ static int local_ipcp_query(const uint8_t * hash) { int ret; - ret = (shim_data_reg_has(local_data.shim_data, hash) ? 0 : -1); + ret = (reg_has(local_data.reg, hash) ? 0 : -1); return ret; } @@ -297,6 +298,38 @@ static int local_ipcp_flow_dealloc(int fd) return 0; } +/* Loopback relay: deliver the update back to the peer end (same IRMd). */ +static int local_ipcp_flow_update(int fd, + const buffer_t * data) +{ + int out_fd; + int out_flow_id; + + pthread_rwlock_rdlock(&local_data.lock); + + out_fd = local_data.in_out[fd]; + + pthread_rwlock_unlock(&local_data.lock); + + if (out_fd == -1) { + log_err("Flow update on fd %d with no peer.", fd); + return -1; + } + + out_flow_id = np1_flow_id(out_fd); + if (out_flow_id < 0) { + log_err("No flow_id for peer fd %d.", out_fd); + return -1; + } + + if (ipcp_flow_update_arr(out_flow_id, data) < 0) { + log_err("Failed to relay flow update to fd %d.", out_fd); + return -1; + } + + return 0; +} + static struct ipcp_ops local_ops = { .ipcp_bootstrap = local_ipcp_bootstrap, .ipcp_enroll = NULL, @@ -308,7 +341,8 @@ static struct ipcp_ops local_ops = { .ipcp_flow_alloc = local_ipcp_flow_alloc, .ipcp_flow_join = NULL, .ipcp_flow_alloc_resp = local_ipcp_flow_alloc_resp, - .ipcp_flow_dealloc = local_ipcp_flow_dealloc + .ipcp_flow_dealloc = local_ipcp_flow_dealloc, + .ipcp_flow_update = local_ipcp_flow_update }; int main(int argc, diff --git a/src/ipcpd/local/reg.c b/src/ipcpd/local/reg.c new file mode 100644 index 00000000..36f19b16 --- /dev/null +++ b/src/ipcpd/local/reg.c @@ -0,0 +1,217 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Names registered with the local IPCP + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#define _DEFAULT_SOURCE +#else +#define _POSIX_C_SOURCE 200112L +#endif + +#define OUROBOROS_PREFIX "local-reg" + +#include <ouroboros/hash.h> +#include <ouroboros/list.h> +#include <ouroboros/logs.h> + +#include "reg.h" +#include "ipcp.h" + +#include <assert.h> +#include <pthread.h> +#include <stdlib.h> +#include <string.h> + +struct reg_entry { + struct list_head list; + uint8_t * hash; +}; + +struct reg { + struct list_head names; + pthread_rwlock_t lock; +}; + +static struct reg_entry * reg_entry_create(uint8_t * hash) +{ + struct reg_entry * entry; + + entry = malloc(sizeof(*entry)); + if (entry == NULL) + return NULL; + + list_head_init(&entry->list); + + entry->hash = hash; + + return entry; +} + +static void reg_entry_destroy(struct reg_entry * entry) +{ + assert(entry); + + free(entry->hash); + free(entry); +} + +/* Call with the lock held. */ +static struct reg_entry * reg_find(struct reg * reg, + const uint8_t * hash) +{ + struct list_head * p; + + list_for_each(p, ®->names) { + struct reg_entry * e; + + e = list_entry(p, struct reg_entry, list); + if (memcmp(e->hash, hash, ipcp_dir_hash_len()) == 0) + return e; + } + + return NULL; +} + +struct reg * reg_create(void) +{ + struct reg * reg; + + reg = malloc(sizeof(*reg)); + if (reg == NULL) + goto fail_malloc; + + list_head_init(®->names); + + if (pthread_rwlock_init(®->lock, NULL) < 0) + goto fail_lock; + + return reg; + + fail_lock: + free(reg); + fail_malloc: + return NULL; +} + +void reg_destroy(struct reg * reg) +{ + if (reg == NULL) + return; + + pthread_rwlock_wrlock(®->lock); + + while (!list_is_empty(®->names)) { + struct reg_entry * e; + + e = list_first_entry(®->names, struct reg_entry, list); + + list_del(&e->list); + + reg_entry_destroy(e); + } + + pthread_rwlock_unlock(®->lock); + + pthread_rwlock_destroy(®->lock); + + free(reg); +} + +int reg_add(struct reg * reg, + const uint8_t * hash) +{ + struct reg_entry * entry; + uint8_t * dup; + + assert(reg); + assert(hash); + + pthread_rwlock_wrlock(®->lock); + + if (reg_find(reg, hash) != NULL) { + pthread_rwlock_unlock(®->lock); + log_dbg(HASH_FMT32 " was already registered.", + HASH_VAL32(hash)); + return 0; + } + + dup = ipcp_hash_dup(hash); + if (dup == NULL) + goto fail; + + entry = reg_entry_create(dup); + if (entry == NULL) { + free(dup); + goto fail; + } + + list_add(&entry->list, ®->names); + + pthread_rwlock_unlock(®->lock); + + return 0; + + fail: + pthread_rwlock_unlock(®->lock); + return -1; +} + +int reg_del(struct reg * reg, + const uint8_t * hash) +{ + struct reg_entry * e; + + if (reg == NULL) + return -1; + + pthread_rwlock_wrlock(®->lock); + + e = reg_find(reg, hash); + if (e == NULL) { + pthread_rwlock_unlock(®->lock); + return 0; /* nothing to do */ + } + + list_del(&e->list); + + pthread_rwlock_unlock(®->lock); + + reg_entry_destroy(e); + + return 0; +} + +bool reg_has(struct reg * reg, + const uint8_t * hash) +{ + bool ret; + + assert(reg); + assert(hash); + + pthread_rwlock_rdlock(®->lock); + + ret = reg_find(reg, hash) != NULL; + + pthread_rwlock_unlock(®->lock); + + return ret; +} diff --git a/src/ipcpd/eth/llc.c b/src/ipcpd/local/reg.h index a772e86e..2c6142bb 100644 --- a/src/ipcpd/eth/llc.c +++ b/src/ipcpd/local/reg.h @@ -1,7 +1,7 @@ /* * Ouroboros - Copyright (C) 2016 - 2026 * - * IPC processes over Ethernet - LLC + * Names registered with the local IPCP * * Dimitri Staessens <dimitri@ouroboros.rocks> * Sander Vrijders <sander@ouroboros.rocks> @@ -20,7 +20,26 @@ * Foundation, Inc., http://www.fsf.org/about/contact/. */ -#define BUILD_ETH_LLC -#define OUROBOROS_PREFIX "ipcpd/eth-llc" +#ifndef OUROBOROS_IPCPD_LOCAL_REG_H +#define OUROBOROS_IPCPD_LOCAL_REG_H -#include "eth.c" +#include <stdbool.h> +#include <stdint.h> + +/* The hashes of the names registered with this IPCP. */ +struct reg; + +struct reg * reg_create(void); + +void reg_destroy(struct reg * reg); + +int reg_add(struct reg * reg, + const uint8_t * hash); + +int reg_del(struct reg * reg, + const uint8_t * hash); + +bool reg_has(struct reg * reg, + const uint8_t * hash); + +#endif /* OUROBOROS_IPCPD_LOCAL_REG_H */ diff --git a/src/ipcpd/shim-data.c b/src/ipcpd/shim-data.c deleted file mode 100644 index 90a676da..00000000 --- a/src/ipcpd/shim-data.c +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * IPC process utilities - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#if defined(__linux__) || defined(__CYGWIN__) -#define _DEFAULT_SOURCE -#else -#define _POSIX_C_SOURCE 200112L -#endif - -#include "config.h" - -#define OUROBOROS_PREFIX "shim-data" - -#include <ouroboros/errno.h> -#include <ouroboros/hash.h> -#include <ouroboros/list.h> -#include <ouroboros/logs.h> -#include <ouroboros/time.h> - -#include "shim-data.h" -#include "ipcp.h" - -#include <assert.h> -#include <stdlib.h> -#include <string.h> - -struct reg_entry { - struct list_head list; - uint8_t * hash; -}; - -struct dir_entry { - struct list_head list; - uint8_t * hash; - struct addr addr; -}; - -static void destroy_dir_query(struct dir_query * query) -{ - assert(query); - - pthread_mutex_lock(&query->lock); - - switch (query->state) { - case QUERY_INIT: - query->state = QUERY_DONE; - break; - case QUERY_PENDING: - query->state = QUERY_DESTROY; - pthread_cond_broadcast(&query->cond); - break; - case QUERY_RESPONSE: - case QUERY_DONE: - break; - case QUERY_DESTROY: - pthread_mutex_unlock(&query->lock); - return; - default: - assert(false); - return; - } - - while (query->state != QUERY_DONE) - pthread_cond_wait(&query->cond, &query->lock); - - pthread_mutex_unlock(&query->lock); - - pthread_cond_destroy(&query->cond); - pthread_mutex_destroy(&query->lock); - - free(query->hash); - free(query); -} - -static struct reg_entry * reg_entry_create(uint8_t * hash) -{ - struct reg_entry * entry = malloc(sizeof(*entry)); - if (entry == NULL) - return NULL; - - assert(hash); - - entry->hash = hash; - - return entry; -} - -static void reg_entry_destroy(struct reg_entry * entry) -{ - assert(entry); - - free(entry->hash); - free(entry); -} - -static struct dir_entry * dir_entry_create(uint8_t * hash, - struct addr addr) -{ - struct dir_entry * entry = malloc(sizeof(*entry)); - if (entry == NULL) - return NULL; - - assert(hash); - - entry->addr = addr; - entry->hash = hash; - - return entry; -} - -static void dir_entry_destroy(struct dir_entry * entry) -{ - assert(entry); - - free(entry->hash); - free(entry); -} - -struct shim_data * shim_data_create(void) -{ - struct shim_data * sd; - - sd = malloc(sizeof(*sd)); - if (sd == NULL) - return NULL; - - /* init the lists */ - list_head_init(&sd->registry); - list_head_init(&sd->directory); - list_head_init(&sd->dir_queries); - - /* init the locks */ - if (pthread_rwlock_init(&sd->reg_lock, NULL) < 0) - goto fail_reg_lock_init; - - if (pthread_rwlock_init(&sd->dir_lock, NULL) < 0) - goto fail_dir_lock_init; - - if (pthread_mutex_init(&sd->dir_queries_lock, NULL) < 0) - goto fail_mutex_init; - - return sd; - - fail_mutex_init: - pthread_rwlock_destroy(&sd->dir_lock); - fail_dir_lock_init: - pthread_rwlock_destroy(&sd->reg_lock); - fail_reg_lock_init: - return NULL; -} - -static void clear_registry(struct shim_data * data) -{ - struct list_head * h; - struct list_head * t; - - assert(data); - - list_for_each_safe(h, t, &data->registry) { - struct reg_entry * e = list_entry(h, struct reg_entry, list); - list_del(&e->list); - reg_entry_destroy(e); - } -} - -static void clear_directory(struct shim_data * data) -{ - struct list_head * h; - struct list_head * t; - - assert(data); - - list_for_each_safe(h, t, &data->directory) { - struct dir_entry * e = list_entry(h, struct dir_entry, list); - list_del(&e->list); - dir_entry_destroy(e); - } -} - -static void clear_dir_queries(struct shim_data * data) -{ - struct list_head * h; - struct list_head * t; - - assert(data); - - list_for_each_safe(h, t, &data->dir_queries) { - struct dir_query * e = list_entry(h, struct dir_query, next); - list_del(&e->next); - destroy_dir_query(e); - } -} - -void shim_data_destroy(struct shim_data * data) -{ - if (data == NULL) - return; - - /* clear the lists */ - pthread_rwlock_wrlock(&data->reg_lock); - clear_registry(data); - pthread_rwlock_unlock(&data->reg_lock); - - pthread_rwlock_wrlock(&data->dir_lock); - clear_directory(data); - pthread_rwlock_unlock(&data->dir_lock); - - pthread_mutex_lock(&data->dir_queries_lock); - clear_dir_queries(data); - pthread_mutex_unlock(&data->dir_queries_lock); - - pthread_rwlock_destroy(&data->dir_lock); - pthread_rwlock_destroy(&data->reg_lock); - pthread_mutex_destroy(&data->dir_queries_lock); - - free(data); -} - -static struct reg_entry * find_reg_entry_by_hash(struct shim_data * data, - const uint8_t * hash) -{ - struct list_head * h; - - assert(data); - assert(hash); - - list_for_each(h, &data->registry) { - struct reg_entry * e = list_entry(h, struct reg_entry, list); - if (!memcmp(e->hash, hash, ipcp_dir_hash_len())) - return e; - } - - return NULL; -} - -static struct dir_entry * find_dir_entry(struct shim_data * data, - const uint8_t * hash, - struct addr addr) -{ - struct list_head * h; - list_for_each(h, &data->directory) { - struct dir_entry * e = list_entry(h, struct dir_entry, list); - if (memcmp(&e->addr, &addr, sizeof(addr)) != 0) - continue; - - if (memcmp(e->hash, hash, ipcp_dir_hash_len()) == 0) - return e; - } - - return NULL; -} - -static struct dir_entry * find_dir_entry_any(struct shim_data * data, - const uint8_t * hash) -{ - struct list_head * h; - list_for_each(h, &data->directory) { - struct dir_entry * e = list_entry(h, struct dir_entry, list); - if (!memcmp(e->hash, hash, ipcp_dir_hash_len())) - return e; - } - - return NULL; -} - -int shim_data_reg_add_entry(struct shim_data * data, - const uint8_t * hash) -{ - struct reg_entry * entry; - uint8_t * hash_dup; - - assert(data); - assert(hash); - - pthread_rwlock_wrlock(&data->reg_lock); - - if (find_reg_entry_by_hash(data, hash)) { - pthread_rwlock_unlock(&data->reg_lock); - log_dbg(HASH_FMT32 " was already in the directory.", - HASH_VAL32(hash)); - return 0; - } - - hash_dup = ipcp_hash_dup(hash); - if (hash_dup == NULL) { - pthread_rwlock_unlock(&data->reg_lock); - return -1; - } - - entry = reg_entry_create(hash_dup); - if (entry == NULL) { - pthread_rwlock_unlock(&data->reg_lock); - return -1; - } - - list_add(&entry->list, &data->registry); - - pthread_rwlock_unlock(&data->reg_lock); - - return 0; -} - -int shim_data_reg_del_entry(struct shim_data * data, - const uint8_t * hash) -{ - struct reg_entry * e; - if (data == NULL) - return -1; - - pthread_rwlock_wrlock(&data->reg_lock); - - e = find_reg_entry_by_hash(data, hash); - if (e == NULL) { - pthread_rwlock_unlock(&data->reg_lock); - return 0; /* nothing to do */ - } - - list_del(&e->list); - - pthread_rwlock_unlock(&data->reg_lock); - - reg_entry_destroy(e); - - return 0; -} - -bool shim_data_reg_has(struct shim_data * data, - const uint8_t * hash) -{ - bool ret = false; - - assert(data); - assert(hash); - - pthread_rwlock_rdlock(&data->reg_lock); - - ret = (find_reg_entry_by_hash(data, hash) != NULL); - - pthread_rwlock_unlock(&data->reg_lock); - - return ret; -} - -int shim_data_dir_add_entry(struct shim_data * data, - const uint8_t * hash, - struct addr addr) -{ - struct dir_entry * entry; - uint8_t * entry_hash; - - assert(data); - assert(hash); - - pthread_rwlock_wrlock(&data->dir_lock); - - if (find_dir_entry(data, hash, addr) != NULL) { - pthread_rwlock_unlock(&data->dir_lock); - return -1; - } - - entry_hash = ipcp_hash_dup(hash); - if (entry_hash == NULL) { - pthread_rwlock_unlock(&data->dir_lock); - return -1; - } - - entry = dir_entry_create(entry_hash, addr); - if (entry == NULL) { - pthread_rwlock_unlock(&data->dir_lock); - return -1; - } - - list_add(&entry->list,&data->directory); - - pthread_rwlock_unlock(&data->dir_lock); - - return 0; -} - -int shim_data_dir_del_entry(struct shim_data * data, - const uint8_t * hash, - struct addr addr) -{ - struct dir_entry * e; - if (data == NULL) - return -1; - - pthread_rwlock_wrlock(&data->dir_lock); - - e = find_dir_entry(data, hash, addr); - if (e == NULL) { - pthread_rwlock_unlock(&data->dir_lock); - return 0; /* nothing to do */ - } - - list_del(&e->list); - - pthread_rwlock_unlock(&data->dir_lock); - - dir_entry_destroy(e); - - return 0; -} - -bool shim_data_dir_has(struct shim_data * data, - const uint8_t * hash) -{ - bool ret = false; - - pthread_rwlock_rdlock(&data->dir_lock); - - ret = (find_dir_entry_any(data, hash) != NULL); - - pthread_rwlock_unlock(&data->dir_lock); - - return ret; -} - -struct addr shim_data_dir_get_addr(struct shim_data * data, - const uint8_t * hash) -{ - struct dir_entry * entry; - struct addr addr = {0}; - - pthread_rwlock_rdlock(&data->dir_lock); - - entry = find_dir_entry_any(data, hash); - if (entry == NULL) { - pthread_rwlock_unlock(&data->dir_lock); - log_warn("No address for " HASH_FMT32 ".", HASH_VAL32(hash)); - return addr; /* undefined behaviour, 0 may be a valid address */ - } - - addr = entry->addr; - - pthread_rwlock_unlock(&data->dir_lock); - - return addr; -} - -struct dir_query * shim_data_dir_query_create(struct shim_data * data, - const uint8_t * hash) -{ - struct dir_query * query; - pthread_condattr_t cattr; - - query = malloc(sizeof(*query)); - if (query == NULL) - return NULL; - - query->hash = ipcp_hash_dup(hash); - if (query->hash == NULL) { - free(query); - return NULL; - } - - query->state = QUERY_INIT; - - pthread_condattr_init(&cattr); -#ifndef __APPLE__ - pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); -#endif - pthread_cond_init(&query->cond, &cattr); - pthread_mutex_init(&query->lock, NULL); - - list_head_init(&query->next); - - pthread_mutex_lock(&data->dir_queries_lock); - list_add(&query->next, &data->dir_queries); - pthread_mutex_unlock(&data->dir_queries_lock); - - return query; -} - -void shim_data_dir_query_respond(struct shim_data * data, - const uint8_t * hash) -{ - struct dir_query * e = NULL; - struct list_head * pos; - bool found = false; - - pthread_mutex_lock(&data->dir_queries_lock); - - list_for_each(pos, &data->dir_queries) { - e = list_entry(pos, struct dir_query, next); - - if (memcmp(e->hash, hash, ipcp_dir_hash_len()) == 0) { - found = true; - break; - } - } - - if (!found) { - pthread_mutex_unlock(&data->dir_queries_lock); - return; - } - - pthread_mutex_lock(&e->lock); - - if (e->state != QUERY_PENDING) { - pthread_mutex_unlock(&e->lock); - pthread_mutex_unlock(&data->dir_queries_lock); - return; - } - - e->state = QUERY_RESPONSE; - pthread_cond_broadcast(&e->cond); - - while (e->state == QUERY_RESPONSE) - pthread_cond_wait(&e->cond, &e->lock); - - pthread_mutex_unlock(&e->lock); - - pthread_mutex_unlock(&data->dir_queries_lock); -} - -void shim_data_dir_query_destroy(struct shim_data * data, - struct dir_query * query) -{ - pthread_mutex_lock(&data->dir_queries_lock); - - list_del(&query->next); - destroy_dir_query(query); - - pthread_mutex_unlock(&data->dir_queries_lock); -} - -int shim_data_dir_query_wait(struct dir_query * query, - const struct timespec * timeout) -{ - struct timespec abstime; - int ret = 0; - - assert(query); - assert(timeout); - - clock_gettime(PTHREAD_COND_CLOCK, &abstime); - ts_add(&abstime, timeout, &abstime); - - pthread_mutex_lock(&query->lock); - - if (query->state != QUERY_INIT) { - pthread_mutex_unlock(&query->lock); - return -EINVAL; - } - - query->state = QUERY_PENDING; - - while (query->state == QUERY_PENDING && ret != -ETIMEDOUT) - ret = -pthread_cond_timedwait(&query->cond, - &query->lock, - &abstime); - - if (query->state == QUERY_DESTROY) - ret = -1; - - query->state = QUERY_DONE; - pthread_cond_broadcast(&query->cond); - - pthread_mutex_unlock(&query->lock); - - return ret; -} diff --git a/src/ipcpd/shim-data.h b/src/ipcpd/shim-data.h deleted file mode 100644 index fbadb4d4..00000000 --- a/src/ipcpd/shim-data.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * Utitilies for building IPC processes - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#ifndef OUROBOROS_IPCPD_IPCP_DATA_H -#define OUROBOROS_IPCPD_IPCP_DATA_H - -#include <ouroboros/list.h> - -#include <pthread.h> -#include <stdint.h> -#include <netinet/in.h> -#include <sys/types.h> - -#define MAC_SIZE 6 - -enum dir_query_state { - QUERY_INIT = 0, - QUERY_PENDING, - QUERY_RESPONSE, - QUERY_DONE, - QUERY_DESTROY -}; - -struct dir_query { - struct list_head next; - uint8_t * hash; - enum dir_query_state state; - - pthread_mutex_t lock; - pthread_cond_t cond; -}; - -struct addr { - union { - uint8_t mac[MAC_SIZE]; - struct in_addr ip4; - struct in6_addr ip6; - }; -}; - -struct shim_data { - struct list_head registry; - pthread_rwlock_t reg_lock; - - struct list_head directory; - pthread_rwlock_t dir_lock; - - struct list_head dir_queries; - pthread_mutex_t dir_queries_lock; -}; - -struct shim_data * shim_data_create(void); - -void shim_data_destroy(struct shim_data * data); - -int shim_data_reg_add_entry(struct shim_data * data, - const uint8_t * hash); - -int shim_data_reg_del_entry(struct shim_data * data, - const uint8_t * hash); - -bool shim_data_reg_has(struct shim_data * data, - const uint8_t * hash); - -int shim_data_dir_add_entry(struct shim_data * data, - const uint8_t * hash, - struct addr addr); - -int shim_data_dir_del_entry(struct shim_data * data, - const uint8_t * hash, - struct addr addr); - -bool shim_data_dir_has(struct shim_data * data, - const uint8_t * hash); - -struct addr shim_data_dir_get_addr(struct shim_data * data, - const uint8_t * hash); - -struct dir_query * shim_data_dir_query_create(struct shim_data * data, - const uint8_t * hash); - -void shim_data_dir_query_destroy(struct shim_data * data, - struct dir_query * query); - -void shim_data_dir_query_respond(struct shim_data * data, - const uint8_t * hash); - -int shim_data_dir_query_wait(struct dir_query * query, - const struct timespec * timeout); -#endif /* OUROBOROS_IPCPD_SHIM_DATA_H */ diff --git a/src/ipcpd/udp/CMakeLists.txt b/src/ipcpd/udp/CMakeLists.txt deleted file mode 100644 index a98f0919..00000000 --- a/src/ipcpd/udp/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# UDP IPCPs build configuration (UDP4 and UDP6) -# DDNS detection is in cmake/dependencies/udp/ddns.cmake - -add_executable(${IPCP_UDP4_TARGET} udp4.c ${IPCP_SOURCES}) -add_executable(${IPCP_UDP6_TARGET} udp6.c ${IPCP_SOURCES}) - -foreach(target ${IPCP_UDP4_TARGET} ${IPCP_UDP6_TARGET}) - target_include_directories(${target} PRIVATE ${IPCP_INCLUDE_DIRS}) - target_link_libraries(${target} PRIVATE ouroboros-dev) - ouroboros_target_debug_definitions(${target}) -endforeach() - -install(TARGETS ${IPCP_UDP4_TARGET} ${IPCP_UDP6_TARGET} - RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}) diff --git a/src/ipcpd/udp/udp.c b/src/ipcpd/udp/udp.c deleted file mode 100644 index 93e88b9b..00000000 --- a/src/ipcpd/udp/udp.c +++ /dev/null @@ -1,1254 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * IPC process over UDP - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#include "config.h" - -#include <ouroboros/bitmap.h> -#include <ouroboros/endian.h> -#include <ouroboros/hash.h> -#include <ouroboros/list.h> -#include <ouroboros/utils.h> -#include <ouroboros/dev.h> -#include <ouroboros/fqueue.h> -#include <ouroboros/errno.h> -#include <ouroboros/logs.h> -#include <ouroboros/pthread.h> - -#include "ipcp.h" -#include "np1.h" -#include "shim-data.h" - -#include <string.h> -#include <sys/socket.h> -#include <sys/select.h> -#include <arpa/inet.h> -#include <netdb.h> -#include <netinet/in.h> -#include <signal.h> -#include <stdlib.h> -#include <sys/wait.h> -#include <fcntl.h> -#include <unistd.h> -#if defined(__linux__) -#include <netinet/ip.h> -#endif - -#define FLOW_REQ 1 -#define FLOW_REPLY 2 - -#define OUR_HEADER_LEN sizeof(uint32_t) /* adds eid */ - -#define IPCP_UDP_BUF_SIZE IPCP_UDP_MAX_PACKET_SIZE -#define IPCP_UDP_MSG_SIZE IPCP_UDP_MAX_PACKET_SIZE - -#define DNS_TTL 86400 - -#define SADDR ((struct sockaddr *) &udp_data.s_saddr) -#define SADDR_SIZE (sizeof(udp_data.s_saddr)) -#define LOCAL_IP (udp_data.s_saddr.sin_addr.s_addr) - -#define MGMT_EID 0 -#define MGMT_FRAME_SIZE (sizeof(struct mgmt_msg)) -#define MGMT_FRAME_BUF_SIZE 2048 - -#ifdef __linux__ -#define SENDTO_FLAGS MSG_CONFIRM -#else -#define SENDTO_FLAGS 0 -#endif - -/* Keep order for alignment. */ -struct mgmt_msg { - uint32_t eid; - uint32_t s_eid; - uint32_t d_eid; - int32_t response; - uint64_t bandwidth; - uint32_t delay; - uint32_t loss; - uint32_t ber; - uint32_t max_gap; - uint32_t timeout; - uint8_t code; - /* QoS parameters from spec */ - uint8_t availability; - uint8_t service; -} __attribute__((packed)); - -struct mgmt_frame { - struct list_head next; - struct __SOCKADDR r_saddr; - uint8_t buf[MGMT_FRAME_BUF_SIZE]; - size_t len; -}; - -/* UDP flow */ -struct uf { - int d_eid; - struct __SOCKADDR r_saddr; -}; - -struct { - struct shim_data * shim_data; - - struct __ADDR dns_addr; - struct __SOCKADDR s_saddr; - int s_fd; - - fset_t * np1_flows; - struct uf fd_to_uf[SYS_MAX_FLOWS]; - pthread_rwlock_t flows_lock; - - pthread_t packet_writer[IPCP_UDP_WR_THR]; - pthread_t packet_reader[IPCP_UDP_RD_THR]; - - /* Handle mgmt frames in a different thread */ - pthread_t mgmt_handler; - pthread_mutex_t mgmt_lock; - pthread_cond_t mgmt_cond; - struct list_head mgmt_frames; -} udp_data; - -static const char * __inet_ntop(const struct __ADDR * addr, - char * buf) -{ - return inet_ntop(__AF, addr, buf, __ADDRSTRLEN); -} - -#if defined(BUILD_IPCP_UDP4) -#define UDP_MTU_FALLBACK IPCP_UDP4_MTU -#define UDP_IP_OVERHEAD 28U /* IPv4 + UDP */ -#else -#define UDP_MTU_FALLBACK IPCP_UDP6_MTU -#define UDP_IP_OVERHEAD 48U /* IPv6 + UDP */ -#endif - -static uint32_t udp_query_mtu(const struct __SOCKADDR * saddr) -{ -#if defined(__linux__) && (defined(IP_MTU) || defined(IPV6_MTU)) - int sock; - int mtu = 0; - socklen_t len = sizeof(mtu); - - sock = socket(__AF, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) - return UDP_MTU_FALLBACK; - - if (connect(sock, (const struct sockaddr *) saddr, - sizeof(*saddr)) < 0) - goto fallback; - -#if defined(BUILD_IPCP_UDP4) && defined(IP_MTU) - if (getsockopt(sock, IPPROTO_IP, IP_MTU, &mtu, &len) < 0) - goto fallback; -#elif defined(BUILD_IPCP_UDP6) && defined(IPV6_MTU) - if (getsockopt(sock, IPPROTO_IPV6, IPV6_MTU, &mtu, &len) < 0) - goto fallback; -#else - goto fallback; -#endif - close(sock); - - if (mtu <= (int) UDP_IP_OVERHEAD) - return UDP_MTU_FALLBACK; - - return (uint32_t) mtu - UDP_IP_OVERHEAD; - - fallback: - close(sock); -#else - (void) saddr; -#endif - return UDP_MTU_FALLBACK; -} - -static int udp_data_init(void) -{ - int i; - pthread_condattr_t cattr; - - if (pthread_rwlock_init(&udp_data.flows_lock, NULL)) - goto fail_rwlock_init; - - if (pthread_condattr_init(&cattr)) - goto fail_condattr; -#ifndef __APPLE__ - pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); -#endif - if (pthread_cond_init(&udp_data.mgmt_cond, &cattr)) - goto fail_mgmt_cond; - - if (pthread_mutex_init(&udp_data.mgmt_lock, NULL)) - goto fail_mgmt_lock; - - for (i = 0; i < SYS_MAX_FLOWS; ++i) - udp_data.fd_to_uf[i].d_eid = -1; - - udp_data.np1_flows = fset_create(); - if (udp_data.np1_flows == NULL) - goto fail_fset; - - udp_data.shim_data = shim_data_create(); - if (udp_data.shim_data == NULL) - goto fail_data; - - pthread_condattr_destroy(&cattr); - - list_head_init(&udp_data.mgmt_frames); - - return 0; - - fail_data: - fset_destroy(udp_data.np1_flows); - fail_fset: - pthread_mutex_destroy(&udp_data.mgmt_lock); - fail_mgmt_lock: - pthread_cond_destroy(&udp_data.mgmt_cond); - fail_mgmt_cond: - pthread_condattr_destroy(&cattr); - fail_condattr: - pthread_rwlock_destroy(&udp_data.flows_lock); - fail_rwlock_init: - return -1; -} - -static void udp_data_fini(void) -{ - shim_data_destroy(udp_data.shim_data); - - fset_destroy(udp_data.np1_flows); - - pthread_rwlock_destroy(&udp_data.flows_lock); - pthread_cond_destroy(&udp_data.mgmt_cond); - pthread_mutex_destroy(&udp_data.mgmt_lock); -} - -static int udp_ipcp_port_alloc(const struct __SOCKADDR * r_saddr, - uint32_t s_eid, - const uint8_t * dst, - qosspec_t qs, - const buffer_t * data) -{ - uint8_t * buf; - struct mgmt_msg * msg; - size_t len; - - assert(data->len > 0 ? data->data != NULL : data->data == NULL); - - len = sizeof(*msg) + ipcp_dir_hash_len(); - - buf = malloc(len + data->len); - if (buf == NULL) - return -1; - - memset(buf, 0, len + data->len); - - msg = (struct mgmt_msg *) buf; - msg->eid = hton32(MGMT_EID); - msg->code = FLOW_REQ; - msg->s_eid = hton32(s_eid); - msg->delay = hton32(qs.delay); - msg->bandwidth = hton64(qs.bandwidth); - msg->availability = qs.availability; - msg->loss = hton32(qs.loss); - msg->ber = hton32(qs.ber); - msg->service = qs.service; - msg->max_gap = hton32(qs.max_gap); - msg->timeout = hton32(qs.timeout); - - memcpy(msg + 1, dst, ipcp_dir_hash_len()); - if (data->len > 0) - memcpy(buf + len, data->data, data->len); - - if (sendto(udp_data.s_fd, msg, len + data->len, - SENDTO_FLAGS, - (const struct sockaddr *) r_saddr, sizeof(*r_saddr)) < 0) { - log_err("Failed to send flow allocation request: %s.", - strerror(errno)); - free(buf); - return -1; - } - - free(buf); - - return 0; -} - -static int udp_ipcp_port_alloc_resp(const struct __SOCKADDR * r_saddr, - uint32_t s_eid, - uint32_t d_eid, - int32_t response, - const buffer_t * data) -{ - struct mgmt_msg * msg; - - msg = malloc(sizeof(*msg) + data->len); - if (msg == NULL) - return -1; - - memset(msg, 0, sizeof(*msg) + data->len); - - msg->eid = hton32(MGMT_EID); - msg->code = FLOW_REPLY; - msg->s_eid = hton32(s_eid); - msg->d_eid = hton32(d_eid); - msg->response = hton32(response); - - if (data->len > 0) - memcpy(msg + 1, data->data, data->len); - - if (sendto(udp_data.s_fd, msg, sizeof(*msg) + data->len, - SENDTO_FLAGS, - (const struct sockaddr *) r_saddr, sizeof(*r_saddr)) < 0 ) { - free(msg); - return -1; - } - - free(msg); - - return 0; -} - -static int udp_ipcp_port_req(struct __SOCKADDR * c_saddr, - int d_eid, - const uint8_t * dst, - qosspec_t qs, - const buffer_t * data) -{ - int fd; - - fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UDP_MPL, - udp_query_mtu(c_saddr), data); - if (fd < 0) { - log_err("Could not get new flow from IRMd."); - return -1; - } - - pthread_rwlock_wrlock(&udp_data.flows_lock); - - udp_data.fd_to_uf[fd].r_saddr = *c_saddr; - udp_data.fd_to_uf[fd].d_eid = d_eid; - - pthread_rwlock_unlock(&udp_data.flows_lock); - - log_dbg("Pending allocation request, fd %d, remote eid %d.", - fd, d_eid); - - return 0; -} - -static int udp_ipcp_port_alloc_reply(const struct __SOCKADDR * saddr, - uint32_t s_eid, - uint32_t d_eid, - int32_t response, - const buffer_t * data) -{ - time_t mpl = IPCP_UDP_MPL; - - pthread_rwlock_wrlock(&udp_data.flows_lock); - - if (memcmp(&udp_data.fd_to_uf[s_eid].r_saddr, saddr, sizeof(*saddr))) { - char ipstr[__ADDRSTRLEN]; - pthread_rwlock_unlock(&udp_data.flows_lock); - #ifdef BUILD_IPCP_UDP4 - __inet_ntop(&saddr->sin_addr, ipstr); - #else - __inet_ntop(&saddr->sin6_addr, ipstr); - #endif - log_err("Flow allocation reply for %u from wrong source %s.", - s_eid, ipstr); - return -1; - } - - if (response == 0) - udp_data.fd_to_uf[s_eid].d_eid = d_eid; - - pthread_rwlock_unlock(&udp_data.flows_lock); - - if (ipcp_flow_alloc_reply(s_eid, response, mpl, - udp_query_mtu(saddr), data) < 0) { - log_err("Failed to reply to flow allocation."); - return -1; - } - - log_dbg("Flow allocation completed on eids (%d, %d).", - s_eid, d_eid); - - return 0; -} - -static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr, - const uint8_t * buf, - size_t len) -{ - struct mgmt_msg * msg; - size_t msg_len; - qosspec_t qs; - buffer_t data; - - /* Defence against malformed/corrupted wire input. */ - if (len < sizeof(*msg)) - return -1; - - msg = (struct mgmt_msg *) buf; - - switch (msg->code) { - case FLOW_REQ: - msg_len = sizeof(*msg) + ipcp_dir_hash_len(); - - if (len < msg_len) - return -1; - - data.len = len - msg_len; - data.data = (uint8_t *) buf + msg_len; - - - qs.delay = ntoh32(msg->delay); - qs.bandwidth = ntoh64(msg->bandwidth); - qs.availability = msg->availability; - qs.loss = ntoh32(msg->loss); - qs.ber = ntoh32(msg->ber); - qs.service = msg->service; - qs.max_gap = ntoh32(msg->max_gap); - qs.timeout = ntoh32(msg->timeout); - - return udp_ipcp_port_req(&c_saddr, ntoh32(msg->s_eid), - (uint8_t *) (msg + 1), qs, - &data); - case FLOW_REPLY: - data.len = len - sizeof(*msg); - data.data = (uint8_t *) buf + sizeof(*msg); - - return udp_ipcp_port_alloc_reply(&c_saddr, - ntoh32(msg->s_eid), - ntoh32(msg->d_eid), - ntoh32(msg->response), - &data); - default: - log_err("Unknown message received %d.", msg->code); - return -1; - } -} - -static void * udp_ipcp_mgmt_handler(void * o) -{ - (void) o; - - pthread_cleanup_push(__cleanup_mutex_unlock, &udp_data.mgmt_lock); - - while (true) { - struct mgmt_frame * frame; - - pthread_mutex_lock(&udp_data.mgmt_lock); - - while (list_is_empty(&udp_data.mgmt_frames)) - pthread_cond_wait(&udp_data.mgmt_cond, - &udp_data.mgmt_lock); - - frame = list_first_entry((&udp_data.mgmt_frames), - struct mgmt_frame, next); - assert(frame != NULL); - list_del(&frame->next); - - pthread_mutex_unlock(&udp_data.mgmt_lock); - - udp_ipcp_mgmt_frame(frame->r_saddr, frame->buf, frame->len); - - free(frame); - } - - pthread_cleanup_pop(false); - - return (void *) 0; -} - -static void * udp_ipcp_packet_reader(void * o) -{ - uint8_t buf[IPCP_UDP_MAX_PACKET_SIZE]; - uint8_t * data; - ssize_t n; - uint32_t eid; - uint32_t * eid_p; - - (void) o; - - ipcp_lock_to_core(); - - data = buf + sizeof(uint32_t); - eid_p = (uint32_t *) buf; - - while (true) { - struct mgmt_frame * frame; - struct __SOCKADDR r_saddr; - socklen_t len; - struct ssm_pk_buff * spb; - uint8_t * head; - - len = sizeof(r_saddr); - - n = recvfrom(udp_data.s_fd, buf, IPCP_UDP_MAX_PACKET_SIZE, 0, - (struct sockaddr *) &r_saddr, &len); - if (n < 0) - continue; - - if (n == 0) - log_dbg("Got a 0 frame."); - - if ((size_t) n < sizeof(eid)) { - log_dbg("Dropped bad frame."); - continue; - } - - eid = ntoh32(*eid_p); - - /* pass onto mgmt queue */ - if (eid == MGMT_EID) { - if ((size_t) n < MGMT_FRAME_SIZE) { - log_warn("Dropped runt mgmt frame."); - continue; - } - - frame = malloc(sizeof(*frame)); - if (frame == NULL) - continue; - - memcpy(frame->buf, buf, n); - memcpy(&frame->r_saddr, &r_saddr, sizeof(r_saddr)); - frame->len = n; - - pthread_mutex_lock(&udp_data.mgmt_lock); - list_add(&frame->next, &udp_data.mgmt_frames); - pthread_cond_signal(&udp_data.mgmt_cond); - pthread_mutex_unlock(&udp_data.mgmt_lock); - continue; - } - - n-= sizeof(eid); - - if (ipcp_spb_reserve(&spb, n)) - continue; - - head = ssm_pk_buff_head(spb); - memcpy(head, data, n); - if (np1_flow_write(eid, spb, NP1_GET_POOL(eid)) < 0) - ipcp_spb_release(spb); - } - - return (void *) 0; -} - -static void cleanup_fqueue(void * fq) -{ - fqueue_destroy((fqueue_t *) fq); -} - -static void cleanup_spb(void * spb) -{ - ipcp_spb_release((struct ssm_pk_buff *) spb); -} - -static void * udp_ipcp_packet_writer(void * o) -{ - fqueue_t * fq; - - fq = fqueue_create(); - if (fq == NULL) - return (void *) -1; - - (void) o; - - ipcp_lock_to_core(); - - pthread_cleanup_push(cleanup_fqueue, fq); - - while (true) { - struct __SOCKADDR saddr; - int eid; - int fd; - fevent(udp_data.np1_flows, fq, NULL); - while ((fd = fqueue_next(fq)) >= 0) { - struct ssm_pk_buff * spb; - uint8_t * buf; - uint16_t len; - - if (fqueue_type(fq) != FLOW_PKT) - continue; - - if (np1_flow_read(fd, &spb, NP1_GET_POOL(fd))) { - log_dbg("Bad read from fd %d.", fd); - continue; - } - - len = ssm_pk_buff_len(spb); - if (len > IPCP_UDP_MAX_PACKET_SIZE) { - log_dbg("Packet length exceeds MTU."); - ipcp_spb_release(spb); - continue; - } - - buf = ssm_pk_buff_push(spb, OUR_HEADER_LEN); - if (buf == NULL) { - log_dbg("Failed to allocate header."); - ipcp_spb_release(spb); - continue; - } - - pthread_rwlock_rdlock(&udp_data.flows_lock); - - eid = hton32(udp_data.fd_to_uf[fd].d_eid); - saddr = udp_data.fd_to_uf[fd].r_saddr; - - pthread_rwlock_unlock(&udp_data.flows_lock); - - memcpy(buf, &eid, sizeof(eid)); - - pthread_cleanup_push(cleanup_spb, spb); - - if (sendto(udp_data.s_fd, buf, len + OUR_HEADER_LEN, - SENDTO_FLAGS, - (const struct sockaddr *) &saddr, - sizeof(saddr)) < 0) - log_err("Failed to send packet."); - - pthread_cleanup_pop(true); - } - } - - pthread_cleanup_pop(true); - - return (void *) 1; -} - -static bool is_addr_specified(const struct __ADDR * addr) -{ -#ifdef BUILD_IPCP_UDP4 - return addr->s_addr != 0; -#else - return !IN6_IS_ADDR_UNSPECIFIED(addr); -#endif -} - -static int udp_ipcp_bootstrap(struct ipcp_config * conf) -{ - char ipstr[__ADDRSTRLEN]; - char dnsstr[__ADDRSTRLEN]; - int i = 1; -#ifdef BUILD_IPCP_UDP4 - struct udp4_config * udp; - udp = &conf->udp4; -#else - struct udp6_config * udp; - udp = &conf->udp6; -#endif - - assert(conf != NULL); - assert(conf->type == THIS_TYPE); - assert(conf->layer_info.dir_hash_algo == (enum pol_dir_hash) HASH_MD5); - - if (__inet_ntop(&udp->ip_addr, ipstr) == NULL) { - log_err("Failed to convert IP address."); - return -1; - } - - if (is_addr_specified(&udp->dns_addr)) { - if (__inet_ntop(&udp->dns_addr, dnsstr) == NULL) { - log_err("Failed to convert DNS address."); - return -1; - } -#ifndef HAVE_DDNS - log_warn("DNS disabled at compile time, address ignored."); -#endif - } else { - strcpy(dnsstr, "not set"); - } - - /* UDP listen server */ - udp_data.s_fd = socket(__AF, SOCK_DGRAM, IPPROTO_UDP); - if (udp_data.s_fd < 0) { - log_err("Can't create socket: %s", strerror(errno)); - goto fail_socket; - } - - memset((char *) &udp_data.s_saddr, 0, sizeof(udp_data.s_saddr)); -#ifdef BUILD_IPCP_UDP4 - udp_data.s_saddr.sin_family = AF_INET; - udp_data.s_saddr.sin_addr = udp->ip_addr; - udp_data.s_saddr.sin_port = htons(udp->port); -#else - udp_data.s_saddr.sin6_family = AF_INET6; - udp_data.s_saddr.sin6_addr = udp->ip_addr; - udp_data.s_saddr.sin6_port = htons(udp->port); -#endif - if (bind(udp_data.s_fd, SADDR, SADDR_SIZE) < 0) { - log_err("Couldn't bind to %s:%d. %s.", - ipstr, udp->port, strerror(errno)); - goto fail_bind; - } - - udp_data.dns_addr = udp->dns_addr; - - if (pthread_create(&udp_data.mgmt_handler, NULL, - udp_ipcp_mgmt_handler, NULL)) { - log_err("Failed to create management thread."); - goto fail_bind; - } - - for (i = 0; i < IPCP_UDP_RD_THR; ++i) { - if (pthread_create(&udp_data.packet_reader[i], NULL, - udp_ipcp_packet_reader, NULL)) { - log_err("Failed to create reader thread."); - goto fail_packet_reader; - } - } - - for (i = 0; i < IPCP_UDP_WR_THR; ++i) { - if (pthread_create(&udp_data.packet_writer[i], NULL, - udp_ipcp_packet_writer, NULL)) { - log_err("Failed to create writer thread."); - goto fail_packet_writer; - } - } - - log_dbg("Bootstrapped " TYPE_STR " with pid %d.", getpid()); - log_dbg("Bound to IP address %s.", ipstr); - log_dbg("Using port %u.", udp->port); - if (is_addr_specified(&udp_data.dns_addr)) - log_dbg("DNS server address is %s.", dnsstr); - else - log_dbg("DNS server not in use."); - - return 0; - - fail_packet_writer: - while (i-- > 0) { - pthread_cancel(udp_data.packet_writer[i]); - pthread_join(udp_data.packet_writer[i], NULL); - } - i = IPCP_UDP_RD_THR; - fail_packet_reader: - while (i-- > 0) { - pthread_cancel(udp_data.packet_reader[i]); - pthread_join(udp_data.packet_reader[i], NULL); - } - pthread_cancel(udp_data.mgmt_handler); - pthread_join(udp_data.mgmt_handler, NULL); - fail_bind: - close(udp_data.s_fd); - fail_socket: - return -1; -} - -#ifdef HAVE_DDNS -/* FIXME: Dependency on nsupdate to be removed in the end */ -/* NOTE: Disgusted with this crap */ -static int ddns_send(char * cmd) -{ - pid_t pid; - int wstatus; - int pipe_fd[2]; - char * argv[] = {NSUPDATE_EXEC, 0}; - char * envp[] = {0}; - - if (pipe(pipe_fd)) { - log_err("Failed to create pipe: %s.", strerror(errno)); - return -1; - } - - pid = fork(); - if (pid == -1) { - log_err("Failed to fork: %s.", strerror(errno)); - close(pipe_fd[0]); - close(pipe_fd[1]); - return -1; - } - - if (pid == 0) { - close(pipe_fd[1]); - dup2(pipe_fd[0], 0); - execve(argv[0], &argv[0], envp); - log_err("Failed to execute: %s", strerror(errno)); - exit(1); - } - - close(pipe_fd[0]); - - if (write(pipe_fd[1], cmd, strlen(cmd)) == -1) { - log_err("Failed to communicate with nsupdate: %s.", - strerror(errno)); - close(pipe_fd[1]); - return -1; - } - - waitpid(pid, &wstatus, 0); - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) - log_dbg("Succesfully communicated with DNS server."); - else - log_err("Failed to register with DNS server."); - - close(pipe_fd[1]); - - return 0; -} - -static struct __ADDR ddns_resolve(char * name, - struct __ADDR dns_addr) -{ - pid_t pid = -1; - int wstatus; - int pipe_fd[2]; - char dnsstr[__ADDRSTRLEN]; - char buf[IPCP_UDP_BUF_SIZE]; - ssize_t count = 0; - char * substr = NULL; - char * substr2 = NULL; - char * addr_str = "Address:"; - struct __ADDR ip_addr = __ADDR_ANY_INIT; - - if (__inet_ntop(&dns_addr, dnsstr) == NULL) - return ip_addr; - - if (pipe(pipe_fd)) { - log_err("Failed to create pipe: %s.", strerror(errno)); - return ip_addr; - } - - pid = fork(); - if (pid == -1) { - log_err("Failed to fork: %s.", strerror(errno)); - close(pipe_fd[0]); - close(pipe_fd[1]); - return ip_addr; - } - - if (pid == 0) { - char * argv[] = {NSLOOKUP_EXEC, name, dnsstr, 0}; - char * envp[] = {0}; - - close(pipe_fd[0]); - dup2(pipe_fd[1], 1); - execve(argv[0], &argv[0], envp); - log_err("Failed to execute: %s", strerror(errno)); - exit(1); - } - - close(pipe_fd[1]); - - count = read(pipe_fd[0], buf, IPCP_UDP_BUF_SIZE - 1); - if (count <= 0) { - log_err("Failed to communicate with nslookup."); - close(pipe_fd[0]); - return ip_addr; - } - - close(pipe_fd[0]); - - waitpid(pid, &wstatus, 0); - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0 && - count != IPCP_UDP_BUF_SIZE - 1) - log_dbg("Succesfully communicated with nslookup."); - else - log_err("Failed to resolve DNS address."); - - buf[count] = '\0'; - substr = strtok(buf, "\n"); - while (substr != NULL) { - substr2 = substr; - substr = strtok(NULL, "\n"); - } - - if (substr2 == NULL || strstr(substr2, addr_str) == NULL) { - log_err("Failed to resolve DNS address."); - return ip_addr; - } - - if (inet_pton(__AF, substr2 + strlen(addr_str) + 1, &ip_addr) != 1) { - log_err("Failed to resolve DNS address."); - assert(!is_addr_specified(&ip_addr)); - return ip_addr; - } - - return ip_addr; -} -#endif - -static int udp_ipcp_reg(const uint8_t * hash) -{ -#ifdef HAVE_DDNS - char ipstr[__ADDRSTRLEN]; - char dnsstr[__ADDRSTRLEN]; - char cmd[1000]; - struct __ADDR dns_addr; - struct __ADDR ip_addr; -#endif - char * hashstr; - - hashstr = malloc(ipcp_dir_hash_strlen() + 1); - if (hashstr == NULL) { - log_err("Failed to malloc hashstr."); - return -1; - } - - assert(hash); - - ipcp_hash_str(hashstr, hash); - - if (shim_data_reg_add_entry(udp_data.shim_data, hash)) { - log_err("Failed to add " HASH_FMT32 " to local registry.", - HASH_VAL32(hash)); - free(hashstr); - return -1; - } - -#ifdef HAVE_DDNS - /* register application with DNS server */ - - dns_addr = udp_data.dns_addr; - - if (is_addr_specified(&dns_addr)) { -#ifdef BUILD_IPCP_UDP4 - ip_addr = udp_data.s_saddr.sin_addr; -#else - ip_addr = udp_data.s_saddr.sin6_addr; -#endif - if (__inet_ntop(&ip_addr, ipstr) == NULL) { - log_err("Failed to convert IP address to string."); - free(hashstr); - return -1; - } - - if (__inet_ntop(&dns_addr, dnsstr) == NULL) { - log_err("Failed to convert DNS address to string."); - free(hashstr); - return -1; - } - - sprintf(cmd, "server %s\nupdate add %s %d A %s\nsend\nquit\n", - dnsstr, hashstr, DNS_TTL, ipstr); - - if (ddns_send(cmd)) { - log_err("Failed to send DDNS message."); - shim_data_reg_del_entry(udp_data.shim_data, hash); - free(hashstr); - return -1; - } - } -#endif - free(hashstr); - - return 0; -} - -static int udp_ipcp_unreg(const uint8_t * hash) -{ -#ifdef HAVE_DDNS - char dnsstr[__ADDRSTRLEN]; - /* max DNS name length + max IP length + max command length */ - char cmd[100]; - struct __ADDR dns_addr; -#endif - char * hashstr; - - assert(hash); - - hashstr = malloc(ipcp_dir_hash_strlen() + 1); - if (hashstr == NULL) { - log_err("Failed to malloc hashstr."); - return -1; - } - - ipcp_hash_str(hashstr, hash); - -#ifdef HAVE_DDNS - /* unregister application with DNS server */ - - dns_addr = udp_data.dns_addr; - - if (is_addr_specified(&dns_addr)) { - if (__inet_ntop(&dns_addr, dnsstr) == NULL) { - log_err("Failed to convert DNS address to string."); - free(hashstr); - return -1; - } - sprintf(cmd, "server %s\nupdate delete %s A\nsend\nquit\n", - dnsstr, hashstr); - - ddns_send(cmd); - } -#endif - - shim_data_reg_del_entry(udp_data.shim_data, hash); - - free(hashstr); - - return 0; -} - -static int udp_ipcp_query(const uint8_t * hash) -{ - struct addr addr = {}; - char * hashstr; - struct addrinfo hints; - struct addrinfo * ai; -#ifdef HAVE_DDNS - struct __ADDR dns_addr = __ADDR_ANY_INIT; - struct __ADDR ip_addr = __ADDR_ANY_INIT; -#endif - assert(hash); - - hashstr = malloc(ipcp_dir_hash_strlen() + 1); - if (hashstr == NULL) { - log_err("Failed to malloc hashstr."); - return -ENOMEM; - } - - ipcp_hash_str(hashstr, hash); - - if (shim_data_dir_has(udp_data.shim_data, hash)) { - free(hashstr); - return 0; - } - -#ifdef HAVE_DDNS - dns_addr = udp_data.dns_addr; - - if (is_addr_specified(&dns_addr)) { - ip_addr = ddns_resolve(hashstr, dns_addr); - if (!is_addr_specified(&ip_addr)) { - log_err("Could not resolve %s.", hashstr); - free(hashstr); - return -1; - } - } else { -#endif - memset(&hints, 0, sizeof(hints)); - - hints.ai_family = __AF; - if (getaddrinfo(hashstr, NULL, &hints, &ai) != 0) { - log_err("Could not resolve %s: %s.", hashstr, - gai_strerror(errno)); - free(hashstr); - return -1; - } - - if (ai->ai_family != __AF) { - log_err("Wrong addres family for %s.", hashstr); - freeaddrinfo(ai); - free(hashstr); - return -1; - } - - #ifdef BUILD_IPCP_UDP4 - addr.ip4 = ((struct sockaddr_in *) (ai->ai_addr))->sin_addr; - #else - addr.ip6 = ((struct sockaddr_in6 *) (ai->ai_addr))->sin6_addr; - #endif - freeaddrinfo(ai); -#ifdef HAVE_DDNS - } -#endif - if (shim_data_dir_add_entry(udp_data.shim_data, hash, addr)) { - log_err("Failed to add directory entry."); - free(hashstr); - return -1; - } - - free(hashstr); - - return 0; -} - -static int udp_ipcp_flow_alloc(int fd, - const uint8_t * dst, - qosspec_t qs, - const buffer_t * data) -{ - struct __SOCKADDR r_saddr; /* Server address */ - struct __ADDR ip_addr; - struct addr addr; - char ipstr[__ADDRSTRLEN]; - - (void) qs; - - assert(dst); - - if (!shim_data_dir_has(udp_data.shim_data, dst)) { - log_err("Could not resolve destination."); - return -1; - } - - addr = shim_data_dir_get_addr(udp_data.shim_data, dst); -#ifdef BUILD_IPCP_UDP4 - ip_addr = addr.ip4; -#else - ip_addr = addr.ip6; -#endif - if (__inet_ntop(&ip_addr, ipstr) == NULL) { - log_err("Could not convert IP address."); - return -1; - } - - log_dbg("Destination " HASH_FMT32 " resolved at IP %s.", - HASH_VAL32(dst), ipstr); - - memset((char *) &r_saddr, 0, sizeof(r_saddr)); -#ifdef BUILD_IPCP_UDP4 - r_saddr.sin_family = AF_INET; - r_saddr.sin_addr = addr.ip4; - r_saddr.sin_port = udp_data.s_saddr.sin_port; -#else - r_saddr.sin6_family = AF_INET6; - r_saddr.sin6_addr = addr.ip6; - r_saddr.sin6_port = udp_data.s_saddr.sin6_port; -#endif - - if (udp_ipcp_port_alloc(&r_saddr, fd, dst, qs, data) < 0) { - log_err("Could not allocate port."); - return -1; - } - - pthread_rwlock_wrlock(&udp_data.flows_lock); - - udp_data.fd_to_uf[fd].d_eid = -1; - udp_data.fd_to_uf[fd].r_saddr = r_saddr; - - pthread_rwlock_unlock(&udp_data.flows_lock); - - fset_add(udp_data.np1_flows, fd); - - return 0; -} - -static int udp_ipcp_flow_alloc_resp(int fd, - int resp, - const buffer_t * data) -{ - struct __SOCKADDR saddr; - int d_eid; - - if (ipcp_wait_flow_resp(fd) < 0) { - log_err("Failed to wait for flow response."); - return -1; - } - - pthread_rwlock_rdlock(&udp_data.flows_lock); - - saddr = udp_data.fd_to_uf[fd].r_saddr; - d_eid = udp_data.fd_to_uf[fd].d_eid; - - pthread_rwlock_unlock(&udp_data.flows_lock); - - if (udp_ipcp_port_alloc_resp(&saddr, d_eid, fd, resp, data) < 0) { - fset_del(udp_data.np1_flows, fd); - log_err("Failed to respond to flow request."); - return -1; - } - - fset_add(udp_data.np1_flows, fd); - - return 0; -} - -static int udp_ipcp_flow_dealloc(int fd) -{ - ipcp_flow_fini(fd); - - fset_del(udp_data.np1_flows, fd); - - pthread_rwlock_wrlock(&udp_data.flows_lock); - - udp_data.fd_to_uf[fd].d_eid = -1; - memset(&udp_data.fd_to_uf[fd].r_saddr, 0, SADDR_SIZE); - - pthread_rwlock_unlock(&udp_data.flows_lock); - - ipcp_flow_dealloc(fd); - - return 0; -} - -static struct ipcp_ops udp_ops = { - .ipcp_bootstrap = udp_ipcp_bootstrap, - .ipcp_enroll = NULL, - .ipcp_connect = NULL, - .ipcp_disconnect = NULL, - .ipcp_reg = udp_ipcp_reg, - .ipcp_unreg = udp_ipcp_unreg, - .ipcp_query = udp_ipcp_query, - .ipcp_flow_alloc = udp_ipcp_flow_alloc, - .ipcp_flow_join = NULL, - .ipcp_flow_alloc_resp = udp_ipcp_flow_alloc_resp, - .ipcp_flow_dealloc = udp_ipcp_flow_dealloc -}; - -int main(int argc, - char * argv[]) -{ - int i; - - - if (udp_data_init() < 0) { - log_err("Failed to init udp data."); - goto fail_data_init; - } - - if (ipcp_init(argc, argv, &udp_ops, THIS_TYPE) < 0) { - log_err("Failed to initialize IPCP."); - goto fail_init; - } - - if (ipcp_start() < 0) { - log_err("Failed to start IPCP."); - goto fail_start; - } - - ipcp_sigwait(); - - if (ipcp_get_state() == IPCP_SHUTDOWN) { - for (i = 0; i < IPCP_UDP_WR_THR; ++i) - pthread_cancel(udp_data.packet_writer[i]); - for (i = 0; i < IPCP_UDP_RD_THR; ++i) - pthread_cancel(udp_data.packet_reader[i]); - pthread_cancel(udp_data.mgmt_handler); - - for (i = 0; i < IPCP_UDP_WR_THR; ++i) - pthread_join(udp_data.packet_writer[i], NULL); - for (i = 0; i < IPCP_UDP_RD_THR; ++i) - pthread_join(udp_data.packet_reader[i], NULL); - pthread_join(udp_data.mgmt_handler, NULL); - close(udp_data.s_fd); - } - - ipcp_stop(); - - ipcp_fini(); - - udp_data_fini(); - - exit(EXIT_SUCCESS); - - fail_start: - ipcp_fini(); - fail_init: - udp_data_fini(); - fail_data_init: - exit(EXIT_FAILURE); -} diff --git a/src/ipcpd/udp/udp4.c b/src/ipcpd/udp/udp4.c deleted file mode 100644 index ff57bc09..00000000 --- a/src/ipcpd/udp/udp4.c +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * IPC process over UDP/IPv4 - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#if defined(__linux__) || defined(__CYGWIN__) -#define _DEFAULT_SOURCE -#else -#define _POSIX_C_SOURCE 200112L -#endif - -#include <ouroboros/ipcp-dev.h> - -#define BUILD_IPCP_UDP4 -#define THIS_TYPE IPCP_UDP4 -#define TYPE_STR "IPCP over UDP/IPv4" -#define OUROBOROS_PREFIX "ipcpd/udp4" -#define IPCP_UDP_MAX_PACKET_SIZE 8980 -#define __AF AF_INET -#define __ADDRSTRLEN INET_ADDRSTRLEN -#define __SOCKADDR sockaddr_in -#define __ADDR in_addr -#define __ADDR_ANY_INIT { .s_addr = INADDR_ANY } - -#include "udp.c" diff --git a/src/ipcpd/udp/udp6.c b/src/ipcpd/udp/udp6.c deleted file mode 100644 index 2ceb95f0..00000000 --- a/src/ipcpd/udp/udp6.c +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2026 - * - * IPC process over UDP/IPv6 - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. - */ - -#if defined(__linux__) || defined(__CYGWIN__) -#define _DEFAULT_SOURCE -#else -#define _POSIX_C_SOURCE 200112L -#endif - -#include <ouroboros/ipcp-dev.h> - -#define BUILD_IPCP_UDP6 -#define THIS_TYPE IPCP_UDP6 -#define TYPE_STR "IPCP over UDP/IPv6" -#define OUROBOROS_PREFIX "ipcpd/udp6" -#define IPCP_UDP_MAX_PACKET_SIZE 8952 -#define __AF AF_INET6 -#define __ADDRSTRLEN INET6_ADDRSTRLEN -#define __SOCKADDR sockaddr_in6 -#define __ADDR in6_addr -#define __ADDR_ANY_INIT IN6ADDR_ANY_INIT - -#include "udp.c" diff --git a/src/ipcpd/unicast/CMakeLists.txt b/src/ipcpd/unicast/CMakeLists.txt index d3388112..2373f877 100644 --- a/src/ipcpd/unicast/CMakeLists.txt +++ b/src/ipcpd/unicast/CMakeLists.txt @@ -6,6 +6,7 @@ protobuf_generate_c(DHT_PROTO_SRCS DHT_PROTO_HDRS set(UNICAST_SOURCES addr-auth.c ca.c + cap.c connmgr.c dir.c dt.c @@ -43,7 +44,9 @@ ouroboros_target_debug_definitions(${IPCP_UNICAST_TARGET}) install(TARGETS ${IPCP_UNICAST_TARGET} RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR}) if(BUILD_TESTS) + add_subdirectory(ca/tests) add_subdirectory(dir/tests) add_subdirectory(pff/tests) add_subdirectory(routing/tests) + add_subdirectory(tests) endif() diff --git a/src/ipcpd/unicast/ca.c b/src/ipcpd/unicast/ca.c index a1751672..290c817e 100644 --- a/src/ipcpd/unicast/ca.c +++ b/src/ipcpd/unicast/ca.c @@ -22,17 +22,48 @@ #define OUROBOROS_PREFIX "ca" +#include "config.h" + +#include <ouroboros/list.h> #include <ouroboros/logs.h> #include "ca.h" #include "ca/pol.h" +#include <pthread.h> +#include <stdlib.h> + +/* + * A ca_ctx holds congestion state for a (peer address, qos cube) PATH, + * not for a flow. In the default build the façade interns one ctx per + * (addr, qc) and shares it across every flow on that path; the policy + * runs on the shared ctx and cannot tell one flow from many. Per-flow + * ctx (IPCP_CA_PER_FLOW) is a testing build only: it skips interning so + * every flow gets its own ctx. + */ + +struct ca_ctx { + uint64_t addr; + qoscube_t qc; + size_t refs; + void * pol; /* policy ctx (ops->ctx_create result) */ + struct list_head next; +}; + struct { - struct ca_ops * ops; + struct ca_ops * ops; +#ifndef IPCP_CA_PER_FLOW + struct list_head buckets[CA_BUCKETS]; + pthread_mutex_t mtx; +#endif } ca; -int ca_init(enum pol_cong_avoid pol) +int ca_init(enum pol_cong_avoid pol, + uint32_t rtt_ms) { +#ifndef IPCP_CA_PER_FLOW + size_t i; +#endif switch(pol) { case CA_NONE: log_dbg("Disabling congestion control."); @@ -41,68 +72,196 @@ int ca_init(enum pol_cong_avoid pol) case CA_MB_ECN: log_dbg("Using multi-bit ECN."); ca.ops = &mb_ecn_ca_ops; + mb_ecn_init(rtt_ms); break; default: return -1; } +#ifndef IPCP_CA_PER_FLOW + for (i = 0; i < CA_BUCKETS; i++) + list_head_init(&ca.buckets[i]); + + if (pthread_mutex_init(&ca.mtx, NULL) != 0) + return -1; +#endif return 0; } void ca_fini(void) { +#ifndef IPCP_CA_PER_FLOW + size_t i; + + /* Data path is stopped; drain any ctx a flow left interned. */ + for (i = 0; i < CA_BUCKETS; i++) { + struct list_head * p; + struct list_head * h; + + list_for_each_safe(p, h, &ca.buckets[i]) { + struct ca_ctx * ctx; + ctx = list_entry(p, struct ca_ctx, next); + list_del(&ctx->next); + ca.ops->ctx_destroy(ctx->pol); + free(ctx); + } + } + + pthread_mutex_destroy(&ca.mtx); +#endif ca.ops = NULL; } -void * ca_ctx_create(void) +#ifndef IPCP_CA_PER_FLOW +static size_t ca_bucket(uint64_t addr, + qoscube_t qc) { - return ca.ops->ctx_create(); + return (addr ^ (addr >> 32) ^ (uint64_t) qc) & (CA_BUCKETS - 1); +} +#endif + +void * ca_ctx_get(uint64_t addr, + qoscube_t qc) +{ + struct ca_ctx * ctx; +#ifndef IPCP_CA_PER_FLOW + struct list_head * p; + size_t b = ca_bucket(addr, qc); + + pthread_mutex_lock(&ca.mtx); + + list_for_each(p, &ca.buckets[b]) { + ctx = list_entry(p, struct ca_ctx, next); + if (ctx->addr == addr && ctx->qc == qc) { + ctx->refs++; + pthread_mutex_unlock(&ca.mtx); + return ctx; + } + } +#endif + ctx = malloc(sizeof(*ctx)); + if (ctx == NULL) + goto fail_ctx; + + ctx->pol = ca.ops->ctx_create(); + if (ctx->pol == NULL) + goto fail_pol; + + ctx->addr = addr; + ctx->qc = qc; + ctx->refs = 1; + +#ifndef IPCP_CA_PER_FLOW + list_add(&ctx->next, &ca.buckets[b]); + + pthread_mutex_unlock(&ca.mtx); +#endif + return ctx; + fail_pol: + free(ctx); + fail_ctx: +#ifndef IPCP_CA_PER_FLOW + pthread_mutex_unlock(&ca.mtx); +#endif + return NULL; } -void ca_ctx_destroy(void * ctx) +void ca_ctx_put(void * _ctx) { - return ca.ops->ctx_destroy(ctx); + struct ca_ctx * ctx = _ctx; + +#ifndef IPCP_CA_PER_FLOW + pthread_mutex_lock(&ca.mtx); + + if (--ctx->refs > 0) { + pthread_mutex_unlock(&ca.mtx); + return; + } + + list_del(&ctx->next); + + pthread_mutex_unlock(&ca.mtx); +#endif + ca.ops->ctx_destroy(ctx->pol); + + free(ctx); } -ca_wnd_t ca_ctx_update_snd(void * ctx, - size_t len) +time_t ca_ctx_update_snd(void * _ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag) { - return ca.ops->ctx_update_snd(ctx, len); + struct ca_ctx * ctx = _ctx; + + return ca.ops->ctx_update_snd(ctx->pol, len, lecn, ctx->refs, ftag); } -bool ca_ctx_update_rcv(void * ctx, +bool ca_ctx_update_rcv(void * _ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) { - return ca.ops->ctx_update_rcv(ctx, len, ecn, ece); + struct ca_ctx * ctx = _ctx; + + return ca.ops->ctx_update_rcv(ctx->pol, len, ecn, cap, ece, fcap); } -void ca_ctx_update_ece(void * ctx, - uint16_t ece) +void ca_ctx_update_ece(void * _ctx, + uint16_t ece, + uint8_t cap) { - return ca.ops->ctx_update_ece(ctx, ece); + struct ca_ctx * ctx = _ctx; + + return ca.ops->ctx_update_ece(ctx->pol, ece, cap); } -void ca_wnd_wait(ca_wnd_t wnd) +bool ca_ctx_hb_due(void * _ctx, + uint64_t now) { - return ca.ops->wnd_wait(wnd); + struct ca_ctx * ctx = _ctx; + + if (ca.ops->ctx_hb_due == NULL) + return false; + + return ca.ops->ctx_hb_due(ctx->pol, now); +} + +void ca_ctx_rtt(void * _ctx, + uint64_t now, + uint64_t rtt) +{ + struct ca_ctx * ctx = _ctx; + + if (ca.ops->ctx_rtt == NULL) + return; + + ca.ops->ctx_rtt(ctx->pol, now, rtt); } -int ca_calc_ecn(int fd, +int ca_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len) + size_t mean) { - return ca.ops->calc_ecn(fd, ecn, qc, len); + return ca.ops->calc_ecn(queued, ecn, qc, mean); } -ssize_t ca_print_stats(void * ctx, +bool ca_marks_ecn(void) +{ + return ca.ops->marks_ecn; +} + +ssize_t ca_print_stats(void * _ctx, char * buf, size_t len) { + struct ca_ctx * ctx = _ctx; + if (ca.ops->print_stats == NULL) return 0; - return ca.ops->print_stats(ctx, buf, len); + return ca.ops->print_stats(ctx->pol, buf, len); } diff --git a/src/ipcpd/unicast/ca.h b/src/ipcpd/unicast/ca.h index 47ea15a0..188fb08a 100644 --- a/src/ipcpd/unicast/ca.h +++ b/src/ipcpd/unicast/ca.h @@ -29,37 +29,54 @@ #include <stdbool.h> #include <sys/types.h> -typedef union { - time_t wait; -} ca_wnd_t; +/* Buffer a policy's ca_print_stats output must fit in. */ +#define CA_STATS_STRLEN 2048 -int ca_init(enum pol_cong_avoid ca); +int ca_init(enum pol_cong_avoid ca, + uint32_t rtt_ms); void ca_fini(void); /* OPS */ -void * ca_ctx_create(void); +void * ca_ctx_get(uint64_t addr, + qoscube_t qc); -void ca_ctx_destroy(void * ctx); +void ca_ctx_put(void * ctx); -ca_wnd_t ca_ctx_update_snd(void * ctx, - size_t len); +time_t ca_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + uint64_t * ftag); bool ca_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void ca_ctx_update_ece(void * ctx, - uint16_t ece); + uint16_t ece, + uint8_t cap); + +bool ca_ctx_hb_due(void * ctx, + uint64_t now); -void ca_wnd_wait(ca_wnd_t wnd); +void ca_ctx_rtt(void * ctx, + uint64_t now, + uint64_t rtt); -int ca_calc_ecn(int fd, +/* + * Marks congestion from the egress queue. Both queued and mean are + * in bytes, so their ratio is the queue depth in packets. + */ +int ca_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len); + size_t mean); + +bool ca_marks_ecn(void); ssize_t ca_print_stats(void * ctx, char * buf, diff --git a/src/ipcpd/unicast/ca/mb-ecn.c b/src/ipcpd/unicast/ca/mb-ecn.c index b310c4fc..59f1cae5 100644 --- a/src/ipcpd/unicast/ca/mb-ecn.c +++ b/src/ipcpd/unicast/ca/mb-ecn.c @@ -28,9 +28,10 @@ #include "config.h" -#include <ouroboros/ipcp-dev.h> #include <ouroboros/time.h> +#include <ouroboros/utils.h> +#include "cap.h" #include "mb-ecn.h" #include <inttypes.h> @@ -38,47 +39,284 @@ #include <string.h> #include <stdio.h> -/* congestion avoidance constants */ -#define CA_SHFT 5 /* Average over 32 pkts */ -#define CA_WND (1 << CA_SHFT) /* 32 pkts receiver wnd */ -#define CA_UPD (1 << (CA_SHFT - 2)) /* Update snd every 8 pkt */ -#define CA_SLOT 24 /* Initial slot = 16 ms */ -#define CA_INC 1UL << 16 /* ~4MiB/s^2 additive inc */ -#define CA_IWL 1UL << 16 /* Initial limit ~4MiB/s */ -#define CA_MINPS 8 /* Mimimum pkts / slot */ -#define CA_MAXPS 64 /* Maximum pkts / slot */ -#define ECN_Q_SHFT 4 -#define ts_to_ns(ts) ((size_t) ts.tv_sec * BILLION + ts.tv_nsec) +/* + * Multi-bit ECN congestion avoidance: a rate-based controller. The + * sender paces a token bucket at a rate steered by graded ECN + * feedback, so the backoff is proportional to the congestion. A + * backlogged flow ramps in slow start to find the path capacity, + * then settles into AIMD around its fair share. There is no sliding + * window and no per-flow timer; the control runs on sends. + * + * Rate law, per control step of dt seconds (r bytes/s, m the mark + * in ece units, m_ref = CA_ECE_REF, ai the additive slope): + * + * slow start dr = r * dt / ss_tc + * increase dr = (ai + r / T_probe) * dt + * decrease dr = -r * (min(m, CA_ECE_MAX) / m_ref) * dt + L, + * cut capped at r/2 + * lead L = -dm * r / (m_ref * CA_MD_KD_DIV) + * + * dm is the mark's step since the last decrease, clamped to + * +-m_ref. On a rise L joins the cut before the r/2 cap; on a + * fall it returns after that cap, bounded on its own to + * +-r / CA_MD_KD_DIV, so a full cut is never handed back in one + * step. + * + * Every step scales by elapsed wall-clock time, not by packet + * count, so the per-second dynamics are RTT-independent. + * + * Pacer: a virtual clock vt advances at r; a packet's start tag is + * max(tag, vt) and it waits (tag - vt) / r. + * + * Receiver: ece is the time integral of ecn over a pricing window, + * ece = integral(ecn dt) / T. The window is a per-layer constant so + * every flow prices one bottleneck alike; it stretches only for a + * flow too slow to fill it with samples. + * + * Marking (mb_ecn_calc_ecn): ecn is the quarter-log2 of the queue + * measured in mark units U (U = CA_MARK_KNEE * mean), so the mark is + * a log-scale queue depth. Equilibrium is where increase balances + * decrease: + * + * ecn* = (m_ref / 32) * (ai * n / C + 1 / T_probe) = n + 2 + * + * for n backlogged flows, i.e. a standing queue of 2^((n+2)/4) * U. + * This is the zero-delay fixpoint; feedback delay raises the real + * standing queue above it. + */ + +/* ECE fixed point */ +#define CA_SHFT 5 /* ece fixed point: 32 * ecn */ + +/* Receiver averaging window */ +#define CA_TW (1ULL << 26) /* pricing window ~67 ms */ +#define CA_TW_MIN (4ULL * MILLION) /* pricing window floor 4 ms */ +#define CA_TW_RTT_MUL 2 /* T_w = 2 * layer RTT */ +#define CA_TW_ABSMAX (1ULL << 32) /* window ceiling ~4.3 s */ +/* Quiet horizon, in windows (1 << shift): gap restart and the TTLs. */ +#define CA_TW_GAP_SHFT 2 +#define CA_RX_WBYTES 16000ULL /* 16 pkts x 1000 B a window */ +#define CA_RX_WCLOSE (2 * CA_RX_WBYTES) /* byte-triggered early close */ +#define CA_TW_SM_SHFT 2 /* window EWMA weight 1/4 */ + +/* Congestion marking */ +#define CA_MARK_KNEE 1 /* mark onset (packets) */ + +/* Rate machine */ +#define CA_RATE_MIN (1ULL << 13) /* 8 KiB/s rate floor */ +#define CA_RATE_INIT (1ULL << 16) /* slow start seed 64 KiB/s */ +/* Rate cap; also keeps rate * dt and rate * rise below 2^64. */ +#define CA_RATE_MAX (1ULL << 37) +#define CA_INV_SHFT 32 /* reciprocal-rate fixp */ +#define CA_AI_RATE (1ULL << 17) /* 128 KiB/s^2 additive inc */ +#define CA_PROBE_TC (8ULL * BILLION) /* proportional probe TC 8s */ +#define CA_ECE_REF (16 << CA_SHFT) /* full congestion: ecn 16 */ +/* Decrease saturation, and the level below which the hold clears. */ +#define CA_ECE_MAX (2 * CA_ECE_REF) /* ecn 32 */ +#define CA_MD_KD_DIV 16 /* lead gain 1/16 */ + +/* Control cadence */ +#define CA_DT_CTRL (BILLION / 1000) /* min rate-update spacing */ +#define CA_DT_CAP (BILLION / 20) /* idle-resume Δt clamp 50ms */ +#define CA_IDLE_PKTS 4 /* idle: gap over 4 packets */ +/* Feedback staleness floor; ctx->ece_ttl rides above it by rate. */ +#define CA_ECE_TTL (1ULL << 28) /* ~268 ms */ + +/* Slow start */ +#define CA_SS_RTT_MUL 2 /* ss_tc = 2 * layer RTT */ +#define CA_SS_TC_MIN (BILLION / 1000) /* ramp floor 1 ms */ +#define CA_SS_TC_MAX (4ULL * BILLION) /* ramp ceiling 4 s */ +#define CA_RTT_SHFT 2 /* ss_tc EWMA weight 1/4 */ +#define CA_SS_TC_GRW 1 /* ramp climb cap 2x a sample */ +#define CA_SS_RTT_DEF 200 /* default layer RTT (ms) */ + +/* Heartbeat */ +#define CA_HB_MIN (40 * MILLION) /* heartbeat interval floor */ +#define CA_HB_LOSS 4 /* stale horizons -> restart */ + +/* Path capacity */ +#define CA_CAP_SHFT 5 /* floor = capacity / 32 */ +#define CA_CAP_SM_SHFT 1 /* capacity EWMA weight 1/2 */ +/* Outlives ece_ttl 16x: onset-fresh fcap re-seeds each episode. */ +#define CA_CAP_TTL_SHFT 4 +#define CA_RMIN_MAX (1ULL << 32) /* derived floor ceiling */ + +/* Sender utilisation */ +#define CA_SND_WIN (1ULL << 26) /* sender util window ~67 ms */ +#define CA_USE_NUM 3 /* backlogged: offered >= */ +#define CA_USE_DEN 4 /* 3/4 * window-start rate */ +#define CA_SND_DEC_SHFT 4 /* offered max-filter 1/16 */ +#define CA_SND_DEC_CAP 16 /* bound gapped-close decay */ +#define CA_SND_BYT_MAX (1ULL << 33) /* offered-byte saturation */ +#define CA_PAC_DEN 4 /* backlogged: 1/4 deferred */ + +/* + * Retuning invariants (pinned by the unit tests): + * - (1 << CA_TW_GAP_SHFT) * CA_TW > S * BILLION / CA_RATE_MIN, or + * a floor-rate flow's onset restart-loops (S ~ one MTU; both ns). + * - CA_RX_WBYTES * BILLION / CA_RATE_MIN < CA_TW_ABSMAX: the + * floor-rate window must clear the ceiling. + * - CA_TW < CA_RX_WBYTES * BILLION / CA_RATE_MIN: at the rate + * floor the sample budget, not the horizon, sizes the window. + * - CA_TW << CA_TW_GAP_SHFT <= CA_ECE_TTL: the estimator must + * not call a gap fresh that the sender still counts as live. + * - CA_ECE_TTL > S * BILLION / CA_RATE_MIN: the idle cap clears a + * floor-rate flow's inter-send gap, so pacing never reads as idle. + * - CA_DT_CAP < CA_ECE_TTL: the idle clamp needs the TTL above it, + * or every slow flow reads idle on every send. + * - CA_RATE_MAX * CA_DT_CAP, the folded lead * inv_rate at + * CA_RATE_MIN, and owed * BILLION (owed clamped in mb_ecn_snd) all + * keep the pacer arithmetic below 2^64. + * - CA_RATE_MIN <= CA_RATE_INIT and CA_RMIN_MAX < CA_RATE_MAX. + * - cap_enc(16 * mean) - cap_enc(mean) == CA_ECE_REF >> CA_SHFT: a + * queue of 16 packets is what reads as full congestion. + * - CA_MD_KD_DIV sets the lead gain. The term acts both ways (cut on + * a rise, give back on a fall), which cancels the DC bias a + * one-sided term would rectify into a standing rate difference + * between flows pricing one queue; that is what lets the gain run + * at 1/16 instead of the deadzone below 1/8. + * - T_w = clamp(CA_TW_RTT_MUL * RTT, CA_TW_MIN, CA_TW) scales only + * the receiver pricing window; CA_ECE_TTL, CA_SND_WIN, CA_DT_CAP + * and CA_DT_CTRL are absolute and must not be derived from it. + * - The gap-restart horizon is floored at CA_ECE_TTL, so a + * floor-rate flow's inter-packet gap never reads as an onset. + * - The ai_hold release threshold equals the decrease saturation + * clamp: a standing mark that is a legal equilibrium must be able + * to clear the hold. + * + * Structural invariants (not exercised by the unit tests): + * - CA_MARK_KNEE <= 4: the full decrease range must fit the ring + * (SSM_RBUFF_SIZE, not visible from this file). + * - ecn* = 2 + n holds for n <= 29 (the decrease clamp) and only + * with live capacity feedback. + */ struct mb_ecn_ctx { - uint16_t rx_ece; /* Level of congestion (upstream) */ - size_t rx_ctr; /* Receiver side packet counter */ - - uint16_t tx_ece; /* Level of congestion (downstream) */ - size_t tx_ctr; /* Sender side packet counter */ - size_t tx_wbc; /* Window byte count */ - size_t tx_wpc; /* Window packet count */ - size_t tx_wbl; /* Window byte limit */ - bool tx_cav; /* Congestion avoidance */ - size_t tx_mul; /* Slot size multiplier */ - size_t tx_inc; /* Additive increase */ - size_t tx_slot; + uint16_t rx_ece; /* smoothed congestion echo (32 * ecn) */ + uint64_t rx_acc; /* window integral of ecn * dt */ + uint64_t rx_byt; /* bytes arrived in current window */ + uint64_t rx_ts; /* last packet arrival (ns) */ + uint64_t rx_win; /* window start (ns) */ + uint64_t rx_tw; /* adaptive averaging window (ns) */ + uint8_t rx_cap; /* window bottleneck capacity code */ + + uint16_t tx_ece; /* congestion reported from downstream */ + uint16_t tx_ecp; /* previous tx_ece (rise detection) */ + uint8_t tx_loc; /* local first-hop ecn mark (fallback) */ + bool tx_cav; /* past slow start */ + bool ai_hold; /* freeze AI after loss until clear */ + uint64_t rate; /* paced send rate (bytes/s) */ + uint64_t rate_min; /* capacity-derived rate floor (B/s) */ + uint64_t ai_rate; /* additive-increase slope (B/s^2) */ + uint64_t ece_ttl; /* how long feedback stays valid (ns) */ + uint64_t ss_tc; /* slow-start time constant (ns) */ + uint64_t dec_acc; /* sub-ms decrease time carried (ns) */ + uint64_t inv_rate; /* fixed-point 1/rate for pacing */ + uint64_t vt; /* virtual service clock (bytes) */ + uint64_t lead; /* pacer lead of last send (bytes) */ + uint64_t last_ts; /* last clock advance (ns) */ + uint64_t last_ctrl; /* last rate update (ns) */ + uint64_t last_fb; /* last congestion feedback (ns) */ + uint64_t last_sig; /* last liveness signal, incl. hb (ns) */ + uint64_t n_fb; /* feedback updates received */ + uint64_t n_rtt; /* heartbeat RTT samples folded */ + uint64_t last_hb; /* last heartbeat emitted (ns) */ + uint64_t last_res; /* last resume from idle (ns) */ + uint64_t last_loc; /* last local mark seen (ns) */ + uint64_t last_cap; /* last capacity applied (ns) */ + + uint64_t snd_byt; /* bytes offered this window (capped) */ + size_t snd_flows; /* flows sharing the ctx, >= 1 */ + uint64_t snd_pac; /* bytes the pacer held back this win */ + uint64_t snd_win; /* utilisation window start (ns) */ + uint64_t snd_r0; /* rate at window start */ + uint64_t snd_rate; /* max-filter of offered rate (B/s) */ + bool backlogged; /* offered load keeps the pacer busy */ + bool src_limited; /* rate held at offered-load ceiling */ + bool started; /* a real send has occurred */ + + /* Diagnostics only, read by mb_ecn_print_stats. */ + uint8_t tx_cap; /* path capacity code fed back to us */ + uint64_t n_ctrl; /* control steps taken */ + uint64_t t_ctrl; /* wall time covered by steps (ns) */ + uint64_t t_bank; /* increase time banked in steps (ns) */ + uint64_t n_ttl; /* feedback aged out (TTL) */ + uint64_t n_cap; /* capacity updates applied */ + uint64_t n_loss; /* signal-loss cuts (collapse) */ + uint64_t ss_peak; /* peak rate in slow start (bytes/s) */ }; +/* Layer slow-start time constant (ns), from the declared RTT. */ +static uint64_t mb_ecn_ss_tc = (uint64_t) CA_SS_RTT_MUL * + CA_SS_RTT_DEF * MILLION; + +/* Layer pricing window (ns), from the declared RTT. */ +static uint64_t mb_ecn_tw = CA_TW; + struct ca_ops mb_ecn_ca_ops = { .ctx_create = mb_ecn_ctx_create, .ctx_destroy = mb_ecn_ctx_destroy, .ctx_update_snd = mb_ecn_ctx_update_snd, .ctx_update_rcv = mb_ecn_ctx_update_rcv, .ctx_update_ece = mb_ecn_ctx_update_ece, - .wnd_wait = mb_ecn_wnd_wait, + .ctx_hb_due = mb_ecn_ctx_hb_due, + .ctx_rtt = mb_ecn_ctx_rtt, .calc_ecn = mb_ecn_calc_ecn, + .marks_ecn = true, .print_stats = mb_ecn_print_stats }; +static uint64_t mb_ecn_rate_inv(uint64_t rate) +{ + return ((uint64_t) BILLION << CA_INV_SHFT) / rate; +} + +/* + * Feedback arrives once per receiver window, and the window tracks + * the flow's byte rate. Mirror it: age the signal out only past the + * quiet horizon at the current rate, floored for fast flows. + */ +static uint64_t mb_ecn_ece_ttl(uint64_t rate) +{ + uint64_t ttl; + + ttl = (1 << CA_TW_GAP_SHFT) * CA_RX_WBYTES * BILLION / rate; + + return ttl > (uint64_t) CA_ECE_TTL ? ttl : (uint64_t) CA_ECE_TTL; +} + +/* Derive the layer slow-start slope from the declared RTT (ms). */ +void mb_ecn_init(uint32_t rtt_ms) +{ + uint64_t tc; + uint64_t rtt; + uint64_t tw; + + if (rtt_ms == 0) /* unspecified: safe default */ + rtt_ms = CA_SS_RTT_DEF; + + tc = (uint64_t) CA_SS_RTT_MUL * rtt_ms * MILLION; + if (tc < (uint64_t) CA_SS_TC_MIN) + tc = CA_SS_TC_MIN; + + mb_ecn_ss_tc = tc; + + rtt = (uint64_t) rtt_ms * MILLION; + + tw = (uint64_t) CA_TW_RTT_MUL * rtt; + if (tw < CA_TW_MIN) + tw = CA_TW_MIN; + + if (tw > CA_TW) + tw = CA_TW; + + mb_ecn_tw = tw; +} + void * mb_ecn_ctx_create(void) { struct timespec now; + uint64_t t; struct mb_ecn_ctx * ctx; ctx = malloc(sizeof(*ctx)); @@ -89,10 +327,29 @@ void * mb_ecn_ctx_create(void) memset(ctx, 0, sizeof(*ctx)); - ctx->tx_mul = CA_SLOT; - ctx->tx_wbl = CA_IWL; - ctx->tx_inc = CA_INC; - ctx->tx_slot = ts_to_ns(now) >> ctx->tx_mul; + t = TS_TO_UINT64(now); + + ctx->rate = CA_RATE_INIT; + ctx->rate_min = CA_RATE_MIN; + ctx->ai_rate = CA_AI_RATE; + ctx->ss_tc = mb_ecn_ss_tc; + ctx->ece_ttl = mb_ecn_ece_ttl(CA_RATE_INIT); + ctx->inv_rate = mb_ecn_rate_inv(CA_RATE_INIT); + ctx->rx_ts = t; + ctx->rx_win = t; + ctx->rx_tw = mb_ecn_tw; + ctx->last_ts = t; + ctx->last_ctrl = t; + ctx->last_fb = t; + ctx->last_sig = t; + ctx->last_loc = t; + ctx->last_cap = t; + + /* snd_win/last_ts re-seeded lazily on the first real send. */ + ctx->snd_r0 = CA_RATE_INIT; + ctx->snd_rate = CA_RATE_INIT; + ctx->snd_flows = 1; + ctx->backlogged = true; return (void *) ctx; } @@ -102,158 +359,670 @@ void mb_ecn_ctx_destroy(void * ctx) free(ctx); } -#define _slot_after(new, old) ((int64_t) (old - new) < 0) +/* Local first-hop mark exits slow start and covers dead feedback. */ +static void mb_ecn_loc(struct mb_ecn_ctx * ctx, + uint8_t lecn, + uint64_t t) +{ + if (lecn == 0) + return; + + ctx->tx_loc = lecn; + ctx->tx_cav = true; + ctx->last_loc = t; +} + +/* Slow start: ramp only while backlogged. */ +static void mb_ecn_slow_start(struct mb_ecn_ctx * ctx, + uint64_t dta) +{ + if (ctx->backlogged) + ctx->rate += ctx->rate * dta / ctx->ss_tc; +} + +/* Additive increase plus a rate-independent proportional probe. */ +static void mb_ecn_increase(struct mb_ecn_ctx * ctx, + uint64_t dta) +{ + if (!ctx->backlogged) + return; + + /* After a loss, hold until a clean signal drains the queue. */ + if (ctx->ai_hold) + return; + + ctx->rate += ctx->ai_rate * dta / BILLION; + ctx->rate += ctx->rate * dta / CA_PROBE_TC; +} + +/* + * Multiplicative decrease: cut proportional to mark x elapsed time, + * plus a lead term on the mark's step, clamped and acting both ways. + */ +static void mb_ecn_decrease(struct mb_ecn_ctx * ctx, + uint64_t dtc) +{ + uint64_t dtm; + uint64_t mark; + uint64_t step; + uint64_t lead; + uint64_t cut; + uint16_t m; + bool up; + + m = ctx->tx_ece > 0 ? ctx->tx_ece + : (uint16_t) (ctx->tx_loc << CA_SHFT); + if (m == 0) { + ctx->dec_acc = 0; /* unmarked time is not banked */ + ctx->tx_ecp = 0; + return; + } + + mark = MIN(m, CA_ECE_MAX); + + /* Lead on the mark step; the clamp bounds it to rate/KD. */ + up = m > ctx->tx_ecp; + step = up ? m - ctx->tx_ecp : ctx->tx_ecp - m; + step = MIN(step, CA_ECE_REF); + lead = ctx->rate * step / (CA_ECE_REF * CA_MD_KD_DIV); + + cut = up ? lead : 0; + + /* + * Bank the remainder: at a 1 ms control cadence, truncating + * to whole milliseconds would drop up to half of every cut. + */ + ctx->dec_acc += dtc; + dtm = ctx->dec_acc / MILLION; + ctx->dec_acc -= dtm * MILLION; + if (mark * dtm >= CA_ECE_REF * 500) + cut += ctx->rate / 2; + else + cut += ctx->rate * mark * dtm / (CA_ECE_REF * 1000); + + if (cut > ctx->rate / 2) + cut = ctx->rate / 2; + + ctx->rate -= cut; + + if (!up) + ctx->rate += lead; + + ctx->tx_ecp = m; +} + +/* Offered-load ceiling backstop while source-limited. */ +static void mb_ecn_ceiling(struct mb_ecn_ctx * ctx) +{ + uint64_t hi; + + if (ctx->backlogged) { + ctx->src_limited = false; + return; + } + + /* Land on the backlog level; a ceiling above it never clears. */ + hi = ctx->snd_rate > CA_RATE_MAX / CA_USE_DEN * CA_USE_NUM + ? (uint64_t) CA_RATE_MAX + : ctx->snd_rate * CA_USE_DEN / CA_USE_NUM; + if (hi < CA_RATE_MIN) + hi = CA_RATE_MIN; + + ctx->src_limited = ctx->rate > hi; + if (ctx->src_limited) + ctx->rate = hi; +} + +static void mb_ecn_ctrl(struct mb_ecn_ctx * ctx, + uint64_t dtc) +{ + uint64_t dta; + uint64_t lo; + + /* AI and slow start bank at most CA_DT_CAP of idle time. */ + dta = MIN(dtc, (uint64_t) CA_DT_CAP); + + ctx->n_ctrl++; + ctx->t_ctrl += dtc; + ctx->t_bank += dta; + + if (ctx->tx_cav) { + mb_ecn_increase(ctx, dta); + mb_ecn_decrease(ctx, dtc); + } else { + mb_ecn_slow_start(ctx, dta); + } + + mb_ecn_ceiling(ctx); + + /* Capacity floor only while backlogged; else the absolute floor. */ + lo = ctx->backlogged ? ctx->rate_min : (uint64_t) CA_RATE_MIN; + if (ctx->rate < lo) + ctx->rate = lo; + + if (ctx->rate > CA_RATE_MAX) + ctx->rate = CA_RATE_MAX; + + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->ece_ttl = mb_ecn_ece_ttl(ctx->rate); + + if (!ctx->tx_cav && ctx->rate > ctx->ss_peak) + ctx->ss_peak = ctx->rate; +} + +/* Fold offered into the max filter: rise at once, decay 1/16 per window. */ +static void mb_ecn_offered(struct mb_ecn_ctx * ctx, + uint64_t offered, + uint64_t elapsed) +{ + uint64_t n; + + if (offered >= ctx->snd_rate) { + ctx->snd_rate = offered; + return; + } + + n = MIN(elapsed / CA_SND_WIN, CA_SND_DEC_CAP); + while (n-- > 0 && ctx->snd_rate > offered) + ctx->snd_rate -= (ctx->snd_rate - offered) >> CA_SND_DEC_SHFT; +} + +/* Open a fresh utilisation window at t. */ +static void mb_ecn_win_open(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + ctx->snd_win = t; + ctx->snd_byt = 0; + ctx->snd_pac = 0; + ctx->snd_r0 = ctx->rate; +} + +/* + * Note the flow count; a window spanning two populations measures + * neither, so a change opens a fresh one. + */ +static void mb_ecn_flows(struct mb_ecn_ctx * ctx, + size_t flows, + uint64_t t) +{ + size_t n = flows > 0 ? flows : 1; + + if (n == ctx->snd_flows) + return; + + ctx->snd_flows = n; + + mb_ecn_win_open(ctx, t); +} + +/* + * Close the utilisation window: set backlogged from the level test, + * fold offered into the max filter, then reset the window. + */ +static void mb_ecn_win(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + uint64_t elapsed = t - ctx->snd_win; + uint64_t offered; + bool was = ctx->backlogged; + + /* + * snd_byt is the whole ctx's offered bytes but rate is what one + * flow may send, so share it out before either is compared. + */ + offered = ctx->snd_byt * BILLION / elapsed / ctx->snd_flows; + + /* + * Offered load is counted past the pacer, so it cannot tell a + * quiet source from one the pacer is holding back, and idle + * flows on the context drag it down. A window the pacer had to + * defer is rate-limited whatever the bytes say. + */ + ctx->backlogged = offered * CA_USE_DEN >= ctx->snd_r0 * CA_USE_NUM + || ctx->snd_pac * CA_PAC_DEN >= ctx->snd_byt; + + if (!was && ctx->backlogged) /* resume: fresh liveness baseline */ + ctx->last_res = t; + + mb_ecn_offered(ctx, offered, elapsed); + + if (ctx->backlogged) + ctx->src_limited = false; + + mb_ecn_win_open(ctx, t); +} + +/* Age out congestion, local-mark and capacity signals once stale. */ +/* Heartbeat interval: ~1 RTT, floored so fast links don't over-probe. */ +static uint64_t mb_ecn_t_hb(const struct mb_ecn_ctx * ctx) +{ + uint64_t t = ctx->ss_tc >> 1; + + return t > (uint64_t) CA_HB_MIN ? t : CA_HB_MIN; +} + +/* Feedback collapsed while backlogged: halve like an RTO, stay in AIMD. */ +static void mb_ecn_loss(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + ctx->rate -= ctx->rate / 2; + if (ctx->rate < (uint64_t) CA_RATE_MIN) + ctx->rate = CA_RATE_MIN; + + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->ece_ttl = mb_ecn_ece_ttl(ctx->rate); + ctx->last_sig = t; + ctx->ai_hold = true; + ctx->n_loss++; +} + +static void mb_ecn_age(struct mb_ecn_ctx * ctx, + uint64_t t) +{ + uint64_t ttl = ctx->ece_ttl; + uint64_t ref = ctx->last_sig > ctx->last_res + ? ctx->last_sig : ctx->last_res; + uint64_t gap = t - ref; + + /* + * Sustained silence while backlogged is feedback collapse: cut + * the rate in half and stay in AIMD, so a recovering flow climbs + * back additively instead of re-ramping. Repeated silence decays + * it geometrically toward the floor. + */ + if (ctx->backlogged && ctx->n_fb + ctx->n_rtt > 0 + && gap > (uint64_t) CA_HB_LOSS * ttl) { + mb_ecn_loss(ctx, t); + return; + } + + if (t - ctx->last_fb > ctx->ece_ttl) { + if (ctx->tx_ece > 0) + ctx->n_ttl++; + ctx->tx_ece = 0; + } + + if (t - ctx->last_loc > ctx->ece_ttl) + ctx->tx_loc = 0; + + /* Stale capacity: fall back to the compile-time defaults. */ + if (t - ctx->last_cap > ctx->ece_ttl << CA_CAP_TTL_SHFT) { + ctx->rate_min = CA_RATE_MIN; + ctx->ai_rate = CA_AI_RATE; + ctx->tx_cap = 0; + } +} + +/* Advance the virtual clock; a gap past CA_DT_CAP credits a burst. */ +static void mb_ecn_advance(struct mb_ecn_ctx * ctx, + uint64_t dt, + size_t len, + uint64_t ftag) +{ + uint64_t burst; + uint64_t owed; + + if (dt <= (uint64_t) CA_DT_CAP) { + ctx->vt += ctx->rate * dt / BILLION; + return; + } + + burst = ctx->rate * CA_DT_CAP / BILLION; + if (burst < (uint64_t) len) + burst = len; -ca_wnd_t mb_ecn_ctx_update_snd(void * _ctx, - size_t len) + owed = ftag > ctx->vt ? ftag - ctx->vt + burst : burst; + + /* Clamp so owed * BILLION cannot wrap (2^33 B backlog). */ + if (owed > (1ULL << 33)) + owed = 1ULL << 33; + + if (dt >= owed * BILLION / ctx->rate) + ctx->vt += owed; + else + ctx->vt += ctx->rate * dt / BILLION; +} + +static time_t mb_ecn_snd(struct mb_ecn_ctx * ctx, + size_t len, + uint64_t t, + uint64_t * ftag) +{ + uint64_t dt; + uint64_t dtc; + uint64_t idle; + uint64_t s; + + /* Lazy warm-up seed: packet #1 is never an idle resume. */ + if (!ctx->started) { + ctx->started = true; + ctx->last_ts = t; + ctx->last_res = t; + ctx->snd_win = t; + ctx->snd_r0 = ctx->rate; + } + + dt = t - ctx->last_ts; + ctx->last_ts = t; + + /* + * Idle gap clears backlog before aging: no false loss on resume. + * Measured against the pacer's own spacing, so a flow paced + * slower than CA_DT_CAP per packet does not read as idle on + * every send, and bounded by the staleness horizon. + */ + idle = CA_IDLE_PKTS * len * BILLION / ctx->rate; + idle = MAX(idle, (uint64_t) CA_DT_CAP); + idle = MIN(idle, (uint64_t) CA_ECE_TTL); + if (dt > idle) + ctx->backlogged = false; + + mb_ecn_age(ctx, t); + + /* Offered-load estimator: accumulate, gate growth, size ceiling. */ + ctx->snd_byt += len; + if (ctx->snd_byt > (uint64_t) CA_SND_BYT_MAX) + ctx->snd_byt = CA_SND_BYT_MAX; + + if (t - ctx->snd_win >= (uint64_t) CA_SND_WIN) + mb_ecn_win(ctx, t); + + /* Rate update before the vt advance: burst uses the clamped rate. */ + dtc = t - ctx->last_ctrl; + if (dtc >= (uint64_t) CA_DT_CTRL) { + ctx->last_ctrl = t; + mb_ecn_ctrl(ctx, dtc); + } + + mb_ecn_advance(ctx, dt, len, *ftag); + + /* SFQ start tag: behind the clock starts now, ahead waits. */ + s = *ftag > ctx->vt ? *ftag : ctx->vt; + *ftag = s + len; + + if (s > ctx->vt) + ctx->snd_pac += len; + + ctx->lead = s - ctx->vt; + + /* Reciprocal pacing; folded so any lead * rate stays in range. */ + if (s > ctx->vt) + return (time_t) ((ctx->lead * (ctx->inv_rate >> 16)) + >> (CA_INV_SHFT - 16)); + + return 0; +} + +time_t mb_ecn_ctx_update_snd(void * _ctx, + size_t len, + uint8_t lecn, + size_t flows, + uint64_t * ftag) { struct timespec now; - size_t slot; - ca_wnd_t wnd; + uint64_t t; struct mb_ecn_ctx * ctx = _ctx; clock_gettime(PTHREAD_COND_CLOCK, &now); - slot = ts_to_ns(now) >> ctx->tx_mul; + t = TS_TO_UINT64(now); - ctx->tx_ctr++; - ctx->tx_wpc++; - ctx->tx_wbc += len; + mb_ecn_flows(ctx, flows, t); - if (ctx->tx_ctr > CA_WND) - ctx->tx_ece = 0; + mb_ecn_loc(ctx, lecn, t); - if (_slot_after(slot, ctx->tx_slot)) { - bool carry = false; /* may carry over if window increases */ + return mb_ecn_snd(ctx, len, t, ftag); +} - ctx->tx_slot = slot; +/* Estimator idle, or a quiet gap past the horizon: restart fresh. */ +static bool mb_ecn_rcv_fresh(const struct mb_ecn_ctx * ctx, + uint64_t dt) +{ + uint64_t gap; - if (!ctx->tx_cav) { /* Slow start */ - if (ctx->tx_wbc > ctx->tx_wbl) - ctx->tx_wbl <<= 1; - } else { - if (ctx->tx_ece) /* Mult. Decrease */ - ctx->tx_wbl -= (ctx->tx_wbl * ctx->tx_ece) - >> (CA_SHFT + 8); - else /* Add. Increase */ - ctx->tx_wbl = ctx->tx_wbc + ctx->tx_inc; - } + if (ctx->rx_ece == 0 && ctx->rx_acc == 0) + return true; - /* Window scaling */ - if (ctx->tx_wpc < CA_MINPS) { - size_t fact = 0; /* factor to scale the window up */ - size_t pkts = ctx->tx_wpc; - while (pkts < CA_MINPS) { - pkts <<= 1; - fact++; - } - ctx->tx_mul += fact; - ctx->tx_slot >>= fact; - if ((ctx->tx_slot & ((1 << fact) - 1)) == 0) { - carry = true; - ctx->tx_slot += 1; - } - ctx->tx_wbl <<= fact; - ctx->tx_inc <<= fact; - } else if (ctx->tx_wpc > CA_MAXPS) { - size_t fact = 0; /* factor to scale the window down */ - size_t pkts = ctx->tx_wpc; - while (pkts > CA_MAXPS) { - pkts >>= 1; - fact++; - } - ctx->tx_mul -= fact; - ctx->tx_slot <<= fact; - ctx->tx_wbl >>= fact; - ctx->tx_inc >>= fact; - } else { - ctx->tx_slot = slot; - } + gap = ctx->rx_tw << CA_TW_GAP_SHFT; - if (!carry) { - ctx->tx_wbc = 0; - ctx->tx_wpc = 0; - } - } + return dt > MAX(gap, (uint64_t) CA_ECE_TTL); +} - if (ctx->tx_wbc > ctx->tx_wbl) - wnd.wait = ((ctx->tx_slot + 1) << ctx->tx_mul) - ts_to_ns(now); +/* + * Size the next averaging window to ~16 packets at this rate, floored + * at the price horizon: a flow fast enough to fill the horizon + * integrates over CA_TW, a slower one stretches for its samples. + */ +static void mb_ecn_resize(struct mb_ecn_ctx * ctx, + uint64_t win) +{ + uint64_t tw = CA_RX_WBYTES * win / ctx->rx_byt; + + if (tw > ctx->rx_tw) + ctx->rx_tw += (tw - ctx->rx_tw) >> CA_TW_SM_SHFT; else - wnd.wait = 0; + ctx->rx_tw -= (ctx->rx_tw - tw) >> CA_TW_SM_SHFT; - return wnd; + if (ctx->rx_tw < mb_ecn_tw) + ctx->rx_tw = mb_ecn_tw; + + if (ctx->rx_tw > CA_TW_ABSMAX) + ctx->rx_tw = CA_TW_ABSMAX; } -void mb_ecn_wnd_wait(ca_wnd_t wnd) +static bool mb_ecn_rcv(struct mb_ecn_ctx * ctx, + size_t len, + uint8_t ecn, + uint8_t cap, + uint16_t * ece, + uint8_t * fcap, + uint64_t t) { - if (wnd.wait > 0) { - struct timespec s = TIMESPEC_INIT_S(0); - if (wnd.wait > BILLION) /* Don't care throttling < 1s */ - s.tv_sec = 1; - else - s.tv_nsec = wnd.wait; + uint64_t dt; + uint64_t win; + + dt = t - ctx->rx_ts; + ctx->rx_ts = t; - nanosleep(&s, NULL); + if (ctx->rx_ece == 0 && ctx->rx_acc == 0 && ecn == 0) + return false; + + /* Onset, or ~4 windows of silence: emit fresh, undiluted. */ + if (mb_ecn_rcv_fresh(ctx, dt)) { + ctx->rx_win = t; + ctx->rx_acc = 0; + ctx->rx_byt = len; + ctx->rx_cap = cap; /* fresh, seeds the new window */ + ctx->rx_ece = (uint16_t) (ecn << CA_SHFT); + *ece = ctx->rx_ece; + *fcap = ctx->rx_cap; + return true; + } + + /* Dwell clamp: one packet weighs at most one window of mark. */ + ctx->rx_acc += ecn * MIN(dt, ctx->rx_tw); + ctx->rx_byt += len; + + ctx->rx_cap = cap_min(ctx->rx_cap, cap); + win = t - ctx->rx_win; + if (win < ctx->rx_tw) { + /* Early close once 2x target bytes arrive (speed-up). */ + if (ctx->rx_byt < CA_RX_WCLOSE || win < mb_ecn_tw) { + *ece = ctx->rx_ece; + return false; + } } + + /* Time-integral mean over the actual window elapsed (never rx_tw). */ + ctx->rx_ece = (uint16_t) ((ctx->rx_acc << CA_SHFT) / win); + + if (ctx->rx_byt > 0) + mb_ecn_resize(ctx, win); + + *fcap = ctx->rx_cap; + + ctx->rx_win = t; + ctx->rx_acc = 0; + ctx->rx_byt = 0; + ctx->rx_cap = 0; /* the next window starts unknown */ + + *ece = ctx->rx_ece; + + return true; } bool mb_ecn_ctx_update_rcv(void * _ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) +{ + struct timespec now; + struct mb_ecn_ctx * ctx = _ctx; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + return mb_ecn_rcv(ctx, len, ecn, cap, ece, fcap, TS_TO_UINT64(now)); +} + +static void mb_ecn_ece(struct mb_ecn_ctx * ctx, + uint16_t ece, + uint8_t cap, + uint64_t t) { - struct mb_ecn_ctx* ctx = _ctx; - bool update; + uint64_t tgt; - (void) len; + ctx->tx_ece = ece; + ctx->tx_cav = true; /* closed-loop feedback: leave slow start */ - if ((ctx->rx_ece | ecn) == 0) - return false; + /* An unsaturated signal means the queue drained: resume. */ + if (ece < (uint16_t) CA_ECE_MAX) + ctx->ai_hold = false; - if (ecn == 0) { /* End of congestion */ - ctx->rx_ece >>= 2; - update = ctx->rx_ece == 0; - } else { - if (ctx->rx_ece == 0) { /* Start of congestion */ - ctx->rx_ece = ecn; - ctx->rx_ctr = 0; - update = true; - } else { /* Congestion update */ - ctx->rx_ece -= ctx->rx_ece >> CA_SHFT; - ctx->rx_ece += ecn; - update = (ctx->rx_ctr++ & (CA_UPD - 1)) == true; - } + ctx->last_fb = t; + ctx->last_sig = t; + ctx->n_fb++; + + /* Scale the floor and AI slope to the path bottleneck. */ + if (cap != 0) { + tgt = cap_dec(cap) >> CA_CAP_SHFT; + if (tgt < CA_RATE_MIN) + tgt = CA_RATE_MIN; + + if (tgt > CA_RMIN_MAX) + tgt = CA_RMIN_MAX; + + if (tgt > ctx->rate_min) + ctx->rate_min += (tgt - ctx->rate_min) + >> CA_CAP_SM_SHFT; + else + ctx->rate_min -= (ctx->rate_min - tgt) + >> CA_CAP_SM_SHFT; + + ctx->ai_rate = 2 * ctx->rate_min; + ctx->tx_cap = cap; + ctx->last_cap = t; + ctx->n_cap++; } - *ece = ctx->rx_ece; + /* Control from the feedback path: a starved sender recovers. */ + if (t - ctx->last_ctrl < (uint64_t) CA_DT_CTRL) + return; - return update; -} + mb_ecn_ctrl(ctx, t - ctx->last_ctrl); + ctx->last_ctrl = t; +} void mb_ecn_ctx_update_ece(void * _ctx, - uint16_t ece) + uint16_t ece, + uint8_t cap) { - struct mb_ecn_ctx* ctx = _ctx; + struct timespec now; + struct mb_ecn_ctx * ctx = _ctx; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + mb_ecn_ece(ctx, ece, cap, TS_TO_UINT64(now)); +} + +/* Due when the path stayed quiet for a heartbeat interval; arms the gap. */ +bool mb_ecn_ctx_hb_due(void * _ctx, + uint64_t now) +{ + struct mb_ecn_ctx * ctx = _ctx; + uint64_t t_hb = mb_ecn_t_hb(ctx); + uint64_t last; - ctx->tx_ece = ece; - ctx->tx_ctr = 0; - ctx->tx_cav = true; + last = ctx->last_sig > ctx->last_hb ? ctx->last_sig : ctx->last_hb; + if (now - last < t_hb) + return false; + + ctx->last_hb = now; + + return true; } -int mb_ecn_calc_ecn(int fd, +/* Fold a heartbeat RTT sample into the ramp clock; also counts as life. */ +void mb_ecn_ctx_rtt(void * _ctx, + uint64_t now, + uint64_t rtt) +{ + struct mb_ecn_ctx * ctx = _ctx; + uint64_t tgt; + + tgt = (uint64_t) CA_SS_RTT_MUL * rtt; + if (tgt < (uint64_t) CA_SS_TC_MIN) /* track the true RTT both */ + tgt = CA_SS_TC_MIN; /* ways: overshoot ~e^{1/2} */ + + if (tgt > (uint64_t) CA_SS_TC_MAX) /* at the real RTT, not the */ + tgt = CA_SS_TC_MAX; /* declared worst case */ + + /* + * A control packet stuck behind a stalled reader returns an RTT + * worth seconds on a path worth milliseconds. Cap how far one + * sample carries the ramp, so a stall costs a step and a rise + * that holds still arrives within a few samples. + */ + if (tgt > ctx->ss_tc << CA_SS_TC_GRW) + tgt = ctx->ss_tc << CA_SS_TC_GRW; + + ctx->ss_tc += (tgt >> CA_RTT_SHFT) - (ctx->ss_tc >> CA_RTT_SHFT); + + ctx->last_sig = now; /* liveness only: never ages the ece signal */ + ctx->n_rtt++; +} + +int mb_ecn_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len) + size_t mean) { - size_t q; + uint64_t u; + int q; + uint8_t mark; - (void) len; (void) qc; - q = ipcp_flow_queued(fd); + if (queued == 0 || mean == 0) + return 0; + + u = (uint64_t) CA_MARK_KNEE * mean; + + /* + * Difference of two quarter-log2 codes is a log-scale ratio: + * the same queue in units of U marks the same on any link. + */ + q = (int) cap_enc(queued) - (int) cap_enc(u); + if (q <= 0) + return 0; - *ecn |= (uint8_t) (q >> ECN_Q_SHFT); + /* Saturate: a deeper queue must not wrap to a low mark. */ + mark = q > 255 ? (uint8_t) 255 : (uint8_t) q; + + if (mark > *ecn) + *ecn = mark; return 0; } @@ -262,35 +1031,71 @@ ssize_t mb_ecn_print_stats(void * _ctx, char * buf, size_t len) { - struct mb_ecn_ctx* ctx = _ctx; - char * regime; + struct mb_ecn_ctx * ctx = _ctx; + char * regime; + uint64_t rate; + uint64_t peak; + int code; + uint16_t m; - if (len < 1024) + if (len < CA_STATS_STRLEN) return 0; - if (!ctx->tx_cav) + /* No signal seen: the rate is unconstrained drift, not a target. */ + rate = ctx->tx_cav ? ctx->rate : 0; + peak = ctx->tx_cav ? ctx->ss_peak : 0; + + /* Match the controller: MD fires on m, incl. the local fallback. */ + m = ctx->tx_ece > 0 ? ctx->tx_ece + : (uint16_t) (ctx->tx_loc << CA_SHFT); + + if (!ctx->tx_cav) { regime = "Slow start"; - else if (ctx->tx_ece) - regime = "Multiplicative dec"; - else + code = 0; + } else if (ctx->ai_hold) { + regime = "Loss recovery"; + code = 4; + } else if (ctx->src_limited) { + regime = "Source limited"; + code = 3; + } else if (m > 0) { + regime = "Proportional dec"; + code = 2; + } else { regime = "Additive inc"; + code = 1; + } sprintf(buf, "Congestion avoidance algorithm: %20s\n" "Upstream congestion level: %20u\n" - "Upstream packet counter: %20zu\n" "Downstream congestion level: %20u\n" - "Downstream packet counter: %20zu\n" - "Congestion window size (ns): %20" PRIu64 "\n" - "Packets in this window: %20zu\n" - "Bytes in this window: %20zu\n" - "Max bytes in this window: %20zu\n" - "Current congestion regime: %20s\n", + "Paced rate (bytes/s): %20" PRIu64 "\n" + "Pacer lead (bytes): %20" PRIu64 "\n" + "Congestion regime (code): %20d\n" + "Current congestion regime: %20s\n" + "Control steps (count): %20" PRIu64 "\n" + "Control time elapsed (ns): %20" PRIu64 "\n" + "Control time banked (ns): %20" PRIu64 "\n" + "Feedback updates (count): %20" PRIu64 "\n" + "Feedback timeouts (count): %20" PRIu64 "\n" + "Path capacity (bytes/s): %20" PRIu64 "\n" + "Capacity rate floor (bytes/s): %20" PRIu64 "\n" + "Capacity updates (count): %20" PRIu64 "\n" + "Slow start peak rate (bytes/s): %20" PRIu64 "\n" + "Signal-loss cuts (count): %20" PRIu64 "\n" + "Heartbeat RTT samples (count): %20" PRIu64 "\n" + "Ramp time constant (ns): %20" PRIu64 "\n", "Multi-bit ECN", - ctx->tx_ece, ctx->tx_ctr, - ctx->rx_ece, ctx->rx_ctr, (uint64_t) (1ULL << ctx->tx_mul), - ctx->tx_wpc, ctx->tx_wbc, ctx->tx_wbl, - regime); + ctx->tx_ece, + ctx->rx_ece, + rate, ctx->lead, code, + regime, + ctx->n_ctrl, ctx->t_ctrl, ctx->t_bank, + ctx->n_fb, ctx->n_ttl, + cap_dec(ctx->tx_cap), ctx->rate_min, ctx->n_cap, + peak, + ctx->n_loss, ctx->n_rtt, ctx->ss_tc); return strlen(buf); } diff --git a/src/ipcpd/unicast/ca/mb-ecn.h b/src/ipcpd/unicast/ca/mb-ecn.h index 1be27764..08bb542d 100644 --- a/src/ipcpd/unicast/ca/mb-ecn.h +++ b/src/ipcpd/unicast/ca/mb-ecn.h @@ -25,27 +25,40 @@ #include "ops.h" +void mb_ecn_init(uint32_t rtt_ms); + void * mb_ecn_ctx_create(void); void mb_ecn_ctx_destroy(void * ctx); -ca_wnd_t mb_ecn_ctx_update_snd(void * ctx, - size_t len); +time_t mb_ecn_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + size_t flows, + uint64_t * ftag); bool mb_ecn_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void mb_ecn_ctx_update_ece(void * ctx, - uint16_t ece); + uint16_t ece, + uint8_t cap); + +bool mb_ecn_ctx_hb_due(void * ctx, + uint64_t now); -void mb_ecn_wnd_wait(ca_wnd_t wnd); +void mb_ecn_ctx_rtt(void * ctx, + uint64_t now, + uint64_t rtt); -int mb_ecn_calc_ecn(int fd, +int mb_ecn_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len); + size_t mean); ssize_t mb_ecn_print_stats(void * ctx, char * buf, diff --git a/src/ipcpd/unicast/ca/nop.c b/src/ipcpd/unicast/ca/nop.c index e5cacf66..7a2f72db 100644 --- a/src/ipcpd/unicast/ca/nop.c +++ b/src/ipcpd/unicast/ca/nop.c @@ -30,8 +30,8 @@ struct ca_ops nop_ca_ops = { .ctx_update_snd = nop_ctx_update_snd, .ctx_update_rcv = nop_ctx_update_rcv, .ctx_update_ece = nop_ctx_update_ece, - .wnd_wait = nop_wnd_wait, .calc_ecn = nop_calc_ecn, + .marks_ecn = false, .print_stats = NULL }; @@ -45,52 +45,55 @@ void nop_ctx_destroy(void * ctx) (void) ctx; } -ca_wnd_t nop_ctx_update_snd(void * ctx, - size_t len) +time_t nop_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + size_t flows, + uint64_t * ftag) { - ca_wnd_t wnd; - (void) ctx; (void) len; + (void) lecn; + (void) flows; + (void) ftag; - memset(&wnd, 0, sizeof(wnd)); - - return wnd; -} - -void nop_wnd_wait(ca_wnd_t wnd) -{ - (void) wnd; + return 0; } bool nop_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece) + uint8_t cap, + uint16_t * ece, + uint8_t * fcap) { (void) ctx; (void) len; (void) ecn; + (void) cap; (void) ece; + (void) fcap; return false; } void nop_ctx_update_ece(void * ctx, - uint16_t ece) + uint16_t ece, + uint8_t cap) { (void) ctx; (void) ece; + (void) cap; } -int nop_calc_ecn(int fd, +int nop_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len) + size_t mean) { - (void) fd; - (void) len; + (void) queued; + (void) mean; (void) ecn; (void) qc; diff --git a/src/ipcpd/unicast/ca/nop.h b/src/ipcpd/unicast/ca/nop.h index 8b892e61..386a5310 100644 --- a/src/ipcpd/unicast/ca/nop.h +++ b/src/ipcpd/unicast/ca/nop.h @@ -29,23 +29,27 @@ void * nop_ctx_create(void); void nop_ctx_destroy(void * ctx); -ca_wnd_t nop_ctx_update_snd(void * ctx, - size_t len); +time_t nop_ctx_update_snd(void * ctx, + size_t len, + uint8_t lecn, + size_t flows, + uint64_t * ftag); bool nop_ctx_update_rcv(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void nop_ctx_update_ece(void * ctx, - uint16_t ece); - -void nop_wnd_wait(ca_wnd_t wnd); + uint16_t ece, + uint8_t cap); -int nop_calc_ecn(int fd, +int nop_calc_ecn(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len); + size_t mean); extern struct ca_ops nop_ca_ops; diff --git a/src/ipcpd/unicast/ca/ops.h b/src/ipcpd/unicast/ca/ops.h index 6d2ddf1d..835fe0c5 100644 --- a/src/ipcpd/unicast/ca/ops.h +++ b/src/ipcpd/unicast/ca/ops.h @@ -30,23 +30,39 @@ struct ca_ops { void (* ctx_destroy)(void * ctx); - ca_wnd_t (* ctx_update_snd)(void * ctx, - size_t len); + time_t (* ctx_update_snd)(void * ctx, + size_t len, + uint8_t lecn, + size_t flows, + uint64_t * ftag); bool (* ctx_update_rcv)(void * ctx, size_t len, uint8_t ecn, - uint16_t * ece); + uint8_t cap, + uint16_t * ece, + uint8_t * fcap); void (* ctx_update_ece)(void * ctx, - uint16_t ece); + uint16_t ece, + uint8_t cap); + + /* Optional, can be NULL: heartbeat pacing + RTT feedback. */ + bool (* ctx_hb_due)(void * ctx, + uint64_t now); - void (* wnd_wait)(ca_wnd_t wnd); + void (* ctx_rtt)(void * ctx, + uint64_t now, + uint64_t rtt); - int (* calc_ecn)(int fd, + /* queued and mean are bytes; their ratio is packets. */ + int (* calc_ecn)(size_t queued, uint8_t * ecn, qoscube_t qc, - size_t len); + size_t mean); + + /* True if calc_ecn inspects the queue; gates the lookup. */ + bool marks_ecn; /* Optional, can be NULL */ ssize_t (* print_stats)(void * ctx, diff --git a/src/ipcpd/unicast/ca/tests/CMakeLists.txt b/src/ipcpd/unicast/ca/tests/CMakeLists.txt new file mode 100644 index 00000000..20e2349d --- /dev/null +++ b/src/ipcpd/unicast/ca/tests/CMakeLists.txt @@ -0,0 +1,78 @@ +get_filename_component(CURRENT_SOURCE_PARENT_DIR + ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +get_filename_component(CURRENT_BINARY_PARENT_DIR + ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY) + +get_filename_component(UNICAST_SOURCE_DIR ${CURRENT_SOURCE_PARENT_DIR} DIRECTORY) +get_filename_component(UNICAST_BINARY_DIR ${CURRENT_BINARY_PARENT_DIR} DIRECTORY) + +get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +get_filename_component(PARENT_DIR ${PARENT_PATH} NAME) + +compute_test_prefix() + +create_test_sourcelist(${PARENT_DIR}_tests test_suite.c + # Add new tests here + mb_ecn_test.c + ca_test.c + ) + +add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests} + ${UNICAST_SOURCE_DIR}/ca.c + ${UNICAST_SOURCE_DIR}/cap.c + ${CURRENT_SOURCE_PARENT_DIR}/nop.c + ) + +target_include_directories(${PARENT_DIR}_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CURRENT_SOURCE_PARENT_DIR} + ${CURRENT_BINARY_PARENT_DIR} + ${UNICAST_SOURCE_DIR} + ${UNICAST_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${CMAKE_SOURCE_DIR}/src/ipcpd + ${CMAKE_BINARY_DIR}/src/ipcpd +) + +disable_test_logging_for_target(${PARENT_DIR}_test) +target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common) + +add_dependencies(build_tests ${PARENT_DIR}_test) + +ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests}) + +# The lab includes mb-ecn.c for its statics, so it needs its own binary +create_test_sourcelist(${PARENT_DIR}_lab_tests test_lab_suite.c + mb_ecn_lab_test.c + ) + +add_executable(${PARENT_DIR}_lab_test ${${PARENT_DIR}_lab_tests} + ${UNICAST_SOURCE_DIR}/cap.c + ) + +target_include_directories(${PARENT_DIR}_lab_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CURRENT_SOURCE_PARENT_DIR} + ${CURRENT_BINARY_PARENT_DIR} + ${UNICAST_SOURCE_DIR} + ${UNICAST_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${CMAKE_SOURCE_DIR}/src/ipcpd + ${CMAKE_BINARY_DIR}/src/ipcpd +) + +disable_test_logging_for_target(${PARENT_DIR}_lab_test) +target_link_libraries(${PARENT_DIR}_lab_test PRIVATE ouroboros-common) + +if(MB_ECN_LAB_FULL) + target_compile_definitions(${PARENT_DIR}_lab_test PRIVATE MB_ECN_LAB_FULL) +endif() + +add_dependencies(build_tests ${PARENT_DIR}_lab_test) + +ouroboros_register_tests(TARGET ${PARENT_DIR}_lab_test + TESTS ${${PARENT_DIR}_lab_tests}) diff --git a/src/ipcpd/unicast/ca/tests/ca_test.c b/src/ipcpd/unicast/ca/tests/ca_test.c new file mode 100644 index 00000000..1b86eab8 --- /dev/null +++ b/src/ipcpd/unicast/ca/tests/ca_test.c @@ -0,0 +1,392 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Unit tests for the congestion-avoidance interface + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#include "config.h" + +#include "ca.h" + +#include <test/test.h> + +#define ADDR_A 0x1111ULL +#define ADDR_B 0x2222ULL + +static const struct { + enum pol_cong_avoid pol; + const char * name; +} ca_pols[] = { + { CA_NONE, "none" }, + { CA_MB_ECN, "mb-ecn" } +}; + +#define CA_POLS (sizeof(ca_pols) / sizeof(ca_pols[0])) + +static int test_ca_init_fini(enum pol_cong_avoid pol, + const char * name) +{ + TEST_START("(%s)", name); + + if (ca_init(pol, 100) < 0) { + printf("Failed to init ca for %s.\n", name); + goto fail; + } + + ca_fini(); + + TEST_SUCCESS("(%s)", name); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL("(%s)", name); + return TEST_RC_FAIL; +} + +static int test_ca_init_fini_all(void) +{ + int ret = 0; + size_t i; + + for (i = 0; i < CA_POLS; i++) + ret |= test_ca_init_fini(ca_pols[i].pol, ca_pols[i].name); + + return ret; +} + +static int test_ca_init_invalid(void) +{ + TEST_START(); + + if (ca_init(CA_INVALID, 100) == 0) { + printf("Init accepted an invalid policy.\n"); + ca_fini(); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ca_ctx_share(enum pol_cong_avoid pol, + const char * name) +{ + void * c1; + void * c2; + + TEST_START("(%s)", name); + + if (ca_init(pol, 100) < 0) { + printf("Failed to init ca for %s.\n", name); + goto fail; + } + + c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); + if (c1 == NULL) { + printf("Failed to get ctx.\n"); + goto fail_init; + } + + c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); + if (c2 == NULL) { + printf("Failed to get second ctx.\n"); + goto fail_c1; + } + +#ifdef IPCP_CA_PER_FLOW + if (c1 == c2) { + printf("Per-flow build shared a ctx across flows.\n"); + goto fail_c2; + } +#else + if (c1 != c2) { + printf("Aggregate build did not share ctx per (addr, qc).\n"); + goto fail_c2; + } +#endif + ca_ctx_put(c2); + ca_ctx_put(c1); + + ca_fini(); + + TEST_SUCCESS("(%s)", name); + + return TEST_RC_SUCCESS; + fail_c2: + ca_ctx_put(c2); + fail_c1: + ca_ctx_put(c1); + fail_init: + ca_fini(); + fail: + TEST_FAIL("(%s)", name); + return TEST_RC_FAIL; +} + +static int test_ca_ctx_share_all(void) +{ + int ret = 0; + size_t i; + + for (i = 0; i < CA_POLS; i++) + ret |= test_ca_ctx_share(ca_pols[i].pol, ca_pols[i].name); + + return ret; +} + +static int test_ca_ctx_distinct(void) +{ + void * a_be; + void * b_be; + void * a_video; + + TEST_START(); + + if (ca_init(CA_NONE, 100) < 0) { + printf("Failed to init ca.\n"); + goto fail; + } + + a_be = ca_ctx_get(ADDR_A, QOS_CUBE_BE); + if (a_be == NULL) { + printf("Failed to get ctx.\n"); + goto fail_init; + } + + b_be = ca_ctx_get(ADDR_B, QOS_CUBE_BE); + if (b_be == NULL) { + printf("Failed to get ctx.\n"); + goto fail_a_be; + } + + a_video = ca_ctx_get(ADDR_A, QOS_CUBE_VIDEO); + if (a_video == NULL) { + printf("Failed to get ctx.\n"); + goto fail_b_be; + } + + if (a_be == b_be) { + printf("Distinct addresses shared a ctx.\n"); + goto fail_a_video; + } + + if (a_be == a_video) { + printf("Distinct qos cubes shared a ctx.\n"); + goto fail_a_video; + } + + ca_ctx_put(a_video); + ca_ctx_put(b_be); + ca_ctx_put(a_be); + + ca_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a_video: + ca_ctx_put(a_video); + fail_b_be: + ca_ctx_put(b_be); + fail_a_be: + ca_ctx_put(a_be); + fail_init: + ca_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Refcount survival is an aggregate-only property. */ +#ifndef IPCP_CA_PER_FLOW +static int test_ca_ctx_refcount(void) +{ + void * c1; + void * c3; + + TEST_START(); + + if (ca_init(CA_NONE, 100) < 0) { + printf("Failed to init ca.\n"); + goto fail; + } + + c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */ + if (c1 == NULL) { + printf("Failed to get ctx.\n"); + goto fail_init; + } + + if (ca_ctx_get(ADDR_A, QOS_CUBE_BE) == NULL) { /* refs = 2 */ + printf("Failed to get second ref.\n"); + goto fail_c1; + } + + ca_ctx_put(c1); /* refs = 1 */ + + c3 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2 */ + if (c3 == NULL) { + printf("Failed to get third ref.\n"); + goto fail_c1; + } + + if (c3 != c1) { + printf("Refcounted ctx freed while still referenced.\n"); + goto fail_c3; + } + + ca_ctx_put(c3); + ca_ctx_put(c1); + + ca_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_c3: + ca_ctx_put(c3); + fail_c1: + ca_ctx_put(c1); + fail_init: + ca_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The last put frees the interned ctx; a fresh get recreates it. */ +static int test_ca_ctx_recreate(void) +{ + void * c1; + void * c2; + void * c3; + + TEST_START(); + + if (ca_init(CA_NONE, 100) < 0) { + printf("Failed to init ca.\n"); + goto fail; + } + + c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */ + if (c1 == NULL) { + printf("Failed to get ctx.\n"); + goto fail_init; + } + + ca_ctx_put(c1); /* refs = 0: freed and de-interned */ + + c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* fresh entry */ + if (c2 == NULL) { + printf("Get after release did not recreate.\n"); + goto fail_init; + } + + c3 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2: shares */ + if (c3 == NULL) { + printf("Failed to share recreated ctx.\n"); + goto fail_c2; + } + + if (c3 != c2) { + printf("Recreated ctx did not intern.\n"); + goto fail_c3; + } + + ca_ctx_put(c3); + ca_ctx_put(c2); + + ca_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_c3: + ca_ctx_put(c3); + fail_c2: + ca_ctx_put(c2); + fail_init: + ca_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* ca_fini drains a ctx a flow left interned, with no leak. */ +static int test_ca_fini_drains(void) +{ + void * c1; + void * c2; + + TEST_START(); + + if (ca_init(CA_NONE, 100) < 0) { + printf("Failed to init ca.\n"); + goto fail; + } + + c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */ + if (c1 == NULL) { + printf("Failed to get ctx.\n"); + goto fail_init; + } + + c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2 */ + if (c2 == NULL) { + printf("Failed to get second ref.\n"); + goto fail_init; + } + + /* Leave both refs live: ca_fini must drain and free the ctx. */ + ca_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_init: + ca_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif /* !IPCP_CA_PER_FLOW */ + +int ca_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_ca_init_fini_all(); + ret |= test_ca_init_invalid(); + ret |= test_ca_ctx_share_all(); + ret |= test_ca_ctx_distinct(); +#ifndef IPCP_CA_PER_FLOW + ret |= test_ca_ctx_refcount(); + ret |= test_ca_ctx_recreate(); + ret |= test_ca_fini_drains(); +#endif + return ret; +} diff --git a/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c new file mode 100644 index 00000000..dac5e8ac --- /dev/null +++ b/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c @@ -0,0 +1,1294 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Shared-bottleneck lab for multi-bit ECN congestion avoidance + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#include "mb-ecn.c" +#include <test/test.h> + +#define MS (MILLION) /* one millisecond in ns */ +#define LEN 1000 /* default packet size (bytes) */ + +/* Create a context with the clock zeroed for deterministic time steps. */ +static struct mb_ecn_ctx * mk_ctx(void) +{ + struct mb_ecn_ctx * ctx; + + ctx = mb_ecn_ctx_create(); + if (ctx == NULL) + return NULL; + + ctx->rx_ts = 0; + ctx->rx_win = 0; + ctx->last_ts = 0; + ctx->last_ctrl = 0; + ctx->last_fb = 0; + ctx->last_sig = 0; + ctx->last_loc = 0; + ctx->last_cap = 0; + + ctx->snd_byt = 0; + ctx->snd_win = 0; + ctx->snd_r0 = CA_RATE_INIT; + ctx->snd_rate = CA_RATE_INIT; + ctx->backlogged = true; + ctx->src_limited = false; + ctx->started = false; + ctx->ss_tc = 20 * MS; /* fixed slope for deterministic SS */ + + return ctx; +} + +/* + * ------------------------------------------------------------------ + * Lab: packet-level shared-bottleneck simulator. + * + * Exact-time FIFO link of capacity cap: a packet departs at + * max(enqueue, previous departure) + len / cap. Packets are marked + * at enqueue from the instantaneous byte queue by mb_ecn_calc_ecn, + * the same function the forwarding path calls. Delivered packets + * drive a per-flow receiver estimator (mb_ecn_rcv); every window + * close is fed back to the sender as ece after a one-way lag, + * including the ece 0 release (fa.c). The sender sees its own + * previous packet's mark as the local fallback (fa.c l_ecn) and + * heartbeat pongs keep liveness. Greedy flows send whenever the + * pacer allows; CBR flows follow an absolute schedule. A tick every + * LAB_SAMPLE drains the link between sends, so feedback queued by a + * departure is due on time even while every flow sits idle. With + * cfg.shared every flow runs on one ctx, as a production build does, + * and the flow count follows t0, t1 and the churn period. + * + * The fixpoint tests assert; the sweep always returns success: an + * instrument, not a regression test. + * ------------------------------------------------------------------ + */ + +#define LAB_MAXF 8 /* most flows on one link */ +#define LAB_FIFO 16384 /* bottleneck ring, packets */ +#define LAB_FBQ 64 /* pending feedback ring */ +#define LAB_NONE UINT64_MAX /* no pending event */ +#define LAB_SAMPLE (5 * MS) /* service tick */ + +struct lab_pkt { + uint64_t dep; /* departure time (ns) */ + uint8_t ecn; + uint8_t f; /* flow index */ +}; + +struct lab_fb { + uint64_t t; + uint16_t ece; + uint8_t fcap; +}; + +struct lab_flow { + struct mb_ecn_ctx * snd; + struct mb_ecn_ctx * rcv; + uint64_t t_snd; /* next send attempt (ns) */ + uint64_t last; /* ctx clock high-water (ns) */ + uint64_t ftag; + uint64_t ia; /* app interval, 0 = greedy */ + uint64_t app; /* next app slot (ns) */ + uint64_t lag; /* feedback one-way lag (ns) */ + uint8_t lecn; /* own previous packet's mark */ + struct lab_fb fbq[LAB_FBQ]; + size_t fb_h; + size_t fb_n; + uint64_t hb_t; /* pong due, LAB_NONE = none */ + uint64_t hb_rtt; + /* metrics, accumulated past warmup */ + uint64_t m_t; /* last accounting time */ + uint64_t dlv; /* delivered bytes */ + uint64_t dlv2; /* delivered in score window */ + uint64_t r_int; /* integral of rate dt */ + uint64_t r_lo; + uint64_t r_hi; + uint64_t lead_B; /* lead-term cut volume */ + uint64_t prop_B; /* proportional cut volume */ + uint64_t cuts; /* >45% single-event cuts */ + uint64_t hold_ns; /* time with ai_hold set */ + uint64_t lim_ns; /* time src_limited (latch) */ + uint64_t idl_ns; /* time the backlog test off */ +}; + +struct lab_link { + uint64_t cap; /* bytes/s */ + uint8_t cc; /* stamped capacity code */ + uint64_t t_srv; /* line busy until (ns) */ + uint64_t q; /* queued bytes */ + uint64_t qmax; /* blocking threshold (bytes) */ + struct lab_pkt pk[LAB_FIFO]; + size_t h; + size_t n; + /* metrics */ + uint64_t q_t; /* last q-change time */ + uint64_t q_int; /* integral of q dt */ + uint64_t mk_int; /* integral of ece(q) dt */ + uint64_t e_from; /* empty-dwell start */ + uint64_t e_ns; /* empty time past warmup */ + size_t e_eps; /* empty episodes past warmup */ + uint64_t dlv; /* delivered bytes */ +}; + +struct lab_cfg { + const char * name; + uint64_t cap; /* bytes/s */ + uint64_t dur; /* run length (ns) */ + uint64_t wu; /* warmup excluded (ns) */ + size_t len; /* packet size (bytes) */ + uint64_t qmax; /* bytes */ + size_t n; /* flows, up to LAB_MAXF */ + uint64_t ia[LAB_MAXF]; /* app interval, 0 = greedy */ + uint64_t lag[LAB_MAXF]; /* one-way feedback lag (ns) */ + bool no_loc; /* disable local-mark path */ + uint64_t st_d; /* service stall length (ns) */ + uint64_t st_p; /* stall period, 0 = never */ + unsigned st_f; /* stalled-flow mask, 0 = all */ + uint64_t t0[LAB_MAXF]; /* flow start offsets (ns) */ + uint64_t t1[LAB_MAXF]; /* flow stop, 0 = runs to end */ + uint64_t r0[LAB_MAXF]; /* seed rate, 0 = slow start */ + uint64_t sc_lo; /* score window (ns), as the */ + uint64_t sc_hi; /* integration test scores */ + bool shared; /* one ctx for every flow */ + uint64_t ch_p; /* churn period, 0 = never */ + unsigned ch_f; /* churning flow mask */ +}; + +static struct lab_link lab_lnk; +static struct lab_flow lab_fl[LAB_MAXF]; +static uint64_t lab_sc_lo; +static uint64_t lab_sc_hi; +static size_t lab_len; + +/* + * Does flow i hold the ctx at t? A churning flow holds it for the + * first half of every ch_p and is gone for the second. + */ +static bool lab_up(const struct lab_cfg * c, + size_t i, + uint64_t t) +{ + if (t < c->t0[i]) + return false; + + if (c->t1[i] > 0 && t >= c->t1[i]) + return false; + + if (c->ch_p == 0 || ((c->ch_f >> i) & 1) == 0) + return true; + + return t % c->ch_p < c->ch_p / 2; +} + +/* Flows holding the ctx at t, the count ca_ctx_get refcounts. */ +static size_t lab_live(const struct lab_cfg * c, + uint64_t t) +{ + size_t n = 0; + size_t i; + + for (i = 0; i < c->n; i++) + if (lab_up(c, i, t)) + n++; + + return n > 0 ? n : 1; +} + +/* The bottleneck marks with the production function, nothing else. */ +static uint8_t lab_mark(uint64_t q) +{ + uint8_t e = 0; + + if (q == 0) + return 0; + + mb_ecn_calc_ecn(q, &e, QOS_CUBE_BE, lab_len); + + return e; +} + +/* Track the queue integral, the mark integral and empty dwells. */ +static void lab_q_acct(struct lab_link * l, + uint64_t now, + uint64_t wu) +{ + uint64_t dt = now - l->q_t; + + if (l->q_t >= wu && dt > 0) { + l->q_int += l->q * dt; + l->mk_int += (uint64_t) lab_mark(l->q) * 32 * dt; + } + + if (l->q == 0) { + if (l->e_from == LAB_NONE) + l->e_from = l->q_t; + } else if (l->e_from != LAB_NONE) { + if (now >= wu) { + uint64_t f = l->e_from > wu ? l->e_from : wu; + l->e_ns += l->q_t > f ? l->q_t - f : 0; + l->e_eps++; + } + l->e_from = LAB_NONE; + } + + l->q_t = now; +} + +/* Deliver everything due; receiver estimator feeds the fb ring. */ +static void lab_service(struct lab_link * l, + uint64_t now, + uint64_t wu) +{ + uint16_t ece; + uint8_t fcap; + + while (l->n > 0 && l->pk[l->h].dep <= now) { + struct lab_pkt * p = &l->pk[l->h]; + struct lab_flow * f = &lab_fl[p->f]; + + lab_q_acct(l, p->dep, wu); + l->q -= lab_len; + + if (p->dep >= wu) { + l->dlv += lab_len; + f->dlv += lab_len; + } + + if (p->dep >= lab_sc_lo && p->dep < lab_sc_hi) + f->dlv2 += lab_len; + + if (mb_ecn_rcv(f->rcv, lab_len, p->ecn, l->cc, &ece, &fcap, + p->dep) && + f->fb_n < LAB_FBQ) { + size_t i = (f->fb_h + f->fb_n++) % LAB_FBQ; + f->fbq[i].t = p->dep + f->lag; + f->fbq[i].ece = ece; + f->fbq[i].fcap = fcap; + } + + l->h = (l->h + 1) % LAB_FIFO; + l->n--; + } +} + +/* Integrate rate, regime dwell and extrema between a flow's events. */ +static void lab_f_acct(struct lab_flow * f, + uint64_t now, + uint64_t wu) +{ + struct mb_ecn_ctx * c = f->snd; + uint64_t dt; + uint16_t m; + + if (now < f->m_t) + now = f->m_t; + + dt = now - f->m_t; + if (f->m_t >= wu && dt > 0) { + f->r_int += c->rate * dt; + + if (c->ai_hold) + f->hold_ns += dt; + + if (c->src_limited) + f->lim_ns += dt; + + if (!c->backlogged) + f->idl_ns += dt; + + m = c->tx_ece > 0 ? c->tx_ece + : (uint16_t) (c->tx_loc << CA_SHFT); + + if (m > CA_ECE_MAX) + m = CA_ECE_MAX; + f->prop_B += c->rate / CA_ECE_REF * m * dt / BILLION; + + if (c->rate < f->r_lo) + f->r_lo = c->rate; + + if (c->rate > f->r_hi) + f->r_hi = c->rate; + } + + f->m_t = now; +} + +/* One send attempt; returns false when blocked on a full buffer. */ +static bool lab_send(struct lab_link * l, + struct lab_flow * f, + size_t fi, + size_t nf, + uint64_t wu, + bool no_loc) +{ + uint64_t t = f->t_snd; + uint64_t r0; + uint64_t dep; + uint8_t ecn; + time_t w; + + lab_service(l, t, wu); + + if (l->q + lab_len > l->qmax) { /* blocking write */ + f->t_snd = l->pk[l->h].dep; + return false; + } + + lab_f_acct(f, t, wu); + + ecn = lab_mark(l->q); + + r0 = f->snd->rate; + + mb_ecn_flows(f->snd, nf, t); + + if (!no_loc) + mb_ecn_loc(f->snd, f->lecn, t); + + w = mb_ecn_snd(f->snd, lab_len, t, &f->ftag); + + if (f->snd->rate * 100 < r0 * 55) + f->cuts++; + + f->lecn = ecn; + f->last = t; + + lab_q_acct(l, t, wu); + + dep = (t > l->t_srv ? t : l->t_srv) + lab_len * BILLION / l->cap; + l->t_srv = dep; + l->pk[(l->h + l->n) % LAB_FIFO].dep = dep; + l->pk[(l->h + l->n) % LAB_FIFO].ecn = ecn; + l->pk[(l->h + l->n) % LAB_FIFO].f = (uint8_t) fi; + l->n++; + l->q += lab_len; + + if (mb_ecn_ctx_hb_due(f->snd, t) && f->hb_t == LAB_NONE) { + f->hb_rtt = l->q * BILLION / l->cap + 2 * f->lag; + f->hb_t = t + f->hb_rtt; + } + + if (f->ia == 0) { + f->t_snd = t + (w > 0 ? (uint64_t) w : 1); + } else { + f->app += f->ia; + f->t_snd = f->app > t + (uint64_t) w ? f->app + : t + (uint64_t) w; + } + + return true; +} + +/* Apply one queued feedback to the sender, with lead accounting. */ +static void lab_fb_apply(struct lab_flow * f, + uint64_t wu) +{ + struct lab_fb * fb = &f->fbq[f->fb_h]; + uint64_t t = fb->t > f->last ? fb->t : f->last; + uint64_t r0 = f->snd->rate; + uint16_t step; + bool up; + + lab_f_acct(f, t, wu); + + if (t >= wu) { + up = fb->ece > f->snd->tx_ecp; + step = up ? fb->ece - f->snd->tx_ecp + : f->snd->tx_ecp - fb->ece; + + if (step > CA_ECE_REF) + step = CA_ECE_REF; + + if (up) + f->lead_B += r0 * step + / (CA_ECE_REF * CA_MD_KD_DIV); + } + + mb_ecn_ece(f->snd, fb->ece, fb->fcap, t); + + if (f->snd->rate * 100 < r0 * 55) + f->cuts++; + + f->last = t; + f->fb_h = (f->fb_h + 1) % LAB_FBQ; + f->fb_n--; +} + +static void lab_run(const struct lab_cfg * c) +{ + struct lab_link * l = &lab_lnk; + uint64_t smp = 0; + uint64_t st_t; + uint64_t span; + double secs; + size_t i; + + memset(l, 0, sizeof(*l)); + + l->cap = c->cap; + l->cc = cap_enc(c->cap); + l->qmax = c->qmax; + l->e_from = LAB_NONE; + + memset(lab_fl, 0, sizeof(lab_fl)); + + for (i = 0; i < c->n; i++) { + struct lab_flow * f = &lab_fl[i]; + + /* Production interns one ctx per (peer, qos cube). */ + if (c->shared && i > 0) { + f->snd = lab_fl[0].snd; + f->rcv = lab_fl[0].rcv; + } else { + f->snd = mk_ctx(); + f->rcv = mk_ctx(); + } + + if (f->snd == NULL || f->rcv == NULL) { + printf("lab: no memory.\n"); + goto fail_ctx; + } + + f->ia = c->ia[i]; + f->lag = c->lag[i]; + f->hb_t = LAB_NONE; + f->r_lo = UINT64_MAX; + f->t_snd = c->t0[i]; + f->app = c->t0[i]; + f->m_t = c->t0[i]; + /* Layer-declared RTT seed; pongs then track truth. */ + f->snd->ss_tc = 2 * CA_SS_RTT_DEF * MILLION; + + /* Seeded: start in AIMD, so the sweep probes the + attractor and not the ramp. */ + if (c->r0[i] == 0) + continue; + + f->snd->rate = c->r0[i]; + f->snd->inv_rate = mb_ecn_rate_inv(c->r0[i]); + f->snd->snd_r0 = c->r0[i]; + f->snd->snd_rate = c->r0[i]; + f->snd->tx_cav = true; + } + + lab_len = c->len; + lab_sc_lo = c->sc_lo; + lab_sc_hi = c->sc_hi; + + st_t = c->st_p > 0 ? c->st_p : LAB_NONE; + + while (true) { + uint64_t nxt = LAB_NONE; + int ev = -1; /* flow * 4 + kind */ + + /* + * Sender-side service stall: the scheduler feeding + * the transmit queue pauses for st_d, the queue + * drains clean, and the resume bursts the backlog + * through the marker (dsched untrack/starve model). + * Jitter the period so it cannot phase-lock. + */ + if (st_t != LAB_NONE && smp >= st_t) { + uint64_t end = st_t + c->st_d; + unsigned msk = c->st_f == 0 ? 3 : c->st_f; + + for (i = 0; i < c->n; i++) + if (((msk >> i) & 1) && lab_fl[i].t_snd < end) + lab_fl[i].t_snd = end; + st_t += c->st_p + (st_t / c->st_p % 3) * c->st_p / 5; + } + + for (i = 0; i < c->n; i++) { + struct lab_flow * f = &lab_fl[i]; + + if (c->t1[i] > 0 && f->t_snd >= c->t1[i]) { + f->t_snd = LAB_NONE; + } else if (c->ch_p > 0 && !lab_up(c, i, f->t_snd)) { + /* Gone: the app resumes next period. */ + f->t_snd = (f->t_snd / c->ch_p + 1) * c->ch_p; + f->app = f->t_snd; + } + + if (f->t_snd < nxt) { + nxt = f->t_snd; + ev = (int) i * 4; + } + if (f->fb_n > 0 && f->fbq[f->fb_h].t < nxt) { + nxt = f->fbq[f->fb_h].t; + ev = (int) i * 4 + 1; + } + if (f->hb_t < nxt) { + nxt = f->hb_t; + ev = (int) i * 4 + 2; + } + } + + if (smp < nxt) { + nxt = smp; + ev = -2; + } + + if (nxt >= c->dur) + break; + + if (ev == -2) { + lab_service(l, smp, c->wu); + smp += LAB_SAMPLE; + continue; + } + + i = (size_t) (ev / 4); + switch (ev % 4) { + case 0: + (void) lab_send(l, &lab_fl[i], i, + c->shared ? lab_live(c, nxt) : 1, + c->wu, c->no_loc); + break; + case 1: + lab_fb_apply(&lab_fl[i], c->wu); + break; + default: + lab_f_acct(&lab_fl[i], lab_fl[i].hb_t, c->wu); + if (lab_fl[i].hb_t > lab_fl[i].last) + lab_fl[i].last = lab_fl[i].hb_t; + mb_ecn_ctx_rtt(lab_fl[i].snd, lab_fl[i].last, + lab_fl[i].hb_rtt); + lab_fl[i].hb_t = LAB_NONE; + break; + } + } + + lab_service(l, c->dur, c->wu); + lab_q_acct(l, c->dur, c->wu); + + span = c->dur - c->wu; + secs = (double) span / BILLION; + + printf("%-14s C %5.2f MB/s n %zu | util %5.1f%% " + "q %6.1f pkt mk %5.1f e%% %4.1f eps %3zu\n", + c->name, (double) c->cap / MILLION, c->n, + 100.0 * (double) l->dlv / ((double) c->cap * secs), + (double) l->q_int / ((double) span * c->len), + (double) l->mk_int / (double) span, + 100.0 * (double) l->e_ns / (double) span, + l->e_eps); + + for (i = 0; i < c->n; i++) { + struct lab_flow * f = &lab_fl[i]; + struct mb_ecn_ctx * s = f->snd; + + lab_f_acct(f, c->dur, c->wu); + + if (c->sc_hi > c->sc_lo) + printf(" f%zu score %.3f Mb/s in [%llu,%llu)s\n", + i, 8.0 * (double) f->dlv2 / + ((double) (c->sc_hi - c->sc_lo) / BILLION + * MILLION), + (unsigned long long) (c->sc_lo / BILLION), + (unsigned long long) (c->sc_hi / BILLION)); + printf(" f%zu %s dlv %5.3f Mb/s rate mean %8.0f " + "lo %8" PRIu64 " hi %8" PRIu64 "\n" + " lead %8" PRIu64 " prop %8" PRIu64 + " cuts %4" PRIu64 " hold %4.1f%% lim %4.1f%%" + " idl %4.1f%% loss %" PRIu64 "\n", + i, f->ia == 0 ? "gdy" : "cbr", + 8.0 * (double) f->dlv / ((double) secs * MILLION), + (double) f->r_int / (double) span, + f->r_lo == UINT64_MAX ? 0 : f->r_lo, f->r_hi, + f->lead_B, f->prop_B, f->cuts, + 100.0 * (double) f->hold_ns / (double) span, + 100.0 * (double) f->lim_ns / (double) span, + 100.0 * (double) f->idl_ns / (double) span, + s->n_loss); + + if (c->shared && i > 0) + continue; + + mb_ecn_ctx_destroy(f->snd); + mb_ecn_ctx_destroy(f->rcv); + } + + return; + fail_ctx: + for (i = 0; i < c->n; i++) { + if (c->shared && i > 0) + break; + + mb_ecn_ctx_destroy(lab_fl[i].snd); + mb_ecn_ctx_destroy(lab_fl[i].rcv); + } +} + +/* + * Faithful test_cbr_protection / test_single_flow_slow_link protocol: + * 3-node chain, bottleneck one hop past the sender (no local mark), + * ~2 ms feedback path, CBR from t = 0, greedy joining at 300 ms, + * scored over the integration test's own window (slow start and + * convergence included, as the real assertion sees them). + */ +static void lab_cfg_std(struct lab_cfg * c, + const char * name, + uint64_t cap, + size_t n) +{ + size_t i; + + memset(c, 0, sizeof(*c)); + + c->name = name; + c->cap = cap; + c->n = n; + c->len = LEN; + /* 1024 = SSM_RBUFF_SIZE (cmake/config/lib/ssm.cmake). */ + c->qmax = 1024 * c->len; + c->no_loc = true; + + for (i = 0; i < n; i++) + c->lag[i] = 2 * MS; + + if (n == 2) { /* cbr_protection */ + c->ia[1] = c->len * BILLION / 375000; + c->t0[0] = 300 * MS; + c->dur = 35ULL * BILLION; + c->wu = 30ULL * BILLION; + c->sc_lo = 5ULL * BILLION; + c->sc_hi = 30ULL * BILLION; + } else { /* single_flow_slow_link */ + c->dur = 95ULL * BILLION; + c->wu = 55ULL * BILLION; + c->sc_lo = 20ULL * BILLION; + c->sc_hi = 50ULL * BILLION; + } +} + +/* Spread the seeds may end on and still count as one attractor. */ +#define LAB_FIX_TOL 0.10 + +/* + * Two flows on one bottleneck reach the same split whatever they start + * from: the difference mode contracts, so the seed cannot survive in + * the answer. A second attractor shows as seeds that disagree, a bias + * as agreement away from 1. + */ +static int test_mb_ecn_lab_fixpoint(uint64_t cap, + uint64_t lag) +{ + static struct lab_cfg c; + static const unsigned num[] = { 1, 1, 4 }; + static const unsigned den[] = { 4, 1, 1 }; + uint64_t kb = cap * 8 / 1000; + uint64_t ms = lag / MS; + double r[3]; + double lo; + double hi; + size_t i; + + TEST_START("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + + for (i = 0; i < 3; i++) { + lab_cfg_std(&c, "fixpoint", cap, 2); + + /* 10 Gb/s carries 9000 B frames; below 1 Gb/s, 1000 B. */ + c.len = cap >= 125000000 ? 9000 : LEN; + c.qmax = 1024 * c.len; + + c.ia[1] = 0; /* both greedy */ + c.t0[0] = 0; + c.lag[0] = lag; + c.lag[1] = lag; + c.dur = 60ULL * BILLION; + c.wu = 30ULL * BILLION; + c.sc_lo = 30ULL * BILLION; + c.sc_hi = 60ULL * BILLION; + c.r0[0] = cap * num[i] / (num[i] + den[i]); + c.r0[1] = cap * den[i] / (num[i] + den[i]); + + lab_run(&c); + + if (lab_fl[1].dlv2 == 0) { + printf("seed %u:%u starved a flow.\n", num[i], den[i]); + goto fail; + } + + r[i] = (double) lab_fl[0].dlv2 / (double) lab_fl[1].dlv2; + } + + lo = hi = r[0]; + + for (i = 1; i < 3; i++) { + if (r[i] < lo) + lo = r[i]; + + if (r[i] > hi) + hi = r[i]; + } + + if (hi > lo * (1.0 + LAB_FIX_TOL)) { + printf("seeds disagree: %.3f %.3f %.3f.\n", r[0], r[1], r[2]); + goto fail; + } + + if (lo < 1.0 - LAB_FIX_TOL || hi > 1.0 + LAB_FIX_TOL) { + printf("split %.3f..%.3f is not fair.\n", lo, hi); + goto fail; + } + + TEST_SUCCESS("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_lab_fixpoint_all(void) +{ +#ifdef MB_ECN_LAB_FULL + static const uint64_t cap[] = { 1250000000, 12500000, + 1250000, 62500 }; +#else + static const uint64_t cap[] = { 1250000, 62500 }; +#endif + static const uint64_t lag[] = { 2 * MS, 42 * MS }; + int ret = 0; + size_t i; + size_t j; + + for (i = 0; i < sizeof(cap) / sizeof(cap[0]); i++) + for (j = 0; j < sizeof(lag) / sizeof(lag[0]); j++) + ret |= test_mb_ecn_lab_fixpoint(cap[i], lag[j]); + + return ret; +} + +/* Seed weights: even, graded, and graded reversed. */ +static const unsigned lab_n8_w[3][LAB_MAXF] = { + { 1, 1, 1, 1, 1, 1, 1, 1 }, + { 1, 2, 3, 4, 5, 6, 7, 8 }, + { 8, 7, 6, 5, 4, 3, 2, 1 } +}; + +/* Spread across eight flows that still counts as one even split. */ +#define LAB_N8_TOL 0.10 + +/* + * Below this, packet-size quantisation dominates the spread, so the + * split is not scored here; starvation (lo == 0) and the utilisation + * gate still apply, so the exemption is narrow. + */ +#define LAB_N8_FAIR 125000 /* bytes/s, 1 Mb/s */ + +/* Aggregate the link has to carry for a run to say anything at all. */ +#define LAB_N8_UTIL 2 /* divisor: half of capacity */ + +/* + * Eight flows on one bottleneck. The additive increase is per flow, so + * both the aggregate probe and the contraction rate scale with the flow + * count, and this is where that scaling shows. + */ +static int test_mb_ecn_lab_fixpoint_n8(uint64_t cap, + uint64_t lag) +{ + static struct lab_cfg c; + uint64_t kb = cap * 8 / 1000; + uint64_t ms = lag / MS; + double worst = 1.0; + uint64_t want; + uint64_t tot; + uint64_t sum; + uint64_t lo; + uint64_t hi; + size_t i; + size_t j; + + TEST_START("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + + for (i = 0; i < 3; i++) { + lab_cfg_std(&c, "fixpoint-n8", cap, LAB_MAXF); + + /* 10 Gb/s carries 9000 B frames; below 1 Gb/s, 1000 B. */ + c.len = cap >= 125000000 ? 9000 : LEN; + c.qmax = 1024 * c.len; + + sum = 0; + + for (j = 0; j < LAB_MAXF; j++) + sum += lab_n8_w[i][j]; + + for (j = 0; j < LAB_MAXF; j++) { + c.ia[j] = 0; /* all greedy */ + c.t0[j] = 0; + c.lag[j] = lag; + c.r0[j] = cap * lab_n8_w[i][j] / sum; + } + + c.dur = 400ULL * BILLION; + c.wu = 200ULL * BILLION; + c.sc_lo = 200ULL * BILLION; + c.sc_hi = 400ULL * BILLION; + + lab_run(&c); + + tot = 0; + lo = lab_fl[0].dlv2; + hi = lab_fl[0].dlv2; + + for (j = 0; j < LAB_MAXF; j++) { + tot += lab_fl[j].dlv2; + if (lab_fl[j].dlv2 < lo) + lo = lab_fl[j].dlv2; + + if (lab_fl[j].dlv2 > hi) + hi = lab_fl[j].dlv2; + } + + if (lo == 0) { + printf("seed %zu wedged a flow.\n", i); + goto fail; + } + + want = cap * ((c.sc_hi - c.sc_lo) / BILLION); + if (tot < want / LAB_N8_UTIL) { + printf("seed %zu carried %" PRIu64 " of %" PRIu64 + " bytes.\n", i, tot, want); + goto fail; + } + + if ((double) hi / (double) lo > worst) + worst = (double) hi / (double) lo; + } + + if (cap >= LAB_N8_FAIR && worst > 1.0 + LAB_N8_TOL) { + printf("widest split %.3f across eight flows.\n", worst); + goto fail; + } + + TEST_SUCCESS("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_lab_fixpoint_n8_all(void) +{ +#ifdef MB_ECN_LAB_FULL + static const uint64_t cap[] = { 1250000000, 12500000, + 1250000, 62500 }; +#else + static const uint64_t cap[] = { 1250000, 62500 }; +#endif + static const uint64_t lag[] = { 2 * MS, 42 * MS }; + int ret = 0; + size_t i; + size_t j; + + for (i = 0; i < sizeof(cap) / sizeof(cap[0]); i++) + for (j = 0; j < sizeof(lag) / sizeof(lag[0]); j++) + ret |= test_mb_ecn_lab_fixpoint_n8(cap[i], lag[j]); + + return ret; +} + +/* Jain's fairness index over the score bytes of flows [lo, hi). */ +static double lab_jain(size_t lo, + size_t hi) +{ + double s = 0.0; + double s2 = 0.0; + double x; + size_t i; + + for (i = lo; i < hi; i++) { + x = (double) lab_fl[i].dlv2; + s += x; + s2 += x * x; + } + + if (s2 == 0.0) + return 0.0; + + return s * s / ((double) (hi - lo) * s2); +} + +/* Greedy flows on one ctx, all from t = 0, short feedback path. */ +static void lab_cfg_shared(struct lab_cfg * c, + const char * name, + uint64_t cap, + size_t n) +{ + size_t i; + + lab_cfg_std(c, name, cap, n); + + c->shared = true; + + for (i = 0; i < n; i++) { + c->ia[i] = 0; + c->t0[i] = 0; + c->lag[i] = 2 * MS; + } +} + +/* + * ------------------------------------------------------------------ + * Shared context: n flows, one struct mb_ecn_ctx, one ftag each. + * + * This is what a production build runs: ca_ctx_get interns one ctx + * per (peer, qos cube), so rate, vt and the mark are shared and the + * start tag is all a flow owns. The pacer then admits n * rate, so + * the attractor for rate is C / n, and the offered load the ctx + * measures is n flows' bytes against one flow's rate. + * ------------------------------------------------------------------ + */ + +/* + * Spread the shared attractor may sit in and still count as a + * per-flow share. The flows share one virtual clock, so the pacer + * fires them in one burst per tick; where that burst is a large part + * of the bandwidth-delay product the quarter-log2 mark prices it as + * a queue and the loop settles into a deep sawtooth, down to ~0.55 + * of C / n around 10 Mb/s at 1 kB packets. The band carries that and + * is still an order of magnitude under the path rate C. + */ +#define LAB_SHR_LO 0.45 +#define LAB_SHR_HI 1.10 + +/* Peak of the same sawtooth, over the settled window. */ +#define LAB_SHR_PK 1.35 + +/* One even split across the flows sharing the ctx. */ +#define LAB_SHR_JN 0.98 + +/* + * Greedy flows on one ctx, optionally with half of them joining and + * leaving again mid-run. Scores the epoch after the last change. + */ +static int test_mb_ecn_lab_shared(uint64_t cap, + size_t n, + bool churn) +{ + static struct lab_cfg c; + uint64_t kb = cap * 8 / 1000; + uint64_t tc = 20ULL * BILLION; + uint64_t fair; + uint64_t mean; + double jain; + size_t nl; + size_t i; + + TEST_START("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + + lab_cfg_shared(&c, churn ? "shr-churn" : "shr-gdy", cap, n); + + nl = churn ? n / 2 : n; + /* Half the flows join at tc and are gone again at 2 * tc. */ + for (i = nl; i < n; i++) { + c.t0[i] = tc; + c.t1[i] = 2 * tc; + } + + fair = cap / nl; + + /* + * Warmup ends at the last change, so r_hi is the peak of the + * epoch that has to settle back to the new share. + */ + c.dur = 2 * tc + 60ULL * BILLION; + c.wu = churn ? 2 * tc : 40ULL * BILLION; + c.sc_lo = c.wu + 20ULL * BILLION; + c.sc_hi = c.dur; + + lab_run(&c); + + mean = lab_fl[0].r_int / (c.dur - c.wu); + jain = lab_jain(0, nl); + + if (mean < (uint64_t) (LAB_SHR_LO * (double) fair) + || mean > (uint64_t) (LAB_SHR_HI * (double) fair)) { + printf("rate %" PRIu64 " is not a %" PRIu64 " share.\n", + mean, fair); + goto fail; + } + + if (lab_fl[0].r_hi > (uint64_t) (LAB_SHR_PK * (double) fair)) { + printf("rate peaked at %.2f of the share.\n", + (double) lab_fl[0].r_hi / (double) fair); + goto fail; + } + + if (jain < LAB_SHR_JN) { + printf("fairness %.4f across %zu flows.\n", jain, nl); + goto fail; + } + + TEST_SUCCESS("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + return TEST_RC_FAIL; +} + +/* + * Offered load per flow, as a divisor of the fair share. Low enough + * that the aggregate never fills the link, so nothing marks and the + * offered-load path is the only thing bounding the rate. + */ +#define LAB_SRC_DIV 4 + +/* + * mb_ecn_ceiling admits twice the load ONE flow offers. The slack + * covers the additive increase banked between two window closes and + * the truncation in sharing the offered bytes out. Reading the whole + * ctx's load as one flow's puts the bound n times higher, so a wide + * slack still separates the two. + */ +#define LAB_SRC_CEIL 2.5 + +/* Churn half-period; below CA_SND_WIN no window would ever close. */ +#define LAB_SRC_CHP (100 * MS) + +/* + * Source-limited flows on one ctx. Each offers a fixed rate well + * under its share, so mb_ecn_ceiling and the backlog level are all + * that bound the rate, and both read the offered load. With churn, + * the flows above n / 4 come and go every LAB_SRC_CHP, so a window + * that does not restart on the count change never measures one + * population. + */ +static int test_mb_ecn_lab_shared_load(uint64_t cap, + size_t n, + bool churn) +{ + static struct lab_cfg c; + uint64_t kb = cap * 8 / 1000; + uint64_t off = cap / (LAB_SRC_DIV * n); + uint64_t hi; + double jain; + size_t ns; + size_t i; + + TEST_START("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + + lab_cfg_shared(&c, churn ? "src-churn" : "src-cbr", cap, n); + + for (i = 0; i < n; i++) + c.ia[i] = c.len * BILLION / off; + + if (churn) { + c.ch_p = 2 * LAB_SRC_CHP; + c.ch_f = ~0u << (n / 4); + } + + c.dur = 120ULL * BILLION; + c.wu = 60ULL * BILLION; + c.sc_lo = 60ULL * BILLION; + c.sc_hi = 120ULL * BILLION; + + lab_run(&c); + + hi = lab_fl[0].r_hi; + + /* Score the flows that shared the ctx over the same epochs. */ + ns = churn ? n / 4 : 0; + jain = lab_jain(ns, n); + + if (hi > (uint64_t) (LAB_SRC_CEIL * (double) off)) { + printf("rate peaked at %.2f of the %" PRIu64 " offered, " + "%.2f of the %" PRIu64 " path.\n", + (double) hi / (double) off, off, + (double) hi / (double) cap, cap); + goto fail; + } + + if (jain < LAB_SHR_JN) { + printf("fairness %.4f across %zu flows.\n", jain, n - ns); + goto fail; + } + + TEST_SUCCESS("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL("(%" PRIu64 " kb/s, %zu flows%s)", kb, n, + churn ? ", churn" : ""); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_lab(void) +{ + static const uint64_t gc_cap[] = { 625000, 1250000, 12500000 }; + static const char * gc_nm[] = { "gc-5M", "gc-10M", "gc-100M" }; + static const uint64_t sf_cap[] = { 62500, 125000, 1250000 }; + static const char * sf_nm[] = { "sf-500k", "sf-1M", "sf-10M" }; + static const uint64_t g2_cap[] = { + 1250000, 1250000, 1250000, 62500, 62500, 62500 + }; + static const uint64_t g2_lag[] = { + 2 * MS, 20 * MS, 42 * MS, 2 * MS, 20 * MS, 42 * MS + }; + static const char * g2_nm[] = { + "g2-10M-2", "g2-10M-20", "g2-10M-42", + "g2-500k-2", "g2-500k-20", "g2-500k-42" + }; + static const uint64_t ul_lag[] = { 20 * MS, 42 * MS }; + static const char * ul_nm[] = { "g2-ul20", "g2-ul42" }; + static struct lab_cfg c; + size_t i; + + TEST_START(); + + /* + * cbr_protection over capacity: a 3 Mb/s CBR flow shares the + * link with a greedy flow joining at 300 ms, so the share the + * CBR has to hold runs 60%, 30% and 3% of the link. + */ + for (i = 0; i < 3; i++) { + lab_cfg_std(&c, gc_nm[i], gc_cap[i], 2); + lab_run(&c); + } + + /* + * single_flow_slow_link over capacity: one greedy flow alone. + * The marking quantum is fixed in bytes, so capacity alone + * decides how much queueing delay one ecn step prices. + */ + for (i = 0; i < 3; i++) { + lab_cfg_std(&c, sf_nm[i], sf_cap[i], 1); + lab_run(&c); + } + + /* + * Two greedy flows over capacity and equal feedback lag: the + * split they settle on and how a long loop degrades it. + */ + for (i = 0; i < 6; i++) { + lab_cfg_std(&c, g2_nm[i], g2_cap[i], 2); + + c.ia[1] = 0; + c.lag[0] = g2_lag[i]; + c.lag[1] = g2_lag[i]; + c.dur = 65ULL * BILLION; + c.wu = 35ULL * BILLION; + + lab_run(&c); + } + + /* Unequal lag: flow 0 keeps 2 ms, flow 1 reacts slower. */ + for (i = 0; i < 2; i++) { + lab_cfg_std(&c, ul_nm[i], 1250000, 2); + + c.ia[1] = 0; + c.lag[1] = ul_lag[i]; + c.dur = 65ULL * BILLION; + c.wu = 35ULL * BILLION; + + lab_run(&c); + } + + /* + * Service stalls: the scheduler feeding the transmit queue + * pauses, the queue drains clean and the resume bursts the + * backlog through the marker. + */ + lab_cfg_std(&c, "st-gc", 1250000, 2); + + c.st_d = 60 * MS; + c.st_p = 400 * MS; + + lab_run(&c); + + lab_cfg_std(&c, "st-sf", 125000, 1); + + c.st_d = 200 * MS; + c.st_p = BILLION; + + lab_run(&c); + + /* Per-flow starvation: only the sparse CBR flow stalls. */ + lab_cfg_std(&c, "st-pf", 1250000, 2); + + c.st_d = 100 * MS; + c.st_p = 300 * MS; + c.st_f = 2; + + lab_run(&c); + + /* + * Greedy joins 10 s in, once the CBR flow has settled: a step + * into contention rather than a shared ramp. + */ + lab_cfg_std(&c, "gc-late", 1250000, 2); + + c.t0[0] = 10ULL * BILLION; + c.dur = 45ULL * BILLION; + c.wu = 40ULL * BILLION; + c.sc_lo = 15ULL * BILLION; + c.sc_hi = 40ULL * BILLION; + + lab_run(&c); + + /* + * Second greedy flow joins 5 s in: the incumbent has to give + * back half to a newcomer that is still in slow start. + */ + lab_cfg_std(&c, "g2-stag", 1250000, 2); + + c.ia[1] = 0; + c.t0[0] = 0; + c.t0[1] = 5ULL * BILLION; + c.dur = 65ULL * BILLION; + c.wu = 35ULL * BILLION; + + lab_run(&c); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; +} + +int mb_ecn_lab_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_mb_ecn_lab_shared(1250000, 5, false); + ret |= test_mb_ecn_lab_shared(1250000, 8, false); + ret |= test_mb_ecn_lab_shared(1250000, 8, true); + ret |= test_mb_ecn_lab_shared_load(1250000, 5, false); + ret |= test_mb_ecn_lab_shared_load(1250000, 8, false); + ret |= test_mb_ecn_lab_shared_load(1250000, 5, true); + ret |= test_mb_ecn_lab_shared_load(1250000, 8, true); + ret |= test_mb_ecn_lab_fixpoint_all(); + ret |= test_mb_ecn_lab_fixpoint_n8_all(); + ret |= test_mb_ecn_lab(); + + return ret; +} diff --git a/src/ipcpd/unicast/ca/tests/mb_ecn_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c new file mode 100644 index 00000000..7186d3af --- /dev/null +++ b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c @@ -0,0 +1,3156 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Unit tests for multi-bit ECN congestion avoidance + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#include "mb-ecn.c" + +#include <test/test.h> + +#define MS (MILLION) /* one millisecond in ns */ +#define LEN 1000 /* default packet size (bytes) */ + +/* Create a context with the clock zeroed for deterministic time steps. */ +static struct mb_ecn_ctx * mk_ctx(void) +{ + struct mb_ecn_ctx * ctx; + + ctx = mb_ecn_ctx_create(); + if (ctx == NULL) + return NULL; + + ctx->rx_ts = 0; + ctx->rx_win = 0; + ctx->last_ts = 0; + ctx->last_ctrl = 0; + ctx->last_fb = 0; + ctx->last_sig = 0; + ctx->last_loc = 0; + ctx->last_cap = 0; + + ctx->snd_byt = 0; + ctx->snd_win = 0; + ctx->snd_r0 = CA_RATE_INIT; + ctx->snd_rate = CA_RATE_INIT; + ctx->backlogged = true; + ctx->src_limited = false; + ctx->started = false; + ctx->ss_tc = 20 * MS; /* fixed slope for deterministic SS */ + + return ctx; +} + +/* + * Drive ctx as a fully backlogged flow: offer a packet every paced + * wait, so the offered load tracks the paced rate. Returns end time. + */ +static uint64_t drive_backlogged(struct mb_ecn_ctx * ctx, + uint64_t * ftag, + uint64_t t, + uint64_t dur, + size_t len) +{ + uint64_t end = t + dur; + time_t w; + + while (t < end) { + w = mb_ecn_snd(ctx, len, t, ftag); + t += w > 0 ? (uint64_t) w : 1; + } + + return t; +} + +static int test_mb_ecn_ctx_create_destroy(void) +{ + struct mb_ecn_ctx * ctx; + + TEST_START(); + + ctx = mb_ecn_ctx_create(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + if (ctx->rate != CA_RATE_INIT) { + printf("Bad initial rate %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + if (ctx->rate_min != CA_RATE_MIN) { + printf("Bad initial floor %" PRIu64 ".\n", ctx->rate_min); + goto fail_ctx; + } + + if (ctx->vt != 0) { + printf("Bad initial virtual clock %" PRIu64 ".\n", ctx->vt); + goto fail_ctx; + } + + if (ctx->tx_cav) { + printf("Context did not start in slow start.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The pricing window derives from the declared RTT. */ +static int test_mb_ecn_init_window(void) +{ + TEST_START(); + + /* A fabric RTT lands on the floor, not below it. */ + mb_ecn_init(1); + + if (mb_ecn_tw != CA_TW_MIN) { + printf("fabric window %" PRIu64 ".\n", mb_ecn_tw); + goto fail; + } + + /* A WAN RTT caps the window. */ + mb_ecn_init(200); + + if (mb_ecn_tw != CA_TW) { + printf("wan window %" PRIu64 ".\n", mb_ecn_tw); + goto fail; + } + + /* An unspecified RTT takes the default and caps the window. */ + mb_ecn_init(0); + + if (mb_ecn_tw != CA_TW) { + printf("default window %" PRIu64 ".\n", mb_ecn_tw); + goto fail; + } + + mb_ecn_init(CA_SS_RTT_DEF); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + mb_ecn_init(CA_SS_RTT_DEF); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Queue depth (packets) that reads as full congestion. */ +#define FULL_PKTS (CA_MARK_KNEE << ((CA_ECE_REF >> CA_SHFT) / 4)) + +static int test_mb_ecn_calc_ecn(void) +{ + uint8_t ecn; + + TEST_START(); + + /* One packet in the queue is the floor: it marks nothing. */ + ecn = 0; + + mb_ecn_calc_ecn(1400, &ecn, QOS_CUBE_BE, 1400); + + if (ecn != 0) { + printf("Single packet marked %u.\n", ecn); + goto fail; + } + + /* An unknown mean packet size cannot mark. */ + ecn = 0; + + mb_ecn_calc_ecn(1400, &ecn, QOS_CUBE_BE, 0); + + if (ecn != 0) { + printf("Unknown mean marked %u.\n", ecn); + goto fail; + } + + /* Each doubling of the queue adds 4. */ + ecn = 0; + + mb_ecn_calc_ecn(2 * 1400, &ecn, QOS_CUBE_BE, 1400); + + if (ecn != 4) { + printf("Expected ecn 4 at 2 packets, got %u.\n", ecn); + goto fail; + } + + /* FULL_PKTS packets is full congestion. */ + ecn = 0; + + mb_ecn_calc_ecn(FULL_PKTS * 1400, &ecn, QOS_CUBE_BE, 1400); + + if (ecn != (CA_ECE_REF >> CA_SHFT)) { + printf("Expected ecn %u at full, got %u.\n", + CA_ECE_REF >> CA_SHFT, ecn); + goto fail; + } + + /* The same packet count marks the same at any packet size. */ + ecn = 0; + + mb_ecn_calc_ecn(FULL_PKTS * 200, &ecn, QOS_CUBE_BE, 200); + + if (ecn != (CA_ECE_REF >> CA_SHFT)) { + printf("Size dependence: exp %u, got %u.\n", + CA_ECE_REF >> CA_SHFT, ecn); + goto fail; + } + + /* MAX keeps the larger value; a smaller mark cannot lower it. */ + ecn = 0x80; + + mb_ecn_calc_ecn(2 * 1400, &ecn, QOS_CUBE_BE, 1400); + + if (ecn != 0x80) { + printf("Expected ecn 0x80, got 0x%x.\n", ecn); + goto fail; + } + + ecn = 3; + + mb_ecn_calc_ecn(4 * 1400, &ecn, QOS_CUBE_BE, 1400); + + if (ecn != 8) { + printf("Expected ecn 8, got %u.\n", ecn); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The first mark after idle emits the raw value with zero latency. */ +static int test_mb_ecn_rcv_onset_immediate(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + if (!mb_ecn_rcv(ctx, LEN, 4, 0, &ece, &fcap, MS)) { + printf("Onset did not update.\n"); + goto fail_ctx; + } + + if (ece != 4 << CA_SHFT) { + printf("Onset ece: exp %u, got %u.\n", 4 << CA_SHFT, ece); + goto fail_ctx; + } + + if (mb_ecn_rcv(ctx, LEN, 4, 0, &ece, &fcap, 2 * MS)) { + printf("Mid-window packet updated.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Two flows on the same wall-clock mark timeline, 15x apart in byte + * rate: the same congestion estimate, but the faster flow's window is + * shorter, so it feeds back more often (cadence tracks byte rate). + */ +static int test_mb_ecn_rcv_rate_independent(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint16_t ea; + uint16_t eb; + uint8_t fcap; + size_t ua; + size_t ub; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + ea = 0; + eb = 0; + ua = 0; + ub = 0; + + /* 300 ms of sustained mark 8; a at 1 kpps, b at ~66 pps. */ + for (i = 1; i <= 300; i++) { + ua += mb_ecn_rcv(a, LEN, 8, 0, &ea, &fcap, i * MS) ? 1 : 0; + if (i % 15 != 0) + continue; + + ub += mb_ecn_rcv(b, LEN, 8, 0, &eb, &fcap, i * MS) ? 1 : 0; + } + + if (ea > eb + 32 || eb > ea + 32) { + printf("estimates diverge: %u vs %u.\n", ea, eb); + goto fail_ctx; + } + + if (ua < ub + 2) { + printf("cadence not rate-scaled: %zu vs %zu.\n", ua, ub); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Two flows on one bottleneck, equal byte rate but 7.5x apart in + * packet size: the same congestion estimate and the same feedback + * cadence. Framing does not skew the control signal (fair share). + */ +static int test_mb_ecn_rcv_size_fair(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint16_t ea; + uint16_t eb; + uint8_t fcap; + size_t ua; + size_t ub; + uint64_t ta; + uint64_t tb; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + ea = 0; + eb = 0; + ua = 0; + ub = 0; + ta = 0; + tb = 0; + + /* 1 MB/s each: a at 200 B / 200 us, b at 1500 B / 1.5 ms. */ + while (ta < 500 * MS) { + ta += 200 * 1000; + ua += mb_ecn_rcv(a, 200, 8, 0, &ea, &fcap, ta) ? 1 : 0; + } + + while (tb < 500 * MS) { + tb += 1500 * 1000; + ub += mb_ecn_rcv(b, 1500, 8, 0, &eb, &fcap, tb) ? 1 : 0; + } + + if (ea != 8 << CA_SHFT || eb != 8 << CA_SHFT) { + printf("size-skewed estimate: %u vs %u.\n", ea, eb); + goto fail_ctx; + } + + if (ua > ub + 3 || ub > ua + 3) { + printf("cadence skewed by size: %zu vs %zu.\n", ua, ub); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Release emits exactly one 0 and leaves the estimator fully idle. */ +static int test_mb_ecn_rcv_release_exact_zero(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + size_t ends; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + for (i = 1; i <= 140; i++) + mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, i * MS); + + ends = 0; + for (i = 141; i <= 350; i++) { + if (!mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, i * MS)) + continue; + + if (ece == 0) + ends++; + } + + if (ends != 1) { + printf("end of congestion fired %zu times.\n", ends); + goto fail_ctx; + } + + if (ctx->rx_ece != 0 || ctx->rx_acc != 0) { + printf("estimator not idle: ece %u acc %" PRIu64 ".\n", + ctx->rx_ece, ctx->rx_acc); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A gap past the window restarts fresh: no stale, diluted estimate. */ +static int test_mb_ecn_rcv_gap_restart(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t t; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + mb_ecn_rcv(ctx, LEN, 6, 0, &ece, &fcap, MS); + mb_ecn_rcv(ctx, LEN, 6, 0, &ece, &fcap, 2 * MS); + + t = 2 * MS + 10 * CA_TW; + if (!mb_ecn_rcv(ctx, LEN, 5, 0, &ece, &fcap, t)) { + printf("gap restart did not update.\n"); + goto fail_ctx; + } + + if (ece != 5 << CA_SHFT) { + printf("gap restart: exp %u, got %u.\n", 5 << CA_SHFT, ece); + goto fail_ctx; + } + + t += 10 * CA_TW; + if (!mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, t) || ece != 0) { + printf("gap with clean packet did not end: %u.\n", ece); + goto fail_ctx; + } + + if (mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, t + MS)) { + printf("idle packet updated.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * At a floored layer RTT, rx_tw sits at CA_TW_MIN, so 4 * rx_tw is + * well under CA_ECE_TTL. A gap in that band must still close the + * window as a diluted average, not restart fresh: a fresh restart + * always emits the raw undiluted mark (ecn << CA_SHFT), so an ece + * that low pins the CA_ECE_TTL floor in mb_ecn_rcv_fresh. + */ +static int test_mb_ecn_rcv_gap_floor(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t t; + + TEST_START(); + + mb_ecn_init(2); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + if (ctx->rx_tw != CA_TW_MIN) { + printf("window not floored: %" PRIu64 ".\n", ctx->rx_tw); + goto fail_ctx; + } + + /* Onset, then a second packet inside the window: mark banked. */ + mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, MS); + mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, 2 * MS); + + /* 20 ms gap: past 4 * rx_tw (16 ms), well under CA_ECE_TTL. */ + + t = 2 * MS + 20 * MS; + if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) { + printf("window did not close.\n"); + goto fail_ctx; + } + + /* A fresh restart would emit the raw mark 8 << CA_SHFT, undiluted. */ + if (ece >= (8 << CA_SHFT)) { + printf("gap read as a fresh onset: ece %u.\n", ece); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + mb_ecn_init(CA_SS_RTT_DEF); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + mb_ecn_init(CA_SS_RTT_DEF); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Max marks at max gaps: exact ceiling, no overflow past the edge. */ +static int test_mb_ecn_rcv_accum_bounds(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t t; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, MS); + + /* Two packets at dt just under CA_TW straddle the boundary. */ + t = MS + CA_TW - 1; + if (mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, t)) { + printf("update before the window closed.\n"); + goto fail_ctx; + } + + t += CA_TW - 1; + if (!mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, t)) { + printf("no update at the window boundary.\n"); + goto fail_ctx; + } + + if (ece != 15 << CA_SHFT) { + printf("ceiling: exp %u, got %u.\n", 15 << CA_SHFT, ece); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * The window floors at CA_TW, tracks the rate below the knee, + * and only a pathological fold hits the CA_TW_ABSMAX ceiling. + */ +static int test_mb_ecn_rcv_window_clip_bounds(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t ia; + uint64_t t; + size_t closes; + + TEST_START(); + + /* 1 GbE is above the high knee: the window floors at CA_TW. */ + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ia = 8000ULL * BILLION / 1000000000ULL; + t = 0; + closes = 0; + while (closes < 40) { + t += ia; + if (mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) + closes++; + } + + if (ctx->rx_tw != CA_TW) { + printf("high-rate window: exp %" PRIu64 ", got %" PRIu64 + ".\n", (uint64_t) CA_TW, ctx->rx_tw); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + /* 1 Mbps: below the knee, ~16 pkts = 16 * 8 ms = 131 ms. */ + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ia = 8000ULL * BILLION / 1000000ULL; + t = 0; + closes = 0; + while (closes < 40) { + t += ia; + if (mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) + closes++; + } + + if (ctx->rx_tw < 120 * MS || ctx->rx_tw > 140 * MS) { + printf("low-rate window: exp ~131 ms, got %" PRIu64 ".\n", + ctx->rx_tw); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + /* A near-empty window folds a huge target: ceiling holds. */ + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + mb_ecn_rcv(ctx, 10, 8, 0, &ece, &fcap, MS); + mb_ecn_rcv(ctx, 10, 8, 0, &ece, &fcap, MS + CA_TW); + + if (ctx->rx_tw != CA_TW_ABSMAX) { + printf("window ceiling breached: %" PRIu64 ".\n", + ctx->rx_tw); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A CA-limited slow flow grows its window to hold ~16 packets. */ +static int test_mb_ecn_rcv_slow_window(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* 1400 B every 171 ms (~8 KB/s), sustained mark 8. */ + for (i = 1; i <= 100; i++) + mb_ecn_rcv(ctx, 1400, 8, 0, &ece, &fcap, i * 171 * MS); + + /* Target window 16 * 1000 B at 8187 B/s ~= 2.0 s. */ + if (ctx->rx_tw < 1400 * MS || ctx->rx_tw > 2800 * MS) { + printf("slow window: exp ~2 s, got %" PRIu64 ".\n", + ctx->rx_tw); + goto fail_ctx; + } + + /* Steady mark 8 emits exactly 256 once the window settles. */ + if (ece != 8 << CA_SHFT) { + printf("slow-flow estimate: exp %u, got %u.\n", + 8 << CA_SHFT, ece); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A physically maximal window must fold without overflow or wrap. */ +static int test_mb_ecn_rcv_no_overflow_highrate(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + bool ok; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Open a window, then inject a maximal byte count and span. */ + mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, 0); + ctx->rx_byt = CA_RATE_MAX / 8; + ctx->rx_ts = 2 * CA_TW - 2; + + ok = mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, 2 * CA_TW - 1); + + if (!ok) { + printf("max-window close did not fire.\n"); + goto fail_ctx; + } + + if (ece > 8160) { + printf("estimate %u wrapped.\n", ece); + goto fail_ctx; + } + + /* A wrapped numerator drives rx_tw to MAX; it must descend. */ + if (ctx->rx_tw > CA_TW || ctx->rx_tw < CA_TW) { + printf("window %" PRIu64 " did not descend.\n", ctx->rx_tw); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * The sender holds a mark across the full inter-feedback gap (TTL > + * 2 * CA_TW); a repeated mark adds only the proportional cut. + */ +static int test_mb_ecn_ece_ttl_covers_cadence(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t prev; + uint64_t t; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 100 << 20; + ctx->tx_cav = true; + + mb_ecn_ece(ctx, 100, 0, MS); + mb_ecn_snd(ctx, LEN, MS, &ftag); + + /* Sends between feedbacks spaced 2 * CA_TW + 5 ms apart. */ + t = MS; + for (i = 0; i < 4; i++) { + t += (2 * CA_TW + 5 * MS) / 4; + mb_ecn_snd(ctx, LEN, t, &ftag); + if (ctx->tx_ece == 0) { + printf("mark cleared inside the feedback gap.\n"); + goto fail_ctx; + } + } + + /* Same mark again: rise 0, so only the proportional cut. */ + prev = ctx->rate; + t += MS; + mb_ecn_ece(ctx, 100, 0, t); + mb_ecn_snd(ctx, LEN, t, &ftag); + + if (prev - ctx->rate > prev / 100) { + printf("phantom lead cut: %" PRIu64 " -> %" PRIu64 ".\n", + prev, ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_slow_start(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t prev; + uint64_t t; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + prev = ctx->rate; + t = 0; + + /* No feedback: the flow stays in slow start and grows each step. */ + for (i = 0; i < 16; i++) { + t += MS; + mb_ecn_snd(ctx, LEN, t, &ftag); + if (ctx->rate <= prev) { + printf("rate did not grow: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + prev = ctx->rate; + } + + /* Exponential ramp doubles in ~ln2 * CA_SS_TC ~= 14 ms. */ + if (ctx->rate < 2 * CA_RATE_INIT) { + printf("slow start too slow: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_dt_scaling_invariant(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t inc_a; + uint64_t inc_b; + uint64_t t; + uint64_t fta = 0; + uint64_t ftb = 0; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + /* Leave slow start; seed a realistic rate (truncation-free). */ + mb_ecn_ece(a, 0, 0, 0); + mb_ecn_ece(b, 0, 0, 0); + a->rate = (uint64_t) 10 << 20; + b->rate = (uint64_t) 10 << 20; + + /* a: one 30 ms step. */ + mb_ecn_snd(a, LEN, 30 * MS, &fta); + + /* b: thirty 1 ms steps over the same 30 ms. */ + t = 0; + for (i = 0; i < 30; i++) { + t += MS; + mb_ecn_snd(b, LEN, t, &ftb); + } + + inc_a = a->rate - ((uint64_t) 10 << 20); + inc_b = b->rate - ((uint64_t) 10 << 20); + + /* Equal within 1 %; the small gap is per-step integer truncation. */ + if (inc_a == 0 || inc_b == 0) { + printf("no additive increase: %" PRIu64 " %" PRIu64 ".\n", + inc_a, inc_b); + goto fail_ctx; + } + + if (inc_a > inc_b + inc_a / 100 || inc_b > inc_a + inc_a / 100) { + printf("cadence-dependent AI: %" PRIu64 " vs %" PRIu64 ".\n", + inc_a, inc_b); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_multiplicative_decrease(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t prev; + uint64_t t; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 100 << 20; + prev = ctx->rate; + t = 0; + + for (i = 0; i < 10; i++) { + t += MS; + mb_ecn_ece(ctx, CA_ECE_REF, 0, t); + mb_ecn_snd(ctx, LEN, t, &ftag); + if (ctx->rate >= prev) { + printf("rate did not shrink: %" PRIu64 ".\n", + ctx->rate); + goto fail_ctx; + } + + prev = ctx->rate; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The hold releases on any unsaturated mark, including above REF. */ +static int test_mb_ecn_ai_hold_release(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t r0 = (uint64_t) 100 << 20; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* A saturated mark keeps the hold: the queue has not drained. */ + ctx->ai_hold = true; + + mb_ecn_ece(ctx, CA_ECE_MAX, 0, MS); + + if (!ctx->ai_hold) { + printf("saturated feedback released the hold.\n"); + goto fail_ctx; + } + + /* A standing mark of 20 flows is unsaturated: release. */ + ctx->ai_hold = true; + + mb_ecn_ece(ctx, 22 << CA_SHFT, 0, 2 * MS); + + if (ctx->ai_hold) { + printf("unsaturated feedback held the increase.\n"); + goto fail_ctx; + } + + /* The decrease still scales with the mark saturated at CA_ECE_MAX. */ + ctx->rate = r0; + ctx->tx_cav = true; + ctx->tx_ece = CA_ECE_MAX; + ctx->tx_ecp = CA_ECE_MAX; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, MILLION); + + if (ctx->rate > r0 - r0 / 700) { + printf("clamped decrease too weak: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_rate_floor(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* + * A starved 600 ms at full mark takes the rate/2 branch, far + * below the floor; stays inside the initial ~976 ms mark TTL. + */ + ctx->rate = CA_RATE_MIN + 1000; + mb_ecn_ece(ctx, CA_ECE_REF, 0, 0); + mb_ecn_snd(ctx, LEN, 600 * MS, &ftag); + + if (ctx->rate != CA_RATE_MIN) { + printf("rate floor breached: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_mb_ecn_fixed_point(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t exp; + uint64_t t; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + exp = CA_AI_RATE * CA_ECE_REF / + (128 - CA_ECE_REF * BILLION / CA_PROBE_TC); + t = 0; + + /* ~20 s: the probe raises the loop time constant to CA_PROBE_TC. */ + for (i = 0; i < 20000; i++) { + t += MS; + mb_ecn_ece(ctx, 128, 0, t); + mb_ecn_snd(ctx, LEN, t, &ftag); + } + + if (ctx->rate < exp - exp / 4 || ctx->rate > exp + exp / 4) { + printf("no fixed point: exp ~%" PRIu64 ", got %" PRIu64 ".\n", + exp, ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The lead is two-sided: it cuts on a rise and gives back on a fall. */ +static int test_mb_ecn_lead_symmetric(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t r0 = (uint64_t) 100 << 20; + uint64_t net; + uint64_t drop; + uint64_t gain; + uint64_t kd2; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->tx_cav = true; + + /* Rise of one reference: cut rate / CA_MD_KD_DIV, no more. */ + ctx->rate = r0; + ctx->tx_ece = CA_ECE_REF; + ctx->tx_ecp = 0; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, 0); + + drop = r0 - ctx->rate; + if (drop != r0 / CA_MD_KD_DIV) { + printf("rise cut %" PRIu64 ", want %" PRIu64 ".\n", + drop, r0 / CA_MD_KD_DIV); + goto fail_ctx; + } + + /* Fall of one reference: give the same fraction back. */ + ctx->rate = r0; + ctx->tx_ece = 1; + ctx->tx_ecp = CA_ECE_REF + 1; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, 0); + + gain = ctx->rate - r0; + if (gain != r0 / CA_MD_KD_DIV) { + printf("fall boost %" PRIu64 ", want %" PRIu64 ".\n", + gain, r0 / CA_MD_KD_DIV); + goto fail_ctx; + } + + /* A collapse from deep saturation is clamped to the same. */ + ctx->rate = r0; + ctx->tx_ece = 1; + ctx->tx_ecp = 255 << CA_SHFT; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, 0); + + gain = ctx->rate - r0; + if (gain != r0 / CA_MD_KD_DIV) { + printf("unclamped fall boost %" PRIu64 ".\n", gain); + goto fail_ctx; + } + + /* A cycle that stays marked nets out: no standing bias. */ + ctx->rate = r0; + ctx->tx_ecp = 4 << CA_SHFT; + ctx->tx_ece = 12 << CA_SHFT; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, 0); + + ctx->tx_ece = 4 << CA_SHFT; + ctx->dec_acc = 0; + + mb_ecn_decrease(ctx, 0); + + net = ctx->rate > r0 ? ctx->rate - r0 : r0 - ctx->rate; + /* + * The two lead steps compound to (1 - x)(1 + x), x = 1 / + * (2 * CA_MD_KD_DIV), so net ~= r0 / (4 * CA_MD_KD_DIV^2). + * Band it a factor of 2 either side so a materially weaker + * gain (e.g. KD off by a factor of 4) fails the floor. + */ + kd2 = (uint64_t) CA_MD_KD_DIV * CA_MD_KD_DIV; + if (net > r0 / (2 * kd2)) { + printf("cycle bias %" PRIu64 " of %" PRIu64 ".\n", net, r0); + goto fail_ctx; + } + + if (net < r0 / (8 * kd2)) { + printf("lead gain weaker than expected: net %" PRIu64 + " of %" PRIu64 ".\n", net, r0); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A local first-hop mark exits slow start with no feedback needed. */ +static int test_mb_ecn_slow_start_local_brake(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t prev; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + for (i = 1; i <= 50; i++) + mb_ecn_snd(ctx, LEN, i * MS, &ftag); + + if (ctx->tx_cav) { + printf("Left slow start without any signal.\n"); + goto fail_ctx; + } + + prev = ctx->rate; + mb_ecn_loc(ctx, 1, 50 * MS); + if (!ctx->tx_cav) { + printf("Local mark did not exit slow start.\n"); + goto fail_ctx; + } + + mb_ecn_snd(ctx, LEN, 51 * MS, &ftag); + if (ctx->rate > prev + prev / 20) { + printf("SS ramp survived the brake: %" PRIu64 ".\n", + ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A clean path still ramps to line rate in well under a second. */ +static int test_mb_ecn_slow_start_clean_ramp(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Backlogged, no marks: slow start sprints in a couple windows. */ + drive_backlogged(ctx, &ftag, MS, 2 * CA_SND_WIN, LEN); + + if (ctx->rate < (1ULL << 24)) { + printf("backlogged slow start too slow: %" PRIu64 ".\n", + ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A sender starved of send-path control steps recovers through the + * feedback path: honest elapsed time, at most a 50% cut per step. + */ +static int test_mb_ecn_starved_decrease_escape(void) +{ + struct mb_ecn_ctx * ctx; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 100 << 20; + ctx->tx_cav = true; + + /* Feedback arrives once per second; no sends at all. */ + for (i = 1; i <= 6; i++) + mb_ecn_ece(ctx, 480, 0, i * BILLION); + + if (ctx->rate > (5ULL << 19)) { + printf("still wedged at %" PRIu64 " B/s.\n", ctx->rate); + goto fail_ctx; + } + + if (ctx->rate < CA_RATE_MIN) { + printf("rate floor breached: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* With feedback fully dead, the local mark alone recovers the rate. */ +static int test_mb_ecn_starved_local_fallback(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t t; + uint64_t ftag = 0; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 100 << 20; + + for (i = 1; i <= 7; i++) { + t = i * BILLION; + mb_ecn_loc(ctx, 15, t); + mb_ecn_snd(ctx, LEN, t, &ftag); + } + + if (!ctx->tx_cav) { + printf("Local mark did not exit slow start.\n"); + goto fail_ctx; + } + + if (ctx->rate > (5ULL << 19)) { + printf("still wedged at %" PRIu64 " B/s.\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * MD Δt-invariance: the same elapsed time under the same mark cuts + * the same, in one big step or five small ones. + */ +static int test_mb_ecn_decrease_dt_invariant(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t cut_a; + uint64_t cut_b; + uint64_t r0; + uint64_t fta = 0; + uint64_t ftb = 0; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + r0 = (uint64_t) 100 << 20; + a->rate = r0; + b->rate = r0; + + mb_ecn_ece(a, 256, 0, 0); + mb_ecn_ece(b, 256, 0, 0); + + /* a: one 50 ms step; b: five 10 ms steps (both within DT_CAP). */ + mb_ecn_snd(a, LEN, 50 * MS, &fta); + + for (i = 1; i <= 5; i++) + mb_ecn_snd(b, LEN, i * 10 * MS, &ftb); + + cut_a = r0 - a->rate; + cut_b = r0 - b->rate; + + if (cut_a == 0 || cut_b == 0) { + printf("no cut: %" PRIu64 " %" PRIu64 ".\n", cut_a, cut_b); + goto fail_ctx; + } + + /* Within 10%: residual is Euler compounding of MD and the probe. */ + if (cut_a > cut_b + cut_a / 10 || cut_b > cut_a + cut_a / 10) { + printf("cadence-dependent MD: %" PRIu64 " vs %" PRIu64 + ".\n", cut_a, cut_b); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Sub-ms control steps must not lose decrease time to truncation. */ +static int test_mb_ecn_decrease_subms_carry(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t cut_a; + uint64_t cut_b; + uint64_t r0; + uint64_t fta = 0; + uint64_t ftb = 0; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + r0 = (uint64_t) 100 << 20; + a->rate = r0; + b->rate = r0; + + mb_ecn_ece(a, 256, 0, 0); + mb_ecn_ece(b, 256, 0, 0); + + /* Same 30 ms of marked time; b's steps have a 0.5 ms tail. */ + for (i = 1; i <= 10; i++) + mb_ecn_snd(a, LEN, i * 3 * MS, &fta); + + for (i = 1; i <= 20; i++) + mb_ecn_snd(b, LEN, i * 3 * MS / 2, &ftb); + + cut_a = r0 - a->rate; + cut_b = r0 - b->rate; + + if (cut_a == 0 || cut_b == 0) { + printf("no cut: %" PRIu64 " %" PRIu64 ".\n", cut_a, cut_b); + goto fail_ctx; + } + + /* Within 10%: the sub-ms remainder must carry, not vanish. */ + if (cut_a > cut_b + cut_a / 10 || cut_b > cut_a + cut_a / 10) { + printf("sub-ms decrease lost: %" PRIu64 " vs %" PRIu64 + ".\n", cut_a, cut_b); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Resuming after a long idle gap: bounded AI, no cut from stale marks. */ +static int test_mb_ecn_idle_resume_bounded(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t prev; + uint64_t bump; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 10 << 20; + + mb_ecn_loc(ctx, 15, MS); + mb_ecn_ece(ctx, 480, 0, MS); + + prev = ctx->rate; + + /* 600 s later: both signals stale; one capped AI + probe step. */ + mb_ecn_snd(ctx, LEN, 600 * BILLION, &ftag); + + if (ctx->rate < prev) { + printf("stale mark cut the rate: %" PRIu64 ".\n", + ctx->rate); + goto fail_ctx; + } + + bump = CA_AI_RATE * CA_DT_CAP / BILLION; + bump += (prev + bump) * CA_DT_CAP / CA_PROBE_TC; + + if (ctx->rate > prev + bump + 2) { + printf("idle resume cap breached: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The congestion signal ages out on wall-clock time, not packet count. */ +static int test_mb_ecn_ece_staleness(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* A fast flow's control step caches the floor TTL. */ + ctx->rate = (uint64_t) 1 << 20; + ctx->tx_cav = true; + mb_ecn_snd(ctx, LEN, MS, &ftag); + + if (ctx->ece_ttl != CA_ECE_TTL) { + printf("Fast-flow TTL: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) CA_ECE_TTL, ctx->ece_ttl); + goto fail_ctx; + } + + mb_ecn_ece(ctx, CA_ECE_REF, 0, 2 * MS); + + /* Just inside the TTL: the signal is still held. */ + mb_ecn_snd(ctx, LEN, 2 * MS + ctx->ece_ttl, &ftag); + if (ctx->tx_ece == 0) { + printf("signal aged out too early.\n"); + goto fail_ctx; + } + + /* Past the TTL without feedback: the signal is cleared. */ + mb_ecn_snd(ctx, LEN, 2 * MS + ctx->ece_ttl + 1, &ftag); + if (ctx->tx_ece != 0) { + printf("stale signal not cleared: %u.\n", ctx->tx_ece); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The staleness horizon stretches with a slow flow's window. */ +static int test_mb_ecn_ece_ttl_tracks_rate(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t want; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = 8192; + ctx->rate_min = 8192; + ctx->ai_rate = 0; + ctx->tx_cav = true; + mb_ecn_snd(ctx, 1400, MS, &ftag); + + want = (1 << CA_TW_GAP_SHFT) * CA_RX_WBYTES * BILLION / ctx->rate; + if (ctx->ece_ttl != want) { + printf("Slow-flow TTL: exp %" PRIu64 ", got %" PRIu64 ".\n", + want, ctx->ece_ttl); + goto fail_ctx; + } + + if (ctx->ece_ttl < 7 * (uint64_t) BILLION) { + printf("TTL did not stretch: %" PRIu64 ".\n", + ctx->ece_ttl); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * First packet of a flow starts at the clock; a same-instant second + * packet leads by its length and is paced by lead / rate. + */ +static int test_mb_ecn_sfq_pace(void) +{ + struct mb_ecn_ctx * ctx; + time_t wait; + uint64_t ftag = 0; + time_t want; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = 1U << 20; + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + + /* First send: start tag equals the clock, so no wait. */ + wait = mb_ecn_snd(ctx, 1500, 0, &ftag); + if (wait != 0) { + printf("first packet waited %ld, expected 0.\n", (long) wait); + goto fail_ctx; + } + + /* Same instant (dt = 0): the flow now leads by 1500 B. */ + wait = mb_ecn_snd(ctx, 1500, 0, &ftag); + want = (time_t) ((uint64_t) 1500 * BILLION / ctx->rate); + + if (wait != want) { + printf("paced wait %ld, expected %ld.\n", + (long) wait, (long) want); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * The proportional probe grows the rate by the same fraction per unit + * time regardless of the absolute rate: two clean flows 100x apart in + * rate grow by the same ratio. Deleting the probe leaves only the tiny + * additive increase, failing the growth floor. + */ +static int test_mb_ecn_probe_scale_invariant(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t ra0; + uint64_t rb0; + double ga; + double gb; + uint64_t t; + size_t i; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + /* Clean path (mark 0), out of slow start, backlogged, 100x apart. */ + mb_ecn_ece(a, 0, 0, 0); + mb_ecn_ece(b, 0, 0, 0); + a->rate = (uint64_t) 10 << 20; + b->rate = (uint64_t) 1000 << 20; + a->backlogged = true; + b->backlogged = true; + ra0 = a->rate; + rb0 = b->rate; + + /* Drive control via the feedback path so backlogged stays set. */ + t = 0; + for (i = 0; i < 500; i++) { + t += MS; + mb_ecn_ece(a, 0, 0, t); + mb_ecn_ece(b, 0, 0, t); + } + + ga = (double) a->rate / ra0; + gb = (double) b->rate / rb0; + + if (ga < gb - gb / 50 || gb < ga - ga / 50) { + printf("probe not scale-invariant: %.4f vs %.4f.\n", ga, gb); + goto fail_ctx; + } + + /* And it must actually grow: the probe is present, not deleted. */ + if (ga < 1.05) { + printf("probe did not grow the rate: %.4f.\n", ga); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * The proportional probe e-folds the rate over CA_PROBE_TC: a clean flow + * grows by ~e in 8 s. Pinned to a literal e-band so a mistuned + * CA_PROBE_TC (e.g. 4 s gives e^2) fails. + */ +static int test_mb_ecn_probe_time_constant(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t r0; + double ratio; + uint64_t t; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Clean path, out of slow start, backlogged, below the ceiling. */ + mb_ecn_ece(ctx, 0, 0, 0); + + ctx->rate = (uint64_t) 1 << 30; + ctx->backlogged = true; + r0 = ctx->rate; + + /* 8000 x 1 ms of clean growth, driven via the feedback path. */ + t = 0; + for (i = 0; i < 8000; i++) { + t += MS; + mb_ecn_ece(ctx, 0, 0, t); + } + + /* Undamped probe: 8 s at TC 8 s is one full e-fold, ~2.72x. */ + ratio = (double) ctx->rate / r0; + if (ratio < 2.6 || ratio > 2.85) { + printf("probe TC off: exp ~2.72, got %.3fx over 8 s.\n", ratio); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The fed-back capacity is the MIN of the nonzero caps in the window. */ +static int test_mb_ecn_rcv_cap_window_min(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t t; + bool upd; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Onset packet carries no capacity: feed back unknown. */ + if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, MS)) { + printf("Onset did not update.\n"); + goto fail_ctx; + } + + if (fcap != 0) { + printf("Onset fed back cap: exp 0, got %u.\n", fcap); + goto fail_ctx; + } + + mb_ecn_rcv(ctx, LEN, 8, 40, &ece, &fcap, 2 * MS); + mb_ecn_rcv(ctx, LEN, 8, 36, &ece, &fcap, 3 * MS); + + t = 3 * MS + CA_TW; + if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) { + printf("Window did not close.\n"); + goto fail_ctx; + } + + if (fcap != 36) { + printf("Window min cap: exp 36, got %u.\n", fcap); + goto fail_ctx; + } + + /* The next window starts unknown; follow the adapted rx_tw. */ + upd = false; + for (i = 0; i < 128 && !upd; i++) { + t += CA_TW; + upd = mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t); + } + + if (!upd) { + printf("Second window did not close.\n"); + goto fail_ctx; + } + + if (fcap != 0) { + printf("Stale cap %u leaked into the next window.\n", fcap); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Onset and gap restarts emit the triggering packet's cap, fresh. */ +static int test_mb_ecn_rcv_cap_onset_fresh(void) +{ + struct mb_ecn_ctx * ctx; + uint16_t ece; + uint8_t fcap; + uint64_t t; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + if (!mb_ecn_rcv(ctx, LEN, 4, 77, &ece, &fcap, MS)) { + printf("Onset did not update.\n"); + goto fail_ctx; + } + + if (fcap != 77) { + printf("Onset cap: exp 77, got %u.\n", fcap); + goto fail_ctx; + } + + mb_ecn_rcv(ctx, LEN, 4, 50, &ece, &fcap, 2 * MS); + + /* A gap restart must not fold in the stale window min. */ + t = 2 * MS + 5 * CA_TW; + if (!mb_ecn_rcv(ctx, LEN, 4, 90, &ece, &fcap, t)) { + printf("Gap restart did not update.\n"); + goto fail_ctx; + } + + if (fcap != 90) { + printf("Gap restart cap: exp 90, got %u.\n", fcap); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Fed-back capacity derives the floor and slope: EWMA toward C/32. */ +static int test_mb_ecn_ece_cap_derives_rates(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t tgt; + uint64_t want; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Code 120 = 2^30 B/s; target floor = 2^25 B/s. */ + tgt = cap_dec(120) >> CA_CAP_SHFT; + + mb_ecn_ece(ctx, 100, 120, MS); + + want = CA_RATE_MIN + ((tgt - CA_RATE_MIN) >> CA_CAP_SM_SHFT); + if (ctx->rate_min != want) { + printf("Floor: exp %" PRIu64 ", got %" PRIu64 ".\n", + want, ctx->rate_min); + goto fail_ctx; + } + + if (ctx->ai_rate != 2 * ctx->rate_min) { + printf("AI slope did not track the floor.\n"); + goto fail_ctx; + } + + mb_ecn_ece(ctx, 100, 120, 2 * MS); + + want += (tgt - want) >> CA_CAP_SM_SHFT; + if (ctx->rate_min != want) { + printf("Floor EWMA: exp %" PRIu64 ", got %" PRIu64 ".\n", + want, ctx->rate_min); + goto fail_ctx; + } + + if (ctx->n_cap != 2) { + printf("Capacity updates: exp 2, got %" PRIu64 ".\n", + ctx->n_cap); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Feedback without a capacity leaves the derived rates untouched. */ +static int test_mb_ecn_ece_cap_zero_keeps_rates(void) +{ + struct mb_ecn_ctx * ctx; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + mb_ecn_ece(ctx, 100, 0, MS); + + if (ctx->rate_min != CA_RATE_MIN || ctx->ai_rate != CA_AI_RATE) { + printf("Unknown cap moved the derived rates.\n"); + goto fail_ctx; + } + + if (ctx->n_cap != 0) { + printf("Unknown cap counted as an update.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The derived floor clamps to [CA_RATE_MIN, CA_RMIN_MAX]. */ +static int test_mb_ecn_ece_cap_clamps(void) +{ + struct mb_ecn_ctx * ctx; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* A path slower than the default floor cannot lower it. */ + mb_ecn_ece(ctx, 100, 1, MS); + + if (ctx->rate_min != CA_RATE_MIN) { + printf("Slow path lowered the floor: %" PRIu64 ".\n", + ctx->rate_min); + goto fail_ctx; + } + + /* A absurdly fast path saturates at the ceiling. */ + for (i = 1; i <= 40; i++) + mb_ecn_ece(ctx, 100, 255, (1 + i) * MS); + + if (ctx->rate_min > CA_RMIN_MAX) { + printf("Floor above the ceiling: %" PRIu64 ".\n", + ctx->rate_min); + goto fail_ctx; + } + + if (ctx->rate_min < CA_RMIN_MAX - 4) { + printf("Floor did not reach the ceiling: %" PRIu64 ".\n", + ctx->rate_min); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Stale capacity reverts the derived rates to the defaults. */ +static int test_mb_ecn_cap_ttl_reverts(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + mb_ecn_ece(ctx, 100, 120, MS); + + if (ctx->rate_min == CA_RATE_MIN) { + printf("Capacity did not derive a floor.\n"); + goto fail_ctx; + } + + /* Just inside the TTL: the derived rates hold. */ + mb_ecn_snd(ctx, LEN, MS + (ctx->ece_ttl << CA_CAP_TTL_SHFT), &ftag); + + if (ctx->rate_min == CA_RATE_MIN) { + printf("Derived rates reverted too early.\n"); + goto fail_ctx; + } + + /* Past the TTL: back to the defaults. */ + mb_ecn_snd(ctx, LEN, MS + (ctx->ece_ttl << CA_CAP_TTL_SHFT) + 1, + &ftag); + + if (ctx->rate_min != CA_RATE_MIN || ctx->ai_rate != CA_AI_RATE) { + printf("Stale capacity kept the derived rates.\n"); + goto fail_ctx; + } + + if (ctx->tx_cap != 0) { + printf("Stale capacity code not cleared.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The control law uses the per-ctx AI slope. */ +static int test_mb_ecn_ctrl_per_ctx_ai(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t want; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* Leave slow start; raise the slope as capacity would. */ + mb_ecn_ece(ctx, 0, 0, 0); + + ctx->rate = (uint64_t) 10 << 20; + ctx->ai_rate = 16 * CA_AI_RATE; + + want = ctx->rate + ctx->ai_rate * (30 * MS) / BILLION; + want += want * (30 * MS) / CA_PROBE_TC; + + mb_ecn_snd(ctx, LEN, 30 * MS, &ftag); + + if (ctx->rate != want) { + printf("AI not per-ctx: exp %" PRIu64 ", got %" PRIu64 ".\n", + want, ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The rate clamp honours the per-ctx derived floor. */ +static int test_mb_ecn_ctrl_per_ctx_floor(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate_min = (uint64_t) 1 << 20; + ctx->rate = ((uint64_t) 1 << 20) + 1000; + + mb_ecn_ece(ctx, CA_ECE_REF, 0, 0); + mb_ecn_snd(ctx, LEN, 30 * MS, &ftag); + + if (ctx->rate != ctx->rate_min) { + printf("Floor not per-ctx: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A paced flow slower than one packet per CA_DT_CAP must not decay: + * the gap credit law grants its true elapsed service, so the lead + * stays pinned at ~one packet instead of growing without bound. + */ +static int test_mb_ecn_snd_slow_rate_paced(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + uint64_t t = 0; + time_t wait; + size_t i; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* 8 KB/s, 1400 B packets: inter-send gap ~171 ms > CA_DT_CAP. */ + ctx->rate = 8192; + ctx->rate_min = 8192; + ctx->ai_rate = 0; + ctx->inv_rate = mb_ecn_rate_inv(8192); + ctx->tx_cav = true; + + for (i = 0; i < 50; i++) { + wait = mb_ecn_snd(ctx, 1400, t, &ftag); + t += wait > 0 ? (uint64_t) wait : 1; + } + + if (ctx->lead > 2 * 1400) { + printf("Pacer starves a slow flow: lead %" PRIu64 ".\n", + ctx->lead); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A long idle makes the aggregate source-limited, so the offered-load + * ceiling bounds the resume rate (hence the burst) well below the + * pre-idle rate. + */ +static int test_mb_ecn_snd_idle_burst_bound(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = (uint64_t) 1 << 20; + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->tx_cav = true; + + mb_ecn_snd(ctx, 1400, 0, &ftag); /* warm-up: sets started */ + + /* 600 s idle. */ + mb_ecn_snd(ctx, 1400, 600 * BILLION, &ftag); + + if (ctx->backlogged) { + printf("long idle did not clear backlogged.\n"); + goto fail_ctx; + } + + if (ctx->rate >= ((uint64_t) 1 << 20)) { + printf("idle resume rate not ceiling-bounded: %" PRIu64 + ".\n", ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A deep backlog at a low rate must not wrap the wait computation. */ +static int test_mb_ecn_snd_wait_no_overflow(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t want; + uint64_t ftag; + time_t wait; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = 8192; + ctx->inv_rate = mb_ecn_rate_inv(8192); + + /* 128 flows x 1400 B of SFQ lead at the floor rate. */ + ftag = 128 * 1400; + + wait = mb_ecn_snd(ctx, 1400, 0, &ftag); + want = (uint64_t) 128 * 1400 * BILLION / 8192; + + if ((uint64_t) wait < want - want / 100 || + (uint64_t) wait > want + want / 100) { + printf("Wait wrapped: exp ~%" PRIu64 ", got %" PRIu64 ".\n", + want, (uint64_t) wait); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A fully paced-backlogged flow reads backlogged after a window. */ +static int test_mb_ecn_backlogged_paced(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->backlogged = false; /* prove a window close re-earns it */ + + drive_backlogged(ctx, &ftag, MS, 4 * CA_SND_WIN, LEN); + + if (!ctx->backlogged) { + printf("paced-backlogged flow read source-limited.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A source-limited flow is capped to the backlog level above the + * offered estimate, and NOT re-floored to a high capacity rate_min. + */ +static int test_mb_ecn_source_limited_ceiling(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + uint64_t t = 10 * MS; + uint64_t expect; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->tx_cav = true; + ctx->started = true; + ctx->backlogged = false; + ctx->rate = (uint64_t) 100 << 20; + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->rate_min = (uint64_t) 50 << 20; + ctx->snd_rate = (uint64_t) 1 << 20; + ctx->snd_r0 = ctx->rate; + ctx->snd_win = t; + ctx->last_ts = t; + ctx->last_ctrl = t; + + mb_ecn_snd(ctx, LEN, t + 2 * MS, &ftag); + + expect = 1398101; /* (1 << 20) * 4 / 3, truncated */ + if (ctx->rate != expect) { + printf("ceiling: exp %" PRIu64 ", got %" PRIu64 ".\n", + expect, ctx->rate); + goto fail_ctx; + } + + if (!ctx->src_limited) { + printf("ceiling bound but src_limited not set.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + /* Non-power-of-two case: verify the exact level, not a step. */ + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->tx_cav = true; + ctx->started = true; + ctx->backlogged = false; + ctx->rate = (uint64_t) 100 << 20; + ctx->inv_rate = mb_ecn_rate_inv(ctx->rate); + ctx->rate_min = (uint64_t) 50 << 20; + ctx->snd_rate = (uint64_t) 303 << 12; + ctx->snd_r0 = ctx->rate; + ctx->snd_win = t; + ctx->last_ts = t; + ctx->last_ctrl = t; + + mb_ecn_snd(ctx, LEN, t + 2 * MS, &ftag); + + expect = 1654784; /* (303 << 12) * 4 / 3, exact */ + if (ctx->rate != expect) { + printf("exact ceiling: exp %" PRIu64 ", got %" PRIu64 + ".\n", expect, ctx->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* One quiet window must not collapse the max-filter; it decays ~1/16. */ +static int test_mb_ecn_max_filter(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + uint64_t hi = (uint64_t) 10 << 20; + uint64_t t = 10 * MS; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->started = true; + ctx->snd_rate = hi; + ctx->snd_r0 = hi; + ctx->snd_byt = 0; + ctx->snd_win = t; + ctx->last_ts = t; + ctx->last_ctrl = t; + + /* Close one window with almost no bytes offered. */ + mb_ecn_snd(ctx, LEN, t + CA_SND_WIN + 1, &ftag); + + if (ctx->snd_rate >= hi || ctx->snd_rate < hi - hi / 8) { + printf("max-filter: exp ~15/16 of %" PRIu64 ", got %" + PRIu64 " after one quiet window.\n", + hi, ctx->snd_rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A >CA_DT_CAP gap clears backlogged without touching the estimate. */ +static int test_mb_ecn_idle_clears_backlogged(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + uint64_t snd_rate = (uint64_t) 5 << 20; + uint64_t t = 10 * MS; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->started = true; + ctx->backlogged = true; + ctx->rate = snd_rate; + ctx->snd_rate = snd_rate; + ctx->snd_win = t; + ctx->last_ts = t; + ctx->last_ctrl = t; + + /* 55 ms gap: past CA_DT_CAP, under CA_SND_WIN (no window close). */ + mb_ecn_snd(ctx, LEN, t + 55 * MS, &ftag); + + if (ctx->backlogged) { + printf("idle gap did not clear backlogged.\n"); + goto fail_ctx; + } + + if (ctx->snd_rate != snd_rate) { + printf("idle step altered snd_rate %" PRIu64 ".\n", + ctx->snd_rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The first send is never misread as idle, whatever the wall clock. */ +static int test_mb_ecn_first_send_warmup(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t ftag = 0; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + /* started == false; a large first timestamp must not look idle. */ + mb_ecn_snd(ctx, LEN, 500 * MS, &ftag); + + if (!ctx->started) { + printf("first send did not set the warm-up sentinel.\n"); + goto fail_ctx; + } + + if (!ctx->backlogged) { + printf("first send misclassified as idle.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Couple one or two backlogged flows through a shared bottleneck of + * capacity cap: each step marks the shared queue with the real + * calc_ecn, feeds each flow that mark delayed by its own lag (in + * steps), drives it backlogged for one step, then drains the queue. + * A faithful discrete run of the fluid model on the real control law, + * with the forwarder abstracted to a single shared price. b may be + * NULL for a single-flow run. + */ +#define TF_STEP (5 * MS) /* control step (ns) */ +#define TF_HIST 64 /* mark ring, bounds max lag */ +#define TF_QMAX (8192 * LEN) /* bottleneck buffer (bytes) */ +#define TF_MAXN 32 /* flows per shared-link run */ + +static void shared_link_run(struct mb_ecn_ctx * a, + struct mb_ecn_ctx * b, + uint64_t cap, + size_t lag_a, + size_t lag_b, + size_t steps) +{ + uint16_t hist[TF_HIST]; + uint64_t ta = 0; + uint64_t tb = 0; + uint64_t fta = 0; + uint64_t ftb = 0; + uint64_t q = 0; + uint64_t drain = cap * TF_STEP / BILLION; + uint64_t tgt; + uint64_t arr; + uint8_t cc = cap_enc(cap); + uint8_t ecn; + uint16_t ea; + uint16_t eb; + size_t k; + + memset(hist, 0, sizeof(hist)); + + for (k = 0; k < steps; k++) { + tgt = (k + 1) * TF_STEP; + ecn = 0; + + mb_ecn_calc_ecn(q, &ecn, QOS_CUBE_BE, LEN); + hist[k % TF_HIST] = (uint16_t) (ecn << CA_SHFT); + + ea = k < lag_a ? 0 : hist[(k - lag_a) % TF_HIST]; + if (ea > 0) /* no feedback until congestion */ + mb_ecn_ece(a, ea, cc, ta); + + /* Active flow: heartbeat/window liveness stays fresh. */ + a->last_sig = ta; + if (tgt > ta) + ta = drive_backlogged(a, &fta, ta, tgt - ta, LEN); + + arr = a->rate * TF_STEP / BILLION; + + if (b != NULL) { + eb = k < lag_b ? 0 : hist[(k - lag_b) % TF_HIST]; + if (eb > 0) + mb_ecn_ece(b, eb, cc, tb); + + b->last_sig = tb; + if (tgt > tb) + tb = drive_backlogged(b, &ftb, tb, + tgt - tb, LEN); + + arr += b->rate * TF_STEP / BILLION; + } + + q += arr; + q = q > drain ? q - drain : 0; + if (q > (uint64_t) TF_QMAX) + q = TF_QMAX; + } +} + +/* + * Two flows sharing one bottleneck, both fed every queue mark, must + * converge from a lopsided start toward an equal split (the fluid + * model's drho/dt -> 0) rather than latch winner-take-all. Isolates + * the rate law from the forwarder: neither flow is starved of marks. + */ +static int test_mb_ecn_two_flow_converge(void) +{ + struct mb_ecn_ctx * a; + struct mb_ecn_ctx * b; + uint64_t cap = 1ULL << 20; + uint64_t fair = (1ULL << 20) / 2; + uint64_t lo; + uint64_t hi; + + TEST_START(); + + a = mk_ctx(); + b = mk_ctx(); + if (a == NULL || b == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + a->rate = cap; /* a hogs, b starts small */ + b->rate = CA_RATE_INIT; + + shared_link_run(a, b, cap, 0, 0, 6000); + + lo = a->rate < b->rate ? a->rate : b->rate; + hi = a->rate > b->rate ? a->rate : b->rate; + + if (lo < fair / 4) { + printf("flow starved: %" PRIu64 " / %" PRIu64 ".\n", + a->rate, b->rate); + goto fail_ctx; + } + + if (hi > 3 * lo) { + printf("did not converge: %" PRIu64 " / %" PRIu64 ".\n", + a->rate, b->rate); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(a); + mb_ecn_ctx_destroy(b); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A remote bottleneck exits slow start only on fed-back congestion, + * so a longer feedback delay lets the exponential ramp overshoot + * further: the slow-start peak grows with RTT. Nothing caps the ramp + * at the path capacity, so the sender latches near the first-hop rate. + */ +static int test_mb_ecn_ramp_overshoot_grows_with_rtt(void) +{ + struct mb_ecn_ctx * lo_rtt; + struct mb_ecn_ctx * hi_rtt; + uint64_t cap = 1ULL << 20; + + TEST_START(); + + lo_rtt = mk_ctx(); + hi_rtt = mk_ctx(); + if (lo_rtt == NULL || hi_rtt == NULL) { + printf("Failed to create contexts.\n"); + goto fail_ctx; + } + + /* Same bottleneck; feedback lags 1 vs 8 steps (~5 vs 40 ms). */ + shared_link_run(lo_rtt, NULL, cap, 1, 0, 2000); + shared_link_run(hi_rtt, NULL, cap, 8, 0, 2000); + + if (hi_rtt->ss_peak <= lo_rtt->ss_peak) { + printf("overshoot did not grow with RTT: %" PRIu64 + " vs %" PRIu64 ".\n", + hi_rtt->ss_peak, lo_rtt->ss_peak); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(lo_rtt); + mb_ecn_ctx_destroy(hi_rtt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(lo_rtt); + mb_ecn_ctx_destroy(hi_rtt); + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Flows sharing a context offer bytes together but each may send at + * rate, so the window must be shared out before the backlog level is + * read. Four flows offering two thirds of a share each stay below it. + */ +static int test_mb_ecn_shared_ctx_offered_per_flow(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t rate = 1000000; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = rate; + ctx->snd_flows = 4; + ctx->snd_r0 = rate; + ctx->snd_win = 0; + ctx->snd_byt = 4 * rate * 2 / 3; + + mb_ecn_win(ctx, BILLION); + + if (ctx->backlogged) { + printf("aggregate load read as one flow's backlog.\n"); + goto fail_ctx; + } + + /* The close left a fresh window; a full share each clears it. */ + ctx->snd_byt = 4 * rate; + + mb_ecn_win(ctx, 2 * BILLION); + + if (!ctx->backlogged) { + printf("per-flow share did not read as backlogged.\n"); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A join or a leave opens a fresh window, so none divides the bytes + * one population offered by the count of another. An unchanged count + * leaves the running window alone. + */ +static int test_mb_ecn_flow_count_restarts_window(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t rate = 1000000; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->rate = rate; + ctx->snd_flows = 2; + ctx->snd_win = MS; + ctx->snd_byt = 12345; + ctx->snd_r0 = 7; + + mb_ecn_flows(ctx, 5, 8 * MS); + + if (ctx->snd_flows != 5 || ctx->snd_byt != 0 + || ctx->snd_win != 8 * MS || ctx->snd_r0 != rate) { + printf("count change left a stale window: flows=%zu " + "byt=%" PRIu64 " win=%" PRIu64 " r0=%" PRIu64 + ".\n", ctx->snd_flows, ctx->snd_byt, + ctx->snd_win, ctx->snd_r0); + goto fail_ctx; + } + + ctx->snd_byt = 999; + + mb_ecn_flows(ctx, 5, 20 * MS); + + if (ctx->snd_byt != 999 || ctx->snd_win != 8 * MS) { + printf("unchanged count restarted the window.\n"); + goto fail_ctx; + } + + /* An empty context still measures a single sender. */ + mb_ecn_flows(ctx, 0, 30 * MS); + + if (ctx->snd_flows != 1) { + printf("zero flows did not floor at one: %zu.\n", + ctx->snd_flows); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * The ceiling must land where a window of the delivered rate reads + * backlogged again: a context clamped above that level can never + * leave the clamp, and loses its capacity floor with it. + */ +static int test_mb_ecn_ceiling_clears_backlog(void) +{ + struct mb_ecn_ctx * ctx; + uint64_t x = 1 << 20; + + TEST_START(); + + ctx = mk_ctx(); + if (ctx == NULL) { + printf("Failed to create context.\n"); + goto fail; + } + + ctx->backlogged = false; + ctx->snd_rate = x; + ctx->rate = 100 * x; + ctx->rate_min = CA_RATE_MIN; + + mb_ecn_ceiling(ctx); + + if (ctx->rate >= 100 * x) { + printf("ceiling did not bind: %" PRIu64 ".\n", ctx->rate); + goto fail_ctx; + } + + /* One window delivering x, with the pacer deferring nothing. */ + ctx->snd_r0 = ctx->rate; + ctx->snd_flows = 1; + ctx->snd_win = 0; + ctx->snd_byt = x; + ctx->snd_pac = 0; + + mb_ecn_win(ctx, BILLION); + + if (!ctx->backlogged) { + printf("clamped at %" PRIu64 " cannot clear on %" PRIu64 + ".\n", ctx->snd_r0, x); + goto fail_ctx; + } + + mb_ecn_ctx_destroy(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + mb_ecn_ctx_destroy(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int mb_ecn_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_mb_ecn_ctx_create_destroy(); + ret |= test_mb_ecn_init_window(); + ret |= test_mb_ecn_calc_ecn(); + ret |= test_mb_ecn_rcv_onset_immediate(); + ret |= test_mb_ecn_rcv_rate_independent(); + ret |= test_mb_ecn_rcv_size_fair(); + ret |= test_mb_ecn_rcv_release_exact_zero(); + ret |= test_mb_ecn_rcv_gap_restart(); + ret |= test_mb_ecn_rcv_gap_floor(); + ret |= test_mb_ecn_rcv_accum_bounds(); + ret |= test_mb_ecn_rcv_window_clip_bounds(); + ret |= test_mb_ecn_rcv_slow_window(); + ret |= test_mb_ecn_rcv_no_overflow_highrate(); + ret |= test_mb_ecn_ece_ttl_covers_cadence(); + ret |= test_mb_ecn_slow_start(); + ret |= test_mb_ecn_dt_scaling_invariant(); + ret |= test_mb_ecn_probe_scale_invariant(); + ret |= test_mb_ecn_multiplicative_decrease(); + ret |= test_mb_ecn_ai_hold_release(); + ret |= test_mb_ecn_fixed_point(); + ret |= test_mb_ecn_lead_symmetric(); + ret |= test_mb_ecn_slow_start_local_brake(); + ret |= test_mb_ecn_slow_start_clean_ramp(); + ret |= test_mb_ecn_starved_decrease_escape(); + ret |= test_mb_ecn_starved_local_fallback(); + ret |= test_mb_ecn_decrease_dt_invariant(); + ret |= test_mb_ecn_decrease_subms_carry(); + ret |= test_mb_ecn_idle_resume_bounded(); + ret |= test_mb_ecn_rate_floor(); + ret |= test_mb_ecn_ece_staleness(); + ret |= test_mb_ecn_ece_ttl_tracks_rate(); + ret |= test_mb_ecn_sfq_pace(); + ret |= test_mb_ecn_probe_time_constant(); + ret |= test_mb_ecn_rcv_cap_window_min(); + ret |= test_mb_ecn_rcv_cap_onset_fresh(); + ret |= test_mb_ecn_ece_cap_derives_rates(); + ret |= test_mb_ecn_ece_cap_zero_keeps_rates(); + ret |= test_mb_ecn_ece_cap_clamps(); + ret |= test_mb_ecn_cap_ttl_reverts(); + ret |= test_mb_ecn_ctrl_per_ctx_ai(); + ret |= test_mb_ecn_ctrl_per_ctx_floor(); + ret |= test_mb_ecn_snd_slow_rate_paced(); + ret |= test_mb_ecn_snd_idle_burst_bound(); + ret |= test_mb_ecn_snd_wait_no_overflow(); + ret |= test_mb_ecn_backlogged_paced(); + ret |= test_mb_ecn_source_limited_ceiling(); + ret |= test_mb_ecn_max_filter(); + ret |= test_mb_ecn_idle_clears_backlogged(); + ret |= test_mb_ecn_first_send_warmup(); + ret |= test_mb_ecn_two_flow_converge(); + ret |= test_mb_ecn_ramp_overshoot_grows_with_rtt(); + ret |= test_mb_ecn_shared_ctx_offered_per_flow(); + ret |= test_mb_ecn_flow_count_restarts_window(); + ret |= test_mb_ecn_ceiling_clears_backlog(); + + return ret; +} diff --git a/src/ipcpd/unicast/cap.c b/src/ipcpd/unicast/cap.c new file mode 100644 index 00000000..67b7967c --- /dev/null +++ b/src/ipcpd/unicast/cap.c @@ -0,0 +1,99 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Link capacity codes + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +/* + * Rate <-> 8-bit code (cap_enc / cap_dec): the high 6 bits hold a + * band e = floor(log2 rate), the low 2 a quarter k splitting the + * band at 256 * 2^(k/4) = {256, 304, 362, 431}; code = 4 * e + k. + * Capacity is only ever needed to order-of-magnitude accuracy. + */ + +#include "cap.h" + +uint8_t cap_enc(uint64_t rate) +{ + static const uint16_t thr[3] = {304, 362, 431}; + uint64_t r = rate; /* copy halved to find band */ + unsigned e = 0; /* band: floor log2 rate */ + unsigned k = 0; /* quarter within band 0..3 */ + unsigned c; /* code = 4 * band + quarter */ + uint16_t top; /* rate scaled to [256, 512) */ + + if (rate == 0) + return 0; + + while (r > 1) { + r >>= 1; + e++; + } + + if (e >= 8) + top = (uint16_t) (rate >> (e - 8)); + else + top = (uint16_t) (rate << (8 - e)); + + while (k < 3 && top >= thr[k]) + k++; + + c = 4 * e + k; + if (c == 0) + c = 1; /* 0 means unknown */ + + return (uint8_t) c; +} + +uint64_t cap_dec(uint8_t c) +{ + static const uint16_t m[4] = {256, 304, 362, 431}; + unsigned e = c >> 2; /* band = c >> 2 */ + unsigned k = c & 3; /* quarter = c & 3 */ + + if (c == 0) + return 0; + + if (e >= 8) + return (uint64_t) m[k] << (e - 8); + + return ((uint64_t) m[k] << e) >> 8; +} + +uint8_t cap_min(uint8_t a, + uint8_t b) +{ + if (a == 0) + return b; + + if (b == 0) + return a; + + return a < b ? a : b; +} + +void cap_stamp(uint8_t * pci, + uint8_t own) +{ + if (own == 0) + return; + + if (*pci == 0 || own < *pci) + *pci = own; +} diff --git a/src/ipcpd/eth/dix.c b/src/ipcpd/unicast/cap.h index cf8253bd..ca6b6355 100644 --- a/src/ipcpd/eth/dix.c +++ b/src/ipcpd/unicast/cap.h @@ -1,7 +1,7 @@ /* * Ouroboros - Copyright (C) 2016 - 2026 * - * IPC processes over Ethernet - DIX + * Link capacity codes * * Dimitri Staessens <dimitri@ouroboros.rocks> * Sander Vrijders <sander@ouroboros.rocks> @@ -20,7 +20,21 @@ * Foundation, Inc., http://www.fsf.org/about/contact/. */ -#define BUILD_ETH_DIX -#define OUROBOROS_PREFIX "ipcpd/eth-dix" +#ifndef OUROBOROS_IPCPD_UNICAST_CAP_H +#define OUROBOROS_IPCPD_UNICAST_CAP_H -#include "eth.c" +#include <stdint.h> + +/* Quarter-log2 capacity code: ~2^(c / 4) bytes/s, 0 = unknown. */ +uint8_t cap_enc(uint64_t rate); + +uint64_t cap_dec(uint8_t c); + +uint8_t cap_min(uint8_t a, + uint8_t b); + +/* MIN-combine the own link code into the PCI byte. */ +void cap_stamp(uint8_t * pci, + uint8_t own); + +#endif /* OUROBOROS_IPCPD_UNICAST_CAP_H */ diff --git a/src/ipcpd/unicast/dir/dht.c b/src/ipcpd/unicast/dir/dht.c index 8eeea800..9d60ce30 100644 --- a/src/ipcpd/unicast/dir/dht.c +++ b/src/ipcpd/unicast/dir/dht.c @@ -1597,6 +1597,7 @@ static ssize_t dht_kv_get_contacts(const uint8_t * key, fail_contact: while (i-- > 0) dht_contact_msg__free_unpacked((*msgs)[i], NULL); + free(*msgs); *msgs = NULL; fail_msgs: @@ -1763,6 +1764,7 @@ static int split_bucket(struct bucket * b) fail_child: while (i-- > 0) bucket_destroy(b->children[i]); + return -1; } @@ -2236,7 +2238,7 @@ static int dht_send_msg(dht_msg_t * msg, dht_msg__pack(msg, ssm_pk_buff_head(spb)); - if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb) < 0) { + if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb, NULL) < 0) { log_warn("%s write failed", DHT_CODE(msg)); goto fail_send; } @@ -2849,6 +2851,7 @@ static dht_msg_t * do_dht_kv_find_node_req(const dht_find_req_msg_t * req) fail_msg: while (len-- > 0) dht_contact_msg__free_unpacked(contacts[len], NULL); + free(contacts); fail_contacts: return NULL; @@ -2951,8 +2954,9 @@ static dht_msg_t * do_dht_kv_find_value_req(const dht_find_req_msg_t * req) fail_msg: freebufs(vals, n_vals); fail_vals: - while (n_contacts-- > 0) + while (contacts != NULL && n_contacts-- > 0) dht_contact_msg__free_unpacked(contacts[n_contacts], NULL); + free(contacts); fail_contacts: return NULL; @@ -3312,6 +3316,42 @@ static int emergency_peer(struct list_head * pl) return -ENOMEM; } +static bool __dht_kv_bucket_has_addr(struct bucket * b, + uint64_t addr) +{ + struct list_head * p; + size_t i; + + assert(b != NULL); + + if (*b->children != NULL) + for (i = 0; i < (1L << DHT_BETA); ++i) + if (__dht_kv_bucket_has_addr(b->children[i], addr)) + return true; + + llist_for_each(p, &b->contacts) { + struct contact * c; + c = list_entry(p, struct contact, next); + if (c->addr == addr) + return true; + } + + return false; +} + +static bool dht_kv_knows_peer(void) +{ + bool found; + + pthread_rwlock_rdlock(&dht.db.lock); + + found = __dht_kv_bucket_has_addr(dht.db.contacts.root, dht.peer); + + pthread_rwlock_unlock(&dht.db.lock); + + return found; +} + static int dht_kv_seed_bootstrap_peer(void) { struct list_head pl; @@ -3323,6 +3363,9 @@ static int dht_kv_seed_bootstrap_peer(void) return 0; } + if (dht_kv_knows_peer()) + return 0; + if (emergency_peer(&pl) < 0) { log_err("Could not create emergency peer."); goto fail_peer; @@ -3338,7 +3381,8 @@ static int dht_kv_seed_bootstrap_peer(void) peer_list_destroy(&pl); - return 0; + /* Sent, but not bootstrapped until the peer is in the DHT. */ + return -EAGAIN; fail_query: peer_list_destroy(&pl); fail_peer: @@ -3427,6 +3471,8 @@ static void value_list_destroy(struct list_head * vl) #define MUST_REPLICATE(v, now) ((now)->tv_sec > (v)->t_repl + dht.t_repl) #define MUST_REPUBLISH(v, now) /* Close to expiry deadline */ \ (((v)->t_exp - (now)->tv_sec) < (DHT_N_REPUB * dht.t_repl)) +/* A local value must be (re)stored if near expiry or never stored. */ +#define MUST_STORE_LVAL(v, now) (MUST_REPUBLISH(v, now) || (v)->t_repl == 0) static void dht_entry_get_repl_lists(const struct dht_entry * e, struct list_head * repl, struct list_head * rebl, @@ -3448,7 +3494,7 @@ static void dht_entry_get_repl_lists(const struct dht_entry * e, llist_for_each(p, &e->lvals) { struct val_entry * v = list_entry(p, struct val_entry, next); - if (MUST_REPLICATE(v, now) && MUST_REPUBLISH(v, now)) { + if (MUST_REPLICATE(v, now) && MUST_STORE_LVAL(v, now)) { /* Add expire time here, to allow creating val_entry */ n = val_entry_create(v->val, now->tv_sec + dht.t_exp); if (n == NULL) @@ -3466,7 +3512,7 @@ static int dht_kv_next_values(uint8_t * key, struct timespec now; struct list_head * p; struct list_head * h; - struct dht_entry * e = NULL; + struct dht_entry * e; assert(key != NULL); assert(repl != NULL); @@ -3479,20 +3525,19 @@ static int dht_kv_next_values(uint8_t * key, pthread_rwlock_rdlock(&dht.db.lock); - if (llist_is_empty(&dht.db.kv.ll)) - goto no_entries; - llist_for_each_safe(p, h, &dht.db.kv.ll) { e = list_entry(p, struct dht_entry, next); - if (IS_CLOSER(e->key, key)) + if (!IS_CLOSER(key, e->key)) continue; /* Already processed */ - } - if (e != NULL) { memcpy(key, e->key, dht.id.len); + dht_entry_get_repl_lists(e, repl, rebl, &now); + + if (!list_is_empty(repl) || !list_is_empty(rebl)) + break; } - no_entries: + pthread_rwlock_unlock(&dht.db.lock); return list_is_empty(repl) && list_is_empty(rebl) ? -ENOENT : 0; @@ -3738,6 +3783,9 @@ static void * work(void * o) log_dbg("DHT worker starting %ld seconds interval.", intv * n); + /* Flush names registered before we had peers to store them. */ + dht_kv_replicate(); + while (true) { int i = 0; while (tasks[i] != NULL) { diff --git a/src/ipcpd/unicast/dir/tests/dht_test.c b/src/ipcpd/unicast/dir/tests/dht_test.c index 1f7026b3..ee6861a0 100644 --- a/src/ipcpd/unicast/dir/tests/dht_test.c +++ b/src/ipcpd/unicast/dir/tests/dht_test.c @@ -796,6 +796,68 @@ static int test_dht_kv_get_values(void) return TEST_RC_FAIL; } +static int test_dht_kv_next_values(void) +{ + struct list_head repl; + struct list_head rebl; + uint8_t * key; + size_t n; + size_t i; + + TEST_START(); + + list_head_init(&repl); + list_head_init(&rebl); + + if (dht_init(&test_dht_config) < 0) { + printf("Failed to create dht.\n"); + goto fail_init; + } + + if (fill_store_with_random_values(NULL, sizeof(uint64_t), 3) < 0) { + printf("Failed to fill store with random values.\n"); + goto fail_fill; + } + + key = dht_dup_key(dht.id.data); + if (key == NULL) { + printf("Failed to duplicate DHT ID.\n"); + goto fail_fill; + } + + n = 0; + + for (i = 0; i < 5; ++i) { + if (dht_kv_next_values(key, &repl, &rebl) < 0) + break; + + ++n; + value_list_destroy(&repl); + value_list_destroy(&rebl); + } + + if (n != 3) { + printf("Failed to visit each entry once (%zu != 3).\n", n); + goto fail_next; + } + + free(key); + + dht_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_next: + free(key); + fail_fill: + dht_fini(); + fail_init: + TEST_FAIL(); + return TEST_RC_FAIL; +} + static int test_dht_kv_find_node_req_msg(void) { dht_msg_t * msg; @@ -1894,6 +1956,7 @@ int dht_test(int argc, rc |= test_dht_kv_contact_list(); rc |= test_dht_kv_update_bucket(); rc |= test_dht_kv_get_values(); + rc |= test_dht_kv_next_values(); rc |= test_dht_kv_find_node_req_msg(); rc |= test_dht_kv_find_node_rsp_msg(); rc |= test_dht_kv_find_node_rsp_msg_contacts(); diff --git a/src/ipcpd/unicast/dt.c b/src/ipcpd/unicast/dt.c index 8f5eb775..84e62f05 100644 --- a/src/ipcpd/unicast/dt.c +++ b/src/ipcpd/unicast/dt.c @@ -31,10 +31,12 @@ #define DT "dt" #define OUROBOROS_PREFIX DT +#include <ouroboros/atomics.h> #include <ouroboros/bitmap.h> #include <ouroboros/errno.h> #include <ouroboros/logs.h> #include <ouroboros/dev.h> +#include <ouroboros/ipcp-dev.h> #include <ouroboros/notifier.h> #include <ouroboros/rib.h> #ifdef IPCP_FLOW_STATS @@ -45,6 +47,7 @@ #include "common/comp.h" #include "common/connmgr.h" #include "ca.h" +#include "cap.h" #include "ipcp.h" #include "dt.h" #include "pff.h" @@ -77,12 +80,14 @@ struct comp_info { #define TTL_LEN 1 #define QOS_LEN 1 #define ECN_LEN 1 +#define CAP_LEN 1 struct dt_pci { uint64_t dst_addr; qoscube_t qc; uint8_t ttl; uint8_t ecn; + uint8_t cap; uint64_t eid; }; @@ -95,6 +100,7 @@ struct { size_t qc_o; size_t ttl_o; size_t ecn_o; + size_t cap_o; size_t eid_o; /* Initial TTL value */ @@ -114,6 +120,7 @@ static void dt_pci_ser(uint8_t * head, memcpy(head + dt_pci_info.qc_o, &dt_pci->qc, QOS_LEN); memcpy(head + dt_pci_info.ttl_o, &ttl, TTL_LEN); memcpy(head + dt_pci_info.ecn_o, &dt_pci->ecn, ECN_LEN); + memcpy(head + dt_pci_info.cap_o, &dt_pci->cap, CAP_LEN); memcpy(head + dt_pci_info.eid_o, &dt_pci->eid, dt_pci_info.eid_size); } @@ -132,6 +139,7 @@ static void dt_pci_des(uint8_t * head, memcpy(&dt_pci->qc, head + dt_pci_info.qc_o, QOS_LEN); memcpy(&dt_pci->ttl, head + dt_pci_info.ttl_o, TTL_LEN); memcpy(&dt_pci->ecn, head + dt_pci_info.ecn_o, ECN_LEN); + memcpy(&dt_pci->cap, head + dt_pci_info.cap_o, CAP_LEN); memcpy(&dt_pci->eid, head + dt_pci_info.eid_o, dt_pci_info.eid_size); } @@ -150,6 +158,7 @@ struct { struct pff * pff[QOS_CUBE_MAX]; struct routing_i * routing[QOS_CUBE_MAX]; #ifdef IPCP_FLOW_STATS + /* Flow stats use lock-free atomics; stamp is the validity flag. */ struct { time_t stamp; uint64_t addr; @@ -167,7 +176,6 @@ struct { size_t w_drp_bytes[QOS_CUBE_MAX]; size_t f_nhp_pkt[QOS_CUBE_MAX]; size_t f_nhp_bytes[QOS_CUBE_MAX]; - pthread_mutex_t lock; } stat[PROC_MAX_FLOWS]; size_t n_flows; @@ -179,11 +187,18 @@ struct { pthread_t listener; } dt; +#ifdef IPCP_FLOW_STATS +#define dt_stat_inc(idx, name, qc, len) \ + do { \ + FETCH_ADD_RELAXED(&dt.stat[idx].name ## _pkt[qc], 1); \ + FETCH_ADD_RELAXED(&dt.stat[idx].name ## _bytes[qc], (len)); \ + } while (0) +#define dt_stat_load(idx, field, qc) LOAD_RELAXED(&dt.stat[idx].field[qc]) + static int dt_rib_read(const char * path, char * buf, size_t len) { -#ifdef IPCP_FLOW_STATS int fd; int i; char str[QOS_BLOCK_LEN + 1]; @@ -192,6 +207,8 @@ static int dt_rib_read(const char * path, char tmstr[RIB_TM_STRLEN]; size_t rxqlen = 0; size_t txqlen = 0; + time_t stamp; + uint64_t addr; struct tm * tm; /* NOTE: we may need stronger checks. */ @@ -205,19 +222,18 @@ static int dt_rib_read(const char * path, buf[0] = '\0'; - pthread_mutex_lock(&dt.stat[fd].lock); - - if (dt.stat[fd].stamp == 0) { - pthread_mutex_unlock(&dt.stat[fd].lock); + stamp = LOAD_ACQUIRE(&dt.stat[fd].stamp); + if (stamp == 0) return 0; - } - if (dt.stat[fd].addr == dt.addr) + addr = LOAD_RELAXED(&dt.stat[fd].addr); + + if (addr == dt.addr) sprintf(addrstr, "%s", dt.comps[fd].name); else - sprintf(addrstr, ADDR_FMT32, ADDR_VAL32(&dt.stat[fd].addr)); + sprintf(addrstr, ADDR_FMT32, ADDR_VAL32(&addr)); - tm = gmtime(&dt.stat[fd].stamp); + tm = gmtime(&stamp); strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); if (fd >= PROC_RES_FDS) { @@ -249,38 +265,29 @@ static int dt_rib_read(const char * path, " failed nhop (packets): %20zu\n" " failed nhop (bytes): %20zu\n", i, - dt.stat[fd].snd_pkt[i], - dt.stat[fd].snd_bytes[i], - dt.stat[fd].rcv_pkt[i], - dt.stat[fd].rcv_bytes[i], - dt.stat[fd].lcl_w_pkt[i], - dt.stat[fd].lcl_w_bytes[i], - dt.stat[fd].lcl_r_pkt[i], - dt.stat[fd].lcl_r_bytes[i], - dt.stat[fd].r_drp_pkt[i], - dt.stat[fd].r_drp_bytes[i], - dt.stat[fd].w_drp_pkt[i], - dt.stat[fd].w_drp_bytes[i], - dt.stat[fd].f_nhp_pkt[i], - dt.stat[fd].f_nhp_bytes[i] + dt_stat_load(fd, snd_pkt, i), + dt_stat_load(fd, snd_bytes, i), + dt_stat_load(fd, rcv_pkt, i), + dt_stat_load(fd, rcv_bytes, i), + dt_stat_load(fd, lcl_w_pkt, i), + dt_stat_load(fd, lcl_w_bytes, i), + dt_stat_load(fd, lcl_r_pkt, i), + dt_stat_load(fd, lcl_r_bytes, i), + dt_stat_load(fd, r_drp_pkt, i), + dt_stat_load(fd, r_drp_bytes, i), + dt_stat_load(fd, w_drp_pkt, i), + dt_stat_load(fd, w_drp_bytes, i), + dt_stat_load(fd, f_nhp_pkt, i), + dt_stat_load(fd, f_nhp_bytes, i) ); strcat(buf, str); } - pthread_mutex_unlock(&dt.stat[fd].lock); - return RIB_FILE_STRLEN; -#else - (void) path; - (void) buf; - (void) len; - return 0; -#endif } static int dt_rib_readdir(char *** buf) { -#ifdef IPCP_FLOW_STATS char entry[RIB_PATH_LEN + 1]; size_t i; int idx = 0; @@ -296,15 +303,9 @@ static int dt_rib_readdir(char *** buf) if (*buf == NULL) goto fail_entries; - for (i = 0; i < PROC_MAX_FLOWS; ++i) { - pthread_mutex_lock(&dt.stat[i].lock); - - if (dt.stat[i].stamp == 0) { - pthread_mutex_unlock(&dt.stat[i].lock); - break; - } - - pthread_mutex_unlock(&dt.stat[i].lock); + for (i = 0; i < PROC_MAX_FLOWS && idx < (int) dt.n_flows; ++i) { + if (LOAD_RELAXED(&dt.stat[i].stamp) == 0) + continue; /* n-1 flows start at PROC_RES_FDS */ sprintf(entry, "%zu", i); @@ -323,43 +324,35 @@ static int dt_rib_readdir(char *** buf) fail_entry: while (idx-- > 0) free((*buf)[idx]); + free(*buf); fail_entries: pthread_rwlock_unlock(&dt.lock); return -ENOMEM; -#else - (void) buf; - return 0; -#endif } static int dt_rib_getattr(const char * path, struct rib_attr * attr) { -#ifdef IPCP_FLOW_STATS int fd; char * entry; + time_t stamp; entry = strstr(path, RIB_SEPARATOR) + 1; assert(entry); fd = atoi(entry); - pthread_mutex_lock(&dt.stat[fd].lock); + stamp = LOAD_ACQUIRE(&dt.stat[fd].stamp); - if (dt.stat[fd].stamp != -1) { + if (stamp != -1) { attr->size = RIB_FILE_STRLEN; - attr->mtime = dt.stat[fd].stamp; + attr->mtime = stamp; } else { attr->size = 0; attr->mtime = 0; } - pthread_mutex_unlock(&dt.stat[fd].lock); -#else - (void) path; - (void) attr; -#endif return 0; } @@ -369,33 +362,49 @@ static struct rib_ops r_ops = { .getattr = dt_rib_getattr }; -#ifdef IPCP_FLOW_STATS -/* - * Hold dt.lock + per-stat together: dt_rib_readdir samples n_flows - * under rdlock and walks stamps under per-stat; updates must be - * atomic w.r.t. that snapshot or the malloc(n_flows) buffer can - * overflow. - */ static void stat_used(int fd, uint64_t addr) { struct timespec now; + int i; clock_gettime(CLOCK_REALTIME_COARSE, &now); pthread_rwlock_wrlock(&dt.lock); - pthread_mutex_lock(&dt.stat[fd].lock); - memset(&dt.stat[fd], 0, sizeof(dt.stat[fd])); + STORE_RELEASE(&dt.stat[fd].stamp, 0); - dt.stat[fd].stamp = (addr != INVALID_ADDR) ? now.tv_sec : 0; - dt.stat[fd].addr = addr; + /* Don't memset: incremented without locks in fast path. */ + for (i = 0; i < QOS_CUBE_MAX; ++i) { + STORE_RELAXED(&dt.stat[fd].snd_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].snd_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].rcv_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].rcv_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].lcl_r_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].lcl_r_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].lcl_w_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].lcl_w_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].r_drp_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].r_drp_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].w_drp_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].w_drp_bytes[i], 0); + STORE_RELAXED(&dt.stat[fd].f_nhp_pkt[i], 0); + STORE_RELAXED(&dt.stat[fd].f_nhp_bytes[i], 0); + } - (addr != INVALID_ADDR) ? ++dt.n_flows : --dt.n_flows; + STORE_RELAXED(&dt.stat[fd].addr, addr); + + if (addr != INVALID_ADDR) { + STORE_RELEASE(&dt.stat[fd].stamp, now.tv_sec); + ++dt.n_flows; + } else { + --dt.n_flows; + } - pthread_mutex_unlock(&dt.stat[fd].lock); pthread_rwlock_unlock(&dt.lock); } +#else +#define dt_stat_inc(idx, name, qc, len) ((void) 0) #endif static void handle_event(void * self, @@ -415,6 +424,8 @@ static void handle_event(void * self, #ifdef IPCP_FLOW_STATS stat_used(fd, c->conn_info.addr); #endif + if (ipcp_flow_cap_arm(fd) < 0) + log_warn("Failed to arm capacity estimator."); psched_add(dt.psched, fd); log_dbg("Added fd %d to packet scheduler.", fd); break; @@ -431,28 +442,27 @@ static void handle_event(void * self, } } -static void packet_handler(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb) +static time_t packet_handler(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb) { struct dt_pci dt_pci; int ret; int ofd; uint8_t * head; size_t len; + size_t qlen; + size_t mlen; + uint8_t lcap; + bool marks; len = ssm_pk_buff_len(spb); #ifndef IPCP_FLOW_STATS - (void) fd; -#else - pthread_mutex_lock(&dt.stat[fd].lock); - - ++dt.stat[fd].rcv_pkt[qc]; - dt.stat[fd].rcv_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[fd].lock); + (void) fd; #endif + dt_stat_inc(fd, rcv, qc, len); + memset(&dt_pci, 0, sizeof(dt_pci)); head = ssm_pk_buff_head(spb); @@ -462,15 +472,8 @@ static void packet_handler(int fd, if (dt_pci.ttl == 0) { log_dbg("TTL was zero."); ipcp_spb_release(spb); -#ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[fd].lock); - - ++dt.stat[fd].r_drp_pkt[qc]; - dt.stat[fd].r_drp_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[fd].lock); -#endif - return; + dt_stat_inc(fd, r_drp, qc, len); + return 0; } /* FIXME: Use qoscube from PCI instead of incoming flow. */ @@ -479,18 +482,19 @@ static void packet_handler(int fd, log_dbg("No next hop for %" PRIu64 ".", dt_pci.dst_addr); ipcp_spb_release(spb); -#ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[fd].lock); + dt_stat_inc(fd, f_nhp, qc, len); + return 0; + } - ++dt.stat[fd].f_nhp_pkt[qc]; - dt.stat[fd].f_nhp_bytes[qc] += len; + marks = ca_marks_ecn(); + qlen = marks ? ipcp_flow_queued(ofd) : 0; + mlen = marks ? ipcp_flow_mean_len(ofd) : 0; + lcap = marks ? cap_enc(ipcp_flow_cap(ofd)) : 0; - pthread_mutex_unlock(&dt.stat[fd].lock); -#endif - return; - } + (void) ca_calc_ecn(qlen, head + dt_pci_info.ecn_o, qc, mlen); - (void) ca_calc_ecn(ofd, head + dt_pci_info.ecn_o, qc, len); + if (marks) + cap_stamp(head + dt_pci_info.cap_o, lcap); ret = ipcp_flow_write(ofd, spb); if (ret < 0) { @@ -498,55 +502,37 @@ static void packet_handler(int fd, if (ret == -EFLOWDOWN) notifier_event(NOTIFY_DT_FLOW_DOWN, &ofd); ipcp_spb_release(spb); -#ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[ofd].lock); - - ++dt.stat[ofd].w_drp_pkt[qc]; - dt.stat[ofd].w_drp_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[ofd].lock); -#endif - return; + dt_stat_inc(ofd, w_drp, qc, len); + return 0; } -#ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[ofd].lock); - ++dt.stat[ofd].snd_pkt[qc]; - dt.stat[ofd].snd_bytes[qc] += len; + dt_stat_inc(ofd, snd, qc, len); - pthread_mutex_unlock(&dt.stat[ofd].lock); -#endif + if (marks) + ipcp_flow_cap_update(ofd, qlen, len); } else { dt_pci_shrink(spb); if (dt_pci.eid >= PROC_RES_FDS) { uint8_t ecn = *(head + dt_pci_info.ecn_o); - fa_np1_rcv(dt_pci.eid, ecn, spb); - return; + uint8_t cap = *(head + dt_pci_info.cap_o); + fa_np1_rcv(dt_pci.eid, ecn, cap, spb); + return 0; } if (dt.comps[dt_pci.eid].post_packet == NULL) { log_err("No registered component on eid %" PRIu64 ".", dt_pci.eid); ipcp_spb_release(spb); - return; + return 0; } -#ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[fd].lock); + dt_stat_inc(fd, lcl_r, qc, len); + dt_stat_inc(dt_pci.eid, snd, qc, len); - ++dt.stat[fd].lcl_r_pkt[qc]; - dt.stat[fd].lcl_r_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[fd].lock); - pthread_mutex_lock(&dt.stat[dt_pci.eid].lock); - - ++dt.stat[dt_pci.eid].snd_pkt[qc]; - dt.stat[dt_pci.eid].snd_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[dt_pci.eid].lock); -#endif dt.comps[dt_pci.eid].post_packet(dt.comps[dt_pci.eid].comp, spb); } + + return 0; } static void * dt_conn_handle(void * o) @@ -573,7 +559,9 @@ int dt_init(struct dt_config cfg) { int i; int j; +#ifdef IPCP_FLOW_STATS char dtstr[RIB_NAME_STRLEN + 1]; +#endif enum pol_pff pp; struct conn_info info; @@ -603,10 +591,11 @@ int dt_init(struct dt_config cfg) dt_pci_info.qc_o = dt_pci_info.addr_size; dt_pci_info.ttl_o = dt_pci_info.qc_o + QOS_LEN; dt_pci_info.ecn_o = dt_pci_info.ttl_o + TTL_LEN; - dt_pci_info.eid_o = dt_pci_info.ecn_o + ECN_LEN; + dt_pci_info.cap_o = dt_pci_info.ecn_o + ECN_LEN; + dt_pci_info.eid_o = dt_pci_info.cap_o + CAP_LEN; dt_pci_info.head_size = dt_pci_info.eid_o + dt_pci_info.eid_size; - if (connmgr_comp_init(COMPID_DT, &info)) { + if (connmgr_comp_init(COMPID_DT, &info) != 0) { log_err("Failed to register with connmgr."); goto fail_connmgr_comp_init; } @@ -646,31 +635,21 @@ int dt_init(struct dt_config cfg) #ifdef IPCP_FLOW_STATS memset(dt.stat, 0, sizeof(dt.stat)); - for (i = 0; i < PROC_MAX_FLOWS; ++i) - if (pthread_mutex_init(&dt.stat[i].lock, NULL)) { - log_err("Failed to init mutex for flow %d.", i); - for (j = 0; j < i; ++j) - pthread_mutex_destroy(&dt.stat[j].lock); - goto fail_stat_lock; - } - dt.n_flows = 0; -#endif + sprintf(dtstr, "%s." ADDR_FMT32, DT, ADDR_VAL32(&dt.addr)); if (rib_reg(dtstr, &r_ops)) { log_err("Failed to register RIB."); goto fail_rib_reg; } +#endif return 0; - fail_rib_reg: #ifdef IPCP_FLOW_STATS - for (i = 0; i < PROC_MAX_FLOWS; ++i) - pthread_mutex_destroy(&dt.stat[i].lock); - fail_stat_lock: -#endif + fail_rib_reg: bmp_destroy(dt.res_fds); +#endif fail_res_fds: pthread_rwlock_destroy(&dt.lock); fail_rwlock_init: @@ -689,14 +668,14 @@ int dt_init(struct dt_config cfg) void dt_fini(void) { +#ifdef IPCP_FLOW_STATS char dtstr[RIB_NAME_STRLEN + 1]; +#endif int i; +#ifdef IPCP_FLOW_STATS sprintf(dtstr, "%s.%" PRIu64, DT, dt.addr); rib_unreg(dtstr); -#ifdef IPCP_FLOW_STATS - for (i = 0; i < PROC_MAX_FLOWS; ++i) - pthread_mutex_destroy(&dt.stat[i].lock); #endif bmp_destroy(dt.res_fds); @@ -813,13 +792,18 @@ void dt_unreg_comp(int eid) int dt_write_packet(uint64_t dst_addr, qoscube_t qc, uint64_t eid, - struct ssm_pk_buff * spb) + struct ssm_pk_buff * spb, + uint8_t * ecn) { struct dt_pci dt_pci; int fd; int ret; uint8_t * head; size_t len; + size_t qlen; + size_t mlen; + uint8_t lcap; + bool marks; assert(spb); assert(dst_addr != dt.addr); @@ -827,28 +811,16 @@ int dt_write_packet(uint64_t dst_addr, #ifdef IPCP_FLOW_STATS len = ssm_pk_buff_len(spb); - if (eid < PROC_RES_FDS) { - pthread_mutex_lock(&dt.stat[eid].lock); - - ++dt.stat[eid].lcl_r_pkt[qc]; - dt.stat[eid].lcl_r_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[eid].lock); - } + if (eid < PROC_RES_FDS) + dt_stat_inc(eid, lcl_r, qc, len); #endif fd = pff_nhop(dt.pff[qc], dst_addr); if (fd < 0) { log_dbg("Could not get nhop for " ADDR_FMT32 ".", ADDR_VAL32(&dst_addr)); #ifdef IPCP_FLOW_STATS - if (eid < PROC_RES_FDS) { - pthread_mutex_lock(&dt.stat[eid].lock); - - ++dt.stat[eid].lcl_r_pkt[qc]; - dt.stat[eid].lcl_r_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[eid].lock); - } + if (eid < PROC_RES_FDS) + dt_stat_inc(eid, lcl_r, qc, len); #endif return -EPERM; } @@ -865,44 +837,46 @@ int dt_write_packet(uint64_t dst_addr, dt_pci.qc = qc; dt_pci.eid = eid; dt_pci.ecn = 0; + dt_pci.cap = 0; + + marks = ca_marks_ecn(); + qlen = marks ? ipcp_flow_queued(fd) : 0; + mlen = marks ? ipcp_flow_mean_len(fd) : 0; + lcap = marks ? cap_enc(ipcp_flow_cap(fd)) : 0; + + (void) ca_calc_ecn(qlen, &dt_pci.ecn, qc, mlen); + + dt_pci.cap = lcap; - (void) ca_calc_ecn(fd, &dt_pci.ecn, qc, len); + if (ecn != NULL) + *ecn = dt_pci.ecn; dt_pci_ser(head, &dt_pci); ret = ipcp_flow_write(fd, spb); if (ret < 0) { - log_dbg("Failed to write packet to fd %d.", fd); + log_dbg("Failed to write packet to fd %d: %d.", fd, ret); if (ret == -EFLOWDOWN) notifier_event(NOTIFY_DT_FLOW_DOWN, &fd); goto fail_write; } #ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[fd].lock); + if (dt_pci.eid < PROC_RES_FDS) + dt_stat_inc(fd, lcl_w, qc, len); - if (dt_pci.eid < PROC_RES_FDS) { - ++dt.stat[fd].lcl_w_pkt[qc]; - dt.stat[fd].lcl_w_bytes[qc] += len; - } - ++dt.stat[fd].snd_pkt[qc]; - dt.stat[fd].snd_bytes[qc] += len; - - pthread_mutex_unlock(&dt.stat[fd].lock); + dt_stat_inc(fd, snd, qc, len); #endif + if (marks) + ipcp_flow_cap_update(fd, qlen, len); + return 0; fail_write: #ifdef IPCP_FLOW_STATS - pthread_mutex_lock(&dt.stat[fd].lock); - - if (eid < PROC_RES_FDS) { - ++dt.stat[fd].lcl_w_pkt[qc]; - dt.stat[fd].lcl_w_bytes[qc] += len; - } - ++dt.stat[fd].w_drp_pkt[qc]; - dt.stat[fd].w_drp_bytes[qc] += len; + if (eid < PROC_RES_FDS) + dt_stat_inc(fd, lcl_w, qc, len); - pthread_mutex_unlock(&dt.stat[fd].lock); + dt_stat_inc(fd, w_drp, qc, len); #endif return -1; } diff --git a/src/ipcpd/unicast/dt.h b/src/ipcpd/unicast/dt.h index a484377d..a055efea 100644 --- a/src/ipcpd/unicast/dt.h +++ b/src/ipcpd/unicast/dt.h @@ -48,6 +48,7 @@ void dt_unreg_comp(int eid); int dt_write_packet(uint64_t dst_addr, qoscube_t qc, uint64_t eid, - struct ssm_pk_buff * spb); + struct ssm_pk_buff * spb, + uint8_t * ecn); #endif /* OUROBOROS_IPCPD_UNICAST_DT_H */ diff --git a/src/ipcpd/unicast/fa.c b/src/ipcpd/unicast/fa.c index c0447885..1c939fab 100644 --- a/src/ipcpd/unicast/fa.c +++ b/src/ipcpd/unicast/fa.c @@ -31,15 +31,19 @@ #define FA "flow-allocator" #define OUROBOROS_PREFIX FA +#include <ouroboros/atomics.h> +#include <ouroboros/dev.h> #include <ouroboros/endian.h> -#include <ouroboros/logs.h> -#include <ouroboros/fqueue.h> #include <ouroboros/errno.h> -#include <ouroboros/dev.h> +#include <ouroboros/fqueue.h> #include <ouroboros/ipcp-dev.h> -#include <ouroboros/rib.h> -#include <ouroboros/random.h> +#include <ouroboros/logs.h> +#include <ouroboros/np1_flow.h> #include <ouroboros/pthread.h> +#include <ouroboros/qoscube.h> +#include <ouroboros/random.h> +#include <ouroboros/rib.h> +#include <ouroboros/time.h> #include "addr-auth.h" #include "dir.h" @@ -61,9 +65,14 @@ #define TIMEOUT 10 * MILLION /* nanoseconds */ #define MSGBUFSZ 32768 -#define FLOW_REQ 0 -#define FLOW_REPLY 1 -#define FLOW_UPDATE 2 +#define FLOW_REQ 0 +#define FLOW_REPLY 1 +#define FLOW_UPDATE 2 +#define FLOW_IRM_UPDATE 3 +#define FLOW_HB 4 +#define FLOW_ACK 5 + +#define HB_ID_LEN 16 /* 128-bit unguessable heartbeat nonce */ #define STAT_FILE_LEN 0 @@ -79,9 +88,11 @@ struct fa_msg { uint32_t max_gap; uint32_t timeout; uint16_t ece; + uint8_t cap; uint8_t code; uint8_t availability; uint8_t service; + uint8_t hb_id[HB_ID_LEN]; /* heartbeat / ack nonce */ } __attribute__((packed)); struct cmd { @@ -89,6 +100,22 @@ struct cmd { struct ssm_pk_buff * spb; }; +#define HB_TBL_TTL (4ULL * BILLION) /* drop unanswered heartbeats */ +#define HB_TBL_MAX 1024 /* cap outstanding heartbeats */ +#define HB_BUCKETS 256 /* nonce hash buckets (pow2) */ + +/* RIB flow entry: the CA stats string plus the flow header. */ +#define FA_RIB_STRLEN (CA_STATS_STRLEN + 512) + +/* Outstanding heartbeat: send time kept locally, keyed by the nonce. */ +struct hb_ent { + uint8_t id[HB_ID_LEN]; + uint64_t s_eid; /* originating flow (fd-reuse guard) */ + uint64_t t_snd; /* send timestamp (ns) */ + struct list_head hnext; /* nonce hash bucket chain */ + struct list_head qnext; /* expiry FIFO, oldest at head */ +}; + struct fa_flow { #ifdef IPCP_FLOW_STATS time_t stamp; /* Flow creation */ @@ -103,10 +130,12 @@ struct fa_flow { size_t u_snd; /* Flow updates sent */ size_t u_rcv; /* Flow updates received */ #endif - uint64_t s_eid; /* Local endpoint id */ - uint64_t r_eid; /* Remote endpoint id */ + uint64_t s_eid; /* Local PoA id */ + uint64_t r_eid; /* Remote PoA id */ uint64_t r_addr; /* Remote address */ void * ctx; /* Congestion avoidance context */ + uint64_t fair; /* SFQ virtual finish tag (bytes) */ + uint8_t l_ecn; /* Local first-hop mark (relaxed) */ }; struct { @@ -122,21 +151,26 @@ struct { pthread_mutex_t mtx; pthread_t worker; + struct list_head hb_bkt[HB_BUCKETS]; /* nonce hash buckets */ + struct list_head hb_q; /* expiry FIFO, oldest at head */ + size_t n_hbs; + pthread_mutex_t hb_mtx; + struct psched * psched; } fa; +#ifdef IPCP_FLOW_STATS static int fa_rib_read(const char * path, char * buf, size_t len) { -#ifdef IPCP_FLOW_STATS struct fa_flow * flow; int fd; char r_addrstr[21]; char s_eidstr[21]; char r_eidstr[21]; char tmstr[RIB_TM_STRLEN]; - char castr[1024]; + char castr[CA_STATS_STRLEN]; char * entry; struct tm * tm; @@ -148,7 +182,7 @@ static int fa_rib_read(const char * path, if (fd < 0 || fd >= PROC_MAX_FLOWS) return -1; - if (len < 1536) + if (len < FA_RIB_STRLEN) return 0; flow = &fa.flows[fd]; @@ -169,13 +203,13 @@ static int fa_rib_read(const char * path, tm = gmtime(&flow->stamp); strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); - ca_print_stats(flow->ctx, castr, 1024); + ca_print_stats(flow->ctx, castr, CA_STATS_STRLEN); sprintf(buf, "Flow established at: %20s\n" "Remote address: %20s\n" - "Local endpoint ID: %20s\n" - "Remote endpoint ID: %20s\n" + "Local PoA ID: %20s\n" + "Remote PoA ID: %20s\n" "Sent (packets): %20zu\n" "Sent (bytes): %20zu\n" "Send failed (packets): %20zu\n" @@ -199,17 +233,10 @@ static int fa_rib_read(const char * path, pthread_rwlock_unlock(&fa.flows_lock); return strlen(buf); -#else - (void) path; - (void) buf; - (void) len; - return 0; -#endif } static int fa_rib_readdir(char *** buf) { -#ifdef IPCP_FLOW_STATS char entry[RIB_PATH_LEN + 1]; size_t i; int idx = 0; @@ -250,20 +277,16 @@ static int fa_rib_readdir(char *** buf) fail_entry: while (idx-- > 0) free((*buf)[idx]); + free(*buf); fail_entries: pthread_rwlock_unlock(&fa.flows_lock); return -ENOMEM; -#else - (void) buf; - return 0; -#endif } static int fa_rib_getattr(const char * path, struct rib_attr * attr) { -#ifdef IPCP_FLOW_STATS int fd; char * entry; struct fa_flow * flow; @@ -278,7 +301,7 @@ static int fa_rib_getattr(const char * path, pthread_rwlock_rdlock(&fa.flows_lock); if (flow->stamp != 0) { - attr->size = 1536; + attr->size = FA_RIB_STRLEN; attr->mtime = flow->stamp; } else { attr->size = 0; @@ -286,10 +309,7 @@ static int fa_rib_getattr(const char * path, } pthread_rwlock_unlock(&fa.flows_lock); -#else - (void) path; - (void) attr; -#endif + return 0; } @@ -298,6 +318,7 @@ static struct rib_ops r_ops = { .readdir = fa_rib_readdir, .getattr = fa_rib_getattr }; +#endif /* IPCP_FLOW_STATS */ static int eid_to_fd(uint64_t eid) { @@ -329,18 +350,140 @@ static uint64_t gen_eid(int fd) return ((uint64_t) rnd << 32) + fd; } -static void packet_handler(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb) +/* The nonce is uniformly random, so its low word is a fine hash. */ +static size_t fa_hb_hash(const uint8_t * id) +{ + uint32_t h; + + memcpy(&h, id, sizeof(h)); + + return h & (HB_BUCKETS - 1); +} + +/* Record an outstanding heartbeat; expire stale entries as we go. */ +static void fa_hb_record(const uint8_t * id, + uint64_t s_eid, + uint64_t t_snd) +{ + struct hb_ent * ent; + struct list_head * p; + struct list_head * h; + + ent = malloc(sizeof(*ent)); + if (ent == NULL) + return; + + memcpy(ent->id, id, HB_ID_LEN); + ent->s_eid = s_eid; + ent->t_snd = t_snd; + + pthread_mutex_lock(&fa.hb_mtx); + + /* The FIFO is time-ordered; stop at the first fresh entry. */ + list_for_each_safe(p, h, &fa.hb_q) { + struct hb_ent * e = list_entry(p, struct hb_ent, qnext); + if (t_snd - e->t_snd <= HB_TBL_TTL) + break; + list_del(&e->hnext); + list_del(&e->qnext); + free(e); + fa.n_hbs--; + } + + if (fa.n_hbs >= HB_TBL_MAX) { + pthread_mutex_unlock(&fa.hb_mtx); + free(ent); + return; + } + + list_add(&ent->hnext, &fa.hb_bkt[fa_hb_hash(id)]); + list_add_tail(&ent->qnext, &fa.hb_q); + fa.n_hbs++; + + pthread_mutex_unlock(&fa.hb_mtx); +} + +/* Consume a heartbeat nonce, returning the flow and send time it maps to. */ +static int fa_hb_match(const uint8_t * id, + uint64_t * s_eid, + uint64_t * t_snd) +{ + struct list_head * bkt; + struct list_head * p; + struct list_head * h; + + pthread_mutex_lock(&fa.hb_mtx); + + bkt = &fa.hb_bkt[fa_hb_hash(id)]; + list_for_each_safe(p, h, bkt) { + struct hb_ent * e = list_entry(p, struct hb_ent, hnext); + if (memcmp(e->id, id, HB_ID_LEN) == 0) { + *s_eid = e->s_eid; + *t_snd = e->t_snd; + list_del(&e->hnext); + list_del(&e->qnext); + free(e); + fa.n_hbs--; + pthread_mutex_unlock(&fa.hb_mtx); + return 0; + } + } + + pthread_mutex_unlock(&fa.hb_mtx); + + return -1; +} + +/* Send a bare control message (heartbeat or ack) carrying only a nonce. */ +static int fa_send_ctrl(uint64_t r_addr, + uint8_t code, + const uint8_t * id) +{ + struct fa_msg * msg; + struct ssm_pk_buff * spb; + qoscube_t qc = QOS_CUBE_BE; + + if (ipcp_spb_reserve(&spb, sizeof(*msg))) + return -1; + + msg = (struct fa_msg *) ssm_pk_buff_head(spb); + memset(msg, 0, sizeof(*msg)); + + msg->code = code; + msg->s_addr = hton64(addr_auth_address()); + memcpy(msg->hb_id, id, HB_ID_LEN); + + if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) { + ipcp_spb_release(spb); + return -1; + } + + return 0; +} + +static time_t packet_handler(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb) { struct fa_flow * flow; + struct timespec tv; + uint64_t now; uint64_t r_addr; uint64_t r_eid; - ca_wnd_t wnd; + uint64_t s_eid; + bool hb; + uint8_t nonce[HB_ID_LEN]; + time_t wait; size_t len; + uint8_t ecn; flow = &fa.flows[fd]; + ecn = 0; + + clock_gettime(PTHREAD_COND_CLOCK, &tv); + now = TS_TO_UINT64(tv); + pthread_rwlock_wrlock(&fa.flows_lock); len = ssm_pk_buff_len(spb); @@ -349,16 +492,23 @@ static void packet_handler(int fd, ++flow->p_snd; flow->b_snd += len; #endif - wnd = ca_ctx_update_snd(flow->ctx, len); + wait = ca_ctx_update_snd(flow->ctx, len, + LOAD_RELAXED(&flow->l_ecn), &flow->fair); + hb = ca_ctx_hb_due(flow->ctx, now); r_addr = flow->r_addr; r_eid = flow->r_eid; + s_eid = flow->s_eid; pthread_rwlock_unlock(&fa.flows_lock); - ca_wnd_wait(wnd); + if (hb && random_buffer(nonce, HB_ID_LEN) == 0) { + fa_hb_record(nonce, s_eid, now); + fa_send_ctrl(r_addr, FLOW_HB, nonce); + } - if (dt_write_packet(r_addr, qc, r_eid, spb)) { + if (dt_write_packet(r_addr, qc, r_eid, spb, &ecn)) { + STORE_RELAXED(&flow->l_ecn, ecn); ipcp_spb_release(spb); log_dbg("Failed to forward packet."); #ifdef IPCP_FLOW_STATS @@ -367,8 +517,12 @@ static void packet_handler(int fd, flow->b_snd_f += len; pthread_rwlock_unlock(&fa.flows_lock); #endif - return; + return wait; } + + STORE_RELAXED(&flow->l_ecn, ecn); + + return wait; } static int fa_flow_init(struct fa_flow * flow) @@ -382,9 +536,7 @@ static int fa_flow_init(struct fa_flow * flow) flow->s_eid = -1; flow->r_addr = INVALID_ADDR; - flow->ctx = ca_ctx_create(); - if (flow->ctx == NULL) - return -1; + /* ctx is acquired once (r_addr, qc) are known (ca_ctx_get). */ #ifdef IPCP_FLOW_STATS clock_gettime(CLOCK_REALTIME_COARSE, &now); @@ -398,7 +550,8 @@ static int fa_flow_init(struct fa_flow * flow) static void fa_flow_fini(struct fa_flow * flow) { - ca_ctx_destroy(flow->ctx); + if (flow->ctx != NULL) + ca_ctx_put(flow->ctx); memset(flow, 0, sizeof(*flow)); @@ -500,6 +653,9 @@ static int fa_handle_flow_req(struct fa_msg * msg, qs.max_gap = ntoh32(msg->max_gap); qs.timeout = ntoh32(msg->timeout); + /* Ack the seed nonce now: a clean RTT, before any accept delay. */ + fa_send_ctrl(ntoh64(msg->s_addr), FLOW_ACK, msg->hb_id); + fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UNICAST_MPL, IPCP_UNICAST_MTU, &data); if (fd < 0) @@ -515,6 +671,13 @@ static int fa_handle_flow_req(struct fa_msg * msg, flow->r_eid = ntoh64(msg->s_eid); flow->r_addr = ntoh64(msg->s_addr); + flow->ctx = ca_ctx_get(flow->r_addr, qos_spec_to_cube(qs)); + if (flow->ctx == NULL) { + fa_flow_fini(flow); + pthread_rwlock_unlock(&fa.flows_lock); + return -ENOMEM; + } + pthread_rwlock_unlock(&fa.flows_lock); return fd; @@ -592,13 +755,93 @@ static int fa_handle_flow_update(struct fa_msg * msg, #ifdef IPCP_FLOW_STATS flow->u_rcv++; #endif - ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece)); + ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece), msg->cap); pthread_rwlock_unlock(&fa.flows_lock); return 0; } +/* Heartbeat: reflect the nonce straight back to the sender's address. */ +static int fa_handle_flow_hb(struct fa_msg * msg, + size_t len) +{ + if (len < sizeof(*msg)) + return -EINVAL; + + return fa_send_ctrl(ntoh64(msg->s_addr), FLOW_ACK, msg->hb_id); +} + +/* Ack: the reflected nonce yields an RTT sample for its path. */ +static int fa_handle_flow_ack(struct fa_msg * msg, + size_t len) +{ + struct timespec tv; + struct fa_flow * flow; + uint64_t now; + uint64_t t_snd; + uint64_t s_eid; + int fd; + + if (len < sizeof(*msg)) + return -EINVAL; + + if (fa_hb_match(msg->hb_id, &s_eid, &t_snd) < 0) + return 0; /* unknown or stale nonce */ + + clock_gettime(PTHREAD_COND_CLOCK, &tv); + now = TS_TO_UINT64(tv); + + pthread_rwlock_wrlock(&fa.flows_lock); + + fd = eid_to_fd(s_eid); + if (fd >= 0 && now > t_snd) { + flow = &fa.flows[fd]; + ca_ctx_rtt(flow->ctx, now, now - t_snd); + } + + pthread_rwlock_unlock(&fa.flows_lock); + + return 0; +} + +static int fa_handle_flow_irm_update(struct fa_msg * msg, + size_t len) +{ + buffer_t data; + int fd; + int flow_id; + + if (len < sizeof(*msg)) + return -EINVAL; + + data.data = (uint8_t *) msg + sizeof(*msg); + data.len = len - sizeof(*msg); + + pthread_rwlock_rdlock(&fa.flows_lock); + + fd = eid_to_fd(ntoh64(msg->r_eid)); + + pthread_rwlock_unlock(&fa.flows_lock); + + if (fd < 0) { + log_err("Flow update for unknown EID %" PRIu64 ".", + ntoh64(msg->r_eid)); + return -ENOTALLOC; + } + + flow_id = np1_flow_id(fd); + if (flow_id < 0) + return -ENOTALLOC; + + if (ipcp_flow_update_arr(flow_id, &data) < 0) { + log_err("Failed to relay flow update on fd %d.", fd); + return -EIRMD; + } + + return 0; +} + static void * fa_handle_packet(void * o) { (void) o; @@ -627,6 +870,18 @@ static void * fa_handle_packet(void * o) if (fa_handle_flow_update(msg, len) < 0) log_err("Error handling flow update."); break; + case FLOW_IRM_UPDATE: + if (fa_handle_flow_irm_update(msg, len) < 0) + log_err("Error handling flow update."); + break; + case FLOW_HB: + if (fa_handle_flow_hb(msg, len) < 0) + log_err("Error handling heartbeat."); + break; + case FLOW_ACK: + if (fa_handle_flow_ack(msg, len) < 0) + log_err("Error handling heartbeat ack."); + break; default: log_warn("Recieved unknown flow allocation message."); break; @@ -636,45 +891,62 @@ static void * fa_handle_packet(void * o) return (void *) 0; } -int fa_init(void) +int fa_init(uint16_t max_rtt) { pthread_condattr_t cattr; + size_t i; + + ipcp_flow_set_max_rtt(max_rtt); - if (pthread_rwlock_init(&fa.flows_lock, NULL)) + if (pthread_rwlock_init(&fa.flows_lock, NULL) != 0) goto fail_rwlock; - if (pthread_mutex_init(&fa.mtx, NULL)) + if (pthread_mutex_init(&fa.mtx, NULL) != 0) goto fail_mtx; - if (pthread_condattr_init(&cattr)) + if (pthread_mutex_init(&fa.hb_mtx, NULL) != 0) + goto fail_hb_mtx; + + if (pthread_condattr_init(&cattr) != 0) goto fail_cattr; #ifndef __APPLE__ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); #endif - if (pthread_cond_init(&fa.cond, &cattr)) + if (pthread_cond_init(&fa.cond, &cattr) != 0) goto fail_cond; - if (rib_reg(FA, &r_ops)) +#ifdef IPCP_FLOW_STATS + if (rib_reg(FA, &r_ops) != 0) goto fail_rib_reg; +#endif fa.eid = dt_reg_comp(&fa, &fa_post_packet, FA); if ((int) fa.eid < 0) goto fail_dt_reg; list_head_init(&fa.cmds); + for (i = 0; i < HB_BUCKETS; i++) + list_head_init(&fa.hb_bkt[i]); + + list_head_init(&fa.hb_q); + fa.n_hbs = 0; pthread_condattr_destroy(&cattr); return 0; fail_dt_reg: +#ifdef IPCP_FLOW_STATS rib_unreg(FA); fail_rib_reg: +#endif pthread_cond_destroy(&fa.cond); fail_cond: pthread_condattr_destroy(&cattr); fail_cattr: + pthread_mutex_destroy(&fa.hb_mtx); + fail_hb_mtx: pthread_mutex_destroy(&fa.mtx); fail_mtx: pthread_rwlock_destroy(&fa.flows_lock); @@ -684,9 +956,20 @@ int fa_init(void) void fa_fini(void) { + struct list_head * p; + struct list_head * h; + +#ifdef IPCP_FLOW_STATS rib_unreg(FA); +#endif + list_for_each_safe(p, h, &fa.hb_q) { + struct hb_ent * e = list_entry(p, struct hb_ent, qnext); + list_del(&e->qnext); + free(e); + } pthread_cond_destroy(&fa.cond);; + pthread_mutex_destroy(&fa.hb_mtx); pthread_mutex_destroy(&fa.mtx); pthread_rwlock_destroy(&fa.flows_lock); } @@ -769,6 +1052,8 @@ int fa_alloc(int fd, qoscube_t qc = QOS_CUBE_BE; size_t len; uint64_t eid; + struct timespec tv; + uint8_t nonce[HB_ID_LEN]; addr = dir_query(dst); if (addr == 0) @@ -796,11 +1081,18 @@ int fa_alloc(int fd, msg->max_gap = hton32(qs.max_gap); msg->timeout = hton32(qs.timeout); + /* Seed an early RTT sample: peer acks this nonce on arrival. */ + if (random_buffer(nonce, HB_ID_LEN) == 0) { + memcpy(msg->hb_id, nonce, HB_ID_LEN); + clock_gettime(PTHREAD_COND_CLOCK, &tv); + fa_hb_record(nonce, eid, TS_TO_UINT64(tv)); + } + memcpy(msg + 1, dst, ipcp_dir_hash_len()); if (data->len > 0) memcpy(ssm_pk_buff_head(spb) + len, data->data, data->len); - if (dt_write_packet(addr, qc, fa.eid, spb)) { + if (dt_write_packet(addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow allocation request packet."); ipcp_spb_release(spb); return -1; @@ -814,6 +1106,13 @@ int fa_alloc(int fd, flow->r_addr = addr; flow->s_eid = eid; + flow->ctx = ca_ctx_get(addr, qos_spec_to_cube(qs)); + if (flow->ctx == NULL) { + fa_flow_fini(flow); + pthread_rwlock_unlock(&fa.flows_lock); + return -1; + } + pthread_rwlock_unlock(&fa.flows_lock); return 0; @@ -856,7 +1155,7 @@ int fa_alloc_resp(int fd, pthread_rwlock_unlock(&fa.flows_lock); - if (dt_write_packet(flow->r_addr, qc, fa.eid, spb)) { + if (dt_write_packet(flow->r_addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow allocation response packet."); goto fail_packet; } @@ -881,6 +1180,44 @@ int fa_alloc_resp(int fd, return -1; } +int fa_irm_update(int fd, + const buffer_t * data) +{ + struct fa_msg * msg; + struct ssm_pk_buff * spb; + struct fa_flow * flow; + qoscube_t qc = QOS_CUBE_BE; + uint64_t r_addr; + + flow = &fa.flows[fd]; + + if (ipcp_spb_reserve(&spb, sizeof(*msg) + data->len)) + return -1; + + msg = (struct fa_msg *) ssm_pk_buff_head(spb); + memset(msg, 0, sizeof(*msg)); + + msg->code = FLOW_IRM_UPDATE; + if (data->len > 0) + memcpy(msg + 1, data->data, data->len); + + pthread_rwlock_rdlock(&fa.flows_lock); + + msg->r_eid = hton64(flow->r_eid); + msg->s_eid = hton64(flow->s_eid); + r_addr = flow->r_addr; + + pthread_rwlock_unlock(&fa.flows_lock); + + if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) { + log_err("Failed to send flow update packet."); + ipcp_spb_release(spb); + return -1; + } + + return 0; +} + int fa_dealloc(int fd) { if (ipcp_flow_fini(fd) < 0) @@ -900,7 +1237,8 @@ int fa_dealloc(int fd) } static int fa_update_remote(int fd, - uint16_t ece) + uint16_t ece, + uint8_t cap) { struct fa_msg * msg; struct ssm_pk_buff * spb; @@ -924,6 +1262,7 @@ static int fa_update_remote(int fd, msg->code = FLOW_UPDATE; msg->r_eid = hton64(flow->r_eid); msg->ece = hton16(ece); + msg->cap = cap; r_addr = flow->r_addr; #ifdef IPCP_FLOW_STATS @@ -932,7 +1271,7 @@ static int fa_update_remote(int fd, pthread_rwlock_unlock(&fa.flows_lock); - if (dt_write_packet(r_addr, qc, fa.eid, spb)) { + if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) { log_err("Failed to send flow update packet."); ipcp_spb_release(spb); return -1; @@ -943,11 +1282,13 @@ static int fa_update_remote(int fd, void fa_np1_rcv(uint64_t eid, uint8_t ecn, + uint8_t cap, struct ssm_pk_buff * spb) { struct fa_flow * flow; bool update; uint16_t ece; + uint8_t fcap; int fd; size_t len; @@ -969,7 +1310,7 @@ void fa_np1_rcv(uint64_t eid, ++flow->p_rcv; flow->b_rcv += len; #endif - update = ca_ctx_update_rcv(flow->ctx, len, ecn, &ece); + update = ca_ctx_update_rcv(flow->ctx, len, ecn, cap, &ece, &fcap); pthread_rwlock_unlock(&fa.flows_lock); @@ -985,5 +1326,5 @@ void fa_np1_rcv(uint64_t eid, } if (update) - fa_update_remote(eid, ece); + fa_update_remote(eid, ece, fcap); } diff --git a/src/ipcpd/unicast/fa.h b/src/ipcpd/unicast/fa.h index 0c19dc25..504d67d3 100644 --- a/src/ipcpd/unicast/fa.h +++ b/src/ipcpd/unicast/fa.h @@ -26,7 +26,7 @@ #include <ouroboros/qos.h> #include <ouroboros/utils.h> -int fa_init(void); +int fa_init(uint16_t max_rtt); void fa_fini(void); @@ -45,8 +45,12 @@ int fa_alloc_resp(int fd, int fa_dealloc(int fd); +int fa_irm_update(int fd, + const buffer_t * data); + void fa_np1_rcv(uint64_t eid, uint8_t ecn, + uint8_t cap, struct ssm_pk_buff * spb); #endif /* OUROBOROS_IPCPD_UNICAST_FA_H */ diff --git a/src/ipcpd/unicast/main.c b/src/ipcpd/unicast/main.c index 9a35531e..320ce165 100644 --- a/src/ipcpd/unicast/main.c +++ b/src/ipcpd/unicast/main.c @@ -35,6 +35,7 @@ #include <ouroboros/ipcp-dev.h> #include <ouroboros/logs.h> #include <ouroboros/notifier.h> +#include <ouroboros/qos.h> #include <ouroboros/random.h> #include <ouroboros/rib.h> #include <ouroboros/time.h> @@ -67,7 +68,7 @@ static int initialize_components(struct ipcp_config * conf) log_info("IPCP got address %" PRIu64 ".", addr_auth_address()); - if (ca_init(conf->unicast.cong_avoid)) { + if (ca_init(conf->unicast.cong_avoid, conf->unicast.dt.max_rtt)) { log_err("Failed to initialize congestion avoidance."); goto fail_ca; } @@ -84,7 +85,7 @@ static int initialize_components(struct ipcp_config * conf) goto fail_dir; } - if (fa_init()) { + if (fa_init(conf->unicast.dt.max_rtt)) { log_err("Failed to initialize flow allocator component."); goto fail_fa; } @@ -175,12 +176,14 @@ static void stop_components(void) ipcp_set_state(IPCP_BOOT); } -static int unicast_ipcp_enroll(const char * dst, - struct layer_info * info) +static int unicast_ipcp_enroll(const char * dst, + const struct poa_addr * addr, + struct layer_info * info) { struct ipcp_config * conf; struct conn conn; uint8_t id[ENROLL_ID_LEN]; + qosspec_t qs = qos_msg; if (random_buffer(id, ENROLL_ID_LEN) < 0) { log_err("Failed to generate enrollment ID."); @@ -189,7 +192,7 @@ static int unicast_ipcp_enroll(const char * dst, log_info_id(id, "Requesting enrollment."); - if (connmgr_alloc(COMPID_ENROLL, dst, NULL, &conn) < 0) { + if (connmgr_alloc(COMPID_ENROLL, dst, &qs, addr, &conn) < 0) { log_err_id(id, "Failed to get connection."); goto fail_id; } @@ -273,7 +276,8 @@ static struct ipcp_ops unicast_ops = { .ipcp_flow_alloc = fa_alloc, .ipcp_flow_join = NULL, .ipcp_flow_alloc_resp = fa_alloc_resp, - .ipcp_flow_dealloc = fa_dealloc + .ipcp_flow_dealloc = fa_dealloc, + .ipcp_flow_update = fa_irm_update }; int main(int argc, diff --git a/src/ipcpd/unicast/pff/alternate.c b/src/ipcpd/unicast/pff/alternate.c index be1c35c0..1c508c1b 100644 --- a/src/ipcpd/unicast/pff/alternate.c +++ b/src/ipcpd/unicast/pff/alternate.c @@ -211,7 +211,7 @@ struct pff_i * alternate_pff_create(void) if (pthread_rwlock_init(&tmp->lock, NULL)) goto fail_lock; - tmp->pft = pft_create(PFT_SIZE, false); + tmp->pft = pft_create(PFT_SIZE); if (tmp->pft == NULL) goto fail_pft; diff --git a/src/ipcpd/unicast/pff/multipath.c b/src/ipcpd/unicast/pff/multipath.c index c636e789..c2c21078 100644 --- a/src/ipcpd/unicast/pff/multipath.c +++ b/src/ipcpd/unicast/pff/multipath.c @@ -49,7 +49,7 @@ struct pff_ops multipath_pff_ops = { .del = multipath_pff_del, .flush = multipath_pff_flush, .nhop = multipath_pff_nhop, - .flow_state_change = NULL + .flow_state_change = multipath_pff_flow_state_change }; struct pff_i * multipath_pff_create(void) @@ -63,7 +63,7 @@ struct pff_i * multipath_pff_create(void) if (pthread_rwlock_init(&tmp->lock, NULL)) goto fail_rwlock; - tmp->pft = pft_create(PFT_SIZE, false); + tmp->pft = pft_create(PFT_SIZE); if (tmp->pft == NULL) goto fail_pft; @@ -170,6 +170,24 @@ void multipath_pff_flush(struct pff_i * pff_i) pft_flush(pff_i->pft); } +int multipath_pff_flow_state_change(struct pff_i * pff_i, + int fd, + bool up) +{ + assert(pff_i); + + if (up) + return 0; + + pthread_rwlock_wrlock(&pff_i->lock); + + pft_del_fd(pff_i->pft, fd); + + pthread_rwlock_unlock(&pff_i->lock); + + return 0; +} + int multipath_pff_nhop(struct pff_i * pff_i, uint64_t addr) { diff --git a/src/ipcpd/unicast/pff/multipath.h b/src/ipcpd/unicast/pff/multipath.h index 5329f7fc..123030b6 100644 --- a/src/ipcpd/unicast/pff/multipath.h +++ b/src/ipcpd/unicast/pff/multipath.h @@ -53,6 +53,10 @@ void multipath_pff_flush(struct pff_i * pff_i); int multipath_pff_nhop(struct pff_i * pff_i, uint64_t addr); +int multipath_pff_flow_state_change(struct pff_i * pff_i, + int fd, + bool up); + extern struct pff_ops multipath_pff_ops; #endif /* OUROBOROS_IPCPD_UNICAST_MULTIPATH_PFF_H */ diff --git a/src/ipcpd/unicast/pff/pft.c b/src/ipcpd/unicast/pff/pft.c index a0d70799..2a295a40 100644 --- a/src/ipcpd/unicast/pff/pft.c +++ b/src/ipcpd/unicast/pff/pft.c @@ -43,12 +43,10 @@ struct pft_entry { struct pft { struct list_head * buckets; - bool hash_key; uint64_t buckets_size; }; -struct pft * pft_create(uint64_t buckets, - bool hash_key) +struct pft * pft_create(uint64_t buckets) { struct pft * tmp; unsigned int i; @@ -69,7 +67,6 @@ struct pft * pft_create(uint64_t buckets, if (tmp == NULL) return NULL; - tmp->hash_key = hash_key; tmp->buckets_size = buckets; tmp->buckets = malloc(buckets * sizeof(*tmp->buckets)); @@ -94,18 +91,36 @@ void pft_destroy(struct pft * pft) free(pft); } -void pft_flush(struct pft * pft) +void pft_del_fd(struct pft * pft, + int fd) { unsigned int i; struct list_head * p; struct list_head * h; struct pft_entry * entry; + size_t j; + size_t n; assert(pft); for (i = 0; i < pft->buckets_size; i++) { list_for_each_safe(p, h, &(pft->buckets[i])) { entry = list_entry(p, struct pft_entry, next); + + n = 0; + for (j = 0; j < entry->len; j++) { + if (entry->fds[j] != fd) + entry->fds[n++] = entry->fds[j]; + } + + if (n == entry->len) + continue; + + if (n > 0) { + entry->len = n; + continue; + } + list_del(&entry->next); free(entry->fds); free(entry); @@ -113,22 +128,29 @@ void pft_flush(struct pft * pft) } } -static uint64_t hash(uint64_t key) +void pft_flush(struct pft * pft) { - uint64_t res[2]; + unsigned int i; + struct list_head * p; + struct list_head * h; + struct pft_entry * entry; - mem_hash(HASH_MD5, res, (uint8_t *) &key, sizeof(key)); + assert(pft); - return res[0]; + for (i = 0; i < pft->buckets_size; i++) { + list_for_each_safe(p, h, &(pft->buckets[i])) { + entry = list_entry(p, struct pft_entry, next); + list_del(&entry->next); + free(entry->fds); + free(entry); + } + } } static uint64_t calc_key(struct pft * pft, uint64_t dst) { - if (pft->hash_key) - dst = hash(dst); - - return (dst & (pft->buckets_size - 1)); + return hash_mix64(dst) & (pft->buckets_size - 1); } int pft_insert(struct pft * pft, diff --git a/src/ipcpd/unicast/pff/pft.h b/src/ipcpd/unicast/pff/pft.h index 3bb9cff7..3517e0ef 100644 --- a/src/ipcpd/unicast/pff/pft.h +++ b/src/ipcpd/unicast/pff/pft.h @@ -24,19 +24,20 @@ #define OUROBOROS_PFT_H #include <stdint.h> -#include <stdbool.h> #include <stdlib.h> struct pft; /* Buckets is rounded up to the nearest power of 2 */ -struct pft * pft_create(uint64_t buckets, - bool hash_key); +struct pft * pft_create(uint64_t buckets); void pft_destroy(struct pft * table); void pft_flush(struct pft * table); +void pft_del_fd(struct pft * table, + int fd); + /* Passes ownership of the block of memory */ int pft_insert(struct pft * pft, uint64_t dst, diff --git a/src/ipcpd/unicast/pff/simple.c b/src/ipcpd/unicast/pff/simple.c index be542bdb..4347dcba 100644 --- a/src/ipcpd/unicast/pff/simple.c +++ b/src/ipcpd/unicast/pff/simple.c @@ -47,7 +47,7 @@ struct pff_ops simple_pff_ops = { .del = simple_pff_del, .flush = simple_pff_flush, .nhop = simple_pff_nhop, - .flow_state_change = NULL + .flow_state_change = simple_pff_flow_state_change }; struct pff_i * simple_pff_create(void) @@ -63,7 +63,7 @@ struct pff_i * simple_pff_create(void) return NULL; } - tmp->pft = pft_create(PFT_SIZE, false); + tmp->pft = pft_create(PFT_SIZE); if (tmp->pft == NULL) { pthread_rwlock_destroy(&tmp->lock); free(tmp); @@ -170,6 +170,24 @@ void simple_pff_flush(struct pff_i * pff_i) pft_flush(pff_i->pft); } +int simple_pff_flow_state_change(struct pff_i * pff_i, + int fd, + bool up) +{ + assert(pff_i); + + if (up) + return 0; + + pthread_rwlock_wrlock(&pff_i->lock); + + pft_del_fd(pff_i->pft, fd); + + pthread_rwlock_unlock(&pff_i->lock); + + return 0; +} + int simple_pff_nhop(struct pff_i * pff_i, uint64_t addr) { diff --git a/src/ipcpd/unicast/pff/simple.h b/src/ipcpd/unicast/pff/simple.h index 1046e4c4..b72aba21 100644 --- a/src/ipcpd/unicast/pff/simple.h +++ b/src/ipcpd/unicast/pff/simple.h @@ -52,6 +52,10 @@ void simple_pff_flush(struct pff_i * pff_i); int simple_pff_nhop(struct pff_i * pff_i, uint64_t addr); +int simple_pff_flow_state_change(struct pff_i * pff_i, + int fd, + bool up); + extern struct pff_ops simple_pff_ops; #endif /* OUROBOROS_IPCPD_UNICAST_SIMPLE_PFF_H */ diff --git a/src/ipcpd/unicast/pff/tests/pft_test.c b/src/ipcpd/unicast/pff/tests/pft_test.c index 4962c241..0b4a165b 100644 --- a/src/ipcpd/unicast/pff/tests/pft_test.c +++ b/src/ipcpd/unicast/pff/tests/pft_test.c @@ -22,105 +22,321 @@ #include "pft.c" +#include <test/test.h> + #include <stdio.h> #define TBL_SIZE 256 #define INT_TEST 4 -int pft_test(int argc, - char ** argv) +/* Next hops used by the del_fd tests. */ +#define FD_GONE 7 +#define FD_KEEP 8 +#define FD_OTHER 9 + +static int pft_add(struct pft * pft, + uint64_t dst, + const int * fds, + size_t len) +{ + int * blk; + size_t i; + + blk = malloc(sizeof(*blk) * len); + if (blk == NULL) + return -1; + + for (i = 0; i < len; i++) + blk[i] = fds[i]; + + if (pft_insert(pft, dst, blk, len)) { + free(blk); + return -1; + } + + return 0; +} + +static int test_pft_create_destroy(void) { struct pft * pft; - int i; - int * j; - size_t len; - (void) argc; - (void) argv; + TEST_START(); - pft = pft_create(TBL_SIZE, true); + pft = pft_create(TBL_SIZE); if (pft == NULL) { printf("Failed to create.\n"); - return -1; + goto fail; } pft_destroy(pft); - pft = pft_create(TBL_SIZE, false); + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_pft_insert_lookup(void) +{ + struct pft * pft; + int * j; + size_t len; + int i; + + TEST_START(); + + pft = pft_create(TBL_SIZE); if (pft == NULL) { printf("Failed to create.\n"); - return -1; + goto fail; } for (i = 0; i < TBL_SIZE + INT_TEST + 2; i++) { - j = malloc(sizeof(*j)); - if (j == NULL) { - printf("Failed to malloc.\n"); - pft_destroy(pft); - return -1; - } - *j = i; - - if (pft_insert(pft, i, j, 1)) { + if (pft_add(pft, i, &i, 1)) { printf("Failed to insert.\n"); - pft_destroy(pft); - free(j); - return -1; + goto fail_pft; } } if (pft_lookup(pft, INT_TEST, &j, &len)) { printf("Failed to lookup.\n"); - pft_destroy(pft); - return -1; + goto fail_pft; } if (*j != INT_TEST) { printf("Lookup returned wrong value (%d != %d).\n", INT_TEST, *j); - pft_destroy(pft); - return -1; + goto fail_pft; } if (pft_lookup(pft, TBL_SIZE + INT_TEST, &j, &len)) { - printf("Failed to lookup.\n"); - pft_destroy(pft); - return -1; + printf("Failed to lookup on a shared bucket.\n"); + goto fail_pft; } if (*j != TBL_SIZE + INT_TEST) { printf("Lookup returned wrong value (%d != %d).\n", - INT_TEST, *j); - pft_destroy(pft); - return -1; + TBL_SIZE + INT_TEST, *j); + goto fail_pft; + } + + pft_destroy(pft); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pft: + pft_destroy(pft); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_pft_delete(void) +{ + struct pft * pft; + int * j; + size_t len; + int i; + + TEST_START(); + + pft = pft_create(TBL_SIZE); + if (pft == NULL) { + printf("Failed to create.\n"); + goto fail; + } + + for (i = 0; i < TBL_SIZE + INT_TEST + 2; i++) { + if (pft_add(pft, i, &i, 1)) { + printf("Failed to insert.\n"); + goto fail_pft; + } } if (pft_delete(pft, INT_TEST)) { printf("Failed to delete.\n"); - pft_destroy(pft); - return -1; + goto fail_pft; } if (pft_lookup(pft, INT_TEST, &j, &len) == 0) { printf("Failed to delete properly.\n"); - pft_destroy(pft); - return -1; + goto fail_pft; } if (pft_lookup(pft, TBL_SIZE + INT_TEST, &j, &len)) { printf("Failed to lookup after deletion.\n"); - pft_destroy(pft); - return -1; + goto fail_pft; } if (*j != TBL_SIZE + INT_TEST) { printf("Lookup returned wrong value (%d != %d).\n", - INT_TEST, *j); - pft_destroy(pft); - return -1; + TBL_SIZE + INT_TEST, *j); + goto fail_pft; } pft_destroy(pft); - return 0; + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pft: + pft_destroy(pft); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_pft_del_fd_sole(void) +{ + struct pft * pft; + int * j; + size_t len; + int fds[] = {FD_GONE}; + + TEST_START(); + + pft = pft_create(TBL_SIZE); + if (pft == NULL) { + printf("Failed to create.\n"); + goto fail; + } + + if (pft_add(pft, INT_TEST, fds, 1)) { + printf("Failed to insert.\n"); + goto fail_pft; + } + + pft_del_fd(pft, FD_GONE); + + if (pft_lookup(pft, INT_TEST, &j, &len) == 0) { + printf("Route without a next hop survived.\n"); + goto fail_pft; + } + + pft_destroy(pft); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pft: + pft_destroy(pft); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_pft_del_fd_shared(void) +{ + struct pft * pft; + int * j; + size_t len; + int fds[] = {FD_KEEP, FD_GONE, FD_OTHER}; + + TEST_START(); + + pft = pft_create(TBL_SIZE); + if (pft == NULL) { + printf("Failed to create.\n"); + goto fail; + } + + if (pft_add(pft, INT_TEST, fds, 3)) { + printf("Failed to insert.\n"); + goto fail_pft; + } + + pft_del_fd(pft, FD_GONE); + + if (pft_lookup(pft, INT_TEST, &j, &len)) { + printf("Route with next hops left was dropped.\n"); + goto fail_pft; + } + + if (len != 2) { + printf("Expected 2 next hops, got %zu.\n", len); + goto fail_pft; + } + + if (j[0] != FD_KEEP || j[1] != FD_OTHER) { + printf("Next hops not preserved in order (%d, %d).\n", + j[0], j[1]); + goto fail_pft; + } + + pft_destroy(pft); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pft: + pft_destroy(pft); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_pft_del_fd_untouched(void) +{ + struct pft * pft; + int * j; + size_t len; + int fds[] = {FD_KEEP}; + + TEST_START(); + + pft = pft_create(TBL_SIZE); + if (pft == NULL) { + printf("Failed to create.\n"); + goto fail; + } + + if (pft_add(pft, INT_TEST, fds, 1)) { + printf("Failed to insert.\n"); + goto fail_pft; + } + + pft_del_fd(pft, FD_GONE); + + if (pft_lookup(pft, INT_TEST, &j, &len)) { + printf("Unrelated route was dropped.\n"); + goto fail_pft; + } + + if (len != 1 || *j != FD_KEEP) { + printf("Unrelated route was modified.\n"); + goto fail_pft; + } + + pft_destroy(pft); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pft: + pft_destroy(pft); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int pft_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_pft_create_destroy(); + ret |= test_pft_insert_lookup(); + ret |= test_pft_delete(); + ret |= test_pft_del_fd_sole(); + ret |= test_pft_del_fd_shared(); + ret |= test_pft_del_fd_untouched(); + + return ret; } diff --git a/src/ipcpd/unicast/psched.c b/src/ipcpd/unicast/psched.c index 21e23617..dce85120 100644 --- a/src/ipcpd/unicast/psched.c +++ b/src/ipcpd/unicast/psched.c @@ -30,6 +30,7 @@ #include <ouroboros/errno.h> #include <ouroboros/notifier.h> +#include <ouroboros/time.h> #include "common/connmgr.h" #include "ipcp.h" @@ -50,7 +51,7 @@ static int qos_prio [] = { #endif struct psched { - fset_t * set[QOS_CUBE_MAX]; + fset_t * set[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL]; next_packet_fn_t callback; read_fn_t read; pthread_t readers[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL]; @@ -59,23 +60,162 @@ struct psched { struct sched_info { struct psched * sch; qoscube_t qc; + size_t idx; }; +/* Map an FD to one reader's set: one FD, one thread (no shared FDs). */ +static size_t fd_set_idx(int fd, qoscube_t qc) +{ + return qc + ((size_t) fd % IPCP_SCHED_THR_MUL) * QOS_CUBE_MAX; +} + static void cleanup_reader(void * o) { fqueue_destroy((fqueue_t *) o); } +/* + * Per-reader deadline scheduler: a paced flow is served, then deferred + * to its next-send deadline instead of blocking the thread, so it never + * stalls its thread-mates. + */ +struct dsched { + uint64_t deadline[PROC_MAX_FLOWS]; /* absolute ns, per tracked fd */ + int active[PROC_MAX_FLOWS]; /* compact list of tracked fds */ + int posn[PROC_MAX_FLOWS]; /* fd -> active index, -1 = none */ + size_t n; +}; + +static void cleanup_dsched(void * o) +{ + free(o); +} + +static void dsched_track(struct dsched * d, + int fd, + uint64_t deadline) +{ + if (d->posn[fd] >= 0) + return; + + d->deadline[fd] = deadline; + d->posn[fd] = (int) d->n; + d->active[d->n++] = fd; +} + +static void dsched_untrack(struct dsched * d, + int fd) +{ + int i = d->posn[fd]; + + if (i < 0) + return; + + d->active[i] = d->active[--d->n]; + d->posn[d->active[i]] = i; + d->posn[fd] = -1; +} + +/* Fold a deadline into the earliest pending one (0 = none yet). */ +static uint64_t dmin_fold(uint64_t dmin, + uint64_t deadline) +{ + if (dmin == 0 || deadline < dmin) + return deadline; + + return dmin; +} + +static uint64_t dsched_serve(struct dsched * d, + struct psched * sched, + qoscube_t qc, + uint64_t now) +{ + struct ssm_pk_buff * spb; + uint64_t dmin = 0; + size_t i; + int fd; + int ret; + time_t wait; + bool served; + + /* Round-robin one packet per flow so none monopolises egress. */ + do { + served = false; + + for (i = 0; i < d->n; ) { + fd = d->active[i]; + + if (d->deadline[fd] > now) { + dmin = dmin_fold(dmin, d->deadline[fd]); + ++i; + continue; + } + + ret = sched->read(fd, &spb); + if (ret == -EAGAIN) { /* empty now, keep it */ + ++i; + continue; + } + + if (ret < 0) { + dsched_untrack(d, fd); + continue; + } + + wait = sched->callback(fd, qc, spb); + served = true; + + if (wait > 0) { + d->deadline[fd] = now + (uint64_t) wait; + dmin = dmin_fold(dmin, d->deadline[fd]); + } + + ++i; + } + } while (served); + + return dmin; +} + +static void dsched_events(struct dsched * d, + fqueue_t * fq, + uint64_t now) +{ + int fd; + + while ((fd = fqueue_next(fq)) >= 0) { + switch (fqueue_type(fq)) { + case FLOW_DEALLOC: + dsched_untrack(d, fd); + notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd); + break; + case FLOW_DOWN: + notifier_event(NOTIFY_DT_FLOW_DOWN, &fd); + break; + case FLOW_UP: + notifier_event(NOTIFY_DT_FLOW_UP, &fd); + break; + case FLOW_PKT: + dsched_track(d, fd, now); + break; + default: + break; + } + } +} + static void * packet_reader(void * o) { - struct psched * sched; - struct ssm_pk_buff * spb; - int fd; - fqueue_t * fq; - qoscube_t qc; + struct psched * sched; + struct dsched * d; + fqueue_t * fq; + qoscube_t qc; + size_t idx; sched = ((struct sched_info *) o)->sch; qc = ((struct sched_info *) o)->qc; + idx = ((struct sched_info *) o)->idx; ipcp_lock_to_core(); @@ -85,37 +225,51 @@ static void * packet_reader(void * o) if (fq == NULL) return (void *) -1; + d = malloc(sizeof(*d)); + if (d == NULL) { + fqueue_destroy(fq); + return (void *) -1; + } + + memset(d, 0, sizeof(*d)); + memset(d->posn, 0xFF, sizeof(d->posn)); /* -1: nothing tracked yet */ + + pthread_cleanup_push(cleanup_dsched, d); pthread_cleanup_push(cleanup_reader, fq); while (true) { - int ret = fevent(sched->set[qc], fq, NULL); + struct timespec now_ts; + struct timespec to; + struct timespec * timeo; + uint64_t now; + uint64_t dmin; + uint64_t delta; + int ret; + + clock_gettime(PTHREAD_COND_CLOCK, &now_ts); + + now = TS_TO_UINT64(now_ts); + + dmin = dsched_serve(d, sched, qc, now); + + if (dmin == 0) { + timeo = NULL; + } else { + delta = dmin > now ? dmin - now : 1; + to.tv_sec = (time_t) (delta / BILLION); + to.tv_nsec = (long) (delta % BILLION); + timeo = &to; + } + + ret = fevent(sched->set[idx], fq, timeo); if (ret < 0) continue; - while ((fd = fqueue_next(fq)) >= 0) { - switch (fqueue_type(fq)) { - case FLOW_DEALLOC: - notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd); - break; - case FLOW_DOWN: - notifier_event(NOTIFY_DT_FLOW_DOWN, &fd); - break; - case FLOW_UP: - notifier_event(NOTIFY_DT_FLOW_UP, &fd); - break; - case FLOW_PKT: - if (sched->read(fd, &spb) < 0) - continue; - - sched->callback(fd, qc, spb); - break; - default: - break; - } - } + dsched_events(d, fq, now); } pthread_cleanup_pop(true); + pthread_cleanup_pop(true); return (void *) 0; } @@ -137,7 +291,7 @@ struct psched * psched_create(next_packet_fn_t callback, psched->callback = callback; psched->read = read; - for (i = 0; i < QOS_CUBE_MAX; ++i) { + for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) { psched->set[i] = fset_create(); if (psched->set[i] == NULL) { for (j = 0; j < i; ++j) @@ -155,6 +309,7 @@ struct psched * psched_create(next_packet_fn_t callback, } infos[i]->sch = psched; infos[i]->qc = i % QOS_CUBE_MAX; + infos[i]->idx = i; } for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) { @@ -196,11 +351,12 @@ struct psched * psched_create(next_packet_fn_t callback, fail_sched: for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) pthread_cancel(psched->readers[j]); + for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) pthread_join(psched->readers[j], NULL); #endif fail_infos: - for (j = 0; j < QOS_CUBE_MAX; ++j) + for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j) fset_destroy(psched->set[j]); fail_flow_set: free(psched); @@ -219,7 +375,7 @@ void psched_destroy(struct psched * psched) pthread_join(psched->readers[i], NULL); } - for (i = 0; i < QOS_CUBE_MAX; ++i) + for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) fset_destroy(psched->set[i]); free(psched); @@ -233,7 +389,7 @@ void psched_add(struct psched * psched, assert(psched); ipcp_flow_get_qoscube(fd, &qc); - fset_add(psched->set[qc], fd); + fset_add(psched->set[fd_set_idx(fd, qc)], fd); } void psched_del(struct psched * psched, @@ -244,5 +400,5 @@ void psched_del(struct psched * psched, assert(psched); ipcp_flow_get_qoscube(fd, &qc); - fset_del(psched->set[qc], fd); + fset_del(psched->set[fd_set_idx(fd, qc)], fd); } diff --git a/src/ipcpd/unicast/psched.h b/src/ipcpd/unicast/psched.h index d83bb793..8c2914b3 100644 --- a/src/ipcpd/unicast/psched.h +++ b/src/ipcpd/unicast/psched.h @@ -26,9 +26,9 @@ #include <ouroboros/ipcp-dev.h> #include <ouroboros/fqueue.h> -typedef void (* next_packet_fn_t)(int fd, - qoscube_t qc, - struct ssm_pk_buff * spb); +typedef time_t (* next_packet_fn_t)(int fd, + qoscube_t qc, + struct ssm_pk_buff * spb); typedef int (* read_fn_t)(int fd, struct ssm_pk_buff ** spb); diff --git a/src/ipcpd/unicast/routing/link-state.c b/src/ipcpd/unicast/routing/link-state.c index c4ea9e1c..4fba2f05 100644 --- a/src/ipcpd/unicast/routing/link-state.c +++ b/src/ipcpd/unicast/routing/link-state.c @@ -878,9 +878,15 @@ static void handle_event(void * self, break; case NOTIFY_DT_CONN_UP: flow_event(c->flow_info.fd, true); + + if (lsdb_add_link(ls.addr, c->conn_info.addr, 0, &qs) < 0) + log_dbg("Failed to re-add adjacency to lsdb."); break; case NOTIFY_DT_CONN_DOWN: flow_event(c->flow_info.fd, false); + + if (lsdb_del_link(ls.addr, c->conn_info.addr) < 0) + log_dbg("Local link was not in lsdb."); break; case NOTIFY_MGMT_CONN_ADD: fccntl(c->flow_info.fd, FLOWGFLAGS, &flags); diff --git a/src/ipcpd/unicast/tests/CMakeLists.txt b/src/ipcpd/unicast/tests/CMakeLists.txt new file mode 100644 index 00000000..2e35ed66 --- /dev/null +++ b/src/ipcpd/unicast/tests/CMakeLists.txt @@ -0,0 +1,34 @@ +get_filename_component(CURRENT_SOURCE_PARENT_DIR + ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +get_filename_component(CURRENT_BINARY_PARENT_DIR + ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY) + +get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +get_filename_component(PARENT_DIR ${PARENT_PATH} NAME) + +compute_test_prefix() + +create_test_sourcelist(${PARENT_DIR}_tests test_suite.c + # Add new tests here + cap_test.c + ) + +add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests}) + +target_include_directories(${PARENT_DIR}_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CURRENT_SOURCE_PARENT_DIR} + ${CURRENT_BINARY_PARENT_DIR} + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${CMAKE_SOURCE_DIR}/src/ipcpd + ${CMAKE_BINARY_DIR}/src/ipcpd +) + +disable_test_logging_for_target(${PARENT_DIR}_test) +target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common) + +add_dependencies(build_tests ${PARENT_DIR}_test) + +ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests}) diff --git a/src/ipcpd/unicast/tests/cap_test.c b/src/ipcpd/unicast/tests/cap_test.c new file mode 100644 index 00000000..e3c3f8b3 --- /dev/null +++ b/src/ipcpd/unicast/tests/cap_test.c @@ -0,0 +1,177 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Unit tests for link capacity codes + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#include "cap.c" + +#include <test/test.h> + +/* Exact roundtrip holds for codes >= 32 (rates >= 256 B/s). */ +static int test_cap_codec_roundtrip(void) +{ + unsigned c; + + TEST_START(); + + for (c = 32; c <= 255; c++) { + if (cap_enc(cap_dec((uint8_t) c)) != c) { + printf("Code %u does not roundtrip.\n", c); + goto fail; + } + + if (cap_dec((uint8_t) c) <= cap_dec((uint8_t) (c - 1))) { + printf("Decode not monotone at %u.\n", c); + goto fail; + } + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_codec_bounds(void) +{ + TEST_START(); + + if (cap_enc(0) != 0 || cap_dec(0) != 0) { + printf("Zero is not unknown.\n"); + goto fail; + } + + if (cap_enc(1) != 1) { + printf("Rate 1 encoded as %u.\n", cap_enc(1)); + goto fail; + } + + if (cap_enc(UINT64_MAX) != 255) { + printf("Max rate encoded as %u.\n", cap_enc(UINT64_MAX)); + goto fail; + } + + if (cap_dec(255) <= cap_dec(254)) { + printf("Top code does not decode.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_min(void) +{ + TEST_START(); + + if (cap_min(0, 42) != 42 || cap_min(42, 0) != 42) { + printf("Unknown not skipped in min.\n"); + goto fail; + } + + if (cap_min(0, 0) != 0) { + printf("Two unknowns not unknown.\n"); + goto fail; + } + + if (cap_min(97, 42) != 42 || cap_min(42, 97) != 42) { + printf("Min not taken.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_stamp(void) +{ + uint8_t pci; + + TEST_START(); + + pci = 42; + + cap_stamp(&pci, 0); + + if (pci != 42) { + printf("Unknown own code overwrote the byte.\n"); + goto fail; + } + + pci = 0; + + cap_stamp(&pci, 97); + + if (pci != 97) { + printf("Own code not written into unknown.\n"); + goto fail; + } + + pci = 97; + + cap_stamp(&pci, 42); + + if (pci != 42) { + printf("Lower own code did not lower the byte.\n"); + goto fail; + } + + pci = 42; + + cap_stamp(&pci, 97); + + if (pci != 42) { + printf("Higher own code raised the byte.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int cap_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_cap_codec_roundtrip(); + ret |= test_cap_codec_bounds(); + ret |= test_cap_min(); + ret |= test_cap_stamp(); + + return ret; +} diff --git a/src/irmd/CMakeLists.txt b/src/irmd/CMakeLists.txt index 9aa747ca..5aa457ff 100644 --- a/src/irmd/CMakeLists.txt +++ b/src/irmd/CMakeLists.txt @@ -7,11 +7,11 @@ if(HAVE_TOML) set(INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") configure_file("${CMAKE_SOURCE_DIR}/irmd.conf.in" "${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example" @ONLY) - configure_file("${CMAKE_SOURCE_DIR}/enc.conf.in" - "${CMAKE_BINARY_DIR}/enc.conf.example" @ONLY) + configure_file("${CMAKE_SOURCE_DIR}/sec.conf.in" + "${CMAKE_BINARY_DIR}/sec.conf.example" @ONLY) install(FILES "${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example" DESTINATION "${OUROBOROS_CONFIG_DIR}") - install(FILES "${CMAKE_BINARY_DIR}/enc.conf.example" + install(FILES "${CMAKE_BINARY_DIR}/sec.conf.example" DESTINATION "${OUROBOROS_CONFIG_DIR}") install(CODE " if(NOT EXISTS \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\") diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in index df0cd718..2440e180 100644 --- a/src/irmd/config.h.in +++ b/src/irmd/config.h.in @@ -21,10 +21,6 @@ */ -#define IPCP_UDP4_EXEC "@IPCP_UDP4_TARGET@" -#define IPCP_UDP6_EXEC "@IPCP_UDP6_TARGET@" -#define IPCP_ETH_LLC_EXEC "@IPCP_ETH_LLC_TARGET@" -#define IPCP_ETH_DIX_EXEC "@IPCP_ETH_DIX_TARGET@" #define IPCP_UNICAST_EXEC "@IPCP_UNICAST_TARGET@" #define IPCP_BROADCAST_EXEC "@IPCP_BROADCAST_TARGET@" #define IPCP_LOCAL_EXEC "@IPCP_LOCAL_TARGET@" @@ -42,6 +38,9 @@ #define FLOW_DEALLOC_TIMEOUT @FLOW_DEALLOC_TIMEOUT@ #define OAP_REPLAY_TIMER @OAP_REPLAY_TIMER@ +#define OAP_REPLAY_MAX @OAP_REPLAY_MAX@ +#define OAP_REKEY_TIMER @OAP_REKEY_TIMER@ +#cmakedefine01 OAP_CLIENT_AUTH_DEFAULT #define BOOTSTRAP_TIMEOUT @BOOTSTRAP_TIMEOUT@ #define ENROLL_TIMEOUT @ENROLL_TIMEOUT@ @@ -72,6 +71,8 @@ #define OUROBOROS_CLI_CRT_DIR "@OUROBOROS_CLI_CRT_DIR@" #define OUROBOROS_CHAIN_DIR "@OUROBOROS_UNTRUSTED_DIR@" +/* Endpoint peers are keyed on their address, not on a service name. */ + #define IRMD_PKILL_TIMEOUT @IRMD_PKILL_TIMEOUT@ #cmakedefine DISABLE_DIRECT_IPC @@ -79,8 +80,7 @@ #cmakedefine HAVE_LIBGCRYPT #cmakedefine HAVE_OPENSSL #ifdef HAVE_OPENSSL -#cmakedefine HAVE_OPENSSL_ML_KEM -#cmakedefine HAVE_OPENSSL_ML_DSA +#cmakedefine HAVE_ML #endif #define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@ #ifdef CONFIG_OUROBOROS_DEBUG diff --git a/src/irmd/configfile.c b/src/irmd/configfile.c index 53608eee..de02242f 100644 --- a/src/irmd/configfile.c +++ b/src/irmd/configfile.c @@ -43,6 +43,7 @@ #include <assert.h> #include <errno.h> #include <inttypes.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <toml.h> @@ -92,171 +93,173 @@ static int toml_local(toml_table_t * table, return toml_hash(table, &conf->layer_info); } -static int toml_eth_dev(toml_table_t * table, - struct eth_config * conf) -{ - toml_datum_t dev; - dev = toml_string_in(table, "dev"); - if (!dev.ok) { - log_err("Missing device."); - return -1; - } +/* Defined with the peer helpers below; shared grammar for both paths. */ +static int toml_udp_str(const char * str, + char * host, + int * port); - if (strlen(dev.u.s) > DEV_NAME_SIZE) { - log_err("Device name too long: %s", dev.u.s); - free(dev.u.s); +/* Attach the PoAs an IPCP is given; one call per PoA. */ +static int poa_eth_set(struct poa_spec * poa, + const char * dev, + uint16_t ethertype) +{ + if (strlen(dev) > DEV_NAME_SIZE) { + log_err("Invalid device name %s.", dev); return -1; } - strcpy(conf->dev, dev.u.s); - free(dev.u.s); + memset(poa, 0, sizeof(*poa)); + + poa->type = POA_ETH; + poa->eth.ethertype = ethertype; + + strcpy(poa->eth.dev, dev); return 0; } -static int toml_eth_llc(toml_table_t * table, - struct ipcp_config * conf) +static int toml_poa_eth(toml_table_t * tbl, + struct poa_spec * poa) { - *conf = eth_llc_default_conf; - - if (toml_hash(table, &conf->layer_info) < 0) - return -1; - - return toml_eth_dev(table, &conf->eth); -} + toml_datum_t dev; + toml_datum_t et; + uint16_t ethertype = POA_ETHERTYPE; + int ret = -1; + dev = toml_string_in(tbl, "dev"); + if (!dev.ok) { + log_err("An eth PoA needs a device."); + goto fail; + } -static int toml_ethertype(toml_table_t * table, - struct eth_config * conf) -{ - toml_datum_t ethertype; + et = toml_int_in(tbl, "ethertype"); + if (et.ok) { + if (et.u.i < 0 || et.u.i > 0xFFFF) { + log_err("Invalid ethertype."); + goto fail; + } - ethertype = toml_int_in(table, "ethertype"); - if (ethertype.ok) - conf->ethertype = ethertype.u.i; + ethertype = et.u.i; + } - if (conf->ethertype < 0x0600 || conf->ethertype == 0xFFFF) - return -1; + ret = poa_eth_set(poa, dev.u.s, ethertype); + fail: + if (dev.ok) + free(dev.u.s); - return 0; + return ret; } -static int toml_eth_dix(toml_table_t * table, - struct ipcp_config * conf) +/* A PoA is bound locally: only literal addresses are accepted here. */ +static int toml_poa_udp(const char * str, + struct poa_spec * poa) { - *conf = eth_dix_default_conf; + char host[POA_HOST_STRLEN + 1]; + int port; - if (toml_hash(table, &conf->layer_info) < 0) + if (toml_udp_str(str, host, &port) < 0) return -1; - if (toml_eth_dev(table, &conf->eth) < 0) - return -1; + memset(poa, 0, sizeof(*poa)); - if (toml_ethertype(table, &conf->eth) < 0) { - log_err("Ethertype not in valid range."); - return -1; + if (inet_pton(AF_INET, host, &poa->udp4.ip_addr.s_addr) == 1) { + poa->type = POA_UDP4; + poa->udp4.port = port; + return 0; } - return 0; -} - -static int toml_udp4(toml_table_t * table, - struct ipcp_config * conf) -{ - struct udp4_config * udp4; - toml_datum_t ip; - toml_datum_t port; - toml_datum_t dns; - - *conf = udp4_default_conf; - udp4 = &conf->udp4; - - ip = toml_string_in(table, "ip"); - if (!ip.ok) { - log_err("No IP address specified!"); - goto fail_ip; + if (inet_pton(AF_INET6, host, &poa->udp6.ip_addr) == 1) { + poa->type = POA_UDP6; + poa->udp6.port = port; + return 0; } - if (inet_pton (AF_INET, ip.u.s, &udp4->ip_addr.s_addr) != 1) { - log_err("Failed to parse IPv4 address %s.", ip.u.s); - goto fail_addr; - } + log_err("Invalid IP address %s.", host); - port = toml_int_in(table, "port"); - if (port.ok) - udp4->port = port.u.i; + return -1; +} - dns = toml_string_in(table, "dns"); - if (dns.ok) { - if (inet_pton(AF_INET, dns.u.s, &udp4->dns_addr.s_addr) < 0) { - log_err("Failed to parse DNS address %s.", ip.u.s); - goto fail_dns; +/* Attaches every PoA in the "udp" or "eth" array; string or table. */ +static int toml_poa_array(toml_table_t * table, + pid_t pid, + const char * key) +{ + struct poa_spec poa; + toml_array_t * arr; + int n; + int i; + bool is_eth; + + arr = toml_array_in(table, key); + if (arr == NULL) { + /* A table here would otherwise attach nothing, silently. */ + if (toml_table_in(table, key) != NULL) { + log_err("A %s PoA is an array: %s = [...] " + "or [[%s]].", key, key, key); + return -1; } - free(dns.u.s); + return 0; } - free(ip.u.s); + is_eth = strcmp(key, "eth") == 0; - return 0; + n = toml_array_nelem(arr); - fail_dns: - free(dns.u.s); - fail_addr: - free(ip.u.s); - fail_ip: - return -1; -} + for (i = 0; i < n; i++) { + toml_datum_t s; + int ret; -static int toml_udp6(toml_table_t * table, - struct ipcp_config * conf) -{ - struct in6_addr ip6; - struct in6_addr dns6; - toml_datum_t ip; - toml_datum_t port; - toml_datum_t dns; - - *conf = udp6_default_conf; - ip6 = conf->udp6.ip_addr; - dns6 = conf->udp6.dns_addr; - - ip = toml_string_in(table, "ip"); - if (!ip.ok) { - log_err("No IP address specified!"); - goto fail_ip; - } + s = toml_string_at(arr, i); + if (s.ok) { + if (is_eth) + ret = poa_eth_set(&poa, s.u.s, + POA_ETHERTYPE); + else + ret = toml_poa_udp(s.u.s, &poa); - if (inet_pton (AF_INET6, ip.u.s, &ip6.s6_addr) != 1) { - log_err("Failed to parse IPv4 address %s.", ip.u.s); - goto fail_addr; - } + free(s.u.s); + } else { + toml_table_t * d = toml_table_at(arr, i); - port = toml_int_in(table, "port"); - if (port.ok) - conf->udp6.port = port.u.i; + if (d == NULL) { + log_err("Invalid %s PoA entry.", key); + return -1; + } + + if (is_eth) { + ret = toml_poa_eth(d, &poa); + } else { + toml_datum_t addr = toml_string_in(d, "addr"); + + if (!addr.ok) { + log_err("No addr for udp PoA."); + return -1; + } - dns = toml_string_in(table, "dns"); - if (dns.ok) { - if (inet_pton(AF_INET6, dns.u.s, &dns6.s6_addr) < 0) { - log_err("Failed to parse DNS address %s.", ip.u.s); - goto fail_dns; + ret = toml_poa_udp(addr.u.s, &poa); + free(addr.u.s); + } } - free(dns.u.s); - } + if (ret < 0) + return -1; - free(ip.u.s); + if (attach_ipcp(pid, &poa, true) < 0) + return -1; + } return 0; +} - fail_dns: - free(dns.u.s); - fail_addr: - free(ip.u.s); - fail_ip: - return -1; +static int toml_attach(toml_table_t * table, + pid_t pid) +{ + if (toml_poa_array(table, pid, "udp") < 0) + return -1; + + return toml_poa_array(table, pid, "eth"); } static int toml_broadcast(toml_table_t * table, @@ -265,15 +268,13 @@ static int toml_broadcast(toml_table_t * table, (void) table; (void) conf; - /* Nothing to do here. */ - return 0; } #define BETWEEN(a, b, c) ((a) >= (b) && (a) <= (c)) #define DHT(conf, x) (conf)->dht.params.x static int toml_dir(toml_table_t * table, - struct dir_config * conf) + struct dir_config * conf) { toml_datum_t dir; toml_datum_t alpha; @@ -457,7 +458,7 @@ static int toml_congestion(toml_table_t * table, if (congestion.ok) { if (strcmp(congestion.u.s, "none") == 0) conf->cong_avoid = CA_NONE; - else if (strcmp(congestion.u.s, "lfa") == 0) + else if (strcmp(congestion.u.s, "mb-ecn") == 0) conf->cong_avoid = CA_MB_ECN; else conf->cong_avoid = CA_INVALID; @@ -477,6 +478,7 @@ static int toml_dt(toml_table_t * table, toml_datum_t addr; toml_datum_t eid; toml_datum_t ttl; + toml_datum_t max_rtt; addr = toml_int_in(table, "addr_size"); if (addr.ok) @@ -490,6 +492,10 @@ static int toml_dt(toml_table_t * table, if (ttl.ok) conf->max_ttl = ttl.u.i; + max_rtt = toml_int_in(table, "max_rtt"); + if (max_rtt.ok) + conf->max_rtt = max_rtt.u.i; + if (toml_routing(table, conf) < 0) { log_err("Invalid routing option."); return -1; @@ -589,6 +595,207 @@ static int toml_register(toml_table_t * table, return ret; } +/* Fills in a peer's eth PoA; dst MAC stays zero, the resolve marker. */ +static int toml_peer_eth(toml_table_t * tbl, + struct poa_addr * addr) +{ + toml_datum_t dev; + toml_datum_t et; + int ret = -1; + + dev = toml_string_in(tbl, "dev"); + + memset(addr, 0, sizeof(*addr)); + + addr->type = POA_ETH; + addr->eth.src.ethertype = POA_ETHERTYPE; + addr->eth.dst.ethertype = POA_ETHERTYPE; + + et = toml_int_in(tbl, "ethertype"); + if (et.ok) { + if (et.u.i < 0 || et.u.i > 0xFFFF) { + log_err("Invalid ethertype."); + goto fail; + } + + addr->eth.src.ethertype = et.u.i; + addr->eth.dst.ethertype = et.u.i; + } + + if (dev.ok) { + if (strlen(dev.u.s) > DEV_NAME_SIZE) { + log_err("Invalid device name %s.", dev.u.s); + goto fail; + } + + strcpy(addr->eth.src.dev, dev.u.s); + } + + ret = 0; + fail: + if (dev.ok) + free(dev.u.s); + + return ret; +} + +/* Same grammar as the CLI's udp keyword; see irm_utils.c parse_udp_str. */ +static int toml_udp_str(const char * str, + char * host, + int * port) +{ + struct in6_addr v6; + char buf[POA_HOST_STRLEN + 1]; + char * p; + char * end; + long n; + + *port = POA_UDP_PORT; + + if (strlen(str) > POA_HOST_STRLEN) + goto fail; + + strcpy(buf, str); + + if (buf[0] == '[') { + p = strchr(buf, ']'); + if (p == NULL) + goto fail; + + *p++ = '\0'; + + strcpy(host, buf + 1); + + if (*p == '\0') + return 0; + + if (*p != ':') + goto fail; + + ++p; + } else if (inet_pton(AF_INET6, buf, &v6) == 1) { + strcpy(host, buf); + return 0; + } else { + p = strrchr(buf, ':'); + if (p == NULL) { + strcpy(host, buf); + return 0; + } + + *p++ = '\0'; + + strcpy(host, buf); + } + + n = strtol(p, &end, 10); + if (*p == '\0' || *end != '\0' || n < 1 || n > 65535) + goto fail; + + *port = (int) n; + + return 0; + fail: + log_err("Invalid UDP address: %s.", str); + return -1; +} + +/* Fills in a peer's udp PoA; an unresolved name is left for the IRMd. */ +static int toml_peer_udp(const char * str, + struct poa_addr * addr) +{ + char host[POA_HOST_STRLEN + 1]; + int port; + + if (toml_udp_str(str, host, &port) < 0) + return -1; + + memset(addr, 0, sizeof(*addr)); + + if (inet_pton(AF_INET, host, &addr->udp4.ip_addr) == 1) { + addr->type = POA_UDP4; + addr->udp4.port = port; + return 0; + } + + if (inet_pton(AF_INET6, host, &addr->udp6.ip_addr) == 1) { + addr->type = POA_UDP6; + addr->udp6.port = port; + return 0; + } + + addr->type = POA_UDP; + addr->udp4.port = port; + + strcpy(addr->hostname, host); + + return 0; +} + +/* + * An entry is a name, or a table naming a PoA to reach it over. + * On entry, *paddr already points at the caller's struct poa_addr to + * fill in. Set to NULL wherever there is no PoA to dial: a bare + * dst-only table (recursive lookup) or any parse failure. + */ +static int toml_peer(toml_table_t * tbl, + char * dst, + struct poa_addr ** paddr) +{ + struct poa_addr * addr = *paddr; + toml_table_t * eth; + toml_datum_t name; + toml_datum_t udp; + int ret = -1; + + name = toml_string_in(tbl, "dst"); + if (!name.ok) { + log_err("PoA table entry has no dst."); + + *paddr = NULL; + return -1; + } + + if (strlen(name.u.s) > LAYER_NAME_SIZE) { + log_err("Destination name too long: %s.", name.u.s); + free(name.u.s); + + *paddr = NULL; + return -1; + } + + strcpy(dst, name.u.s); + free(name.u.s); + + eth = toml_table_in(tbl, "eth"); + udp = toml_string_in(tbl, "udp"); + if (eth != NULL && udp.ok) { + log_err("A PoA is eth or udp, not both."); + + *paddr = NULL; + goto fail; + } + + if (eth == NULL && !udp.ok) { + *paddr = NULL; + ret = 0; + goto fail; + } + + if (eth != NULL) + ret = toml_peer_eth(eth, addr); + else + ret = toml_peer_udp(udp.u.s, addr); + + if (ret < 0) + *paddr = NULL; + fail: + if (udp.ok) + free(udp.u.s); + + return ret; +} + static int toml_connect(toml_table_t * table, pid_t pid) { @@ -600,21 +807,40 @@ static int toml_connect(toml_table_t * table, if (conn == NULL) return 0; - for (i=0; ret == 0; i++) { - toml_datum_t dst; - qosspec_t qs = qos_raw; + for (i = 0; ret == 0; i++) { + toml_datum_t dst; + toml_table_t * tbl; + struct poa_addr addr; + struct poa_addr * paddr = &addr; + char buf[LAYER_NAME_SIZE + 1]; + const char * d; + qosspec_t qs = qos_raw; dst = toml_string_at(conn, i); - if (!dst.ok) - break; + if (dst.ok) { + d = dst.u.s; + paddr = NULL; + } else { + tbl = toml_table_at(conn, i); + if (tbl == NULL) + break; + + ret = toml_peer(tbl, buf, &paddr); + if (ret < 0) + break; + + d = buf; + } + + log_dbg("Connecting %d to %s", pid, d); - log_dbg("Connecting %d to %s", pid, dst.u.s); + ret = connect_ipcp_resolve(pid, d, MGMT_COMP, qs, paddr); - ret = connect_ipcp(pid, dst.u.s, MGMT_COMP, qs); if (ret == 0) - ret = connect_ipcp(pid, dst.u.s, DT_COMP, qs); + ret = connect_ipcp_resolve(pid, d, DT_COMP, qs, paddr); - free(dst.u.s); + if (dst.ok) + free(dst.u.s); } return ret; @@ -624,9 +850,11 @@ static int toml_ipcp(toml_table_t * table, struct ipcp_info * info, struct ipcp_config * conf) { - toml_datum_t bootstrap; - toml_datum_t enrol; - int ret; + toml_datum_t bootstrap; + toml_datum_t enrol; + toml_table_t * enrol_tbl; + bool have_enrol; + int ret; log_dbg("Found IPCP %s in configuration file.", info->name); @@ -638,21 +866,49 @@ static int toml_ipcp(toml_table_t * table, bootstrap = toml_string_in(table, "bootstrap"); enrol = toml_string_in(table, "enrol"); - if (bootstrap.ok && enrol.ok) { + enrol_tbl = enrol.ok ? NULL : toml_table_in(table, "enrol"); + + have_enrol = enrol.ok || enrol_tbl != NULL; + if (bootstrap.ok && have_enrol) { log_err("Ignoring bootstrap for IPCP %s.", info->name); free(bootstrap.u.s); bootstrap.ok = false; } - if (!bootstrap.ok && !enrol.ok) { + if (!bootstrap.ok && !have_enrol) { log_dbg("Nothing more to do for %s.", info->name); return 0; } - if (enrol.ok) { + /* Endpoints come first: enrolment reaches the peer over one. */ + if (toml_attach(table, info->pid) < 0) { + log_err("Failed to attach PoAs for %s.", info->name); + return -1; + } + + if (have_enrol) { struct layer_info layer; - ret = enroll_ipcp(info->pid, enrol.u.s); - free(enrol.u.s); + struct poa_addr addr; + struct poa_addr * paddr = &addr; + char buf[LAYER_NAME_SIZE + 1]; + const char * dst; + + if (enrol.ok) { + dst = enrol.u.s; + paddr = NULL; + } else { + if (toml_peer(enrol_tbl, buf, &paddr) < 0) { + log_err("Invalid enrol table for %s.", + info->name); + return -1; + } + dst = buf; + } + + ret = enroll_ipcp_resolve(info->pid, dst, paddr); + + if (enrol.ok) + free(enrol.u.s); if (ret < 0) { log_err("Failed to enrol %s.", info->name); return -1; @@ -689,18 +945,6 @@ static int toml_ipcp(toml_table_t * table, case IPCP_LOCAL: ret = toml_local(table, conf); break; - case IPCP_ETH_DIX: - ret = toml_eth_dix(table, conf); - break; - case IPCP_ETH_LLC: - ret = toml_eth_llc(table, conf); - break; - case IPCP_UDP4: - ret = toml_udp4(table, conf); - break; - case IPCP_UDP6: - ret = toml_udp6(table, conf); - break; case IPCP_BROADCAST: ret = toml_broadcast(table, conf); break; @@ -922,10 +1166,10 @@ static int toml_name(toml_table_t * table, toml_array_t * progs; toml_array_t * args; toml_datum_t lb; - toml_datum_t senc; + toml_datum_t ssec; toml_datum_t scrt; toml_datum_t skey; - toml_datum_t cenc; + toml_datum_t csec; toml_datum_t ccrt; toml_datum_t ckey; @@ -957,8 +1201,8 @@ static int toml_name(toml_table_t * table, log_err("Invalid load-balancing policy for %s.", name); return -1; } - senc = toml_string_in(table, "server_enc_file"); - if (senc.ok && cp_chk_path(info.s.enc, senc.u.s) < 0) + ssec = toml_string_in(table, "server_sec_file"); + if (ssec.ok && cp_chk_path(info.s.sec, ssec.u.s) < 0) return -1; scrt = toml_string_in(table, "server_crt_file"); @@ -969,8 +1213,8 @@ static int toml_name(toml_table_t * table, if (skey.ok && cp_chk_path(info.s.key, skey.u.s) < 0) return -1; - cenc = toml_string_in(table, "client_enc_file"); - if (cenc.ok && cp_chk_path(info.c.enc, cenc.u.s) < 0) + csec = toml_string_in(table, "client_sec_file"); + if (csec.ok && cp_chk_path(info.c.sec, csec.u.s) < 0) return -1; ccrt = toml_string_in(table, "client_crt_file"); @@ -1026,14 +1270,6 @@ static int toml_toplevel(toml_table_t * table, return toml_name_list(subtable); else if (strcmp(key, "local") == 0) return toml_ipcp_list(subtable, IPCP_LOCAL); - else if (strcmp(key, "eth-dix") == 0) - return toml_ipcp_list(subtable, IPCP_ETH_DIX); - else if (strcmp(key, "eth-llc") == 0) - return toml_ipcp_list(subtable, IPCP_ETH_LLC); - else if (strcmp(key, "udp4") == 0) - return toml_ipcp_list(subtable, IPCP_UDP4); - else if (strcmp(key, "udp6") == 0) - return toml_ipcp_list(subtable, IPCP_UDP6); else if (strcmp(key, "broadcast") == 0) return toml_ipcp_list(subtable, IPCP_BROADCAST); else if (strcmp(key, "unicast") == 0) diff --git a/src/irmd/ipcp.c b/src/irmd/ipcp.c index a7da186c..cd662221 100644 --- a/src/irmd/ipcp.c +++ b/src/irmd/ipcp.c @@ -34,6 +34,7 @@ #include <ouroboros/utils.h> #include "ipcp.h" +#include "reg/reg.h" #include <fcntl.h> #include <pthread.h> @@ -72,6 +73,10 @@ static char * str_ipcp_cmd(int code) return "alloc_resp"; case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC: return "dealloc"; + case IPCP_MSG_CODE__IPCP_FLOW_UPDATE: + return "flow_update"; + case IPCP_MSG_CODE__IPCP_REPLY: + return "reply"; default: assert(false); return "unknown"; @@ -196,7 +201,9 @@ int ipcp_bootstrap(pid_t pid, msg.conf = ipcp_config_s_to_msg(conf); recv_msg = send_recv_ipcp_msg(pid, &msg); + ipcp_config_msg__free_unpacked(msg.conf, NULL); + if (recv_msg == NULL) return -EIPCP; @@ -225,9 +232,88 @@ int ipcp_bootstrap(pid_t pid, return ret; } -int ipcp_enroll(pid_t pid, - const char * dst, - struct layer_info * info) +ssize_t ipcp_list_poas(pid_t pid, + struct poa_spec ** eps) +{ + ipcp_msg_t msg = IPCP_MSG__INIT; + ipcp_msg_t * recv_msg; + size_t nr; + size_t i; + + if (eps == NULL) + return -EINVAL; + + *eps = NULL; + + msg.code = IPCP_MSG_CODE__IPCP_LIST_POAS; + + recv_msg = send_recv_ipcp_msg(pid, &msg); + if (recv_msg == NULL) + return -EIPCP; + + nr = recv_msg->n_poas; + if (nr == 0) { + ipcp_msg__free_unpacked(recv_msg, NULL); + return 0; + } + + *eps = malloc(nr * sizeof(**eps)); + if (*eps == NULL) { + ipcp_msg__free_unpacked(recv_msg, NULL); + return -ENOMEM; + } + + for (i = 0; i < nr; i++) + (*eps)[i] = poa_spec_msg_to_s(recv_msg->poas[i]); + + ipcp_msg__free_unpacked(recv_msg, NULL); + + return (ssize_t) nr; +} + +int ipcp_attach(pid_t pid, + const struct poa_spec * poa, + bool attach) +{ + ipcp_msg_t msg = IPCP_MSG__INIT; + ipcp_msg_t * recv_msg; + int ret; + + if (poa == NULL) + return -EINVAL; + + if (attach) + msg.code = IPCP_MSG_CODE__IPCP_ATTACH; + else + msg.code = IPCP_MSG_CODE__IPCP_DETACH; + + msg.poa = poa_spec_s_to_msg(poa); + if (msg.poa == NULL) + return -EINVAL; + + recv_msg = send_recv_ipcp_msg(pid, &msg); + + poa_spec_msg__free_unpacked(msg.poa, NULL); + + if (recv_msg == NULL) + return -EIPCP; + + if (!recv_msg->has_result) { + ipcp_msg__free_unpacked(recv_msg, NULL); + return -EIPCP; + } + + ret = recv_msg->result; + + ipcp_msg__free_unpacked(recv_msg, NULL); + + return ret; +} + +int ipcp_enroll(pid_t pid, + const char * dst, + const struct poa_addr * addr, + struct layer_info * info) { ipcp_msg_t msg = IPCP_MSG__INIT; ipcp_msg_t * recv_msg; @@ -239,7 +325,16 @@ int ipcp_enroll(pid_t pid, msg.code = IPCP_MSG_CODE__IPCP_ENROLL; msg.dst = (char *) dst; + if (addr != NULL) { + msg.peer = poa_addr_s_to_msg(addr); + if (msg.peer == NULL) + return -ENOMEM; + } + recv_msg = send_recv_ipcp_msg(pid, &msg); + + if (msg.peer != NULL) + poa_addr_msg__free_unpacked(msg.peer, NULL); if (recv_msg == NULL) return -EIPCP; @@ -267,10 +362,11 @@ int ipcp_enroll(pid_t pid, return 0; } -int ipcp_connect(pid_t pid, - const char * dst, - const char * component, - qosspec_t qs) +int ipcp_connect(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr) { ipcp_msg_t msg = IPCP_MSG__INIT; ipcp_msg_t * recv_msg; @@ -283,8 +379,21 @@ int ipcp_connect(pid_t pid, msg.pid = pid; msg.qosspec = qos_spec_s_to_msg(&qs); + if (addr != NULL) { + msg.peer = poa_addr_s_to_msg(addr); + if (msg.peer == NULL) { + free(msg.qosspec); + return -ENOMEM; + } + } + recv_msg = send_recv_ipcp_msg(pid, &msg); + + if (msg.peer != NULL) + poa_addr_msg__free_unpacked(msg.peer, NULL); + free(msg.qosspec); + if (recv_msg == NULL) return -EIPCP; @@ -444,6 +553,40 @@ int ipcp_flow_join(const struct flow_info * flow, return ret; } +int ipcp_flow_update(const struct flow_info * flow, + const buffer_t data) +{ + ipcp_msg_t msg = IPCP_MSG__INIT; + ipcp_msg_t * recv_msg; + int ret; + + msg.code = IPCP_MSG_CODE__IPCP_FLOW_UPDATE; + msg.has_flow_id = true; + msg.flow_id = flow->id; + msg.has_pk = true; + msg.pk.data = data.data; + msg.pk.len = data.len; + msg.has_is_poa = true; + msg.is_poa = reg_flow_is_poa(flow->id); + + recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg); + if (recv_msg == NULL) { + log_err("Did not receive message."); + return -EIPCP; + } + + if (!recv_msg->has_result) { + log_err("Message has no result"); + ipcp_msg__free_unpacked(recv_msg, NULL); + return -EIPCP; + } + + ret = recv_msg->result; + ipcp_msg__free_unpacked(recv_msg, NULL); + + return ret; +} + int ipcp_flow_alloc(const struct flow_info * flow, const buffer_t dst, const buffer_t data) @@ -506,6 +649,8 @@ int ipcp_flow_alloc_resp(const struct flow_info * flow, msg.has_pk = response == 0; msg.pk.data = data.data; msg.pk.len = data.len; + msg.has_is_poa = true; + msg.is_poa = reg_flow_is_poa(flow->id); recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg); if (recv_msg == NULL) @@ -535,6 +680,8 @@ int ipcp_flow_dealloc(pid_t pid, msg.flow_id = flow_id; msg.has_timeo_sec = true; msg.timeo_sec = timeo; + msg.has_is_poa = true; + msg.is_poa = reg_flow_is_poa(flow_id); recv_msg = send_recv_ipcp_msg(pid, &msg); if (recv_msg == NULL) diff --git a/src/irmd/ipcp.h b/src/irmd/ipcp.h index f1025096..1f257e7d 100644 --- a/src/irmd/ipcp.h +++ b/src/irmd/ipcp.h @@ -27,45 +27,57 @@ #ifndef OUROBOROS_IRMD_IPCP_H #define OUROBOROS_IRMD_IPCP_H -int ipcp_enroll(pid_t pid, - const char * dst, - struct layer_info * info); +int ipcp_enroll(pid_t pid, + const char * dst, + const struct poa_addr * addr, + struct layer_info * info); -int ipcp_bootstrap(pid_t pid, - struct ipcp_config * conf, - struct layer_info * info); +int ipcp_attach(pid_t pid, + const struct poa_spec * poa, + bool attach); -int ipcp_connect(pid_t pid, - const char * dst, - const char * component, - qosspec_t qs); +ssize_t ipcp_list_poas(pid_t pid, + struct poa_spec ** eps); -int ipcp_disconnect(pid_t pid, - const char * dst, - const char * component); +int ipcp_bootstrap(pid_t pid, + struct ipcp_config * conf, + struct layer_info * info); -int ipcp_reg(pid_t pid, - const buffer_t hash); +int ipcp_connect(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr); -int ipcp_unreg(pid_t pid, +int ipcp_disconnect(pid_t pid, + const char * dst, + const char * component); + +int ipcp_reg(pid_t pid, const buffer_t hash); -int ipcp_query(pid_t pid, - const buffer_t dst); +int ipcp_unreg(pid_t pid, + const buffer_t hash); + +int ipcp_query(pid_t pid, + const buffer_t dst); + +int ipcp_flow_alloc(const struct flow_info * flow, + const buffer_t hash, + const buffer_t data); -int ipcp_flow_alloc(const struct flow_info * flow, - const buffer_t hash, - const buffer_t data); +int ipcp_flow_join(const struct flow_info * flow, + const buffer_t dst); -int ipcp_flow_join(const struct flow_info * flow, - const buffer_t dst); +int ipcp_flow_alloc_resp(const struct flow_info * flow, + int response, + const buffer_t data); -int ipcp_flow_alloc_resp(const struct flow_info * flow, - int response, - const buffer_t data); +int ipcp_flow_dealloc(pid_t pid, + int flow_id, + time_t timeo); -int ipcp_flow_dealloc(pid_t pid, - int flow_id, - time_t timeo); +int ipcp_flow_update(const struct flow_info * flow, + const buffer_t data); #endif /* OUROBOROS_IRMD_IPCP_H */ diff --git a/src/irmd/irmd.h b/src/irmd/irmd.h index f88378ad..9d42e248 100644 --- a/src/irmd/irmd.h +++ b/src/irmd/irmd.h @@ -26,29 +26,48 @@ #include <ouroboros/ipcp.h> #include <ouroboros/irm.h> -int create_ipcp(struct ipcp_info * info); +int create_ipcp(struct ipcp_info * info); -int bootstrap_ipcp(pid_t pid, - struct ipcp_config * conf); +int bootstrap_ipcp(pid_t pid, + struct ipcp_config * conf); -int enroll_ipcp(pid_t pid, - const char * dst); +int enroll_ipcp(pid_t pid, + const char * dst, + const struct poa_addr * addr); -int connect_ipcp(pid_t pid, - const char * dst, - const char * component, - qosspec_t qs); +int enroll_ipcp_resolve(pid_t pid, + const char * dst, + struct poa_addr * addr); -int name_create(struct name_info * info); +int attach_ipcp(pid_t pid, + const struct poa_spec * poa, + bool attach); -int name_reg(const char * name, - pid_t pid); +ssize_t list_poas(pid_t pid, + struct poa_spec ** eps); -int bind_process(pid_t pid, - const char * name); +int connect_ipcp(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr); -int bind_program(char ** exec, - const char * name, - uint8_t flags); +int connect_ipcp_resolve(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + struct poa_addr * addr); + +int name_create(struct name_info * info); + +int name_reg(const char * name, + pid_t pid); + +int bind_process(pid_t pid, + const char * name); + +int bind_program(char ** exec, + const char * name, + uint8_t flags); #endif /* OUROBOROS_IRMD_H*/ diff --git a/src/irmd/main.c b/src/irmd/main.c index f91e23fc..e587a552 100644 --- a/src/irmd/main.c +++ b/src/irmd/main.c @@ -36,6 +36,7 @@ #include <ouroboros/crypt.h> #include <ouroboros/errno.h> #include <ouroboros/flow.h> +#include <ouroboros/fqueue.h> #include <ouroboros/hash.h> #include <ouroboros/irm.h> #include <ouroboros/list.h> @@ -60,6 +61,7 @@ #include <dirent.h> #include <grp.h> +#include <netdb.h> #include <pwd.h> #include <signal.h> #include <spawn.h> @@ -86,6 +88,8 @@ #define TIMESYNC_SLACK 100 /* ms */ #define OAP_SEEN_TIMER 20 /* s */ #define DEALLOC_TIME 300 /* s */ +#define REKEY_BATCH 64 /* flows re-keyed per timer pass */ +#define REKEY_RESP_TIMEO 20 /* s; give-up on a re-key RESPONSE */ #define DIRECT_MPL 20 /* ms */ /* bytes; in-process, bounded only by PUP/GSPP. */ #define DIRECT_MTU 65000 @@ -105,13 +109,38 @@ struct cmd { int fd; }; +/* In-flight Tier-2 re-key, owned solely by the re-key worker thread. */ +struct rekey_ctx { + struct list_head next; + + int flow_id; + void * ctx; /* OAP client ctx (opaque) */ + struct timespec deadline; /* reap if no RESPONSE by then */ +}; + +enum rekey_evt_type { + REKEY_INIT = 0, /* start an exchange for flow_id */ + REKEY_REQ, /* a REQUEST arrived for flow_id */ + REKEY_RESP, /* a RESPONSE arrived for flow_id */ + REKEY_DIRECT /* in-process re-key, direct flow */ +}; + +struct rekey_evt { + struct list_head next; + + enum rekey_evt_type type; + int flow_id; + pid_t n_1_pid; /* INIT: flow's lower IPCP */ + buffer_t buf; /* RESP: owned RESPONSE payload */ +}; + struct { bool log_stdout; /* log to stdout */ #ifdef HAVE_TOML char * cfg_file; /* configuration file path */ #endif struct lockfile * lf; /* single irmd per system */ - struct ssm_pool * gspp; /* pool for packets */ + struct ssm_pool * gspp; /* pool for packets */ int sockfd; /* UNIX socket */ @@ -126,6 +155,13 @@ struct { pthread_t irm_sanitize; /* clean up irmd resources */ pthread_t acceptor; /* accept new commands */ + + struct { + pthread_t worker; /* Tier-2 re-key orchestrator */ + struct list_head inbox; /* re-key events for worker */ + pthread_cond_t cond; /* inbox signal condvar */ + pthread_mutex_t mtx; /* inbox lock */ + } rk; } irmd; static enum irm_state irmd_get_state(void) @@ -190,18 +226,6 @@ static pid_t spawn_ipcp(struct ipcp_info * info) case IPCP_BROADCAST: exec_name = IPCP_BROADCAST_EXEC; break; - case IPCP_UDP4: - exec_name = IPCP_UDP4_EXEC; - break; - case IPCP_UDP6: - exec_name = IPCP_UDP6_EXEC; - break; - case IPCP_ETH_LLC: - exec_name = IPCP_ETH_LLC_EXEC; - break; - case IPCP_ETH_DIX: - exec_name = IPCP_ETH_DIX_EXEC; - break; case IPCP_LOCAL: exec_name = IPCP_LOCAL_EXEC; break; @@ -332,9 +356,6 @@ int bootstrap_ipcp(pid_t pid, goto fail; } - if (conf->type == IPCP_UDP4 || conf->type == IPCP_UDP6) - conf->layer_info.dir_hash_algo = (enum pol_dir_hash) HASH_MD5; - if (ipcp_bootstrap(pid, conf, &layer)) { log_err("Could not bootstrap IPCP."); goto fail; @@ -354,8 +375,55 @@ int bootstrap_ipcp(pid_t pid, return -1; } -int enroll_ipcp(pid_t pid, - const char * dst) +ssize_t list_poas(pid_t pid, + struct poa_spec ** eps) +{ + struct ipcp_info info; + + info.pid = pid; + if (reg_get_ipcp(&info, NULL) < 0) { + log_err("Could not find IPCP %d.", pid); + return -1; + } + + if (info.type != IPCP_UNICAST && info.type != IPCP_BROADCAST) + return 0; /* nothing it could be attached to */ + + return ipcp_list_poas(pid, eps); +} + +/* Only the types that can carry a PoA may take one. */ +int attach_ipcp(pid_t pid, + const struct poa_spec * poa, + bool attach) +{ + struct ipcp_info info; + + info.pid = pid; + if (reg_get_ipcp(&info, NULL) < 0) { + log_err("Could not find IPCP %d.", pid); + return -1; + } + + if (info.type != IPCP_UNICAST && info.type != IPCP_BROADCAST) { + log_err("IPCP %d does not support PoAs.", pid); + return -1; + } + + if (ipcp_attach(pid, poa, attach) < 0) { + log_err("Could not %s IPCP %d.", + attach ? "attach" : "detach", pid); + return -1; + } + + log_info("%s IPCP %d.", attach ? "Attached" : "Detached", pid); + + return 0; +} + +int enroll_ipcp(pid_t pid, + const char * dst, + const struct poa_addr * addr) { struct layer_info layer; struct ipcp_info info; @@ -367,7 +435,7 @@ int enroll_ipcp(pid_t pid, goto fail; } - if (ipcp_enroll(pid, dst, &layer) < 0) { + if (ipcp_enroll(pid, dst, addr, &layer) < 0) { log_err("Could not enroll IPCP %d.", pid); goto fail; } @@ -386,10 +454,11 @@ int enroll_ipcp(pid_t pid, return -1; } -int connect_ipcp(pid_t pid, - const char * dst, - const char * component, - qosspec_t qs) +int connect_ipcp(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr) { struct ipcp_info info; @@ -407,7 +476,7 @@ int connect_ipcp(pid_t pid, log_dbg("Connecting %s to %s.", component, dst); - if (ipcp_connect(pid, dst, component, qs)) { + if (ipcp_connect(pid, dst, component, qs, addr) < 0) { log_err("Could not connect IPCP %d to %s.", pid, dst); return -EPERM; } @@ -418,6 +487,172 @@ int connect_ipcp(pid_t pid, return 0; } +/* Resolve the dial name, if any, and fill in one record. */ +static int poa_addr_resolve(struct poa_addr * addr, + struct addrinfo ** res) +{ + struct addrinfo hints; + + *res = NULL; + + if (addr == NULL) + return 0; + + if (addr->hostname[0] == '\0') + return addr->type == POA_UDP ? -EINVAL : 0; + + if (addr->type != POA_UDP) + return -EINVAL; + + memset(&hints, 0, sizeof(hints)); + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + if (getaddrinfo(addr->hostname, NULL, &hints, res) != 0) { + log_err("Failed to resolve %s.", addr->hostname); + return -EADDRNOTAVAIL; + } + + return 0; +} + +static void poa_addr_from_ai(struct poa_addr * addr, + const struct addrinfo * ai, + uint16_t port) +{ + struct sockaddr_in * in; + struct sockaddr_in6 * in6; + + if (ai->ai_family == AF_INET) { + in = (struct sockaddr_in *) ai->ai_addr; + addr->type = POA_UDP4; + addr->udp4.ip_addr = in->sin_addr; + addr->udp4.port = port; + } else { + in6 = (struct sockaddr_in6 *) ai->ai_addr; + addr->type = POA_UDP6; + addr->udp6.ip_addr = in6->sin6_addr; + addr->udp6.port = port; + } +} + +/* Skip families without an attached PoA; keep all if none reported. */ +static void poa_families(pid_t pid, + bool * v4, + bool * v6) +{ + struct poa_spec * eps; + ssize_t n; + ssize_t i; + + eps = NULL; + + *v4 = false; + *v6 = false; + + n = list_poas(pid, &eps); + for (i = 0; i < n; i++) { + if (eps[i].type == POA_UDP4) + *v4 = true; + + if (eps[i].type == POA_UDP6) + *v6 = true; + } + + free(eps); + + if (!*v4 && !*v6) { + *v4 = true; + *v6 = true; + } +} + +/* Try each resolved record in order until an enrolment succeeds. */ +int enroll_ipcp_resolve(pid_t pid, + const char * dst, + struct poa_addr * addr) +{ + struct addrinfo * res; + struct addrinfo * ai; + uint16_t port; + bool v4; + bool v6; + int ret; + + ret = poa_addr_resolve(addr, &res); + if (ret < 0) + return ret; + + if (res == NULL) + return enroll_ipcp(pid, dst, addr); + + port = addr->udp4.port; /* POA_UDP parks it there */ + + poa_families(pid, &v4, &v6); + + ret = -EADDRNOTAVAIL; + + for (ai = res; ai != NULL; ai = ai->ai_next) { + if ((ai->ai_family == AF_INET && !v4) + || (ai->ai_family == AF_INET6 && !v6)) + continue; + + poa_addr_from_ai(addr, ai, port); + + ret = enroll_ipcp(pid, dst, addr); + if (ret == 0) + break; + } + + freeaddrinfo(res); + + return ret; +} + +/* Try each resolved record in order until a connect succeeds. */ +int connect_ipcp_resolve(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + struct poa_addr * addr) +{ + struct addrinfo * res; + struct addrinfo * ai; + uint16_t port; + bool v4; + bool v6; + int ret; + + ret = poa_addr_resolve(addr, &res); + if (ret < 0) + return ret; + + if (res == NULL) + return connect_ipcp(pid, dst, component, qs, addr); + + port = addr->udp4.port; /* POA_UDP parks it there */ + + poa_families(pid, &v4, &v6); + + ret = -EADDRNOTAVAIL; + + for (ai = res; ai != NULL; ai = ai->ai_next) { + if ((ai->ai_family == AF_INET && !v4) + || (ai->ai_family == AF_INET6 && !v6)) + continue; + + poa_addr_from_ai(addr, ai, port); + + ret = connect_ipcp(pid, dst, component, qs, addr); + if (ret == 0) + break; + } + + freeaddrinfo(res); + + return ret; +} + static int disconnect_ipcp(pid_t pid, const char * dst, const char * component) @@ -454,8 +689,8 @@ static void name_update_sec_paths(struct name_info * info) assert(info != NULL); - if (strlen(info->s.enc) == 0) - sprintf(info->s.enc, "%s/%s/enc.conf", srv_dir, info->name); + if (strlen(info->s.sec) == 0) + sprintf(info->s.sec, "%s/%s/sec.conf", srv_dir, info->name); if (strlen(info->s.crt) == 0) sprintf(info->s.crt, "%s/%s/crt.pem", srv_dir, info->name); @@ -463,8 +698,8 @@ static void name_update_sec_paths(struct name_info * info) if (strlen(info->s.key) == 0) sprintf(info->s.key, "%s/%s/key.pem", srv_dir, info->name); - if (strlen(info->c.enc) == 0) - sprintf(info->c.enc, "%s/%s/enc.conf", cli_dir, info->name); + if (strlen(info->c.sec) == 0) + sprintf(info->c.sec, "%s/%s/sec.conf", cli_dir, info->name); if (strlen(info->c.crt) == 0) sprintf(info->c.crt, "%s/%s/crt.pem", cli_dir, info->name); @@ -784,7 +1019,8 @@ static int name_unreg(const char * name, static int get_peer_ids(int fd, uid_t * uid, - gid_t * gid) + gid_t * gid, + pid_t * pid) { #if defined(__linux__) struct ucred ucred; @@ -797,9 +1033,14 @@ static int get_peer_ids(int fd, *uid = ucred.uid; *gid = ucred.gid; + if (pid != NULL) + *pid = ucred.pid; #else if (getpeereid(fd, uid, gid) < 0) goto fail; + + if (pid != NULL) + *pid = -1; /* no portable SO_PEERCRED.pid equivalent */ #endif return 0; fail: @@ -841,6 +1082,22 @@ static void __cleanup_flow(void * flow) reg_destroy_flow(((struct flow_info *) flow)->id); } +/* + * A PoA flow is secured as the IPCP that owns it: trust is in the + * peer itself. Without a security config for that name the flow + * stays in the clear. + */ +static void poa_name_info(const char * name, + struct name_info * info) +{ + if (reg_get_name_info(name, info) == 0) + return; + + memset(info, 0, sizeof(*info)); + + strcpy(info->name, name); +} + static int flow_accept(struct flow_info * flow, buffer_t * data, struct timespec * abstime, @@ -848,6 +1105,7 @@ static int flow_accept(struct flow_info * flow, { buffer_t req_hdr; buffer_t resp_hdr; + buffer_t peer_crt = BUF_INIT; char name[NAME_SIZE + 1]; struct name_info info; int err; @@ -900,7 +1158,9 @@ static int flow_accept(struct flow_info * flow, goto fail_oap; } - if (reg_get_name_info(name, &info) < 0) { + if (reg_flow_is_poa(flow->id)) { + poa_name_info(name, &info); + } else if (reg_get_name_info(name, &info) < 0) { log_err("Failed to get name info for %s.", name); err = -ENAME; goto fail_oap; @@ -911,7 +1171,8 @@ static int flow_accept(struct flow_info * flow, flow->uid = reg_get_proc_uid(flow->n_pid); - err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk); + err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk, + false, NULL, &peer_crt); if (err == -EREPLAY) { log_warn("Dropping replayed alloc request for %s.", name); goto fail_replay; @@ -926,16 +1187,21 @@ static int flow_accept(struct flow_info * flow, log_err("Failed to respond to direct flow."); goto fail_resp; } + if (sk->nid != NID_undef) + reg_flow_set_rekey(flow->id, false, peer_crt); log_info("Flow %d accepted (direct) by %d for %s.", flow->id, flow->n_pid, name); } else if (ipcp_flow_alloc_resp(flow, 0, resp_hdr) < 0) { log_err("Failed to respond to flow allocation."); goto fail_resp; } else { + if (sk->nid != NID_undef) + reg_flow_set_rekey(flow->id, false, peer_crt); log_info("Flow %d accepted by %d for %s (uid %d).", flow->id, flow->n_pid, name, flow->uid); } + freebuf(peer_crt); freebuf(req_hdr); freebuf(resp_hdr); @@ -945,6 +1211,7 @@ static int flow_accept(struct flow_info * flow, if (!reg_flow_is_direct(flow->id)) ipcp_flow_alloc_resp(flow, err, resp_hdr); fail_replay: + freebuf(peer_crt); freebuf(req_hdr); freebuf(resp_hdr); fail_wait: @@ -954,6 +1221,7 @@ static int flow_accept(struct flow_info * flow, fail_resp: flow->state = FLOW_NULL; + freebuf(peer_crt); freebuf(req_hdr); freebuf(resp_hdr); reg_destroy_flow(flow->id); @@ -1202,6 +1470,7 @@ static int flow_alloc_direct(const char * dst, struct flow_info acc; /* server side flow */ buffer_t req_hdr = BUF_INIT; buffer_t resp_hdr = BUF_INIT; + buffer_t no_crt = BUF_INIT; void * ctx; int err; @@ -1211,7 +1480,7 @@ static int flow_alloc_direct(const char * dst, return -EAGAIN; } - if (oap_cli_prepare(&ctx, info, &req_hdr, *data) < 0) { + if (oap_cli_prepare(&ctx, info, NULL, &req_hdr, *data, false) < 0) { log_err("Failed to prepare OAP for %s.", dst); return -EBADF; } @@ -1244,7 +1513,7 @@ static int flow_alloc_direct(const char * dst, return -ETIMEDOUT; } - err = oap_cli_complete(ctx, info, resp_hdr, data, sk); + err = oap_cli_complete(ctx, resp_hdr, data, sk, NULL, NULL); if (err < 0) { log_err("OAP completion failed for %s.", dst); freebuf(resp_hdr); @@ -1257,6 +1526,10 @@ static int flow_alloc_direct(const char * dst, flow->mtu = DIRECT_MTU; flow->state = FLOW_ALLOCATED; + /* Mark encrypted for re-key; the acceptor caches the cert. */ + if (sk->nid != NID_undef) + reg_flow_set_rekey(acc.id, true, no_crt); + log_info("Flow %d allocated (direct) for %d to %s.", flow->id, flow->n_pid, dst); @@ -1275,6 +1548,7 @@ static int flow_alloc(const char * dst, buffer_t req_hdr = BUF_INIT; buffer_t resp_hdr = BUF_INIT; buffer_t hash = BUF_INIT; + buffer_t peer_crt = BUF_INIT; struct name_info info; void * ctx; int err; @@ -1308,6 +1582,8 @@ static int flow_alloc(const char * dst, goto fail_flow; } + reg_set_name_for_flow_id(dst, flow->id); + if (get_ipcp_by_dst(dst, &flow->n_1_pid, &hash) < 0) { log_err("Failed to find IPCP for %s.", dst); err = -EIPCP; @@ -1320,7 +1596,7 @@ static int flow_alloc(const char * dst, goto fail_prepare; } - if (oap_cli_prepare(&ctx, &info, &req_hdr, *data) < 0) { + if (oap_cli_prepare(&ctx, &info, NULL, &req_hdr, *data, false) < 0) { log_err("Failed to prepare OAP request for %s.", dst); err = -EBADF; goto fail_prepare; @@ -1352,12 +1628,16 @@ static int flow_alloc(const char * dst, goto fail_peer; } - err = oap_cli_complete(ctx, &info, resp_hdr, data, sk); + err = oap_cli_complete(ctx, resp_hdr, data, sk, NULL, &peer_crt); if (err < 0) { log_err("OAP completion failed for %s.", dst); goto fail_complete; } + if (sk->nid != NID_undef) + reg_flow_set_rekey(flow->id, true, peer_crt); + + freebuf(peer_crt); freebuf(req_hdr); freebuf(resp_hdr); freebuf(hash); @@ -1365,7 +1645,8 @@ static int flow_alloc(const char * dst, return 0; fail_complete: - ctx = NULL; /* freee'd on complete */ + freebuf(peer_crt); + ctx = NULL; /* free'd on complete */ fail_peer: flow->state = FLOW_DEALLOCATED; fail_wait: @@ -1381,6 +1662,199 @@ static int flow_alloc(const char * dst, return err; } +/* Creates the flow and prepares the key exchange the IPCP will carry. */ +/* The N-1 is only known once the flow exists. */ +static int poa_alloc(struct flow_info * flow, + const char * dst, + buffer_t * data) +{ + struct ipcp_info ipcp; + struct layer_info layer; + struct name_info info; + buffer_t req_hdr = BUF_INIT; + void * ctx; + int err; + + ipcp.pid = flow->n_pid; + if (reg_get_ipcp(&ipcp, &layer) < 0) { + log_err("No IPCP with pid %d.", flow->n_pid); + + err = -EIPCP; + goto fail_flow; + } + + flow->n_1_pid = 0; + if (reg_create_flow(flow) < 0) { + log_err("Failed to create PoA flow."); + + err = -EBADF; + goto fail_flow; + } + + reg_set_name_for_flow_id(ipcp.name, flow->id); + + if (reg_flow_set_poa(flow->id) < 0) { + err = -EBADF; + goto fail_prepare; + } + + flow->uid = reg_get_proc_uid(flow->n_pid); + flow->n_1_pid = flow->n_pid; /* the IPCP is its own N-1 */ + if (reg_prepare_flow_alloc(flow) < 0) { + log_err("Failed to prepare PoA flow allocation."); + + err = -EBADF; + goto fail_prepare; + } + + poa_name_info(ipcp.name, &info); + + if (oap_cli_prepare(&ctx, &info, dst, &req_hdr, *data, false) < 0) { + log_err("Failed to prepare OAP request for %s.", ipcp.name); + + err = -EBADF; + goto fail_prepare; + } + + if (reg_flow_set_oap_ctx(flow->id, ctx) < 0) { + err = -EBADF; + goto fail_ctx; + } + + log_info("Allocating PoA flow %d for %d as %s.", + flow->id, flow->n_pid, ipcp.name); + + *data = req_hdr; + + return 0; + + fail_ctx: + freebuf(req_hdr); + oap_ctx_free(ctx); + fail_prepare: + reg_destroy_flow(flow->id); + fail_flow: + return err; +} + +/* Completes the key exchange once the IPCP has the peer's response. */ +/* + * We present the PoA's own credentials and expect the peer to present + * dst's: the address only says how to reach it. oap_cli_complete + * frees the ctx on every path. + */ +static int poa_complete(struct flow_info * flow, + int response, + buffer_t * data, + struct crypt_sk * sk) +{ + struct name_info info; + buffer_t peer_crt = BUF_INIT; + buffer_t out = BUF_INIT; + buffer_t empty = BUF_INIT; + char name[NAME_SIZE + 1]; + void * ctx; + int err; + + ctx = reg_flow_take_oap_ctx(flow->id); + if (ctx == NULL) { + log_err("No pending PoA flow %d.", flow->id); + return -EBADF; + } + + if (response < 0) { + log_dbg("PoA flow %d refused: %d.", flow->id, response); + + err = response; + goto fail; + } + + if (reg_get_name_for_flow_id(name, flow->id) < 0) { + err = -EBADF; + goto fail; + } + + poa_name_info(name, &info); + + err = oap_cli_complete(ctx, *data, &out, sk, NULL, &peer_crt); + if (err < 0) { + log_err("OAP completion failed for %s.", name); + + ctx = NULL; + goto fail; + } + + if (sk->nid != NID_undef) + reg_flow_set_rekey(flow->id, true, peer_crt); + + flow->state = FLOW_ALLOCATED; + if (reg_respond_alloc(flow, &empty, 0) < 0) { + log_err("Failed to update PoA flow %d.", flow->id); + + err = -EBADF; + goto fail_crt; + } + + log_info("PoA flow %d allocated to %s.", flow->id, name); + + freebuf(peer_crt); + freebuf(out); + + return 0; + + fail_crt: + freebuf(peer_crt); + freebuf(out); + fail: + oap_ctx_free(ctx); + reg_destroy_flow(flow->id); + return err; +} + +/* A peer requested a flow on a PoA of this IPCP. */ +static int poa_req_arr(struct flow_info * flow, + buffer_t * data) +{ + struct ipcp_info ipcp; + struct layer_info layer; + int ret; + + ipcp.pid = flow->n_1_pid; + if (reg_get_ipcp(&ipcp, &layer) < 0) { + log_err("No IPCP with pid %d.", flow->n_1_pid); + + ret = -EIPCP; + goto fail; + } + + log_info("PoA flow request arrived for %s.", ipcp.name); + + ret = wait_for_accept(ipcp.name); + if (ret < 0) { + log_err("No active process for %s.", ipcp.name); + goto fail; + } + + flow->id = ret; + flow->state = FLOW_ALLOCATED; + if (reg_flow_set_poa(flow->id) < 0) { + ret = -EBADF; + goto fail; + } + + reg_set_name_for_flow_id(ipcp.name, flow->id); + + ret = reg_respond_accept(flow, data); + if (ret < 0) { + log_err("Failed to respond to PoA flow %d.", flow->id); + goto fail; + } + + return 0; + fail: + return ret; +} + static int flow_alloc_reply(struct flow_info * flow, int response, buffer_t * data) @@ -1432,6 +1906,746 @@ static int flow_dealloc_resp(struct flow_info * flow) return 0; } +/* + * Inbox producers. Any thread may post; the worker drains. INIT carries + * the flow's lower IPCP pid; RESP transfers ownership of buf. + */ +static void rekey_post(enum rekey_evt_type type, + int flow_id, + pid_t n_1_pid, + buffer_t * buf) +{ + struct rekey_evt * evt; + + evt = malloc(sizeof(*evt)); + if (evt == NULL) { + log_err("Failed to malloc re-key event for flow %d.", flow_id); + if (type == REKEY_INIT || type == REKEY_DIRECT) + reg_flow_clear_in_flight(flow_id); + else + reg_flow_rekey_arr_done(flow_id, type == REKEY_REQ); + + if (buf != NULL) + freebuf(*buf); + + return; + } + + list_head_init(&evt->next); + evt->type = type; + evt->flow_id = flow_id; + evt->n_1_pid = n_1_pid; + clrbuf(evt->buf); + if (buf != NULL) { + evt->buf = *buf; + clrbuf(*buf); + } + + pthread_mutex_lock(&irmd.rk.mtx); + + list_add_tail(&evt->next, &irmd.rk.inbox); + pthread_cond_signal(&irmd.rk.cond); + + pthread_mutex_unlock(&irmd.rk.mtx); +} + +static void rekey_post_init(int flow_id, + pid_t n_1_pid) +{ + rekey_post(REKEY_INIT, flow_id, n_1_pid, NULL); +} + +static void rekey_post_resp(int flow_id, + buffer_t * buf) +{ + rekey_post(REKEY_RESP, flow_id, 0, buf); +} + +static void rekey_post_req(int flow_id, + pid_t n_1_pid, + buffer_t * buf) +{ + rekey_post(REKEY_REQ, flow_id, n_1_pid, buf); +} + +static void rekey_post_direct(int flow_id) +{ + rekey_post(REKEY_DIRECT, flow_id, 0, NULL); +} + +/* Worker-only: find an in-flight entry by flow_id. */ +static struct rekey_ctx * rekey_find(struct list_head * tbl, + int flow_id) +{ + struct list_head * p; + + list_for_each(p, tbl) { + struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next); + if (e->flow_id == flow_id) + return e; + } + + return NULL; +} + +/* Worker-only: drop an entry, freeing its OAP ctx. */ +static void rekey_drop(struct rekey_ctx * e) +{ + if (e->ctx != NULL) + oap_ctx_free(e->ctx); + + list_del(&e->next); + free(e); +} + +/* Resolve a flow's registered name info; < 0 if the flow or name is gone. */ +static int rekey_name_info(int flow_id, + struct name_info * info) +{ + char name[NAME_SIZE + 1]; + + if (reg_get_name_for_flow_id(name, flow_id) < 0) + return -1; + + if (reg_flow_is_poa(flow_id)) { + poa_name_info(name, info); + return 0; + } + + return reg_get_name_info(name, info); +} + +/* Flow-update relay payload: a 1-byte type prefix on an opaque body. */ +enum flow_upd_type { + FLOW_UPD_REKEY_REQ = 0, + FLOW_UPD_REKEY_RESP = 1, +}; + +/* Prepend the update type to body; caller frees out on success. */ +static int flow_upd_wrap(buffer_t * out, + uint8_t type, + const buffer_t * body) +{ + out->len = body->len + 1; + out->data = malloc(out->len); + if (out->data == NULL) + return -ENOMEM; + + out->data[0] = type; + memcpy(out->data + 1, body->data, body->len); + + return 0; +} + +/* Cleanup handlers — the re-key worker is cancelled at shutdown. */ +static void rk_free_evt(void * o) +{ + struct rekey_evt * evt = o; + + freebuf(evt->buf); + free(evt); +} + +static void rk_freebuf(void * o) +{ + freebuf(*(buffer_t *) o); +} + +static void rk_clear_in_flight(void * o) +{ + reg_flow_clear_in_flight(*(int *) o); +} + +static void rk_clear_key(void * o) +{ + crypt_secure_clear(o, SYMMKEYSZ); +} + +static void rekey_do_initiate(struct list_head * tbl, + int flow_id, + pid_t n_1_pid) +{ + struct rekey_ctx * e; + struct flow_info info; + struct name_info name; + buffer_t req = BUF_INIT; + buffer_t upd = BUF_INIT; + buffer_t data = BUF_INIT; + void * ctx = NULL; + int ret; + + e = rekey_find(tbl, flow_id); + if (e != NULL) + rekey_drop(e); /* Replace in-flight entries */ + + if (rekey_name_info(flow_id, &name) < 0) { + log_err("Failed to get name info to re-key flow %d.", flow_id); + goto fail; + } + + if (oap_cli_prepare(&ctx, &name, NULL, &req, data, true) < 0) { + log_err("Failed to prepare re-key for flow %d.", flow_id); + goto fail; + } + + memset(&info, 0, sizeof(info)); + info.id = flow_id; + info.n_1_pid = n_1_pid; + + if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_REQ, &req) < 0) { + log_err("Failed to wrap re-key request for flow %d.", flow_id); + goto fail_ctx; + } + + pthread_cleanup_push(rk_clear_in_flight, &flow_id); + pthread_cleanup_push(oap_ctx_free, ctx); + pthread_cleanup_push(rk_freebuf, &req); + pthread_cleanup_push(rk_freebuf, &upd); + ret = ipcp_flow_update(&info, upd); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + freebuf(upd); + if (ret < 0) { + log_err("Failed to send re-key request for flow %d.", flow_id); + goto fail_ctx; + } + + e = malloc(sizeof(*e)); + if (e == NULL) { + log_err("Failed to malloc re-key ctx for flow %d.", flow_id); + goto fail_ctx; + } + + list_head_init(&e->next); + e->flow_id = flow_id; + e->ctx = ctx; + clock_gettime(PTHREAD_COND_CLOCK, &e->deadline); + e->deadline.tv_sec += REKEY_RESP_TIMEO; + + list_add(&e->next, tbl); + + log_dbg("Re-key request sent for flow %d.", flow_id); + + freebuf(req); + + return; + + fail_ctx: + oap_ctx_free(ctx); + freebuf(req); + fail: + reg_flow_clear_in_flight(flow_id); +} + +/* Worker-only: complete the exchange, install the pending seed. */ +static void rekey_do_complete(struct list_head * tbl, + int flow_id, + buffer_t buf) +{ + struct rekey_ctx * e; + struct name_info info; + struct crypt_sk sk; + uint8_t kbuf[SYMMKEYSZ]; + buffer_t data = BUF_INIT; + buffer_t crt = BUF_INIT; + uint8_t newgen; + + e = rekey_find(tbl, flow_id); + if (e == NULL) { + log_dbg("Stale re-key RESPONSE for flow %d.", flow_id); + return; + } + + /* A concurrent responder already parked a seed; don't overwrite. */ + if (reg_flow_rekey_pending(flow_id)) { + log_dbg("Re-key already pending for flow %d.", flow_id); + goto finish; + } + + if (rekey_name_info(flow_id, &info) < 0) { + log_err("Failed to get name info to re-key flow %d.", flow_id); + goto finish; + } + + sk.key = kbuf; + + reg_flow_get_peer_crt(flow_id, &crt); + + /* oap_cli_complete frees the ctx on every path. */ + if (oap_cli_complete(e->ctx, buf, &data, &sk, &crt, NULL) < 0) { + log_warn("Failed to complete re-key for flow %d.", flow_id); + e->ctx = NULL; + goto finish_clear; + } + + e->ctx = NULL; + + if (data.len != 1) { + log_warn("Re-key reply malformed for flow %d.", flow_id); + goto finish_clear; + } + + newgen = *(uint8_t *) data.data; + + if (newgen >= 16) { + log_warn("Re-key gen %u out of range for flow %d.", + newgen, flow_id); + goto finish_clear; + } + + if (reg_flow_store_pending(flow_id, kbuf, newgen, true) < 0) + log_warn("Flow %d gone during re-key.", flow_id); + else + reg_notify_flow(flow_id, FLOW_UPD); + + log_dbg("Re-key completed for flow %d (gen %u).", flow_id, newgen); + + finish_clear: + crypt_secure_clear(kbuf, SYMMKEYSZ); + freebuf(data); + finish: + freebuf(crt); + rekey_drop(e); + reg_flow_clear_in_flight(flow_id); +} + +/* Worker-only: reap entries whose RESPONSE never arrived. */ +static void rekey_reap_expired(struct list_head * tbl) +{ + struct list_head * p; + struct list_head * h; + struct timespec now; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + list_for_each_safe(p, h, tbl) { + struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next); + if (ts_diff_ns(&e->deadline, &now) > 0) + continue; + + log_warn("Re-key timed out for flow %d.", e->flow_id); + reg_flow_clear_in_flight(e->flow_id); + rekey_drop(e); + } +} + +/* Responder side: process request, install pending seed, send response. */ +static int rekey_respond(struct flow_info * flow, + buffer_t * pk) +{ + struct name_info info; + struct crypt_sk sk; + uint8_t kbuf[SYMMKEYSZ]; + buffer_t rsp = BUF_INIT; + buffer_t upd = BUF_INIT; + buffer_t data = BUF_INIT; + buffer_t crt = BUF_INIT; + uint8_t newgen; + int epoch; + int err; + + epoch = reg_flow_get_epoch(flow->id); + if (epoch < 0) { + log_warn("Re-key for unknown flow %d.", flow->id); + return -EBADF; + } + + /* Collision: we are driving our own exchange; let it win. */ + if (reg_flow_rekey_should_yield(flow->id)) { + log_dbg("Yielding to own re-key for flow %d.", flow->id); + return 0; + } + + if (rekey_name_info(flow->id, &info) < 0) { + log_err("Failed to get name info to re-key flow %d.", flow->id); + return -ENAME; + } + + if (reg_flow_rekey_pending(flow->id)) { + log_dbg("Duplicate re-key request for flow %d.", flow->id); + return 0; + } + + newgen = (uint8_t) ((epoch + 1) & 0x0F); + data.data = &newgen; + data.len = 1; + + sk.key = kbuf; + + reg_flow_get_peer_crt(flow->id, &crt); + + err = oap_srv_process(&info, *pk, &rsp, &data, &sk, true, &crt, NULL); + if (err < 0) { + /* data still points to stack newgen; don't free it. */ + log_err("Failed to process re-key OAP for flow %d.", flow->id); + goto finish; + } + + /* On success oap_srv_process repointed data to client output. */ + freebuf(data); + + if (reg_flow_store_pending(flow->id, kbuf, newgen, false) < 0) { + log_warn("Flow %d gone during re-key.", flow->id); + err = -EBADF; + goto finish; + } + + reg_notify_flow(flow->id, FLOW_UPD); + + if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_RESP, &rsp) == 0) { + pthread_cleanup_push(rk_clear_key, kbuf); + pthread_cleanup_push(rk_freebuf, &rsp); + pthread_cleanup_push(rk_freebuf, &crt); + pthread_cleanup_push(rk_freebuf, &upd); + if (ipcp_flow_update(flow, upd) < 0) + log_err("Failed to send re-key response for flow %d.", + flow->id); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + freebuf(upd); + } + + err = 0; + finish: + crypt_secure_clear(kbuf, SYMMKEYSZ); + freebuf(rsp); + freebuf(crt); + + return err; +} + +/* + * Worker-only: re-key a direct (loopback) flow, the exchange runs in-process: + * build a client request, then derive the shared seed, and hand the one seed + * to both apps with RB_REKEY. + */ +static void rekey_do_direct(int flow_id) +{ + struct name_info info; + struct crypt_sk sk; + uint8_t kbuf[SYMMKEYSZ]; + buffer_t req = BUF_INIT; + buffer_t rsp = BUF_INIT; + buffer_t data = BUF_INIT; + buffer_t crt = BUF_INIT; + void * ctx = NULL; + uint8_t newgen; + int epoch; + + epoch = reg_flow_get_epoch(flow_id); + if (epoch < 0) { + log_warn("Re-key for unknown flow %d.", flow_id); + reg_flow_clear_in_flight(flow_id); + return; + } + + if (rekey_name_info(flow_id, &info) < 0) { + log_err("Failed to get name info to re-key flow %d.", flow_id); + reg_flow_clear_in_flight(flow_id); + return; + } + + if (oap_cli_prepare(&ctx, &info, NULL, &req, data, true) < 0) { + log_err("Failed to prepare re-key for flow %d.", flow_id); + reg_flow_clear_in_flight(flow_id); + return; + } + + newgen = (uint8_t) ((epoch + 1) & 0x0F); + data.data = &newgen; + data.len = 1; + + sk.key = kbuf; + + reg_flow_get_peer_crt(flow_id, &crt); + + if (oap_srv_process(&info, req, &rsp, &data, &sk, true, + &crt, NULL) < 0) { + /* data still points to stack newgen; don't free it. */ + log_err("Failed to process re-key OAP for flow %d.", flow_id); + reg_flow_clear_in_flight(flow_id); + goto out; + } + + /* On success oap_srv_process repointed data to its output. */ + freebuf(data); + + if (reg_flow_store_pending_direct(flow_id, kbuf, newgen) < 0) { + log_warn("Flow %d gone during re-key.", flow_id); + reg_flow_clear_in_flight(flow_id); + goto out; + } + + reg_notify_flow_peers(flow_id, FLOW_UPD); + + log_dbg("Re-key completed (direct) for flow %d (gen %u).", + flow_id, newgen); + out: + crypt_secure_clear(kbuf, SYMMKEYSZ); + oap_ctx_free(ctx); + freebuf(req); + freebuf(rsp); + freebuf(crt); +} + +/* Route one snapshot entry to the wire or in-process re-key path. */ +static void rekey_dispatch(struct list_head * tbl, + const struct rekey_info * ri) +{ + if (ri->direct) + rekey_do_direct(ri->flow_id); + else + rekey_do_initiate(tbl, ri->flow_id, ri->n_1_pid); +} + +static int flow_update_arr(struct flow_info * flow, + buffer_t * pk) +{ + uint8_t type; + bool is_req; + + if (pk->len < 1) + return -EINVAL; + + type = pk->data[0]; + + switch (type) { + case FLOW_UPD_REKEY_REQ: + is_req = true; + break; + case FLOW_UPD_REKEY_RESP: + is_req = false; + break; + default: + log_warn("Unknown flow update type %u.", type); + return -EINVAL; + } + + /* Drop floods/spoofs before allocating a worker event. */ + if (!reg_flow_rekey_arr_admit(flow->id, flow->n_1_pid, is_req)) + return 0; + + /* Strip the type byte, keeping the malloc base for hand-off. */ + memmove(pk->data, pk->data + 1, pk->len - 1); + pk->len -= 1; + + /* Defer to worker; an inline RESP send deadlocks loopback. */ + if (is_req) + rekey_post_req(flow->id, flow->n_1_pid, pk); + else + rekey_post_resp(flow->id, pk); + + return 0; +} + +static int flow_update(struct flow_info * flow, + uid_t uid, + pid_t cpid, + bool rekey, + struct crypt_sk * sk, + bool * has_key, + bool * initiator) +{ + uint8_t seed[SYMMKEYSZ]; + uint8_t epoch; + int rc; + + *has_key = false; + *initiator = false; + + if (rekey) { + pid_t n_1_pid; + + if (!reg_flow_owned_by(flow->id, uid)) + return -EPERM; + + /* Direct flows re-key in-process; no lower IPCP carrier. */ + if (reg_flow_is_direct(flow->id)) { + if (reg_flow_rekey_begin(flow->id)) + rekey_post_direct(flow->id); + + return 0; + } + + /* Watermark re-key: the app can't know its lower IPCP. */ + n_1_pid = reg_flow_get_n_1_pid(flow->id); + if (n_1_pid <= 0) + return 0; + + /* One exchange per flow; the latch arbitrates collisions. */ + if (reg_flow_rekey_begin(flow->id)) + rekey_post_init(flow->id, n_1_pid); + + return 0; + } + + rc = reg_flow_take_pending(flow->id, uid, cpid, seed, &epoch, + initiator); + if (rc == -EPERM) + return -EPERM; + + if (rc != 0) + return 0; + + memcpy(sk->key, seed, SYMMKEYSZ); + sk->epoch = epoch; + *has_key = true; + + crypt_secure_clear(seed, SYMMKEYSZ); + + log_dbg("Delivered re-key seed for flow %d (gen %u).", + flow->id, epoch); + + return 0; +} + +static void rekey_table_cleanup(void * o) +{ + struct list_head * tbl = o; + struct list_head * p; + struct list_head * h; + + list_for_each_safe(p, h, tbl) { + struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next); + rekey_drop(e); + } +} + +static struct rekey_evt * rekey_event_wait(const struct timespec * dl) +{ + struct rekey_evt * evt = NULL; + int ret = 0; + + pthread_mutex_lock(&irmd.rk.mtx); + pthread_cleanup_push(__cleanup_mutex_unlock, &irmd.rk.mtx); + + while (list_is_empty(&irmd.rk.inbox) && ret != -ETIMEDOUT) + ret = -pthread_cond_timedwait(&irmd.rk.cond, &irmd.rk.mtx, dl); + + if (!list_is_empty(&irmd.rk.inbox)) { + evt = list_first_entry(&irmd.rk.inbox, struct rekey_evt, next); + list_del(&evt->next); + } + + pthread_cleanup_pop(true); + + return evt; +} + +static struct timespec rekey_deadline(struct list_head * tbl, + struct timespec next) +{ + struct timespec deadline = next; + struct list_head * p; + + list_for_each(p, tbl) { + struct rekey_ctx * e; + e = list_entry(p, struct rekey_ctx, next); + if (ts_diff_ns(&e->deadline, &deadline) < 0) + deadline = e->deadline; + } + + return deadline; +} + +static void rekey_handle_evt(struct list_head * tbl, + struct rekey_evt * evt) +{ + struct flow_info rinfo; + + pthread_cleanup_push(rk_free_evt, evt); + + switch (evt->type) { + case REKEY_INIT: + rekey_do_initiate(tbl, evt->flow_id, evt->n_1_pid); + break; + case REKEY_REQ: + memset(&rinfo, 0, sizeof(rinfo)); + rinfo.id = evt->flow_id; + rinfo.n_1_pid = evt->n_1_pid; + rekey_respond(&rinfo, &evt->buf); + reg_flow_rekey_arr_done(evt->flow_id, true); + break; + case REKEY_RESP: + rekey_do_complete(tbl, evt->flow_id, evt->buf); + reg_flow_rekey_arr_done(evt->flow_id, false); + break; + case REKEY_DIRECT: + rekey_do_direct(evt->flow_id); + break; + default: + break; + } + + pthread_cleanup_pop(true); +} + +/* On the periodic tick, dispatch all flows due for re-keying. */ +static void rekey_run_periodic(struct list_head * tbl, + struct timespec * next) +{ + struct rekey_info snap[REKEY_BATCH]; + struct timespec now; + int n; + int i; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + if (ts_diff_ns(next, &now) > 0) + return; + + n = reg_flow_snapshot_rekey_due(snap, REKEY_BATCH); + for (i = 0; i < n; ++i) + rekey_dispatch(tbl, &snap[i]); + + clock_gettime(PTHREAD_COND_CLOCK, next); + next->tv_sec += OAP_REKEY_TIMER; +} + +/* + * Single worker owning all in-flight Tier-2 re-keys. It drains the + * inbox, runs the periodic snapshot, and reaps timed-out exchanges. + * The table is touched only here, so it needs no lock. + */ +static void * rekey_worker(void * o) +{ + struct list_head table; + struct timespec next; + + (void) o; + + list_head_init(&table); + + clock_gettime(PTHREAD_COND_CLOCK, &next); + next.tv_sec += OAP_REKEY_TIMER; + + pthread_cleanup_push(rekey_table_cleanup, &table); + + while (true) { + struct rekey_evt * evt; + struct timespec deadline; + + deadline = rekey_deadline(&table, next); + + evt = rekey_event_wait(&deadline); + + if (evt != NULL) + rekey_handle_evt(&table, evt); + + rekey_run_periodic(&table, &next); + + rekey_reap_expired(&table); + } + + pthread_cleanup_pop(true); + + return (void *) 0; +} + static void * acceptloop(void * o) { int csockfd; @@ -1502,8 +2716,17 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, struct timespec now; struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */ int res; + bool has_key = false; + bool initiator = false; + uid_t uid; + gid_t gid; + pid_t cpid; irm_msg_t * ret_msg; buffer_t data; + struct poa_addr poa_addr; + struct poa_addr * pa; + struct poa_spec poa_spec; + struct poa_spec * eps = NULL; memset(&flow, 0, sizeof(flow)); @@ -1547,11 +2770,74 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, res = bootstrap_ipcp(msg->pid, &conf); break; case IRM_MSG_CODE__IRM_ENROLL_IPCP: - res = enroll_ipcp(msg->pid, msg->dst); + if (msg->peer != NULL) { + poa_addr = poa_addr_msg_to_s(msg->peer); + if (poa_addr.type == POA_INVALID) { + res = -EINVAL; + break; + } + } + + if (msg->conf != NULL) + conf = ipcp_config_msg_to_s(msg->conf); + res = enroll_ipcp_resolve(msg->pid, msg->dst, + msg->peer != NULL ? &poa_addr : NULL); + break; + case IRM_MSG_CODE__IRM_ATTACH_IPCP: + poa_spec = poa_spec_msg_to_s(msg->poa); + if (poa_spec.type == POA_INVALID) { + res = -EINVAL; + break; + } + + res = attach_ipcp(msg->pid, &poa_spec, true); + break; + case IRM_MSG_CODE__IRM_DETACH_IPCP: + poa_spec = poa_spec_msg_to_s(msg->poa); + if (poa_spec.type == POA_INVALID) { + res = -EINVAL; + break; + } + + res = attach_ipcp(msg->pid, &poa_spec, false); + break; + case IRM_MSG_CODE__IRM_LIST_POAS: + res = list_poas(msg->pid, &eps); + if (res > 0) { + ssize_t i; + ret_msg->poas = malloc(res * sizeof(*ret_msg->poas)); + if (ret_msg->poas == NULL) { + free(eps); + + res = -ENOMEM; + break; + } + for (i = 0; i < res; i++) { + ret_msg->poas[i] = poa_spec_s_to_msg(&eps[i]); + if (ret_msg->poas[i] == NULL) + break; + ret_msg->n_poas++; + } + res = i == res ? 0 : -ENOMEM; + } + free(eps); break; case IRM_MSG_CODE__IRM_CONNECT_IPCP: flow.qs = qos_spec_msg_to_s(msg->qosspec); - res = connect_ipcp(msg->pid, msg->dst, msg->comp, flow.qs); + + pa = NULL; + + if (msg->peer != NULL) { + poa_addr = poa_addr_msg_to_s(msg->peer); + if (poa_addr.type == POA_INVALID) { + res = -EINVAL; + break; + } + pa = &poa_addr; + } + + res = connect_ipcp_resolve(msg->pid, msg->dst, msg->comp, + flow.qs, pa); break; case IRM_MSG_CODE__IRM_DISCONNECT_IPCP: res = disconnect_ipcp(msg->pid, msg->dst, msg->comp); @@ -1568,7 +2854,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, case IRM_MSG_CODE__IRM_PROC_ANNOUNCE: proc.pid = msg->pid; strcpy(proc.prog, msg->prog); - res = get_peer_ids(fd, &proc.uid, &proc.gid); + res = get_peer_ids(fd, &proc.uid, &proc.gid, NULL); if (res < 0) log_err("Failed to get UID/GID for pid %d.", msg->pid); else @@ -1611,26 +2897,29 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, flow = flow_info_msg_to_s(msg->flow_info); sk.key = kbuf; res = flow_accept(&flow, &data, abstime, &sk); - if (res == 0) { - ret_msg->flow_info = flow_info_s_to_msg(&flow); - ret_msg->has_pk = data.len != 0; - ret_msg->pk.data = data.data; - ret_msg->pk.len = data.len; - ret_msg->has_cipher_nid = true; - ret_msg->cipher_nid = sk.nid; - if (sk.nid != NID_undef) { - hbuf = malloc(SYMMKEYSZ); - if (hbuf == NULL) { - log_err("Failed to malloc key buf"); - return NULL; - } - - memcpy(hbuf, kbuf, SYMMKEYSZ); - ret_msg->sym_key.data = hbuf; - ret_msg->sym_key.len = SYMMKEYSZ; - ret_msg->has_sym_key = true; - } + if (res != 0) + break; + + ret_msg->flow_info = flow_info_s_to_msg(&flow); + ret_msg->has_pk = data.len != 0; + ret_msg->pk.data = data.data; + ret_msg->pk.len = data.len; + ret_msg->has_cipher_nid = true; + ret_msg->cipher_nid = sk.nid; + if (sk.nid == NID_undef) + break; + + hbuf = malloc(SYMMKEYSZ); + if (hbuf == NULL) { + log_err("Failed to malloc key buf"); + res = -ENOMEM; + break; } + + memcpy(hbuf, kbuf, SYMMKEYSZ); + ret_msg->sym_key.data = hbuf; + ret_msg->sym_key.len = SYMMKEYSZ; + ret_msg->has_sym_key = true; break; case IRM_MSG_CODE__IRM_FLOW_ALLOC: data.len = msg->pk.len; @@ -1641,25 +2930,29 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, abstime = abstime == NULL ? &max : abstime; sk.key = kbuf; res = flow_alloc(msg->dst, &flow, &data, abstime, &sk); - if (res == 0) { - ret_msg->flow_info = flow_info_s_to_msg(&flow); - ret_msg->has_pk = data.len != 0; - ret_msg->pk.data = data.data; - ret_msg->pk.len = data.len; - ret_msg->has_cipher_nid = true; - ret_msg->cipher_nid = sk.nid; - if (sk.nid != NID_undef) { - hbuf = malloc(SYMMKEYSZ); - if (hbuf == NULL) { - log_err("Failed to malloc key buf"); - return NULL; - } - memcpy(hbuf, kbuf, SYMMKEYSZ); - ret_msg->sym_key.data = hbuf; - ret_msg->sym_key.len = SYMMKEYSZ; - ret_msg->has_sym_key = true; - } + if (res != 0) + break; + + ret_msg->flow_info = flow_info_s_to_msg(&flow); + ret_msg->has_pk = data.len != 0; + ret_msg->pk.data = data.data; + ret_msg->pk.len = data.len; + ret_msg->has_cipher_nid = true; + ret_msg->cipher_nid = sk.nid; + if (sk.nid == NID_undef) + break; + + hbuf = malloc(SYMMKEYSZ); + if (hbuf == NULL) { + log_err("Failed to malloc key buf"); + res = -ENOMEM; + break; } + + memcpy(hbuf, kbuf, SYMMKEYSZ); + ret_msg->sym_key.data = hbuf; + ret_msg->sym_key.len = SYMMKEYSZ; + ret_msg->has_sym_key = true; break; case IRM_MSG_CODE__IRM_FLOW_JOIN: assert(msg->pk.len == 0 && msg->pk.data == NULL); @@ -1689,6 +2982,67 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, if (res == 0) ret_msg->flow_info = flow_info_s_to_msg(&flow); break; + case IRM_MSG_CODE__IRM_POA_FLOW_ALLOC: + flow = flow_info_msg_to_s(msg->flow_info); + + clrbuf(data); + + res = poa_alloc(&flow, msg->dst, &data); + if (res == 0) { + ret_msg->flow_info = flow_info_s_to_msg(&flow); + ret_msg->has_pk = true; + ret_msg->pk.len = data.len; + ret_msg->pk.data = data.data; + + clrbuf(data); + } + break; + case IRM_MSG_CODE__IRM_POA_FLOW_ALLOC_R: + data.len = msg->pk.len; + data.data = msg->pk.data; + msg->pk.data = NULL; /* pass data */ + msg->pk.len = 0; + flow = flow_info_msg_to_s(msg->flow_info); + sk.key = kbuf; + res = poa_complete(&flow, msg->response, &data, &sk); + + freebuf(data); + + if (res != 0) + break; + + ret_msg->flow_info = flow_info_s_to_msg(&flow); + ret_msg->has_cipher_nid = true; + ret_msg->cipher_nid = sk.nid; + + if (sk.nid == NID_undef) + break; + + hbuf = malloc(SYMMKEYSZ); + if (hbuf == NULL) { + log_err("Failed to malloc key buf"); + + res = -ENOMEM; + break; + } + + memcpy(hbuf, kbuf, SYMMKEYSZ); + + ret_msg->sym_key.data = hbuf; + ret_msg->sym_key.len = SYMMKEYSZ; + ret_msg->has_sym_key = true; + break; + case IRM_MSG_CODE__IPCP_POA_FLOW_REQ_ARR: + data.len = msg->pk.len; + data.data = msg->pk.data; + msg->pk.data = NULL; /* pass data */ + msg->pk.len = 0; + flow = flow_info_msg_to_s(msg->flow_info); + + res = poa_req_arr(&flow, &data); + if (res == 0) + ret_msg->flow_info = flow_info_s_to_msg(&flow); + break; case IRM_MSG_CODE__IPCP_FLOW_ALLOC_REPLY: data.len = msg->pk.len; data.data = msg->pk.data; @@ -1698,6 +3052,51 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, flow = flow_info_msg_to_s(msg->flow_info); res = flow_alloc_reply(&flow, msg->response, &data); break; + case IRM_MSG_CODE__IPCP_FLOW_UPDATE_ARR: + data.len = msg->pk.len; + data.data = msg->pk.data; + msg->pk.data = NULL; /* pass data */ + msg->pk.len = 0; + flow = flow_info_msg_to_s(msg->flow_info); + res = flow_update_arr(&flow, &data); + freebuf(data); + break; + case IRM_MSG_CODE__IRM_FLOW_UPDATE: + flow = flow_info_msg_to_s(msg->flow_info); + if (get_peer_ids(fd, &uid, &gid, &cpid) < 0) { + res = -EPERM; + break; + } + + if (cpid <= 0) /* non-Linux: fall back to asserted pid */ + cpid = flow.n_pid; + + sk.key = kbuf; + res = flow_update(&flow, uid, cpid, msg->rekey, &sk, &has_key, + &initiator); + if (res != 0) + break; + + ret_msg->flow_info = flow_info_s_to_msg(&flow); + if (!has_key) + break; + + hbuf = malloc(SYMMKEYSZ); + if (hbuf == NULL) { + log_err("Failed to malloc key buf"); + res = -ENOMEM; + break; + } + + memcpy(hbuf, kbuf, SYMMKEYSZ); + ret_msg->sym_key.data = hbuf; + ret_msg->sym_key.len = SYMMKEYSZ; + ret_msg->has_sym_key = true; + ret_msg->has_generation = true; + ret_msg->generation = sk.epoch; + ret_msg->has_rk_initiator = true; + ret_msg->rk_initiator = initiator; + break; default: log_err("Don't know that message code."); res = -1; @@ -1717,6 +3116,13 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg, return ret_msg; } +/* Wipe the session key from a reply before its buffers are freed. */ +static void clear_msg_key(irm_msg_t * msg) +{ + if (msg != NULL && msg->has_sym_key) + crypt_secure_clear(msg->sym_key.data, msg->sym_key.len); +} + static void * mainloop(void * o) { int sfd; @@ -1728,6 +3134,7 @@ static void * mainloop(void * o) while (true) { irm_msg_t * ret_msg; struct cmd * cmd; + bool had_key; pthread_mutex_lock(&irmd.cmd_lock); @@ -1791,6 +3198,9 @@ static void * mainloop(void * o) irm_msg__pack(ret_msg, buffer.data); + had_key = ret_msg->has_sym_key; + clear_msg_key(ret_msg); + irm_msg__free_unpacked(ret_msg, NULL); pthread_cleanup_push(__cleanup_close_ptr, &sfd); @@ -1805,6 +3215,9 @@ static void * mainloop(void * o) strerror(errno)); } + if (had_key) + crypt_secure_clear(buffer.data, buffer.len); + pthread_cleanup_pop(true); pthread_cleanup_pop(true); @@ -1812,6 +3225,7 @@ static void * mainloop(void * o) continue; fail: + clear_msg_key(ret_msg); irm_msg__free_unpacked(ret_msg, NULL); fail_msg: close(sfd); @@ -1895,12 +3309,14 @@ void * irm_sanitize(void * o) return (void *) 0; } -static int irm_load_store(char * dpath) +static int irm_load_store(char * dpath, + bool anchor) { struct stat st; struct dirent * dent; DIR * dir; void * crt; + int ret; if (stat(dpath, &st) == -1) { log_dbg("Store directory %s not found.", dpath); @@ -1944,7 +3360,9 @@ static int irm_load_store(char * dpath) goto fail_file; } - if (oap_auth_add_ca_crt(crt) < 0) { + ret = anchor ? oap_auth_add_ca_crt(crt) + : oap_auth_add_chain_crt(crt); + if (ret < 0) { log_err("Failed to add certificate from %s to store.", path); goto fail_crt_add; @@ -2041,6 +3459,29 @@ static int irm_init(void) list_head_init(&irmd.cmds); + if (pthread_mutex_init(&irmd.rk.mtx, NULL)) { + log_err("Failed to initialize mutex."); + goto fail_rk_mtx; + } + + if (pthread_condattr_init(&cattr)) { + log_err("Failed to initialize condattr."); + goto fail_rk_mtx; + } + +#ifndef __APPLE__ + pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); +#endif + if (pthread_cond_init(&irmd.rk.cond, &cattr)) { + log_err("Failed to initialize condvar."); + pthread_condattr_destroy(&cattr); + goto fail_rk_cond; + } + + pthread_condattr_destroy(&cattr); + + list_head_init(&irmd.rk.inbox); + if (stat(SOCK_PATH, &st) == -1) { if (mkdir(SOCK_PATH, 0777)) { log_err("Failed to create sockets directory."); @@ -2088,12 +3529,12 @@ static int irm_init(void) goto fail_oap; } - if (irm_load_store(OUROBOROS_CA_CRT_DIR) < 0) { + if (irm_load_store(OUROBOROS_CA_CRT_DIR, true) < 0) { log_err("Failed to load CA certificates."); goto fail_load_store; } - if (irm_load_store(OUROBOROS_CHAIN_DIR) < 0) { + if (irm_load_store(OUROBOROS_CHAIN_DIR, false) < 0) { log_err("Failed to load intermediate certificates."); goto fail_load_store; } @@ -2144,6 +3585,10 @@ static int irm_init(void) fail_sock_path: unlink(IRM_SOCK_PATH); fail_stat: + pthread_cond_destroy(&irmd.rk.cond); + fail_rk_cond: + pthread_mutex_destroy(&irmd.rk.mtx); + fail_rk_mtx: pthread_cond_destroy(&irmd.cmd_cond); fail_cmd_cond: pthread_mutex_destroy(&irmd.cmd_lock); @@ -2192,13 +3637,28 @@ static void irm_fini(void) pthread_mutex_unlock(&irmd.cmd_lock); + pthread_mutex_lock(&irmd.rk.mtx); + + list_for_each_safe(p, h, &irmd.rk.inbox) { + struct rekey_evt * evt; + evt = list_entry(p, struct rekey_evt, next); + list_del(&evt->next); + freebuf(evt->buf); + free(evt); + } + + pthread_mutex_unlock(&irmd.rk.mtx); + pthread_mutex_destroy(&irmd.cmd_lock); pthread_cond_destroy(&irmd.cmd_cond); + pthread_mutex_destroy(&irmd.rk.mtx); + pthread_cond_destroy(&irmd.rk.cond); pthread_rwlock_destroy(&irmd.state_lock); #ifdef HAVE_FUSE while (rmdir(FUSE_PREFIX) < 0 && retries-- > 0) nanosleep(&wait, NULL); + if (retries < 0) log_err("Failed to remove " FUSE_PREFIX); #endif @@ -2231,10 +3691,18 @@ static int irm_start(void) if (pthread_create(&irmd.acceptor, NULL, acceptloop, NULL)) goto fail_acceptor; + if (OAP_REKEY_TIMER > 0) { + if (pthread_create(&irmd.rk.worker, NULL, rekey_worker, NULL)) + goto fail_rekey_worker; + } + log_info("Ouroboros IPC Resource Manager daemon started..."); return 0; + fail_rekey_worker: + pthread_cancel(irmd.acceptor); + pthread_join(irmd.acceptor, NULL); fail_acceptor: pthread_cancel(irmd.irm_sanitize); pthread_join(irmd.irm_sanitize, NULL); @@ -2274,6 +3742,11 @@ static void irm_sigwait(sigset_t sigset) static void irm_stop(void) { + if (OAP_REKEY_TIMER > 0) { + pthread_cancel(irmd.rk.worker); + pthread_join(irmd.rk.worker, NULL); + } + pthread_cancel(irmd.acceptor); pthread_cancel(irmd.irm_sanitize); diff --git a/src/irmd/oap.h b/src/irmd/oap.h index d6d8dfe2..e9d7511b 100644 --- a/src/irmd/oap.h +++ b/src/irmd/oap.h @@ -28,6 +28,8 @@ #include <ouroboros/name.h> #include <ouroboros/utils.h> +#include <stdbool.h> + /* OAP authentication state (in oap/auth.c) */ int oap_auth_init(void); @@ -35,31 +37,45 @@ void oap_auth_fini(void); int oap_auth_add_ca_crt(void * crt); +int oap_auth_add_chain_crt(void * crt); + /* -* Prepare OAP request header for server, returns context -* Passes client data for srv, returns srv data for client -*/ + * Prepares the request header and returns the context. info holds the + * credentials we present, peer the name the server certificate must + * carry (NULL expects info->name); rekey forces an ephemeral KEX. + */ int oap_cli_prepare(void ** ctx, const struct name_info * info, + const char * peer, buffer_t * req_buf, - buffer_t data); + buffer_t data, + bool rekey); /* - * Server processes header, creates response header, returns secret key. - * data is in/out: input=srv data to send, output=cli data received. + * Answers the request header and returns the secret key. data is + * in/out; rekey verifies against cached_crt, and peer_crt takes a copy + * of the peer cert to cache at the initial handshake. */ int oap_srv_process(const struct name_info * info, buffer_t req_buf, buffer_t * rsp_buf, buffer_t * data, - struct crypt_sk * sk); + struct crypt_sk * sk, + bool rekey, + const buffer_t * cached_crt, + buffer_t * peer_crt); -/* Complete OAP, returns secret key and server data, frees ctx */ -int oap_cli_complete(void * ctx, - const struct name_info * info, - buffer_t rsp_buf, - buffer_t * data, - struct crypt_sk * sk); +/* + * Complete OAP, returns secret key and server data, frees ctx. + * cached_crt verifies a cert-less re-key; peer_crt (or NULL) receives a + * copy of the peer cert to cache at the initial handshake. + */ +int oap_cli_complete(void * ctx, + buffer_t rsp_buf, + buffer_t * data, + struct crypt_sk * sk, + const buffer_t * cached_crt, + buffer_t * peer_crt); /* Free OAP state (on failure before complete) */ void oap_ctx_free(void * ctx); diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c index d165de73..f70f9df1 100644 --- a/src/irmd/oap/auth.c +++ b/src/irmd/oap/auth.c @@ -29,8 +29,8 @@ #define OUROBOROS_PREFIX "irmd/oap" #include <ouroboros/crypt.h> +#include <ouroboros/endian.h> #include <ouroboros/errno.h> -#include <ouroboros/list.h> #include <ouroboros/logs.h> #include <ouroboros/pthread.h> #include <ouroboros/time.h> @@ -44,38 +44,99 @@ #include <stdlib.h> #include <string.h> -struct oap_replay_entry { - struct list_head next; - uint64_t timestamp; - uint8_t id[OAP_ID_SIZE]; +/* + * Replay cache: three timestamp-generation hash buckets. A header's bucket + * is gen(T) = T / OAP_REPLAY_TIMER, taken mod 3. Staleness bounds a valid T + * to generations {G-1, G, G+1} (G is now's generation; a within-slack future + * stamp can reach G+1), which are distinct mod 3; the aliasing generation + * G-3 is always rejected as too old first. Each bucket is an open-addressed + * hash set whose slots are live iff slot.gen == bucket.gen, so a stale bucket + * clears in O(1) by bumping its gen. Overflow fails closed (reject), never + * evicts, so a flood cannot displace a genuine entry into a replayable state. + */ +#define OAP_REPLAY_GENS 3 + +struct oap_replay_slot { + uint64_t gen; /* live iff == bucket gen; 0 = never used */ + uint64_t ts; + uint8_t id[OAP_ID_SIZE]; +}; + +struct oap_replay_bucket { + uint64_t gen; + size_t count; + struct oap_replay_slot * slots; }; static struct { struct auth_ctx * ca_ctx; struct { - struct list_head list; - pthread_mutex_t mtx; + size_t mask; /* slots per bucket - 1 */ + size_t cap; /* fail-closed threshold */ + struct oap_replay_bucket bucket[OAP_REPLAY_GENS]; + pthread_mutex_t mtx; } replay; } oap_auth; +/* FNV-1a over id || ts; the table mask reduces it to a slot index. */ +static size_t replay_hash(const uint8_t * id, + uint64_t ts) +{ + uint64_t hh = 14695981039346656037ULL; + size_t i; + + for (i = 0; i < OAP_ID_SIZE; i++) { + hh ^= id[i]; + hh *= 1099511628211ULL; + } + + for (i = 0; i < sizeof(ts); i++) { + hh ^= (uint8_t) (ts >> (i * 8)); + hh *= 1099511628211ULL; + } + + return (size_t) hh; +} + int oap_auth_init(void) { + size_t m = 1; + int i; + oap_auth.ca_ctx = auth_create_ctx(); if (oap_auth.ca_ctx == NULL) { log_err("Failed to create OAP auth context."); goto fail_ctx; } - list_head_init(&oap_auth.replay.list); + while (m < (size_t) OAP_REPLAY_MAX * 2) + m <<= 1; + + oap_auth.replay.mask = m - 1; + oap_auth.replay.cap = OAP_REPLAY_MAX; + + for (i = 0; i < OAP_REPLAY_GENS; i++) { + struct oap_replay_bucket * b = &oap_auth.replay.bucket[i]; + b->gen = 0; + b->count = 0; + b->slots = calloc(m, sizeof(*b->slots)); + if (b->slots == NULL) { + log_err("Failed to alloc OAP replay bucket."); + goto fail_bucket; + } + } if (pthread_mutex_init(&oap_auth.replay.mtx, NULL)) { log_err("Failed to init OAP replay mutex."); - goto fail_mtx; + goto fail_bucket; } return 0; - fail_mtx: + fail_bucket: + for (i = 0; i < OAP_REPLAY_GENS; i++) + free(oap_auth.replay.bucket[i].slots); + auth_destroy_ctx(oap_auth.ca_ctx); fail_ctx: return -1; @@ -83,16 +144,13 @@ int oap_auth_init(void) void oap_auth_fini(void) { - struct list_head * p; - struct list_head * h; + int i; pthread_mutex_lock(&oap_auth.replay.mtx); - list_for_each_safe(p, h, &oap_auth.replay.list) { - struct oap_replay_entry * e; - e = list_entry(p, struct oap_replay_entry, next); - list_del(&e->next); - free(e); + for (i = 0; i < OAP_REPLAY_GENS; i++) { + free(oap_auth.replay.bucket[i].slots); + oap_auth.replay.bucket[i].slots = NULL; } pthread_mutex_unlock(&oap_auth.replay.mtx); @@ -106,18 +164,214 @@ int oap_auth_add_ca_crt(void * crt) return auth_add_crt_to_store(oap_auth.ca_ctx, crt); } +int oap_auth_add_chain_crt(void * crt) +{ + return auth_add_crt_to_chain(oap_auth.ca_ctx, crt); +} + +/* HKDF info = LABEL (incl. NUL separator) || request-hash [|| response-hash] */ +#define OAP_BIND_LABEL "o7s-oap-bind" +#define OAP_KC_LABEL "o7s-oap-kc" +#define OAP_HS_LABEL "o7s-oap-hs" + +int oap_resp_hash(int md_nid, + buffer_t kex, + buffer_t data, + buffer_t crt, + buffer_t * out) +{ + buffer_t cat = BUF_INIT; + uint8_t * p; + ssize_t len; + + assert(out != NULL); + assert(out->data != NULL); + + cat.len = kex.len + data.len + crt.len; + if (cat.len == 0) + return -EINVAL; + + cat.data = malloc(cat.len); + if (cat.data == NULL) + return -ENOMEM; + + p = cat.data; + if (kex.len > 0) { + memcpy(p, kex.data, kex.len); + p += kex.len; + } + + if (data.len > 0) { + memcpy(p, data.data, data.len); + p += data.len; + } + + if (crt.len > 0) + memcpy(p, crt.data, crt.len); + + len = md_digest(md_nid, cat, out->data); + + freebuf(cat); + + if (len < 0) + return -ECRYPT; + + out->len = (size_t) len; + + return 0; +} + +/* HKDF-expand sk->key with info into out; -ECRYPT on failure. */ +static int oap_hkdf_expand(const struct crypt_sk * sk, + buffer_t info, + uint8_t * out, + size_t outlen) +{ + buffer_t prk; + buffer_t okm; + + prk.len = SYMMKEYSZ; + prk.data = sk->key; + okm.len = outlen; + okm.data = out; + + if (crypt_hkdf_expand(prk, info, okm) < 0) + return -ECRYPT; + + return 0; +} + +/* info = label || H(req) */ +#define OAP_HS_INFO_SZ (sizeof(OAP_HS_LABEL) + MAX_HASH_SIZE) +int oap_derive_hs_key(const struct crypt_sk * sk, + buffer_t req_hash, + uint8_t * out) +{ + uint8_t info_buf[OAP_HS_INFO_SZ]; + buffer_t info; + size_t len; + + assert(sk != NULL); + assert(req_hash.data != NULL); + assert(out != NULL); + + if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE) + return -EINVAL; + + len = sizeof(OAP_HS_LABEL); + memcpy(info_buf, OAP_HS_LABEL, len); + memcpy(info_buf + len, req_hash.data, req_hash.len); + len += req_hash.len; + + info.len = len; + info.data = info_buf; + + return oap_hkdf_expand(sk, info, out, SYMMKEYSZ); +} + +/* info = label || H(req) || H(resp) || cipher_nid || kdf_nid */ +#define OAP_BIND_INFO_SZ \ + (sizeof(OAP_BIND_LABEL) + 2 * MAX_HASH_SIZE + 2 * sizeof(uint16_t)) +int oap_bind_session_key(struct crypt_sk * sk, + buffer_t req_hash, + buffer_t resp_hash, + int kdf_nid) +{ + uint8_t info_buf[OAP_BIND_INFO_SZ]; + uint8_t tmp[SYMMKEYSZ]; + uint16_t suite[2]; + buffer_t info; + size_t len; + + assert(sk != NULL); + assert(req_hash.data != NULL); + assert(resp_hash.data != NULL); + + if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE) + return -EINVAL; + + if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE) + return -EINVAL; + + len = sizeof(OAP_BIND_LABEL); + memcpy(info_buf, OAP_BIND_LABEL, len); + memcpy(info_buf + len, req_hash.data, req_hash.len); + len += req_hash.len; + + memcpy(info_buf + len, resp_hash.data, resp_hash.len); + len += resp_hash.len; + + suite[0] = hton16((uint16_t) sk->nid); + suite[1] = hton16((uint16_t) kdf_nid); + memcpy(info_buf + len, suite, sizeof(suite)); + len += sizeof(suite); + + info.len = len; + info.data = info_buf; + + if (oap_hkdf_expand(sk, info, tmp, SYMMKEYSZ) < 0) + return -ECRYPT; + + memcpy(sk->key, tmp, SYMMKEYSZ); + crypt_secure_clear(tmp, SYMMKEYSZ); + + return 0; +} + +/* info = label || H(req) || H(resp) */ +#define OAP_KC_INFO_SZ (sizeof(OAP_KC_LABEL) + 2 * MAX_HASH_SIZE) +int oap_key_confirm_tag(const struct crypt_sk * sk, + buffer_t req_hash, + buffer_t resp_hash, + uint8_t * out, + size_t outlen) +{ + uint8_t info_buf[OAP_KC_INFO_SZ]; + buffer_t info; + size_t len; + + assert(sk != NULL); + assert(req_hash.data != NULL); + assert(resp_hash.data != NULL); + assert(out != NULL); + + if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE) + return -EINVAL; + + if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE) + return -EINVAL; + + if (outlen > MAX_HASH_SIZE) + return -EINVAL; + + len = sizeof(OAP_KC_LABEL); + memcpy(info_buf, OAP_KC_LABEL, len); + memcpy(info_buf + len, req_hash.data, req_hash.len); + len += req_hash.len; + + memcpy(info_buf + len, resp_hash.data, resp_hash.len); + len += resp_hash.len; + + info.len = len; + info.data = info_buf; + + return oap_hkdf_expand(sk, info, out, outlen); +} + #define TIMESYNC_SLACK 100 /* ms */ #define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0) int oap_check_hdr(const struct oap_hdr * hdr) { - struct list_head * p; - struct list_head * h; - struct timespec now; - struct oap_replay_entry * new; - uint64_t stamp; - uint64_t cur; - uint8_t * id; - ssize_t delta; + struct oap_replay_bucket * b; + struct oap_replay_slot * slots; + struct timespec now; + uint64_t stamp; + uint64_t cur; + uint64_t gen; + uint8_t * id; + size_t h; + ssize_t delta; + int ret = 0; assert(hdr != NULL); @@ -131,64 +385,72 @@ int oap_check_hdr(const struct oap_hdr * hdr) delta = (ssize_t)(cur - stamp) / MILLION; if (delta < -TIMESYNC_SLACK) { log_err_id(id, "OAP header from %zd ms into future.", -delta); - goto fail_stamp; + return -EAUTH; } if (delta > OAP_REPLAY_TIMER * 1000) { log_err_id(id, "OAP header too old (%zd ms).", delta); - goto fail_stamp; + return -EAUTH; } - new = malloc(sizeof(*new)); - if (new == NULL) { - log_err_id(id, "Failed to allocate memory for OAP element."); - goto fail_stamp; - } + gen = stamp / ((uint64_t) OAP_REPLAY_TIMER * BILLION); pthread_mutex_lock(&oap_auth.replay.mtx); - list_for_each_safe(p, h, &oap_auth.replay.list) { - struct oap_replay_entry * e; - e = list_entry(p, struct oap_replay_entry, next); - if (cur > e->timestamp + OAP_REPLAY_TIMER * BILLION) { - list_del(&e->next); - free(e); - continue; - } + b = &oap_auth.replay.bucket[gen % OAP_REPLAY_GENS]; - if (e->timestamp == stamp && ID_IS_EQUAL(e->id, id)) { - log_warn_id(id, "OAP header already known."); - goto fail_replay; - } + /* Rotate a stale bucket in O(1): its old-gen slots become free. */ + if (b->gen != gen) { + b->gen = gen; + b->count = 0; } - memcpy(new->id, id, OAP_ID_SIZE); - new->timestamp = stamp; + slots = b->slots; - list_add_tail(&new->next, &oap_auth.replay.list); + h = replay_hash(id, stamp) & oap_auth.replay.mask; + while (slots[h].gen == gen) { + if (slots[h].ts == stamp && ID_IS_EQUAL(slots[h].id, id)) { + log_warn_id(id, "OAP header already known."); + ret = -EREPLAY; + goto out; + } - pthread_mutex_unlock(&oap_auth.replay.mtx); + h = (h + 1) & oap_auth.replay.mask; + } - return 0; + /* Empty slot found; fail closed when the window is at capacity. */ + if (b->count >= oap_auth.replay.cap) { + log_warn_id(id, "OAP replay cache full; rejecting."); + ret = -EAUTH; + goto out; + } - fail_replay: + slots[h].gen = gen; + slots[h].ts = stamp; + memcpy(slots[h].id, id, OAP_ID_SIZE); + b->count++; + out: pthread_mutex_unlock(&oap_auth.replay.mtx); - free(new); - return -EREPLAY; - fail_stamp: - return -EAUTH; + + return ret; } -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr) +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr, + const buffer_t * cached_crt) { void * crt; void * pk = NULL; - buffer_t sign; /* Signed region */ + void * pin = NULL; + buffer_t crt_der; /* cert source: wire, else cached (re-key) */ + buffer_t sign; /* Signed region */ uint8_t * id = peer_hdr->id.data; + int ret; assert(name != NULL); + assert(cfg != NULL); assert(local_hdr != NULL); assert(peer_hdr != NULL); @@ -197,13 +459,22 @@ int oap_auth_peer(char * name, goto fail_check; } - if (peer_hdr->crt.len == 0) { + /* Re-key drops the wire cert; fall back to the cached peer cert. */ + crt_der = peer_hdr->crt; + if (crt_der.len == 0 && cached_crt != NULL) + crt_der = *cached_crt; + + if (crt_der.len == 0) { + if (cfg->a.req) { + log_err_id(id, "Peer did not provide a certificate."); + goto fail_check; + } log_dbg_id(id, "No crt provided."); name[0] = '\0'; return 0; } - if (crypt_load_crt_der(peer_hdr->crt, &crt) < 0) { + if (crypt_load_crt_der(crt_der, &crt) < 0) { log_err_id(id, "Failed to load crt."); goto fail_check; } @@ -217,26 +488,58 @@ int oap_auth_peer(char * name, log_dbg_id(id, "Got public key from crt."); - if (auth_verify_crt(oap_auth.ca_ctx, crt) < 0) { + if (cfg->a.cacert[0] != '\0') { + if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) { + log_err_id(id, "Failed to load pinned CA %s.", + cfg->a.cacert); + goto fail_crt; + } + } + + ret = auth_verify_crt_pin(oap_auth.ca_ctx, crt, pin); + if (ret == -ENOENT) { + log_err_id(id, "Peer crt not issued by pinned CA %s.", + cfg->a.cacert); + goto fail_pin; + } + + if (ret < 0) { log_err_id(id, "Failed to verify peer with CA store."); - goto fail_crt; + goto fail_pin; } log_dbg_id(id, "Successfully verified peer crt."); - sign = peer_hdr->hdr; + /* Digest pin: peer must sign with the configured digest */ + if (crypt_pk_requires_md(pk) && + cfg->d.nid != NID_undef && peer_hdr->md_nid != cfg->d.nid) { + log_err_id(id, "Peer did not sign with %s.", + md_nid_to_str(cfg->d.nid)); + goto fail_pin; + } + + /* Sealed responses verify over the reconstructed plaintext. */ + sign = peer_hdr->sealed_pt.data != NULL ? + peer_hdr->sealed_pt : peer_hdr->hdr; sign.len -= peer_hdr->sig.len; if (auth_verify_sig(pk, peer_hdr->md_nid, sign, peer_hdr->sig) < 0) { log_err_id(id, "Failed to verify signature."); - goto fail_check_sig; + goto fail_pin; } - if (crypt_get_crt_name(crt, name) < 0) { - log_warn_id(id, "Failed to extract name from certificate."); - name[0] = '\0'; + ret = crypt_get_crt_name(crt, name); + if (ret < 0) { + if (ret == -ENAME) + log_err_id(id, "Certificate CN too long."); + else + log_err_id(id, "No name in certificate."); + goto fail_pin; } + if (pin != NULL) + crypt_free_crt(pin); + crypt_free_key(pk); crypt_free_crt(crt); @@ -244,7 +547,9 @@ int oap_auth_peer(char * name, return 0; - fail_check_sig: + fail_pin: + if (pin != NULL) + crypt_free_crt(pin); fail_crt: crypt_free_key(pk); crypt_free_crt(crt); diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h index 4f748750..72938b53 100644 --- a/src/irmd/oap/auth.h +++ b/src/irmd/oap/auth.h @@ -23,13 +23,46 @@ #ifndef OUROBOROS_IRMD_OAP_AUTH_H #define OUROBOROS_IRMD_OAP_AUTH_H +#include <ouroboros/crypt.h> + #include "hdr.h" int oap_check_hdr(const struct oap_hdr * hdr); -/* name is updated with the peer's certificate name if available */ -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr); +/* + * name is set to the peer crt CN, "" if no crt was presented. + * cached_crt (or NULL) is the peer cert from the initial handshake, used + * to verify a cert-less re-key. + */ +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr, + const buffer_t * cached_crt); + +/* Derive the handshake key that seals the response identity block. */ +int oap_derive_hs_key(const struct crypt_sk * sk, + buffer_t req_hash, + uint8_t * out); + +/* resp_hash = H(kex || data || crt): binds the server response transcript. */ +int oap_resp_hash(int md_nid, + buffer_t kex, + buffer_t data, + buffer_t crt, + buffer_t * out); + +/* Fold request + response transcript + negotiated suite into the key. */ +int oap_bind_session_key(struct crypt_sk * sk, + buffer_t req_hash, + buffer_t resp_hash, + int kdf_nid); + +/* Server->client key-confirmation tag derived from the bound key. */ +int oap_key_confirm_tag(const struct crypt_sk * sk, + buffer_t req_hash, + buffer_t resp_hash, + uint8_t * out, + size_t outlen); #endif /* OUROBOROS_IRMD_OAP_AUTH_H */ diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c index 7a202da7..02ad2005 100644 --- a/src/irmd/oap/cli.c +++ b/src/irmd/oap/cli.c @@ -50,11 +50,12 @@ struct oap_cli_ctx { uint8_t __id[OAP_ID_SIZE]; buffer_t id; - uint8_t kex_buf[CRYPT_KEY_BUFSZ]; + char peer[NAME_SIZE + 1]; /* expected server name */ + uint8_t kex_buf[OAP_KEX_NIDSZ + CRYPT_KEY_BUFSZ]; uint8_t req_hash[MAX_HASH_SIZE]; size_t req_hash_len; int req_md_nid; - struct sec_config kcfg; + struct sec_config scfg; struct oap_hdr local_hdr; void * pkp; /* Ephemeral keypair */ uint8_t * key; /* For client-encap KEM */ @@ -69,7 +70,7 @@ struct oap_cli_ctx { extern int load_cli_credentials(const struct name_info * info, void ** pkp, void ** crt); -extern int load_cli_kex_config(const struct name_info * info, +extern int load_cli_sec_config(const struct name_info * info, struct sec_config * cfg); extern int load_server_kem_pk(const char * name, struct sec_config * cfg, @@ -87,13 +88,18 @@ int load_cli_credentials(const struct name_info * info, return load_credentials(info->name, &info->c, pkp, crt); } -int load_cli_kex_config(const struct name_info * info, +int load_cli_sec_config(const struct name_info * info, struct sec_config * cfg) { assert(info != NULL); assert(cfg != NULL); - return load_kex_config(info->name, info->c.enc, cfg); + memset(cfg, 0, sizeof(*cfg)); + + /* A client authenticates the server by default, like an https client */ + cfg->a.req = OAP_CLIENT_AUTH_DEFAULT; + + return load_sec_config(info->name, info->c.sec, cfg); } int load_server_kem_pk(const char * name, @@ -107,13 +113,13 @@ int load_server_kem_pk(const char * name, assert(cfg != NULL); assert(pk != NULL); - ext = IS_HYBRID_KEM(cfg->x.str) ? "raw" : "pem"; + ext = IS_HYBRID_KEM_NID(cfg->x.nid) ? "raw" : "pem"; snprintf(path, sizeof(path), OUROBOROS_CLI_CRT_DIR "/%s/kex.srv.pub.%s", name, ext); - if (IS_HYBRID_KEM(cfg->x.str)) { - if (crypt_load_pubkey_raw_file(path, pk) < 0) { + if (IS_HYBRID_KEM_NID(cfg->x.nid)) { + if (crypt_load_pubkey_raw_file(path, cfg->x.str, pk) < 0) { log_err("Failed to load %s pubkey from %s.", ext, path); return -1; } @@ -133,13 +139,13 @@ int load_server_kem_pk(const char * name, static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; buffer_t * kex = &s->local_hdr.kex; uint8_t * id = s->id.data; ssize_t len; /* Generate ephemeral keypair, send PK */ - len = kex_pkp_create(kcfg, &s->pkp, kex->data); + len = kex_pkp_create(scfg, &s->pkp, kex->data); if (len < 0) { log_err_id(id, "Failed to generate DHE keypair."); return -ECRYPT; @@ -147,7 +153,7 @@ static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s) kex->len = (size_t) len; log_dbg_id(id, "Generated ephemeral %s keys (%zd bytes).", - kcfg->x.str, len); + scfg->x.str, len); return 0; } @@ -155,24 +161,26 @@ static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s) static int do_client_kex_prepare_kem_encap(const char * server_name, struct oap_cli_ctx * s) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; buffer_t * kex = &s->local_hdr.kex; uint8_t * id = s->id.data; buffer_t server_pk = BUF_INIT; uint8_t key_buf[SYMMKEYSZ]; ssize_t len; - if (load_server_kem_pk(server_name, kcfg, &server_pk) < 0) { + if (load_server_kem_pk(server_name, scfg, &server_pk) < 0) { log_err_id(id, "Failed to load server KEM pk."); return -ECRYPT; } - if (IS_HYBRID_KEM(kcfg->x.str)) - len = kex_kem_encap_raw(server_pk, kex->data, - kcfg->k.nid, key_buf); - else - len = kex_kem_encap(server_pk, kex->data, - kcfg->k.nid, key_buf); + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + len = kex_kem_encap_raw(scfg->x.str, server_pk, + kex->data + OAP_KEX_NIDSZ, + scfg->k.nid, key_buf); + len = oap_kex_tag_nid(kex->data, scfg->x.nid, len); + } else { + len = kex_kem_encap(server_pk, kex->data, scfg->k.nid, key_buf); + } freebuf(server_pk); @@ -198,13 +206,19 @@ static int do_client_kex_prepare_kem_encap(const char * server_name, static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; buffer_t * kex = &s->local_hdr.kex; uint8_t * id = s->id.data; ssize_t len; /* Server encaps: generate keypair, send PK */ - len = kex_pkp_create(kcfg, &s->pkp, kex->data); + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + len = kex_pkp_create(scfg, &s->pkp, kex->data + OAP_KEX_NIDSZ); + len = oap_kex_tag_nid(kex->data, scfg->x.nid, len); + } else { + len = kex_pkp_create(scfg, &s->pkp, kex->data); + } + if (len < 0) { log_err_id(id, "Failed to generate KEM keypair."); return -ECRYPT; @@ -219,13 +233,13 @@ static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s) static int do_client_kex_prepare(const char * server_name, struct oap_cli_ctx * s) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; - if (!IS_KEX_ALGO_SET(kcfg)) + if (!IS_KEX_ALGO_SET(scfg)) return 0; - if (IS_KEM_ALGORITHM(kcfg->x.str)) { - if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) + if (IS_KEM_ALGORITHM(scfg->x.str)) { + if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) return do_client_kex_prepare_kem_encap(server_name, s); else return do_client_kex_prepare_kem_decap(s); @@ -236,13 +250,17 @@ static int do_client_kex_prepare(const char * server_name, int oap_cli_prepare(void ** ctx, const struct name_info * info, + const char * peer, buffer_t * req_buf, - buffer_t data) + buffer_t data, + bool rekey) { struct oap_cli_ctx * s; void * pkp = NULL; void * crt = NULL; + buffer_t no_tag = BUF_INIT; ssize_t ret; + int enc_flags = 0; assert(ctx != NULL); assert(info != NULL); @@ -251,6 +269,11 @@ int oap_cli_prepare(void ** ctx, clrbuf(*req_buf); *ctx = NULL; + if (peer != NULL && strlen(peer) > NAME_SIZE) { + log_err("Peer name too long."); + return -EINVAL; + } + /* Allocate ctx to carry between prepare and complete */ s = malloc(sizeof(*s)); if (s == NULL) { @@ -261,13 +284,15 @@ int oap_cli_prepare(void ** ctx, memset(s, 0, sizeof(*s)); OAP_CLI_CTX_INIT(s); + strcpy(s->peer, peer != NULL ? peer : info->name); + /* Generate session ID */ if (random_buffer(s->__id, OAP_ID_SIZE) < 0) { log_err("Failed to generate OAP session ID."); goto fail_id; } - log_dbg_id(s->id.data, "Preparing OAP request for %s.", info->name); + log_dbg_id(s->id.data, "Preparing OAP request for %s.", s->peer); /* Load client credentials */ if (load_cli_credentials(info, &pkp, &crt) < 0) { @@ -276,22 +301,44 @@ int oap_cli_prepare(void ** ctx, goto fail_id; } - /* Load KEX config */ - if (load_cli_kex_config(info, &s->kcfg) < 0) { - log_err_id(s->id.data, "Failed to load KEX config for %s.", + /* Load security config */ + if (load_cli_sec_config(info, &s->scfg) < 0) { + log_err_id(s->id.data, "Failed to load security config for %s.", + info->name); + goto fail_kex; + } + + /* A re-keyed flow is encrypted; absent config must fail closed. */ + if (rekey && !IS_KEX_ALGO_SET(&s->scfg)) { + log_err_id(s->id.data, "Refusing re-key without KEX for %s.", info->name); goto fail_kex; } - oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->kcfg.c.nid); + /* Re-key forces server-encap: client-encap forfeits FS/PCS. */ + if (rekey && s->scfg.x.mode == KEM_MODE_CLIENT_ENCAP) { + s->scfg.x.mode = KEM_MODE_SERVER_ENCAP; + log_dbg_id(s->id.data, "Re-key forcing ephemeral server KEX."); + } + + /* Re-key omits the cert; the server verifies against its cache. */ + if (rekey && crt != NULL) { + crypt_free_crt(crt); + crt = NULL; + } + + if (rekey) + enc_flags = OAP_ENC_REKEY; + + oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->scfg.c.nid); - if (do_client_kex_prepare(info->name, s) < 0) { + if (do_client_kex_prepare(s->peer, s) < 0) { log_err_id(s->id.data, "Failed to prepare client KEX."); goto fail_kex; } - if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->kcfg, - (buffer_t) BUF_INIT, NID_undef)) { + if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->scfg, + no_tag, NID_undef, NULL, enc_flags)) { log_err_id(s->id.data, "Failed to create OAP request header."); goto fail_hdr; } @@ -299,7 +346,7 @@ int oap_cli_prepare(void ** ctx, debug_oap_hdr_snd(&s->local_hdr); /* Compute and store hash of request for verification in complete */ - s->req_md_nid = s->kcfg.d.nid != NID_undef ? s->kcfg.d.nid : NID_sha384; + s->req_md_nid = s->scfg.d.nid != NID_undef ? s->scfg.d.nid : NID_sha384; ret = md_digest(s->req_md_nid, s->local_hdr.hdr, s->req_hash); if (ret < 0) { log_err_id(s->id.data, "Failed to hash request."); @@ -324,6 +371,7 @@ int oap_cli_prepare(void ** ctx, return 0; fail_hash: + oap_hdr_fini(&s->local_hdr); fail_hdr: crypt_secure_free(s->key, SYMMKEYSZ); crypt_free_key(s->pkp); @@ -358,42 +406,48 @@ static int do_client_kex_complete_kem(struct oap_cli_ctx * s, const struct oap_hdr * peer_hdr, struct crypt_sk * sk) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; uint8_t * id = s->id.data; uint8_t key_buf[SYMMKEYSZ]; + buffer_t ct; - if (kcfg->x.mode == KEM_MODE_SERVER_ENCAP) { - buffer_t ct; + if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) { + /* Key already derived during prepare */ + memcpy(sk->key, s->key, SYMMKEYSZ); + sk->nid = scfg->c.nid; + log_info_id(id, "Negotiated %s + %s.", scfg->x.str, + scfg->c.str); + return 0; + } - if (peer_hdr->kex.len == 0) { - log_err_id(id, "Server did not send KEM CT."); - return -ECRYPT; - } + /* KEM_MODE_SERVER_ENCAP */ + if (peer_hdr->kex.len == 0) { + log_err_id(id, "Server did not send KEM CT."); + return -ECRYPT; + } - ct.data = peer_hdr->kex.data; - ct.len = peer_hdr->kex.len; + ct.data = peer_hdr->kex.data; + ct.len = peer_hdr->kex.len; - if (kex_kem_decap(s->pkp, ct, kcfg->k.nid, key_buf) < 0) { - log_err_id(id, "Failed to decapsulate KEM."); + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&ct, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in CT."); return -ECRYPT; } + } - log_dbg_id(id, "Client decapsulated server CT."); - - } else if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) { - /* Key already derived during prepare */ - memcpy(sk->key, s->key, SYMMKEYSZ); - sk->nid = kcfg->c.nid; - log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, - kcfg->c.str); - return 0; + if (kex_kem_decap(s->pkp, ct, scfg->k.nid, key_buf) < 0) { + log_err_id(id, "Failed to decapsulate KEM."); + return -ECRYPT; } + log_dbg_id(id, "Client decapsulated server CT."); + memcpy(sk->key, key_buf, SYMMKEYSZ); - sk->nid = kcfg->c.nid; + sk->nid = scfg->c.nid; crypt_secure_clear(key_buf, SYMMKEYSZ); - log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str); + log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str); return 0; } @@ -402,7 +456,7 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s, const struct oap_hdr * peer_hdr, struct crypt_sk * sk) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; uint8_t * id = s->id.data; uint8_t key_buf[SYMMKEYSZ]; @@ -412,7 +466,7 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s, return -ECRYPT; } - if (kex_dhe_derive(kcfg, s->pkp, peer_hdr->kex, key_buf) < 0) { + if (kex_dhe_derive(scfg, s->pkp, peer_hdr->kex, key_buf) < 0) { log_err_id(id, "Failed to derive DHE secret."); return -ECRYPT; } @@ -420,10 +474,10 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s, log_dbg_id(id, "DHE: derived shared secret."); memcpy(sk->key, key_buf, SYMMKEYSZ); - sk->nid = kcfg->c.nid; + sk->nid = scfg->c.nid; crypt_secure_clear(key_buf, SYMMKEYSZ); - log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str); + log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str); return 0; } @@ -433,17 +487,17 @@ static int do_client_kex_complete(struct oap_cli_ctx * s, const struct oap_hdr * peer_hdr, struct crypt_sk * sk) { - struct sec_config * kcfg = &s->kcfg; + struct sec_config * scfg = &s->scfg; uint8_t * id = s->id.data; int cipher_nid; int kdf_nid; - if (!IS_KEX_ALGO_SET(kcfg)) + if (!IS_KEX_ALGO_SET(scfg)) return 0; /* Save client's configured minimums */ - cipher_nid = kcfg->c.nid; - kdf_nid = kcfg->k.nid; + cipher_nid = scfg->c.nid; + kdf_nid = scfg->k.nid; /* Accept server's cipher choice */ if (peer_hdr->cipher_str == NULL) { @@ -451,15 +505,16 @@ static int do_client_kex_complete(struct oap_cli_ctx * s, return -ECRYPT; } - SET_KEX_CIPHER(kcfg, peer_hdr->cipher_str); - if (crypt_validate_nid(kcfg->c.nid) < 0) { + SET_KEX_CIPHER(scfg, peer_hdr->cipher_str); + + if (crypt_cipher_rank(scfg->c.nid) < 0) { log_err_id(id, "Server cipher '%s' not supported.", peer_hdr->cipher_str); return -ENOTSUP; } /* Verify server cipher >= client's minimum */ - if (crypt_cipher_rank(kcfg->c.nid) < crypt_cipher_rank(cipher_nid)) { + if (crypt_cipher_rank(scfg->c.nid) < crypt_cipher_rank(cipher_nid)) { log_err_id(id, "Server cipher %s too weak.", peer_hdr->cipher_str); return -ECRYPT; @@ -469,38 +524,44 @@ static int do_client_kex_complete(struct oap_cli_ctx * s, peer_hdr->cipher_str); /* Accept server's KDF for non-client-encap modes */ - if (kcfg->x.mode != KEM_MODE_CLIENT_ENCAP + if (scfg->x.mode != KEM_MODE_CLIENT_ENCAP && peer_hdr->kdf_nid != NID_undef) { if (crypt_kdf_rank(peer_hdr->kdf_nid) < crypt_kdf_rank(kdf_nid)) { log_err_id(id, "Server KDF too weak."); return -ECRYPT; } - SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid); + SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid); log_dbg_id(id, "Accepted server KDF %s.", - md_nid_to_str(kcfg->k.nid)); + md_nid_to_str(scfg->k.nid)); } /* Derive shared secret */ - if (IS_KEM_ALGORITHM(kcfg->x.str)) + if (IS_KEM_ALGORITHM(scfg->x.str)) return do_client_kex_complete_kem(s, peer_hdr, sk); return do_client_kex_complete_dhe(s, peer_hdr, sk); } -int oap_cli_complete(void * ctx, - const struct name_info * info, - buffer_t rsp_buf, - buffer_t * data, - struct crypt_sk * sk) +int oap_cli_complete(void * ctx, + buffer_t rsp_buf, + buffer_t * data, + struct crypt_sk * sk, + const buffer_t * cached_crt, + buffer_t * peer_crt) { struct oap_cli_ctx * s = ctx; struct oap_hdr peer_hdr; char peer[NAME_SIZE + 1]; + uint8_t kc_buf[MAX_HASH_SIZE]; + uint8_t resp_hash_buf[MAX_HASH_SIZE]; + uint8_t hs_key[SYMMKEYSZ]; + buffer_t req_hash = BUF_INIT; + buffer_t resp_hash = BUF_INIT; uint8_t * id; + int rc; assert(ctx != NULL); - assert(info != NULL); assert(data != NULL); assert(sk != NULL); @@ -512,10 +573,10 @@ int oap_cli_complete(void * ctx, id = s->id.data; - log_dbg_id(id, "Completing OAP for %s.", info->name); + log_dbg_id(id, "Completing OAP for %s.", s->peer); /* Decode response header using client's md_nid for hash length */ - if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid) < 0) { + if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid, false) < 0) { log_err_id(id, "Failed to decode OAP response header."); goto fail_oap; } @@ -528,34 +589,99 @@ int oap_cli_complete(void * ctx, goto fail_oap; } - /* Authenticate server */ - if (oap_auth_peer(peer, &s->local_hdr, &peer_hdr) < 0) { - log_err_id(id, "Failed to authenticate server."); + /* Complete key exchange first; the sealed identity needs the secret */ + if (do_client_kex_complete(s, &peer_hdr, sk) < 0) { + log_err_id(id, "Failed to complete key exchange."); goto fail_oap; } - /* Verify request hash in authenticated response */ - if (peer_hdr.req_hash.len == 0) { - log_err_id(id, "Response missing req_hash."); + req_hash.data = s->req_hash; + req_hash.len = s->req_hash_len; + + /* Decrypt the sealed server identity (data+cert+sig) before auth */ + if (sk->nid != NID_undef && peer_hdr.sealed.data != NULL) { + if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) { + log_err_id(id, "Failed to derive handshake key."); + goto fail_oap; + } + + rc = oap_hdr_unseal(&peer_hdr, hs_key); + + crypt_secure_clear(hs_key, SYMMKEYSZ); + + if (rc < 0) { + log_err_id(id, "Failed to unseal server identity."); + goto fail_oap; + } + } + + /* Authenticate server (cert + signature now in cleartext) */ + if (oap_auth_peer(peer, &s->scfg, &s->local_hdr, &peer_hdr, + cached_crt) < 0) { + log_err_id(id, "Failed to authenticate server."); goto fail_oap; } - if (memcmp(peer_hdr.req_hash.data, s->req_hash, s->req_hash_len) != 0) { - log_err_id(id, "Response req_hash mismatch."); + /* Surface the peer cert so the caller can cache it for re-key. */ + if (peer_crt != NULL && peer_hdr.crt.len > 0) { + peer_crt->data = malloc(peer_hdr.crt.len); + if (peer_crt->data == NULL) + goto fail_oap; + + memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len); + peer_crt->len = peer_hdr.crt.len; + } + + /* Response must carry a transcript tag of the expected length */ + if (peer_hdr.rsp_tag.len != s->req_hash_len) { + log_err_id(id, "Response transcript tag mismatch."); goto fail_oap; } /* Verify peer certificate name matches expected destination */ - if (peer_hdr.crt.len > 0 && strcmp(peer, info->name) != 0) { + if (peer_hdr.crt.len > 0 && strcmp(peer, s->peer) != 0) { log_err_id(id, "Peer crt for '%s' does not match '%s'.", - peer, info->name); + peer, s->peer); goto fail_oap; } - /* Complete key exchange */ - if (do_client_kex_complete(s, &peer_hdr, sk) < 0) { - log_err_id(id, "Failed to complete key exchange."); - goto fail_oap; + if (sk->nid != NID_undef) { + /* Encrypted: bind the key and verify key confirmation */ + resp_hash.data = resp_hash_buf; + + if (oap_resp_hash(s->req_md_nid, peer_hdr.kex, + peer_hdr.data, peer_hdr.crt, + &resp_hash) < 0) { + log_err_id(id, "Failed to hash response."); + goto fail_oap; + } + + if (oap_bind_session_key(sk, req_hash, resp_hash, + s->scfg.k.nid) < 0) { + log_err_id(id, "Failed to bind session key."); + goto fail_oap; + } + + if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf, + s->req_hash_len) < 0) { + log_err_id(id, "Failed to confirm session key."); + goto fail_oap; + } + + if (crypt_ct_cmp(peer_hdr.rsp_tag.data, kc_buf, + s->req_hash_len) != 0) { + log_err_id(id, "Key confirmation mismatch."); + goto fail_oap; + } + } else { + /* Cleartext path is config-driven, never a wire downgrade */ + assert(!IS_KEX_ALGO_SET(&s->scfg)); + /* Unencrypted: verify request-echo integrity */ + if (crypt_ct_cmp(peer_hdr.rsp_tag.data, s->req_hash, + s->req_hash_len) != 0) { + log_err_id(id, "Response tag mismatch."); + goto fail_oap; + } } /* Copy piggybacked data from server response */ @@ -564,13 +690,16 @@ int oap_cli_complete(void * ctx, goto fail_oap; } - log_info_id(id, "OAP completed for %s.", info->name); + log_info_id(id, "OAP completed for %s.", s->peer); + + freebuf(peer_hdr.sealed_pt); oap_ctx_free(s); return 0; fail_oap: + freebuf(peer_hdr.sealed_pt); oap_ctx_free(s); return -ECRYPT; } diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c index 5465dd2a..d037d7b8 100644 --- a/src/irmd/oap/hdr.c +++ b/src/irmd/oap/hdr.c @@ -30,6 +30,7 @@ #include <ouroboros/crypt.h> #include <ouroboros/endian.h> +#include <ouroboros/errno.h> #include <ouroboros/hash.h> #include <ouroboros/logs.h> #include <ouroboros/rib.h> @@ -45,9 +46,17 @@ #include <string.h> #include <time.h> +#define OAP_SEAL_TAGSZ 16 /* AEAD tag on the sealed identity block */ +/* Sealed length prefix: data_len ‖ crt_len. */ +#define OAP_SEAL_LENSZ (sizeof(uint16_t) + sizeof(uint16_t)) + +/* hs_key is single-use per handshake, so a fixed nonce is reuse-safe. */ +static const uint8_t oap_seal_nonce[12]; + int oap_hdr_decode(struct oap_hdr * oap_hdr, buffer_t hdr, - int req_md_nid) + int req_md_nid, + bool rekey) { off_t offset; uint16_t kex_len; @@ -88,11 +97,13 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr, oap_hdr->md_str = md_nid_to_str(oap_hdr->md_nid); offset += sizeof(uint16_t); - /* Validate NIDs: NID_undef is valid at parse time, else must be known. + /* + * Validate NIDs: NID_undef is valid at parse time, else must be known. * Note: md_nid=NID_undef only valid for PQC; enforced at sign/verify. */ if (ciph_nid != NID_undef && crypt_validate_nid(ciph_nid) < 0) goto fail_decode; + if (oap_hdr->kdf_nid != NID_undef && md_validate_nid(oap_hdr->kdf_nid) < 0) goto fail_decode; @@ -115,10 +126,37 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr, data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); offset += sizeof(uint16_t); - /* Response includes req_hash when md_nid is set */ + assert((size_t) offset == OAP_HDR_MIN_SIZE); + + /* Response includes rsp_tag when md_nid is set */ hash_len = (req_md_nid != NID_undef) ? (size_t) md_len(req_md_nid) : 0; + /* Encrypted response: sealed block is data_len‖crt_len‖data‖crt‖sig. */ + if (req_md_nid != NID_undef && ciph_nid != NID_undef) { + if (hdr.len < (size_t) offset + oap_hdr->kex.len + hash_len + + OAP_SEAL_TAGSZ + OAP_SEAL_LENSZ) + goto fail_decode; + + oap_hdr->kex.data = hdr.data + offset; + offset += oap_hdr->kex.len; + + oap_hdr->rsp_tag.data = hdr.data + offset; + oap_hdr->rsp_tag.len = hash_len; + offset += hash_len; + + oap_hdr->sealed.data = hdr.data + offset; + oap_hdr->sealed.len = hdr.len - offset; + + /* crt/data/sig lengths are sealed; set by oap_hdr_unseal. */ + oap_hdr->crt.len = crt_len; + oap_hdr->data.len = data_len; + + oap_hdr->hdr = hdr; + + return 0; + } + /* Validate total length */ if (hdr.len < (size_t) offset + crt_len + oap_hdr->kex.len + data_len + hash_len) @@ -128,8 +166,12 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr, sig_len = hdr.len - offset - crt_len - oap_hdr->kex.len - data_len - hash_len; - /* Unsigned packets must not have trailing bytes */ - if (crt_len == 0 && sig_len != 0) + /* + * Unsigned packets must not have trailing bytes. A re-key request + * is signed but cert-less (verified against the cached peer cert), + * so the rekey caller permits crt_len==0 with a signature. + */ + if (crt_len == 0 && sig_len != 0 && !rekey) goto fail_decode; /* Parse variable fields */ @@ -144,8 +186,8 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr, oap_hdr->data.len = data_len; offset += data_len; - oap_hdr->req_hash.data = hdr.data + offset; - oap_hdr->req_hash.len = hash_len; + oap_hdr->rsp_tag.data = hdr.data + offset; + oap_hdr->rsp_tag.len = hash_len; offset += hash_len; oap_hdr->sig.data = hdr.data + offset; @@ -164,10 +206,61 @@ void oap_hdr_fini(struct oap_hdr * oap_hdr) { assert(oap_hdr != NULL); + freebuf(oap_hdr->sealed_pt); freebuf(oap_hdr->hdr); memset(oap_hdr, 0, sizeof(*oap_hdr)); } +uint16_t oap_kex_nid(buffer_t kex) +{ + uint16_t nid; + + if (kex.len <= OAP_KEX_NIDSZ) + return NID_undef; + + memcpy(&nid, kex.data, sizeof(nid)); + + return ntoh16(nid); +} + +void oap_kex_set_nid(uint8_t * buf, + uint16_t nid) +{ + uint16_t v; + + assert(buf != NULL); + + v = hton16(nid); + + memcpy(buf, &v, sizeof(v)); +} + +int oap_kex_strip_nid(buffer_t * kex, + uint16_t nid) +{ + assert(kex != NULL); + + if (oap_kex_nid(*kex) != nid) + return -1; + + kex->data += OAP_KEX_NIDSZ; + kex->len -= OAP_KEX_NIDSZ; + + return 0; +} + +ssize_t oap_kex_tag_nid(uint8_t * buf, + uint16_t nid, + ssize_t len) +{ + if (len < 0) + return len; + + oap_kex_set_nid(buf, nid); + + return len + OAP_KEX_NIDSZ; +} + int oap_hdr_copy_data(const struct oap_hdr * hdr, buffer_t * out) { @@ -207,12 +300,232 @@ void oap_hdr_init(struct oap_hdr * hdr, hdr->nid = nid; } +/* Write the 36-byte fixed header; stamp is already in network order. */ +static void write_oap_fixed(uint8_t * buf, + const struct oap_hdr * hdr, + const struct sec_config * scfg, + size_t crt_len, + size_t data_len, + uint64_t stamp) +{ + uint16_t v; + uint16_t kex_len; + off_t offset = 0; + + memcpy(buf + offset, hdr->id.data, hdr->id.len); + offset += hdr->id.len; + + memcpy(buf + offset, &stamp, sizeof(stamp)); + offset += sizeof(stamp); + + v = hton16(hdr->nid); + memcpy(buf + offset, &v, sizeof(v)); + offset += sizeof(v); + + v = hton16(scfg->k.nid); + memcpy(buf + offset, &v, sizeof(v)); + offset += sizeof(v); + + v = hton16(scfg->d.nid); + memcpy(buf + offset, &v, sizeof(v)); + offset += sizeof(v); + + v = hton16((uint16_t) crt_len); + memcpy(buf + offset, &v, sizeof(v)); + offset += sizeof(v); + + kex_len = (uint16_t) hdr->kex.len; + if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(scfg->x.str)) { + if (IS_HYBRID_KEM_NID(scfg->x.nid)) + kex_len |= OAP_KEX_FMT_BIT; + if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) + kex_len |= OAP_KEX_ROLE_BIT; + } + + v = hton16(kex_len); + memcpy(buf + offset, &v, sizeof(v)); + offset += sizeof(v); + + v = hton16((uint16_t) data_len); + memcpy(buf + offset, &v, sizeof(v)); +} + +/* + * Pack lens ‖ data ‖ crt, sign prefix ‖ body, append the signature, then + * AEAD-seal lens ‖ data ‖ crt ‖ sig under prefix as AAD. The cert, app data + * and their sizes stay confidential; *out is the opaque sealed block. The + * signature rides inside the seal so it can't deanonymise the server. + */ +static int oap_seal_body(int nid, + const uint8_t * seal_key, + void * pkp, + int md_nid, + buffer_t prefix, + buffer_t data, + buffer_t crt, + buffer_t * out) +{ + buffer_t sig = BUF_INIT; + buffer_t sign; + buffer_t aad; + buffer_t plain; + uint8_t * buf; + uint8_t * tmp; + uint16_t datalen; + uint16_t crtlen; + size_t body_len; + off_t offset; + + datalen = hton16((uint16_t) data.len); + crtlen = hton16((uint16_t) crt.len); + + body_len = OAP_SEAL_LENSZ + data.len + crt.len; + + buf = malloc(prefix.len + body_len); + if (buf == NULL) + return -1; + + memcpy(buf, prefix.data, prefix.len); + offset = (off_t) prefix.len; + + memcpy(buf + offset, &datalen, sizeof(datalen)); + offset += sizeof(datalen); + + memcpy(buf + offset, &crtlen, sizeof(crtlen)); + offset += sizeof(crtlen); + + if (data.len != 0) + memcpy(buf + offset, data.data, data.len); + + offset += data.len; + + if (crt.len != 0) + memcpy(buf + offset, crt.data, crt.len); + + /* Sign prefix ‖ lens ‖ data ‖ crt (plaintext, before sealing). */ + sign.data = buf; + sign.len = prefix.len + body_len; + + if (pkp != NULL && auth_sign(pkp, md_nid, sign, &sig) < 0) + goto fail_buf; + + /* Append the signature so the seal covers lens ‖ data ‖ crt ‖ sig. */ + if (sig.len != 0) { + tmp = realloc(buf, prefix.len + body_len + sig.len); + if (tmp == NULL) + goto fail_sig; + + buf = tmp; + memcpy(buf + prefix.len + body_len, sig.data, sig.len); + } + + aad.data = buf; + aad.len = prefix.len; + plain.data = buf + prefix.len; + plain.len = body_len + sig.len; + + if (crypt_oneshot_seal(nid, seal_key, oap_seal_nonce, + aad, plain, out) < 0) + goto fail_sig; + + free(buf); + freebuf(sig); + + return 0; + + fail_sig: + freebuf(sig); + fail_buf: + free(buf); + return -1; +} + +/* Encode an identity-hidden response: wire = prefix ‖ oap_seal_body(...). */ +static int oap_hdr_encode_sealed(struct oap_hdr * hdr, + void * pkp, + void * crt, + struct sec_config * scfg, + buffer_t rsp_tag, + int req_md_nid, + const uint8_t * seal_key, + int flags) +{ + struct timespec now; + uint64_t stamp; + buffer_t der = BUF_INIT; + buffer_t sealed = BUF_INIT; + buffer_t prefix; + off_t offset; + + clock_gettime(CLOCK_REALTIME, &now); + stamp = hton64(TS_TO_UINT64(now)); + + if (crt != NULL && crypt_crt_der(crt, &der) < 0) + goto fail_der; + + prefix.len = OAP_HDR_MIN_SIZE + hdr->kex.len + rsp_tag.len; + prefix.data = malloc(prefix.len); + if (prefix.data == NULL) + goto fail_der; + + /* Cleartext crt_len/data_len are 0; real lengths prefix the seal. */ + write_oap_fixed(prefix.data, hdr, scfg, 0, 0, stamp); + offset = OAP_HDR_MIN_SIZE; + + if (hdr->kex.len != 0) + memcpy(prefix.data + offset, hdr->kex.data, hdr->kex.len); + + offset += hdr->kex.len; + + if (rsp_tag.len != 0) + memcpy(prefix.data + offset, rsp_tag.data, rsp_tag.len); + + offset += rsp_tag.len; + + assert((size_t) offset == prefix.len); + + if (oap_seal_body(hdr->nid, seal_key, pkp, scfg->d.nid, + prefix, hdr->data, der, &sealed) < 0) + goto fail_prefix; + + hdr->hdr.len = prefix.len + sealed.len; + hdr->hdr.data = malloc(hdr->hdr.len); + if (hdr->hdr.data == NULL) + goto fail_sealed; + + memcpy(hdr->hdr.data, prefix.data, prefix.len); + memcpy(hdr->hdr.data + prefix.len, sealed.data, sealed.len); + + freebuf(sealed); + free(prefix.data); + freebuf(der); + + if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, + flags & OAP_ENC_REKEY) < 0) + goto fail_decode; + + return 0; + + fail_decode: + oap_hdr_fini(hdr); + return -1; + fail_sealed: + freebuf(sealed); + fail_prefix: + free(prefix.data); + fail_der: + freebuf(der); + return -1; +} + int oap_hdr_encode(struct oap_hdr * hdr, void * pkp, void * crt, - struct sec_config * kcfg, - buffer_t req_hash, - int req_md_nid) + struct sec_config * scfg, + buffer_t rsp_tag, + int req_md_nid, + const uint8_t * seal_key, + int flags) { struct timespec now; uint64_t stamp; @@ -220,16 +533,15 @@ int oap_hdr_encode(struct oap_hdr * hdr, buffer_t der = BUF_INIT; buffer_t sig = BUF_INIT; buffer_t sign; - uint16_t len; - uint16_t ciph_nid; - uint16_t kdf_nid; - uint16_t md_nid; - uint16_t kex_len; off_t offset; assert(hdr != NULL); assert(hdr->id.data != NULL && hdr->id.len == OAP_ID_SIZE); - assert(kcfg != NULL); + assert(scfg != NULL); + + if (seal_key != NULL) + return oap_hdr_encode_sealed(hdr, pkp, crt, scfg, rsp_tag, + req_md_nid, seal_key, flags); clock_gettime(CLOCK_REALTIME, &now); stamp = hton64(TS_TO_UINT64(now)); @@ -237,86 +549,40 @@ int oap_hdr_encode(struct oap_hdr * hdr, if (crt != NULL && crypt_crt_der(crt, &der) < 0) goto fail_der; - ciph_nid = hton16(hdr->nid); - kdf_nid = hton16(kcfg->k.nid); - md_nid = hton16(kcfg->d.nid); - - /* Build kex_len with flags */ - kex_len = (uint16_t) hdr->kex.len; - if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(kcfg->x.str)) { - if (IS_HYBRID_KEM(kcfg->x.str)) - kex_len |= OAP_KEX_FMT_BIT; - if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) - kex_len |= OAP_KEX_ROLE_BIT; - } - kex_len = hton16(kex_len); - - /* Fixed header (36 bytes) + variable fields + req_hash (if auth) */ + /* Fixed header (36 bytes) + variable fields + rsp_tag (rsp only) */ out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len + - req_hash.len; + rsp_tag.len; out.data = malloc(out.len); if (out.data == NULL) goto fail_out; - offset = 0; - - /* id (16 bytes) */ - memcpy(out.data + offset, hdr->id.data, hdr->id.len); - offset += hdr->id.len; - - /* timestamp (8 bytes) */ - memcpy(out.data + offset, &stamp, sizeof(stamp)); - offset += sizeof(stamp); - - /* cipher_nid (2 bytes) */ - memcpy(out.data + offset, &ciph_nid, sizeof(ciph_nid)); - offset += sizeof(ciph_nid); - - /* kdf_nid (2 bytes) */ - memcpy(out.data + offset, &kdf_nid, sizeof(kdf_nid)); - offset += sizeof(kdf_nid); - - /* md_nid (2 bytes) */ - memcpy(out.data + offset, &md_nid, sizeof(md_nid)); - offset += sizeof(md_nid); - - /* crt_len (2 bytes) */ - len = hton16((uint16_t) der.len); - memcpy(out.data + offset, &len, sizeof(len)); - offset += sizeof(len); - - /* kex_len + flags (2 bytes) */ - memcpy(out.data + offset, &kex_len, sizeof(kex_len)); - offset += sizeof(kex_len); - - /* data_len (2 bytes) */ - len = hton16((uint16_t) hdr->data.len); - memcpy(out.data + offset, &len, sizeof(len)); - offset += sizeof(len); - - /* Fixed header complete (36 bytes) */ - assert((size_t) offset == OAP_HDR_MIN_SIZE); + write_oap_fixed(out.data, hdr, scfg, der.len, hdr->data.len, stamp); + offset = OAP_HDR_MIN_SIZE; /* certificate (variable) */ if (der.len != 0) memcpy(out.data + offset, der.data, der.len); + offset += der.len; /* kex data (variable) */ if (hdr->kex.len != 0) memcpy(out.data + offset, hdr->kex.data, hdr->kex.len); + offset += hdr->kex.len; /* data (variable) */ if (hdr->data.len != 0) memcpy(out.data + offset, hdr->data.data, hdr->data.len); + offset += hdr->data.len; - /* req_hash (variable, only for authenticated responses) */ - if (req_hash.len != 0) - memcpy(out.data + offset, req_hash.data, req_hash.len); - offset += req_hash.len; + /* rsp_tag (variable, response only) */ + if (rsp_tag.len != 0) + memcpy(out.data + offset, rsp_tag.data, rsp_tag.len); + + offset += rsp_tag.len; assert((size_t) offset == out.len); @@ -324,7 +590,7 @@ int oap_hdr_encode(struct oap_hdr * hdr, sign.data = out.data; sign.len = out.len; - if (pkp != NULL && auth_sign(pkp, kcfg->d.nid, sign, &sig) < 0) + if (pkp != NULL && auth_sign(pkp, scfg->d.nid, sign, &sig) < 0) goto fail_sig; hdr->hdr = out; @@ -337,10 +603,13 @@ int oap_hdr_encode(struct oap_hdr * hdr, goto fail_realloc; memcpy(hdr->hdr.data + offset, sig.data, sig.len); - clrbuf(out); } - if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid) < 0) + /* Ownership moved to hdr->hdr; drop the alias to avoid double-free. */ + clrbuf(out); + + if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, + flags & OAP_ENC_REKEY) < 0) goto fail_decode; freebuf(der); @@ -360,28 +629,99 @@ int oap_hdr_encode(struct oap_hdr * hdr, return -1; } +int oap_hdr_unseal(struct oap_hdr * hdr, + const uint8_t * key) +{ + buffer_t pt = BUF_INIT; + buffer_t prefix; + uint8_t * recon; + size_t body_len; + size_t pt_len; + size_t data_len; + size_t crt_len; + + assert(hdr != NULL); + assert(key != NULL); + + if (hdr->sealed.data == NULL || hdr->sealed.len == 0) + return -EINVAL; + + /* AAD prefix is fixed‖kex‖rsp_tag; sealed starts right after. */ + prefix.data = hdr->hdr.data; + prefix.len = (size_t) (hdr->sealed.data - hdr->hdr.data); + + if (crypt_oneshot_open(hdr->nid, key, oap_seal_nonce, prefix, + hdr->sealed, &pt) < 0) + return -ECRYPT; + + pt_len = pt.len; + + /* Plaintext = data_len ‖ crt_len ‖ data ‖ crt ‖ sig. */ + if (pt_len < OAP_SEAL_LENSZ) + goto fail_auth; + + data_len = (size_t) ntoh16(*(uint16_t *) pt.data); + crt_len = (size_t) ntoh16(*(uint16_t *)(pt.data + sizeof(uint16_t))); + + body_len = OAP_SEAL_LENSZ + data_len + crt_len; + if (pt_len < body_len) + goto fail_auth; + + /* Rebuild prefix ‖ lens ‖ data ‖ crt ‖ sig (whole signed region). */ + recon = malloc(prefix.len + pt_len); + if (recon == NULL) + goto fail_mem; + + memcpy(recon, prefix.data, prefix.len); + memcpy(recon + prefix.len, pt.data, pt_len); + + freebuf(pt); + + hdr->sealed_pt.data = recon; + hdr->sealed_pt.len = prefix.len + pt_len; + + hdr->data.data = recon + prefix.len + OAP_SEAL_LENSZ; + hdr->data.len = data_len; + hdr->crt.data = recon + prefix.len + OAP_SEAL_LENSZ + data_len; + hdr->crt.len = crt_len; + hdr->sig.data = recon + prefix.len + body_len; + hdr->sig.len = pt_len - body_len; + + return 0; + + fail_mem: + freebuf(pt); + return -ENOMEM; + fail_auth: + freebuf(pt); + return -EAUTH; +} + #ifdef DEBUG_PROTO_OAP #define OAP_KEX_IS_KEM(hdr) ((hdr)->kex_flags.role | (hdr)->kex_flags.fmt) static void debug_oap_hdr(const struct oap_hdr * hdr) { assert(hdr); + if (hdr->sealed.len > 0) + log_proto(" Sealed block: [%zu bytes] on wire", + hdr->sealed.len); + if (hdr->crt.len > 0) log_proto(" crt: [%zu bytes]", hdr->crt.len); + else if (hdr->sealed.len > 0) + log_proto(" crt: <sealed>"); else log_proto(" crt: <none>"); if (hdr->kex.len > 0) { if (OAP_KEX_IS_KEM(hdr)) - log_proto(" Key Exchange Data:" - " [%zu bytes] [%s]", + log_proto(" Key Exchange Data: [%zu bytes] [%s]", hdr->kex.len, hdr->kex_flags.role ? - "Client encaps" : - "Server encaps"); + "Client encaps" : "Server encaps"); else - log_proto(" Key Exchange Data:" - " [%zu bytes]", + log_proto(" Key Exchange Data: [%zu bytes]", hdr->kex.len); } else log_proto(" Key Exchange Data: <none>"); @@ -403,16 +743,20 @@ static void debug_oap_hdr(const struct oap_hdr * hdr) if (hdr->data.len > 0) log_proto(" Data: [%zu bytes]", hdr->data.len); + else if (hdr->sealed.len > 0) + log_proto(" Data: <sealed>"); else log_proto(" Data: <none>"); - if (hdr->req_hash.len > 0) - log_proto(" Req Hash: [%zu bytes]", hdr->req_hash.len); + if (hdr->rsp_tag.len > 0) + log_proto(" Rsp Tag: [%zu bytes]", hdr->rsp_tag.len); else - log_proto(" Req Hash: <none>"); + log_proto(" Rsp Tag: <none>"); if (hdr->sig.len > 0) log_proto(" Signature: [%zu bytes]", hdr->sig.len); + else if (hdr->sealed.len > 0) + log_proto(" Signature: <sealed>"); else log_proto(" Signature: <none>"); } @@ -432,8 +776,9 @@ void debug_oap_hdr_rcv(const struct oap_hdr * hdr) tm = gmtime(&stamp); strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); - log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--", - HASH_VAL64(hdr->id.data), tmstr); + log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s <--", + HASH_VAL64(hdr->id.data), tmstr, + hdr->sealed.len > 0 ? " [sealed]" : ""); debug_oap_hdr(hdr); #else @@ -455,8 +800,9 @@ void debug_oap_hdr_snd(const struct oap_hdr * hdr) tm = gmtime(&stamp); strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); - log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->", - HASH_VAL64(hdr->id.data), tmstr); + log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s -->", + HASH_VAL64(hdr->id.data), tmstr, + hdr->sealed.len > 0 ? " [sealed]" : ""); debug_oap_hdr(hdr); #else diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h index 6016452c..4c5f885a 100644 --- a/src/irmd/oap/hdr.h +++ b/src/irmd/oap/hdr.h @@ -32,10 +32,13 @@ #define OAP_ID_SIZE (16) #define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 6 * sizeof(uint16_t)) -#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw */ +#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw: NID + bytes */ #define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 0=Server encaps, 1=Client encaps */ #define OAP_KEX_LEN_MASK 0x3FFF /* bits 0-13: Length (0-16383 bytes) */ +/* Raw format kex payloads lead with the algorithm NID */ +#define OAP_KEX_NIDSZ sizeof(uint16_t) + #define OAP_KEX_ROLE(hdr) (hdr->kex_flags.role) #define OAP_KEX_FMT(hdr) (hdr->kex_flags.fmt) @@ -43,6 +46,9 @@ #define OAP_KEX_IS_RAW_FMT(hdr) (((hdr)->kex_flags.fmt) == 1) /* + * Plaintext layout (request, and unencrypted/signed response). The + * signature covers the whole packet except itself. + * * 0 1 2 3 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ @@ -83,8 +89,8 @@ * | | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | * | | | - * + req_hash (variable, response only) + | - * | H(request) using req md_nid / sha384 | | + * + rsp_tag (variable, response only) + | + * | key-confirm tag (enc), else H(request) | | * | | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ * | | @@ -92,6 +98,25 @@ * | DSA signature over signed region | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * + * Encrypted response - wire layout. The certificate, application data and + * signature are AEAD-sealed - hiding the server identity and the cert/data + * sizes; kex and rsp_tag move ahead of the sealed block as cleartext AAD. + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ + * | fixed header (36 bytes, see above) | | + * + id, timestamp, NIDs, crt_len=0, kex_len, data_len=0 + | AAD + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | kex_data (variable) | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | rsp_tag (variable, response only) | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ + * | SEAL( data_len ‖ crt_len ‖ data ‖ crt ‖ sig ) | | + * + encrypted cert, app data and signature + | Sealed + * | + AEAD tag (128 bits) | | area + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ + * * cipher_nid: NID value for symmetric cipher (0 = none) * kdf_nid: NID value for KDF function (0 = none) * md_nid: NID value for signature hash (0 = PQC/no signature) @@ -105,6 +130,11 @@ * Request: sig_len = total - 36 - crt_len - kex_len - data_len * Response: sig_len = total - 36 - crt_len - kex_len - data_len - hash_len * where hash_len = md_len(req_md_nid / sha384) + * + * The signed plaintext inside the seal is prefix ‖ data_len ‖ crt_len ‖ + * data ‖ crt ‖ sig; the cleartext prefix (fixed ‖ kex ‖ rsp_tag) is the + * AEAD AAD. Cleartext crt_len/data_len are 0 - the real lengths are sealed, + * hiding the cert and data sizes; oap_hdr_unseal reads them to split. */ /* Parsed OAP header - buffers pointing to a single memory region */ @@ -120,40 +150,68 @@ struct oap_hdr { bool fmt; /* Format */ bool role; /* Role */ } kex_flags; + buffer_t id; buffer_t crt; buffer_t kex; buffer_t data; - buffer_t req_hash; /* H(request) - response only */ + buffer_t rsp_tag; /* key-confirm tag / H(req), rsp only */ buffer_t sig; + buffer_t sealed; /* wire ciphertext ‖ tag (sealed rsp) */ + buffer_t sealed_pt; /* prefix‖lens‖data‖crt‖sig, owned */ buffer_t hdr; }; -void oap_hdr_init(struct oap_hdr * hdr, - buffer_t id, - uint8_t * kex_buf, - buffer_t data, - uint16_t nid); +void oap_hdr_init(struct oap_hdr * hdr, + buffer_t id, + uint8_t * kex_buf, + buffer_t data, + uint16_t nid); + +void oap_hdr_fini(struct oap_hdr * oap_hdr); + +/* NID leading a raw format kex payload, NID_undef if too short */ +uint16_t oap_kex_nid(buffer_t kex); + +void oap_kex_set_nid(uint8_t * buf, + uint16_t nid); + +/* Verify the NID leading a raw kex payload, then strip it */ +int oap_kex_strip_nid(buffer_t * kex, + uint16_t nid); + +/* Tag a raw payload written at buf + OAP_KEX_NIDSZ, len passthrough */ +ssize_t oap_kex_tag_nid(uint8_t * buf, + uint16_t nid, + ssize_t len); + +/* oap_hdr_encode option flags */ +#define OAP_ENC_REKEY (1U << 0) /* signed, cert-less re-key packet */ -void oap_hdr_fini(struct oap_hdr * oap_hdr); +int oap_hdr_encode(struct oap_hdr * hdr, + void * pkp, + void * crt, + struct sec_config * scfg, + buffer_t rsp_tag, + int req_md_nid, + const uint8_t * seal_key, + int flags); -int oap_hdr_encode(struct oap_hdr * hdr, - void * pkp, - void * crt, - struct sec_config * kcfg, - buffer_t req_hash, - int req_md_nid); +int oap_hdr_decode(struct oap_hdr * hdr, + buffer_t buf, + int req_md_nid, + bool rekey); -int oap_hdr_decode(struct oap_hdr * hdr, - buffer_t buf, - int req_md_nid); +/* Decrypt a sealed response identity block; fills data, crt and sig. */ +int oap_hdr_unseal(struct oap_hdr * hdr, + const uint8_t * key); -void debug_oap_hdr_rcv(const struct oap_hdr * hdr); +void debug_oap_hdr_rcv(const struct oap_hdr * hdr); -void debug_oap_hdr_snd(const struct oap_hdr * hdr); +void debug_oap_hdr_snd(const struct oap_hdr * hdr); -int oap_hdr_copy_data(const struct oap_hdr * hdr, - buffer_t * out); +int oap_hdr_copy_data(const struct oap_hdr * hdr, + buffer_t * out); #endif /* OUROBOROS_IRMD_OAP_HDR_H */ diff --git a/src/irmd/oap/internal.h b/src/irmd/oap/internal.h index 6dd44d56..4a156723 100644 --- a/src/irmd/oap/internal.h +++ b/src/irmd/oap/internal.h @@ -36,12 +36,13 @@ int oap_check_hdr(const struct oap_hdr * hdr); -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr); +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr); int oap_negotiate_cipher(const struct oap_hdr * peer_hdr, - struct sec_config * kcfg); + struct sec_config * scfg); #ifndef OAP_TEST_MODE int load_credentials(const char * name, @@ -49,7 +50,7 @@ int load_credentials(const char * name, void ** pkp, void ** crt); -int load_kex_config(const char * name, +int load_sec_config(const char * name, const char * path, struct sec_config * cfg); #endif @@ -59,7 +60,7 @@ int load_srv_credentials(const struct name_info * info, void ** pkp, void ** crt); -int load_srv_kex_config(const struct name_info * info, +int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg); int load_server_kem_keypair(const char * name, @@ -69,7 +70,7 @@ int load_server_kem_keypair(const char * name, extern int load_srv_credentials(const struct name_info * info, void ** pkp, void ** crt); -extern int load_srv_kex_config(const struct name_info * info, +extern int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg); extern int load_server_kem_keypair(const char * name, struct sec_config * cfg, @@ -78,7 +79,7 @@ extern int load_server_kem_keypair(const char * name, int do_server_kex(const struct name_info * info, struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, buffer_t * kex, struct crypt_sk * sk); @@ -87,7 +88,7 @@ int load_cli_credentials(const struct name_info * info, void ** pkp, void ** crt); -int load_cli_kex_config(const struct name_info * info, +int load_cli_sec_config(const struct name_info * info, struct sec_config * cfg); int load_server_kem_pk(const char * name, @@ -97,21 +98,21 @@ int load_server_kem_pk(const char * name, extern int load_cli_credentials(const struct name_info * info, void ** pkp, void ** crt); -extern int load_cli_kex_config(const struct name_info * info, +extern int load_cli_sec_config(const struct name_info * info, struct sec_config * cfg); extern int load_server_kem_pk(const char * name, struct sec_config * cfg, buffer_t * pk); #endif -int oap_client_kex_prepare(struct sec_config * kcfg, +int oap_client_kex_prepare(struct sec_config * scfg, buffer_t server_pk, buffer_t * kex, uint8_t * key, void ** ephemeral_pkp); int oap_client_kex_complete(const struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, void * pkp, uint8_t * key); diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c index c2c91b91..845723fa 100644 --- a/src/irmd/oap/io.c +++ b/src/irmd/oap/io.c @@ -50,11 +50,17 @@ static bool file_exists(const char * path) { struct stat s; - if (stat(path, &s) < 0 && errno == ENOENT) { + if (stat(path, &s) == 0) + return true; + + if (errno == ENOENT) { log_dbg("File %s does not exist.", path); return false; } + /* Can't stat for another reason; assume present, fail on load */ + log_warn("Failed to stat %s: %s.", path, strerror(errno)); + return true; } @@ -96,16 +102,16 @@ int load_credentials(const char * name, return -EAUTH; } -int load_kex_config(const char * name, +int load_sec_config(const char * name, const char * path, struct sec_config * cfg) { + void * pin; + assert(name != NULL); assert(cfg != NULL); - memset(cfg, 0, sizeof(*cfg)); - - /* Load encryption config */ + /* Load security config */ if (!file_exists(path)) log_dbg("No encryption %s for %s.", path, name); @@ -114,19 +120,33 @@ int load_kex_config(const char * name, return -1; } + if (cfg->a.cacert[0] != '\0') { + if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) { + log_err("Failed to load pinned CA %s for %s.", + cfg->a.cacert, name); + return -EAUTH; + } + crypt_free_crt(pin); + } + if (!IS_KEX_ALGO_SET(cfg)) { log_info("Key exchange not configured for %s.", name); return 0; } -#ifndef HAVE_OPENSSL_ML_KEM +#ifndef HAVE_ML if (IS_KEM_ALGORITHM(cfg->x.str)) { log_err("PQC not available, can't use %s for %s.", cfg->x.str, name); return -ENOTSUP; } #endif - if (cfg->c.nid == NID_undef) { - log_err("Invalid cipher for %s.", name); + if (crypt_kex_rank(cfg->x.nid) < 1) { + log_err("Key exchange not supported for %s.", name); + return -ENOTSUP; + } + + if (crypt_cipher_rank(cfg->c.nid) < 1) { + log_err("Cipher not supported for %s.", name); return -ECRYPT; } diff --git a/src/irmd/oap/io.h b/src/irmd/oap/io.h index 2d47c62f..953e3898 100644 --- a/src/irmd/oap/io.h +++ b/src/irmd/oap/io.h @@ -32,7 +32,7 @@ int load_credentials(const char * name, void ** pkp, void ** crt); -int load_kex_config(const char * name, +int load_sec_config(const char * name, const char * path, struct sec_config * cfg); #endif diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c index 587a8f9f..0ceba7b6 100644 --- a/src/irmd/oap/srv.c +++ b/src/irmd/oap/srv.c @@ -49,11 +49,11 @@ extern int load_srv_credentials(const struct name_info * info, void ** pkp, void ** crt); -extern int load_srv_kex_config(const struct name_info * info, +extern int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg); -extern int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp); +extern int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp); #else int load_srv_credentials(const struct name_info * info, @@ -67,32 +67,38 @@ int load_srv_credentials(const struct name_info * info, return load_credentials(info->name, &info->s, pkp, crt); } -int load_srv_kex_config(const struct name_info * info, +int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg) { assert(info != NULL); assert(cfg != NULL); - return load_kex_config(info->name, info->s.enc, cfg); + memset(cfg, 0, sizeof(*cfg)); + + /* Client auth stays opt-in (mTLS); enable with auth=required */ + return load_sec_config(info->name, info->s.sec, cfg); } -int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp) +int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp) { char path[PATH_MAX]; const char * ext; + bool raw_fmt; assert(name != NULL); + assert(cfg != NULL); assert(pkp != NULL); + raw_fmt = IS_HYBRID_KEM_NID(cfg->x.nid); ext = raw_fmt ? "raw" : "pem"; snprintf(path, sizeof(path), OUROBOROS_SRV_CRT_DIR "/%s/kex.key.%s", name, ext); if (raw_fmt) { - if (crypt_load_privkey_raw_file(path, pkp) < 0) { + if (crypt_load_privkey_raw_file(path, cfg->x.str, pkp) < 0) { log_err("Failed to load %s keypair from %s.", ext, path); return -ECRYPT; @@ -106,6 +112,7 @@ int load_server_kem_keypair(const char * name, } log_dbg("Loaded server KEM keypair from %s.", path); + return 0; } @@ -114,18 +121,19 @@ int load_server_kem_keypair(const char * name, static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr, char * algo_buf) { - uint8_t * id = peer_hdr->id.data; - int ret; + uint8_t * id = peer_hdr->id.data; + const char * name; if (OAP_KEX_IS_RAW_FMT(peer_hdr)) { - ret = kex_get_algo_from_pk_raw(peer_hdr->kex, algo_buf); - if (ret < 0) { - log_err_id(id, "Failed to get algo from raw key."); + name = kex_nid_to_str(oap_kex_nid(peer_hdr->kex)); + if (name == NULL || !IS_HYBRID_KEM(name)) { + log_err_id(id, "Bad algo NID in raw key."); return -ECRYPT; } + + strcpy(algo_buf, name); } else { - ret = kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf); - if (ret < 0) { + if (kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf) < 0) { log_err_id(id, "Failed to get algo from DER key."); return -ECRYPT; } @@ -135,7 +143,7 @@ static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr, } static int negotiate_cipher(const struct oap_hdr * peer_hdr, - struct sec_config * kcfg) + struct sec_config * scfg) { uint8_t * id = peer_hdr->id.data; int cli_nid; @@ -143,27 +151,25 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr, int srv_rank; /* Cipher: select the strongest of client and server */ - cli_nid = peer_hdr->cipher_str != NULL - ? (int) crypt_str_to_nid(peer_hdr->cipher_str) - : NID_undef; + if (peer_hdr->cipher_str != NULL) + cli_nid = (int) crypt_str_to_nid(peer_hdr->cipher_str); + else + cli_nid = NID_undef; - if (cli_nid != NID_undef - && crypt_cipher_rank(cli_nid) < 0) { + if (cli_nid != NID_undef && crypt_cipher_rank(cli_nid) < 0) { log_err_id(id, "Unsupported cipher '%s'.", peer_hdr->cipher_str); return -ENOTSUP; } cli_rank = crypt_cipher_rank(cli_nid); - srv_rank = crypt_cipher_rank(kcfg->c.nid); + srv_rank = crypt_cipher_rank(scfg->c.nid); if (cli_rank > srv_rank) { - SET_KEX_CIPHER_NID(kcfg, cli_nid); - log_dbg_id(id, "Selected client cipher %s.", - kcfg->c.str); + SET_KEX_CIPHER_NID(scfg, cli_nid); + log_dbg_id(id, "Selected client cipher %s.", scfg->c.str); } else if (srv_rank > 0) { - log_dbg_id(id, "Selected server cipher %s.", - kcfg->c.str); + log_dbg_id(id, "Selected server cipher %s.", scfg->c.str); } else { log_err_id(id, "Encryption requested, no cipher."); return -ECRYPT; @@ -178,7 +184,7 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr, } cli_rank = crypt_kdf_rank(peer_hdr->kdf_nid); - srv_rank = crypt_kdf_rank(kcfg->k.nid); + srv_rank = crypt_kdf_rank(scfg->k.nid); /* Client-encap KEM bakes KDF into ciphertext; verify min. */ if (OAP_KEX_ROLE(peer_hdr) == KEM_MODE_CLIENT_ENCAP) { @@ -186,19 +192,19 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr, log_err_id(id, "Client KDF too weak."); return -ECRYPT; } - SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid); + SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid); } else if (cli_rank > srv_rank) { - SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid); + SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid); log_dbg_id(id, "Selected client KDF %s.", - md_nid_to_str(kcfg->k.nid)); + md_nid_to_str(scfg->k.nid)); } else if (srv_rank > 0) { log_dbg_id(id, "Selected server KDF %s.", - md_nid_to_str(kcfg->k.nid)); + md_nid_to_str(scfg->k.nid)); } - if (IS_KEX_ALGO_SET(kcfg)) + if (IS_KEX_ALGO_SET(scfg)) log_info_id(id, "Negotiated %s + %s.", - kcfg->x.str, kcfg->c.str); + scfg->x.str, scfg->c.str); else log_info_id(id, "No key exchange."); @@ -207,7 +213,7 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr, static int do_server_kem_decap(const struct name_info * info, const struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, struct crypt_sk * sk) { buffer_t ct; @@ -215,16 +221,21 @@ static int do_server_kem_decap(const struct name_info * info, int ret; uint8_t * id = peer_hdr->id.data; - ret = load_server_kem_keypair(info->name, - peer_hdr->kex_flags.fmt, - &server_pkp); - if (ret < 0) - return ret; - ct.data = peer_hdr->kex.data; ct.len = peer_hdr->kex.len; - ret = kex_kem_decap(server_pkp, ct, kcfg->k.nid, sk->key); + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&ct, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in CT."); + return -ECRYPT; + } + } + + ret = load_server_kem_keypair(info->name, scfg, &server_pkp); + if (ret < 0) + return ret; + + ret = kex_kem_decap(server_pkp, ct, scfg->k.nid, sk->key); crypt_free_key(server_pkp); @@ -239,7 +250,7 @@ static int do_server_kem_decap(const struct name_info * info, } static int do_server_kem_encap(const struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, buffer_t * kex, struct crypt_sk * sk) { @@ -250,12 +261,20 @@ static int do_server_kem_encap(const struct oap_hdr * peer_hdr, client_pk.data = peer_hdr->kex.data; client_pk.len = peer_hdr->kex.len; - if (IS_HYBRID_KEM(kcfg->x.str)) - ct_len = kex_kem_encap_raw(client_pk, kex->data, - kcfg->k.nid, sk->key); - else + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&client_pk, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in PK."); + return -ECRYPT; + } + + ct_len = kex_kem_encap_raw(scfg->x.str, client_pk, + kex->data + OAP_KEX_NIDSZ, + scfg->k.nid, sk->key); + ct_len = oap_kex_tag_nid(kex->data, scfg->x.nid, ct_len); + } else { ct_len = kex_kem_encap(client_pk, kex->data, - kcfg->k.nid, sk->key); + scfg->k.nid, sk->key); + } if (ct_len < 0) { log_err_id(id, "Failed to encapsulate KEM."); @@ -271,26 +290,26 @@ static int do_server_kem_encap(const struct oap_hdr * peer_hdr, static int do_server_kex_kem(const struct name_info * info, struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, buffer_t * kex, struct crypt_sk * sk) { int ret; - kcfg->x.mode = peer_hdr->kex_flags.role; + scfg->x.mode = peer_hdr->kex_flags.role; - if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) { - ret = do_server_kem_decap(info, peer_hdr, kcfg, sk); + if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) { + ret = do_server_kem_decap(info, peer_hdr, scfg, sk); kex->len = 0; } else { - ret = do_server_kem_encap(peer_hdr, kcfg, kex, sk); + ret = do_server_kem_encap(peer_hdr, scfg, kex, sk); } return ret; } static int do_server_kex_dhe(const struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, buffer_t * kex, struct crypt_sk * sk) { @@ -299,7 +318,7 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr, int ret; uint8_t * id = peer_hdr->id.data; - key_len = kex_pkp_create(kcfg, &epkp, kex->data); + key_len = kex_pkp_create(scfg, &epkp, kex->data); if (key_len < 0) { log_err_id(id, "Failed to generate key pair."); return -ECRYPT; @@ -307,9 +326,9 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr, kex->len = (size_t) key_len; - log_dbg_id(id, "Generated %s ephemeral keys.", kcfg->x.str); + log_dbg_id(id, "Generated %s ephemeral keys.", scfg->x.str); - ret = kex_dhe_derive(kcfg, epkp, peer_hdr->kex, sk->key); + ret = kex_dhe_derive(scfg, epkp, peer_hdr->kex, sk->key); if (ret < 0) { log_err_id(id, "Failed to derive secret."); kex_pkp_destroy(epkp); @@ -323,7 +342,7 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr, int do_server_kex(const struct name_info * info, struct oap_hdr * peer_hdr, - struct sec_config * kcfg, + struct sec_config * scfg, buffer_t * kex, struct crypt_sk * sk) { @@ -335,61 +354,76 @@ int do_server_kex(const struct name_info * info, /* No KEX data from client */ if (peer_hdr->kex.len == 0) { - if (IS_KEX_ALGO_SET(kcfg)) { + if (IS_KEX_ALGO_SET(scfg)) { log_warn_id(id, "KEX requested without info."); return -ECRYPT; } return 0; } - if (negotiate_cipher(peer_hdr, kcfg) < 0) + if (negotiate_cipher(peer_hdr, scfg) < 0) return -ECRYPT; /* Save server's configured KEX before overwriting */ - srv_kex_nid = kcfg->x.nid; + srv_kex_nid = scfg->x.nid; if (OAP_KEX_ROLE(peer_hdr) != KEM_MODE_CLIENT_ENCAP) { /* Server encapsulation or DHE: extract algo from DER PK */ if (get_algo_from_peer_key(peer_hdr, algo_buf) < 0) return -ECRYPT; - SET_KEX_ALGO(kcfg, algo_buf); + SET_KEX_ALGO(scfg, algo_buf); /* Reject if client KEX is weaker than server's */ - if (crypt_kex_rank(kcfg->x.nid) + if (crypt_kex_rank(scfg->x.nid) < crypt_kex_rank(srv_kex_nid)) { log_err_id(id, "Client KEX %s too weak.", - kcfg->x.str); + scfg->x.str); return -ECRYPT; } + } else if (!IS_KEX_ALGO_SET(scfg)) { + /* Client encap decaps with the configured static key */ + log_err_id(id, "Client encap without configured KEX."); + return -ECRYPT; } /* Dispatch based on algorithm type */ - if (IS_KEM_ALGORITHM(kcfg->x.str)) - return do_server_kex_kem(info, peer_hdr, kcfg, kex, sk); + if (IS_KEM_ALGORITHM(scfg->x.str)) + return do_server_kex_kem(info, peer_hdr, scfg, kex, sk); else - return do_server_kex_dhe(peer_hdr, kcfg, kex, sk); + return do_server_kex_dhe(peer_hdr, scfg, kex, sk); } int oap_srv_process(const struct name_info * info, buffer_t req_buf, buffer_t * rsp_buf, buffer_t * data, - struct crypt_sk * sk) + struct crypt_sk * sk, + bool rekey, + const buffer_t * cached_crt, + buffer_t * peer_crt) { - struct oap_hdr peer_hdr; - struct oap_hdr local_hdr; - struct sec_config kcfg; - uint8_t kex_buf[CRYPT_KEY_BUFSZ]; - uint8_t hash_buf[MAX_HASH_SIZE]; - buffer_t req_hash = BUF_INIT; - ssize_t hash_ret; - char cli_name[NAME_SIZE + 1]; - uint8_t * id; - void * pkp = NULL; - void * crt = NULL; - int req_md_nid; - int ret; + struct oap_hdr peer_hdr; + struct oap_hdr local_hdr; + struct sec_config scfg; + uint8_t kex_buf[OAP_KEX_NIDSZ + CRYPT_KEY_BUFSZ]; + uint8_t hash_buf[MAX_HASH_SIZE]; + uint8_t kc_buf[MAX_HASH_SIZE]; + uint8_t resp_hash_buf[MAX_HASH_SIZE]; + uint8_t hs_key[SYMMKEYSZ]; + const uint8_t * seal_key = NULL; + buffer_t req_hash = BUF_INIT; + buffer_t resp_hash = BUF_INIT; + buffer_t crt_der = BUF_INIT; + buffer_t rsp_tag = BUF_INIT; + ssize_t hash_ret; + char cli_name[NAME_SIZE + 1]; + uint8_t * id; + void * pkp = NULL; + void * crt = NULL; + int req_md_nid; + int enc_flags = 0; + int ret; assert(info != NULL); assert(rsp_buf != NULL); @@ -409,13 +443,22 @@ int oap_srv_process(const struct name_info * info, goto fail_cred; } - if (load_srv_kex_config(info, &kcfg) < 0) { - log_err("Failed to load KEX config for %s.", info->name); + /* Re-key omits the cert; the peer verifies against its cache. */ + if (rekey && crt != NULL) { + crypt_free_crt(crt); + crt = NULL; + } + + if (rekey) + enc_flags = OAP_ENC_REKEY; + + if (load_srv_sec_config(info, &scfg) < 0) { + log_err("Failed to load security config for %s.", info->name); goto fail_kex; } /* Decode incoming header (NID_undef = request, no hash) */ - if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef) < 0) { + if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef, rekey) < 0) { log_err("Failed to decode OAP header."); goto fail_auth; } @@ -436,15 +479,32 @@ int oap_srv_process(const struct name_info * info, oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef); - if (oap_auth_peer(cli_name, &local_hdr, &peer_hdr) < 0) { + if (oap_auth_peer(cli_name, &scfg, &local_hdr, &peer_hdr, + cached_crt) < 0) { log_err_id(id, "Failed to authenticate client."); goto fail_auth; } - if (do_server_kex(info, &peer_hdr, &kcfg, &local_hdr.kex, sk) < 0) + /* Surface the peer cert so the caller can cache it for re-key. */ + if (peer_crt != NULL && peer_hdr.crt.len > 0) { + peer_crt->data = malloc(peer_hdr.crt.len); + if (peer_crt->data == NULL) + goto fail_auth; + + memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len); + peer_crt->len = peer_hdr.crt.len; + } + + /* A re-keyed flow is encrypted; refuse a plaintext re-key. */ + if (rekey && peer_hdr.kex.len == 0) { + log_err_id(id, "Re-key request without KEX."); + goto fail_kex; + } + + if (do_server_kex(info, &peer_hdr, &scfg, &local_hdr.kex, sk) < 0) goto fail_kex; - sk->nid = kcfg.c.nid; + sk->nid = scfg.c.nid; /* Build response header with hash of client request */ local_hdr.nid = sk->nid; @@ -460,10 +520,58 @@ int oap_srv_process(const struct name_info * info, goto fail_auth; } req_hash.data = hash_buf; - req_hash.len = (size_t) hash_ret; + req_hash.len = (size_t) hash_ret; + + rsp_tag = req_hash; + + /* Bind the key to the transcript and confirm it to the client */ + if (sk->nid != NID_undef) { + if (crt != NULL && crypt_crt_der(crt, &crt_der) < 0) { + log_err_id(id, "Failed to serialize cert."); + goto fail_auth; + } + + resp_hash.data = resp_hash_buf; + + ret = oap_resp_hash(req_md_nid, local_hdr.kex, *data, + crt_der, &resp_hash); + + freebuf(crt_der); + + if (ret < 0) { + log_err_id(id, "Failed to hash response."); + goto fail_auth; + } + + /* Derive the identity-seal key before bind mutates sk->key */ + if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) { + log_err_id(id, "Failed to derive handshake key."); + goto fail_auth; + } + + seal_key = hs_key; + + if (oap_bind_session_key(sk, req_hash, resp_hash, + scfg.k.nid) < 0) { + log_err_id(id, "Failed to bind session key."); + goto fail_auth; + } - if (oap_hdr_encode(&local_hdr, pkp, crt, &kcfg, - req_hash, req_md_nid) < 0) { + if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf, + (size_t) hash_ret) < 0) { + log_err_id(id, "Failed to confirm session key."); + goto fail_auth; + } + + rsp_tag.data = kc_buf; + } + + ret = oap_hdr_encode(&local_hdr, pkp, crt, &scfg, + rsp_tag, req_md_nid, seal_key, enc_flags); + + crypt_secure_clear(hs_key, SYMMKEYSZ); + + if (ret < 0) { log_err_id(id, "Failed to create OAP response header."); goto fail_auth; } @@ -488,6 +596,7 @@ int oap_srv_process(const struct name_info * info, fail_data: oap_hdr_fini(&local_hdr); fail_auth: + crypt_secure_clear(hs_key, SYMMKEYSZ); crypt_free_crt(crt); crypt_free_key(pkp); fail_cred: diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c index 0a1af100..b65f3997 100644 --- a/src/irmd/oap/tests/common.c +++ b/src/irmd/oap/tests/common.c @@ -29,39 +29,47 @@ #include <string.h> #include <stdio.h> -int load_srv_kex_config(const struct name_info * info, +int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg) { (void) info; memset(cfg, 0, sizeof(*cfg)); + cfg->a.req = test_cfg.srv.req_auth; + + /* Digest is kept without kex, as in parse_sec_config */ + SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md); + if (test_cfg.srv.kex == NID_undef) return 0; SET_KEX_ALGO_NID(cfg, test_cfg.srv.kex); SET_KEX_CIPHER_NID(cfg, test_cfg.srv.cipher); SET_KEX_KDF_NID(cfg, test_cfg.srv.kdf); - SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md); SET_KEX_KEM_MODE(cfg, test_cfg.srv.kem_mode); return 0; } -int load_cli_kex_config(const struct name_info * info, +int load_cli_sec_config(const struct name_info * info, struct sec_config * cfg) { (void) info; memset(cfg, 0, sizeof(*cfg)); + cfg->a.req = test_cfg.cli.req_auth; + + /* Digest is kept without kex, as in parse_sec_config */ + SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md); + if (test_cfg.cli.kex == NID_undef) return 0; SET_KEX_ALGO_NID(cfg, test_cfg.cli.kex); SET_KEX_CIPHER_NID(cfg, test_cfg.cli.cipher); SET_KEX_KDF_NID(cfg, test_cfg.cli.kdf); - SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md); SET_KEX_KEM_MODE(cfg, test_cfg.cli.kem_mode); return 0; @@ -151,14 +159,16 @@ void oap_test_teardown(struct oap_test_ctx * ctx) if (ctx->cli.state != NULL) { res.key = ctx->cli.key; - oap_cli_complete(ctx->cli.state, &ctx->cli.info, dummy, - &ctx->data, &res); + oap_cli_complete(ctx->cli.state, dummy, + &ctx->data, &res, NULL, NULL); ctx->cli.state = NULL; } freebuf(ctx->data); freebuf(ctx->resp_hdr); freebuf(ctx->req_hdr); + freebuf(ctx->srv_crt); + freebuf(ctx->cli_crt); crypt_free_crt(ctx->im_ca); crypt_free_crt(ctx->root_ca); @@ -169,8 +179,8 @@ void oap_test_teardown(struct oap_test_ctx * ctx) int oap_cli_prepare_ctx(struct oap_test_ctx * ctx) { - return oap_cli_prepare(&ctx->cli.state, &ctx->cli.info, &ctx->req_hdr, - ctx->data); + return oap_cli_prepare(&ctx->cli.state, &ctx->cli.info, NULL, + &ctx->req_hdr, ctx->data, ctx->rekey); } int oap_srv_process_ctx(struct oap_test_ctx * ctx) @@ -179,7 +189,9 @@ int oap_srv_process_ctx(struct oap_test_ctx * ctx) int ret; ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr, - &ctx->resp_hdr, &ctx->data, &res); + &ctx->resp_hdr, &ctx->data, &res, ctx->rekey, + ctx->rekey ? &ctx->srv_crt : NULL, + ctx->rekey ? NULL : &ctx->srv_crt); if (ret == 0) ctx->srv.nid = res.nid; @@ -191,8 +203,9 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx) struct crypt_sk res = { .nid = NID_undef, .key = ctx->cli.key }; int ret; - ret = oap_cli_complete(ctx->cli.state, &ctx->cli.info, ctx->resp_hdr, - &ctx->data, &res); + ret = oap_cli_complete(ctx->cli.state, ctx->resp_hdr, &ctx->data, &res, + ctx->rekey ? &ctx->cli_crt : NULL, + ctx->rekey ? NULL : &ctx->cli_crt); ctx->cli.state = NULL; if (ret == 0) @@ -243,6 +256,249 @@ int roundtrip_auth_only(const char * root_ca, return TEST_RC_FAIL; } +static const char * rekey_mode(bool srv_auth, + bool cli_auth) +{ + if (srv_auth && cli_auth) + return "mutual"; + + if (srv_auth) + return "srv-only"; + + if (cli_auth) + return "cli-only"; + + return "none"; +} + +int roundtrip_rekey(const char * root_ca, + const char * im_ca_str, + bool srv_auth, + bool cli_auth) +{ + struct oap_test_ctx ctx; + uint8_t key0[SYMMKEYSZ]; + const char * mode = rekey_mode(srv_auth, cli_auth); + + TEST_START("(%s)", mode); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Initial client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Initial server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Initial client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Initial keys do not match.\n"); + goto fail_cleanup; + } + + /* The client caches the server cert only if the server authed. */ + if (srv_auth && ctx.cli_crt.len == 0) { + printf("Server cert was not cached for re-key.\n"); + goto fail_cleanup; + } + + /* The server caches the client cert only if the client authed. */ + if (cli_auth && ctx.srv_crt.len == 0) { + printf("Client cert was not cached by the server.\n"); + goto fail_cleanup; + } + + memcpy(key0, ctx.cli.key, SYMMKEYSZ); + + /* Re-key: cert dropped on the wire, verified against the cache. */ + freebuf(ctx.req_hdr); + freebuf(ctx.resp_hdr); + freebuf(ctx.data); + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Re-key client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Re-key server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Re-key client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Re-key keys do not match.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) { + printf("Re-key did not produce a fresh key.\n"); + goto fail_cleanup; + } + + if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) { + printf("Cipher not set after re-key.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", mode); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", mode); + return TEST_RC_FAIL; +} + +int roundtrip_rekey_badcache(const char * root_ca, + const char * im_ca_str, + bool cli_auth) +{ + struct oap_test_ctx ctx; + const char * mode = rekey_mode(true, cli_auth); + + TEST_START("(%s)", mode); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Initial client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Initial server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Initial client complete failed.\n"); + goto fail_cleanup; + } + + if (ctx.cli_crt.len == 0) { + printf("Server cert was not cached.\n"); + goto fail_cleanup; + } + + /* Corrupt the client's cached server cert: re-key must fail closed. */ + ctx.cli_crt.data[ctx.cli_crt.len / 2] ^= 0xFF; + + freebuf(ctx.req_hdr); + freebuf(ctx.resp_hdr); + freebuf(ctx.data); + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Re-key client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Re-key server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Re-key accepted a corrupted cached cert.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", mode); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", mode); + return TEST_RC_FAIL; +} + +int roundtrip_rekey_srv_badcache(const char * root_ca, + const char * im_ca_str, + bool srv_auth) +{ + struct oap_test_ctx ctx; + const char * mode = rekey_mode(srv_auth, true); + + TEST_START("(%s)", mode); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Initial client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Initial server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Initial client complete failed.\n"); + goto fail_cleanup; + } + + if (ctx.srv_crt.len == 0) { + printf("Client cert was not cached by the server.\n"); + goto fail_cleanup; + } + + /* Corrupt the server's cached client cert: re-key must fail closed. */ + ctx.srv_crt.data[ctx.srv_crt.len / 2] ^= 0xFF; + + freebuf(ctx.req_hdr); + freebuf(ctx.resp_hdr); + freebuf(ctx.data); + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Re-key client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted a corrupted cached client cert.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", mode); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", mode); + return TEST_RC_FAIL; +} + int roundtrip_kex_only(void) { struct name_info cli_info; @@ -270,15 +526,16 @@ int roundtrip_kex_only(void) goto fail; } - if (oap_cli_prepare(&cli_state, &cli_info, &req_hdr, - data) < 0) { + if (oap_cli_prepare(&cli_state, &cli_info, NULL, &req_hdr, + data, false) < 0) { printf("Client prepare failed.\n"); goto fail_cleanup; } res.key = srv_key; - if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res) < 0) { + if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res, + false, NULL, NULL) < 0) { printf("Server process failed.\n"); goto fail_cleanup; } @@ -287,7 +544,8 @@ int roundtrip_kex_only(void) res.key = cli_key; - if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res) < 0) { + if (oap_cli_complete(cli_state, resp_hdr, &data, &res, + NULL, NULL) < 0) { printf("Client complete failed.\n"); cli_state = NULL; goto fail_cleanup; @@ -316,7 +574,8 @@ int roundtrip_kex_only(void) fail_cleanup: if (cli_state != NULL) { res.key = cli_key; - oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res); + + oap_cli_complete(cli_state, resp_hdr, &data, &res, NULL, NULL); } freebuf(resp_hdr); freebuf(req_hdr); @@ -395,8 +654,8 @@ int corrupted_response(const char * root_ca, res.key = ctx.cli.key; - if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr, - &ctx.data, &res) == 0) { + if (oap_cli_complete(ctx.cli.state, ctx.resp_hdr, + &ctx.data, &res, NULL, NULL) == 0) { printf("Client should reject corrupted response.\n"); ctx.cli.state = NULL; goto fail_cleanup; diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h index d4b6733a..7aead07a 100644 --- a/src/irmd/oap/tests/common.h +++ b/src/irmd/oap/tests/common.h @@ -30,14 +30,18 @@ #include <stdbool.h> +#define AUTH true +#define NO_AUTH false + /* Per-side security configuration for tests */ struct test_sec_cfg { - int kex; /* KEX algorithm NID */ - int cipher; /* Cipher NID for encryption */ - int kdf; /* KDF NID for key derivation */ - int md; /* Digest NID for signatures */ - int kem_mode; /* KEM encapsulation mode (0 for ECDH) */ - bool auth; /* Use authentication (certificates) */ + int kex; /* KEX algorithm NID */ + int cipher; /* Cipher NID for encryption */ + int kdf; /* KDF NID for key derivation */ + int md; /* Digest NID for signatures */ + int kem_mode; /* KEM encapsulation mode (0 for ECDH) */ + bool auth; /* Use authentication (certificates) */ + bool req_auth; /* Require peer authentication */ }; /* Test configuration - set by each test before running roundtrip */ @@ -69,6 +73,11 @@ struct oap_test_ctx { buffer_t data; void * root_ca; void * im_ca; + + /* Re-key (tier iii): drop the cert, verify against the cache. */ + bool rekey; + buffer_t srv_crt; /* client cert cached by server */ + buffer_t cli_crt; /* server cert cached by client */ }; int oap_test_setup(struct oap_test_ctx * ctx, @@ -86,6 +95,19 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx); int roundtrip_auth_only(const char * root_ca, const char * im_ca_str); +int roundtrip_rekey(const char * root_ca, + const char * im_ca_str, + bool srv_auth, + bool cli_auth); + +int roundtrip_rekey_badcache(const char * root_ca, + const char * im_ca_str, + bool cli_auth); + +int roundtrip_rekey_srv_badcache(const char * root_ca, + const char * im_ca_str, + bool srv_auth); + int roundtrip_kex_only(void); int corrupted_request(const char * root_ca, diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c index a525d988..3e2bae56 100644 --- a/src/irmd/oap/tests/oap_test.c +++ b/src/irmd/oap/tests/oap_test.c @@ -42,18 +42,18 @@ #include <test/certs/ecdsa.h> #include "oap.h" +#include "oap/auth.h" #include "common.h" #include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> #include <string.h> #ifdef HAVE_OPENSSL #include <openssl/evp.h> #endif -#define AUTH true -#define NO_AUTH false - extern const uint16_t kex_supported_nids[]; extern const uint16_t md_supported_nids[]; @@ -81,9 +81,9 @@ int mock_load_credentials(void ** pkp, } /* Stub KEM functions - ECDSA tests don't use KEM */ -int load_server_kem_keypair(__attribute__((unused)) const char * name, - __attribute__((unused)) bool raw_fmt, - __attribute__((unused)) void ** pkp) +int load_server_kem_keypair(__attribute__((unused)) const char * name, + __attribute__((unused)) struct sec_config * cfg, + __attribute__((unused)) void ** pkp) { return -1; } @@ -114,6 +114,13 @@ static void test_default_cfg(void) test_cfg.cli.auth = NO_AUTH; } +/* Encrypted, unauthenticated on both sides. */ +static void test_enc_noauth_cfg(void) +{ + test_default_cfg(); + test_cfg.srv.auth = NO_AUTH; +} + static int test_oap_auth_init_fini(void) { TEST_START(); @@ -175,6 +182,7 @@ static int test_oap_roundtrip(int kex) oap_test_teardown(&ctx); TEST_SUCCESS("(%s)", kex_str); + return TEST_RC_SUCCESS; fail_cleanup: @@ -199,23 +207,141 @@ static int test_oap_roundtrip_auth_only(void) return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec); } -static int test_oap_roundtrip_kex_only(void) +static int test_oap_rekey(bool srv_auth, + bool cli_auth) { - memset(&test_cfg, 0, sizeof(test_cfg)); + test_default_cfg(); + test_cfg.srv.auth = srv_auth; + test_cfg.cli.auth = cli_auth; - /* Server: KEX only, no auth */ - test_cfg.srv.kex = NID_X25519; - test_cfg.srv.cipher = NID_aes_256_gcm; - test_cfg.srv.kdf = NID_sha256; - test_cfg.srv.md = NID_sha256; - test_cfg.srv.auth = NO_AUTH; + return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec, + srv_auth, cli_auth); +} - /* Client: KEX only, no auth */ - test_cfg.cli.kex = NID_X25519; - test_cfg.cli.cipher = NID_aes_256_gcm; - test_cfg.cli.kdf = NID_sha256; - test_cfg.cli.md = NID_sha256; - test_cfg.cli.auth = NO_AUTH; +static int test_oap_rekey_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey(AUTH, NO_AUTH); + ret |= test_oap_rekey(AUTH, AUTH); + ret |= test_oap_rekey(NO_AUTH, AUTH); + ret |= test_oap_rekey(NO_AUTH, NO_AUTH); + + return ret; +} + +static int test_oap_rekey_srv_badcache(bool srv_auth) +{ + test_default_cfg(); + test_cfg.srv.auth = srv_auth; + test_cfg.cli.auth = AUTH; + + return roundtrip_rekey_srv_badcache(root_ca_crt_ec, im_ca_crt_ec, + srv_auth); +} + +static int test_oap_rekey_srv_badcache_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey_srv_badcache(AUTH); + ret |= test_oap_rekey_srv_badcache(NO_AUTH); + + return ret; +} + +static int test_oap_rekey_badcache(bool cli_auth) +{ + test_default_cfg(); + test_cfg.cli.auth = cli_auth; + + return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec, + cli_auth); +} + +static int test_oap_rekey_badcache_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey_badcache(NO_AUTH); + ret |= test_oap_rekey_badcache(AUTH); + + return ret; +} + +/* Absent sec config (ENOENT) clears the KEX; a re-key must fail closed. */ +static int test_oap_cli_rejects_rekey_no_kex(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + test_enc_noauth_cfg(); + test_cfg.cli.kex = NID_undef; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) == 0) { + printf("Client prepared a re-key without KEX.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_srv_rejects_rekey_no_kex(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + test_enc_noauth_cfg(); + test_cfg.cli.kex = NID_undef; + test_cfg.srv.kex = NID_undef; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + /* First-contact plaintext request, replayed as a re-key. */ + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + ctx.rekey = true; + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted a re-key without KEX.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_kex_only(void) +{ + test_enc_noauth_cfg(); return roundtrip_kex_only(); } @@ -239,6 +365,7 @@ static int test_oap_piggyback_data(void) ctx.data.data = malloc(ctx.data.len); if (ctx.data.data == NULL) goto fail_cleanup; + memcpy(ctx.data.data, cli_data_str, ctx.data.len); if (oap_cli_prepare_ctx(&ctx) < 0) @@ -289,6 +416,7 @@ static int test_oap_piggyback_data(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -357,6 +485,7 @@ static int test_oap_inflated_length_field(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -401,6 +530,7 @@ static int test_oap_deflated_length_field(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -412,8 +542,13 @@ static int test_oap_deflated_length_field(void) /* Header field offsets for byte manipulation */ #define OAP_CIPHER_NID_OFFSET 24 +#define OAP_KDF_NID_OFFSET 26 +#define OAP_MD_NID_OFFSET 28 #define OAP_KEX_LEN_OFFSET 32 +/* A NID the crypto backend does not recognise; guarded by a test below. */ +#define UNSUPPORTED_NID 9999 + /* Server rejects request when cipher NID set but no KEX data provided */ static int test_oap_nid_without_kex(void) { @@ -421,20 +556,9 @@ static int test_oap_nid_without_kex(void) uint16_t cipher_nid; uint16_t zero = 0; - TEST_START(); + test_enc_noauth_cfg(); - /* Configure unsigned KEX-only mode */ - memset(&test_cfg, 0, sizeof(test_cfg)); - test_cfg.srv.kex = NID_X25519; - test_cfg.srv.cipher = NID_aes_256_gcm; - test_cfg.srv.kdf = NID_sha256; - test_cfg.srv.md = NID_sha256; - test_cfg.srv.auth = NO_AUTH; - test_cfg.cli.kex = NID_X25519; - test_cfg.cli.cipher = NID_aes_256_gcm; - test_cfg.cli.kdf = NID_sha256; - test_cfg.cli.md = NID_sha256; - test_cfg.cli.auth = NO_AUTH; + TEST_START(); if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) goto fail; @@ -459,6 +583,7 @@ static int test_oap_nid_without_kex(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -468,26 +593,44 @@ static int test_oap_nid_without_kex(void) return TEST_RC_FAIL; } -/* Server rejects OAP request with unsupported cipher NID */ -static int test_oap_unsupported_nid(void) +/* Guard: the tamper tests below rely on UNSUPPORTED_NID being invalid. */ +static int test_oap_unsupported_nid_undefined(void) +{ + TEST_START(); + + if (crypt_cipher_rank(UNSUPPORTED_NID) >= 0) { + printf("UNSUPPORTED_NID is a valid cipher NID.\n"); + goto fail; + } + + if (crypt_kdf_rank(UNSUPPORTED_NID) >= 0) { + printf("UNSUPPORTED_NID is a valid KDF NID.\n"); + goto fail; + } + + if (md_validate_nid(UNSUPPORTED_NID) >= 0) { + printf("UNSUPPORTED_NID is a valid digest NID.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Server rejects a request whose cipher/kdf/digest NID is unsupported */ +static int test_oap_unsupported_nid(size_t offset, + const char * label) { struct oap_test_ctx ctx; uint16_t bad_nid; - TEST_START(); + test_enc_noauth_cfg(); - /* Configure unsigned KEX-only mode */ - memset(&test_cfg, 0, sizeof(test_cfg)); - test_cfg.srv.kex = NID_X25519; - test_cfg.srv.cipher = NID_aes_256_gcm; - test_cfg.srv.kdf = NID_sha256; - test_cfg.srv.md = NID_sha256; - test_cfg.srv.auth = NO_AUTH; - test_cfg.cli.kex = NID_X25519; - test_cfg.cli.cipher = NID_aes_256_gcm; - test_cfg.cli.kdf = NID_sha256; - test_cfg.cli.md = NID_sha256; - test_cfg.cli.auth = NO_AUTH; + TEST_START("(%s)", label); if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) goto fail; @@ -497,19 +640,73 @@ static int test_oap_unsupported_nid(void) goto fail_cleanup; } - /* Tamper: set cipher_nid to unsupported value */ - bad_nid = hton16(9999); - memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &bad_nid, - sizeof(bad_nid)); + bad_nid = hton16(UNSUPPORTED_NID); + memcpy(ctx.req_hdr.data + offset, &bad_nid, sizeof(bad_nid)); if (oap_srv_process_ctx(&ctx) == 0) { - printf("Server should reject unsupported cipher NID.\n"); + printf("Server should reject unsupported %s NID.\n", label); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", label); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", label); + return TEST_RC_FAIL; +} + +static int test_oap_unsupported_nid_all(void) +{ + int ret = 0; + + ret |= test_oap_unsupported_nid(OAP_CIPHER_NID_OFFSET, "cipher"); + ret |= test_oap_unsupported_nid(OAP_KDF_NID_OFFSET, "kdf"); + ret |= test_oap_unsupported_nid(OAP_MD_NID_OFFSET, "digest"); + + return ret; +} + +/* Client rejects a response whose key-confirmation tag is tampered */ +static int test_oap_key_confirm_mismatch(void) +{ + struct oap_test_ctx ctx; + + /* Unauthenticated + encrypted: response unsigned, KC is the gate */ + test_enc_noauth_cfg(); + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + /* The key-confirm tag is the last field of an unsigned response */ + ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF; + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted a bad key-confirmation tag.\n"); goto fail_cleanup; } oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -610,6 +807,7 @@ static int test_oap_cipher_mismatch(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -656,6 +854,7 @@ static int test_oap_srv_enc_cli_none(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -725,6 +924,7 @@ static int test_oap_cli_enc_srv_none(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -734,27 +934,15 @@ static int test_oap_cli_enc_srv_none(void) return TEST_RC_FAIL; } -/* Client rejects server response with downgraded cipher */ +/* Unauthenticated server: client floor-rejects a downgraded cipher */ static int test_oap_cli_rejects_downgrade(void) { struct oap_test_ctx ctx; uint16_t weak; - TEST_START(); - - memset(&test_cfg, 0, sizeof(test_cfg)); + test_enc_noauth_cfg(); - test_cfg.srv.kex = NID_X25519; - test_cfg.srv.cipher = NID_aes_256_gcm; - test_cfg.srv.kdf = NID_sha256; - test_cfg.srv.md = NID_sha256; - test_cfg.srv.auth = AUTH; - - test_cfg.cli.kex = NID_X25519; - test_cfg.cli.cipher = NID_aes_256_gcm; - test_cfg.cli.kdf = NID_sha256; - test_cfg.cli.md = NID_sha256; - test_cfg.cli.auth = NO_AUTH; + TEST_START(); if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) goto fail; @@ -770,7 +958,7 @@ static int test_oap_cli_rejects_downgrade(void) } /* Tamper: replace cipher NID with weaker one */ - weak = hton16(NID_aes_128_ctr); + weak = hton16(NID_aes_128_gcm); memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET, &weak, sizeof(weak)); @@ -783,6 +971,60 @@ static int test_oap_cli_rejects_downgrade(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Suite binding: a cipher swapped to a higher rank clears the client floor + * check, but the bound key commits to the negotiated suite, so the swap must + * still fail key confirmation. + */ +static int test_oap_cli_rejects_suite_swap(void) +{ + struct oap_test_ctx ctx; + uint16_t swap; + + /* Both AES-128-GCM: a swap to AES-256 outranks the client floor */ + test_enc_noauth_cfg(); + test_cfg.srv.cipher = NID_aes_128_gcm; + test_cfg.cli.cipher = NID_aes_128_gcm; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + /* Swap the response cipher to a higher-ranked one */ + swap = hton16(NID_aes_256_gcm); + memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET, + &swap, sizeof(swap)); + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted a swapped cipher suite.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -832,6 +1074,7 @@ static int test_oap_srv_rejects_weak_kex(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -891,6 +1134,7 @@ static int test_oap_roundtrip_md(int md) oap_test_teardown(&ctx); TEST_SUCCESS("(%s)", md_str ? md_str : "default"); + return TEST_RC_SUCCESS; fail_cleanup: @@ -918,15 +1162,17 @@ static int test_oap_roundtrip_md_all(void) /* Timestamp is at offset 16 (after the 16-byte ID) */ #define OAP_TIMESTAMP_OFFSET 16 /* Test that packets with outdated timestamps are rejected */ -static int test_oap_outdated_packet(void) +/* Server rejects a request whose timestamp is outside the replay window */ +static int test_oap_ts_reject(int delta_sec, + const char * label) { struct oap_test_ctx ctx; - struct timespec old_ts; - uint64_t old_stamp; + struct timespec ts; + uint64_t stamp; test_default_cfg(); - TEST_START(); + TEST_START("(%s)", label); if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) goto fail; @@ -941,76 +1187,39 @@ static int test_oap_outdated_packet(void) goto fail_cleanup; } - /* Set timestamp to 30 seconds in the past (> 20s replay timer) */ - clock_gettime(CLOCK_REALTIME, &old_ts); - old_ts.tv_sec -= OAP_REPLAY_TIMER + 10; - old_stamp = hton64(TS_TO_UINT64(old_ts)); - memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &old_stamp, - sizeof(old_stamp)); + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += delta_sec; + stamp = hton64(TS_TO_UINT64(ts)); + memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &stamp, + sizeof(stamp)); if (oap_srv_process_ctx(&ctx) == 0) { - printf("Server should reject outdated packet.\n"); + printf("Server should reject %s packet.\n", label); goto fail_cleanup; } oap_test_teardown(&ctx); - TEST_SUCCESS(); + TEST_SUCCESS("(%s)", label); + return TEST_RC_SUCCESS; fail_cleanup: oap_test_teardown(&ctx); fail: - TEST_FAIL(); + TEST_FAIL("(%s)", label); return TEST_RC_FAIL; } -/* Test that packets from the future are rejected */ -static int test_oap_future_packet(void) +static int test_oap_ts_reject_all(void) { - struct oap_test_ctx ctx; - struct timespec future_ts; - uint64_t future_stamp; - - test_default_cfg(); - - TEST_START(); - - if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) - goto fail; - - if (oap_cli_prepare_ctx(&ctx) < 0) { - printf("Client prepare failed.\n"); - goto fail_cleanup; - } - - if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) { - printf("Request too short for test.\n"); - goto fail_cleanup; - } - - /* Set timestamp to 1 second in the future (> 100ms slack) */ - clock_gettime(CLOCK_REALTIME, &future_ts); - future_ts.tv_sec += 1; - future_stamp = hton64(TS_TO_UINT64(future_ts)); - memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &future_stamp, - sizeof(future_stamp)); - - if (oap_srv_process_ctx(&ctx) == 0) { - printf("Server should reject future packet.\n"); - goto fail_cleanup; - } - - oap_test_teardown(&ctx); + int ret = 0; - TEST_SUCCESS(); - return TEST_RC_SUCCESS; + /* Past the 20s replay window, and past the 100ms future slack. */ + ret |= test_oap_ts_reject(-(OAP_REPLAY_TIMER + 10), "outdated"); + ret |= test_oap_ts_reject(1, "future"); - fail_cleanup: - oap_test_teardown(&ctx); - fail: - TEST_FAIL(); - return TEST_RC_FAIL; + return ret; } /* Test that replayed packets (same ID + timestamp) are rejected */ @@ -1063,6 +1272,7 @@ static int test_oap_replay_packet(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -1072,6 +1282,164 @@ static int test_oap_replay_packet(void) return TEST_RC_FAIL; } +/* Encode a distinct OAP session ID from an index */ +static void make_id(uint8_t * id, + size_t idx) +{ + memset(id, 0, OAP_ID_SIZE); + memcpy(id, &idx, sizeof(idx)); +} + +/* + * Replay cache fails closed at capacity: a flood is rejected and no genuine + * entry is evicted (so it cannot be replayed). + */ +static int test_oap_replay_cap(void) +{ + struct oap_hdr h; + struct timespec now; + uint8_t id[OAP_ID_SIZE]; + uint64_t stamp; + size_t i; + + TEST_START(); + + if (oap_auth_init() < 0) { + printf("Failed to init OAP.\n"); + goto fail; + } + + clock_gettime(CLOCK_REALTIME, &now); + stamp = TS_TO_UINT64(now); + + memset(&h, 0, sizeof(h)); + h.id.data = id; + h.id.len = OAP_ID_SIZE; + h.timestamp = stamp; + + /* Fill one generation bucket to capacity with distinct IDs */ + for (i = 0; i < OAP_REPLAY_MAX; i++) { + make_id(id, i); + if (oap_check_hdr(&h) != 0) { + printf("Distinct header %zu rejected.\n", i); + goto fail_fini; + } + } + + /* One past capacity fails closed (rejected, not evict-oldest) */ + make_id(id, OAP_REPLAY_MAX); + if (oap_check_hdr(&h) != -EAUTH) { + printf("Header past capacity not fail-closed.\n"); + goto fail_fini; + } + + /* No genuine entry was evicted: the oldest still reads as a replay */ + make_id(id, 0); + if (oap_check_hdr(&h) != -EREPLAY) { + printf("Genuine entry evicted under flood.\n"); + goto fail_fini; + } + + oap_auth_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_fini: + oap_auth_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Distinct timestamp generations use separate buckets and are detected + * independently (covers the multi-generation / rotation path). + */ +static int test_oap_replay_generations(void) +{ + struct oap_hdr h; + struct timespec now; + struct timespec wait; + uint8_t id[OAP_ID_SIZE]; + uint64_t cur; + uint64_t gen_ns; + uint64_t off; + uint64_t wait_ns; + uint64_t stamp_a; + uint64_t stamp_b; + + TEST_START(); + + if (oap_auth_init() < 0) { + printf("Failed to init OAP.\n"); + goto fail; + } + + gen_ns = (uint64_t) OAP_REPLAY_TIMER * BILLION; + + /* Prev-gen stamp flakes on staleness near a generation top. */ + clock_gettime(CLOCK_REALTIME, &now); + cur = TS_TO_UINT64(now); + off = cur % gen_ns; + if (gen_ns - off < BILLION) { + wait_ns = gen_ns - off + MILLION; + wait.tv_sec = (time_t) (wait_ns / BILLION); + wait.tv_nsec = (long) (wait_ns % BILLION); + nanosleep(&wait, NULL); + clock_gettime(CLOCK_REALTIME, &now); + cur = TS_TO_UINT64(now); + } + + /* stamp_a in the current generation, stamp_b one generation older */ + stamp_a = cur; + stamp_b = (cur / gen_ns) * gen_ns - 1; + + memset(&h, 0, sizeof(h)); + h.id.data = id; + h.id.len = OAP_ID_SIZE; + make_id(id, 1); + + /* First sighting in each generation is accepted */ + h.timestamp = stamp_a; + if (oap_check_hdr(&h) != 0) { + printf("Gen-A header rejected.\n"); + goto fail_fini; + } + + h.timestamp = stamp_b; + if (oap_check_hdr(&h) != 0) { + printf("Gen-B header rejected.\n"); + goto fail_fini; + } + + /* Each generation independently detects its own replay */ + h.timestamp = stamp_a; + if (oap_check_hdr(&h) != -EREPLAY) { + printf("Gen-A replay not detected.\n"); + goto fail_fini; + } + + h.timestamp = stamp_b; + if (oap_check_hdr(&h) != -EREPLAY) { + printf("Gen-B replay not detected.\n"); + goto fail_fini; + } + + oap_auth_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_fini: + oap_auth_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + /* Server rejects client certificate when root CA is missing from store */ static int test_oap_missing_root_ca(void) { @@ -1126,6 +1494,7 @@ static int test_oap_missing_root_ca(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_teardown: @@ -1174,6 +1543,450 @@ static int test_oap_server_name_mismatch(void) oap_test_teardown(&ctx); TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Client requiring auth rejects a response without certificate */ +static int test_oap_cli_requires_srv_auth(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.srv.auth = NO_AUTH; + test_cfg.cli.req_auth = true; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client should reject unauthenticated server.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Server requiring auth rejects a request without certificate */ +static int test_oap_srv_requires_cli_auth(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.srv.req_auth = true; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject unauthenticated client.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Roundtrip succeeds when both sides require and provide auth */ +static int test_oap_mutual_req_auth(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.srv.req_auth = true; + test_cfg.cli.auth = AUTH; + test_cfg.cli.req_auth = true; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Client and server keys do not match!\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Client rejects a server signature with a different digest */ +static int test_oap_cli_rejects_md_mismatch(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.srv.md = NID_sha384; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client should reject digest mismatch.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Server rejects a client signature with a different digest */ +static int test_oap_srv_rejects_md_mismatch(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.cli.auth = AUTH; + test_cfg.cli.md = NID_sha384; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject digest mismatch.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Naive substring search over raw bytes (memmem is not portable here). */ +static bool buf_contains(const uint8_t * hay, + size_t hlen, + const uint8_t * needle, + size_t nlen) +{ + size_t i; + + if (nlen == 0 || nlen > hlen) + return false; + + for (i = 0; i + nlen <= hlen; i++) { + if (memcmp(hay + i, needle, nlen) == 0) + return true; + } + + return false; +} + +/* The server certificate must not appear in cleartext on the wire */ +static int test_oap_server_cert_hidden(void) +{ + struct oap_test_ctx ctx; + void * crt = NULL; + buffer_t der = BUF_INIT; + + test_default_cfg(); + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) { + printf("Failed to load server crt.\n"); + goto fail_cleanup; + } + + if (crypt_crt_der(crt, &der) < 0) { + printf("Failed to DER-encode server crt.\n"); + goto fail_crt; + } + + if (der.len == 0 || der.len > ctx.resp_hdr.len) { + printf("Unexpected cert/response sizes.\n"); + goto fail_der; + } + + if (buf_contains(ctx.resp_hdr.data, ctx.resp_hdr.len, + der.data, der.len)) { + printf("Server certificate found in cleartext.\n"); + goto fail_der; + } + + /* The handshake must still complete and agree on a key */ + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_der; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Client and server keys do not match!\n"); + goto fail_der; + } + + freebuf(der); + crypt_free_crt(crt); + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_der: + freebuf(der); + fail_crt: + crypt_free_crt(crt); + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Tampering the sealed identity block fails the handshake */ +static int test_oap_sealed_tamper(void) +{ + struct oap_test_ctx ctx; + size_t pos; + + test_default_cfg(); + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (ctx.resp_hdr.len < 64) { + printf("Response too short for test.\n"); + goto fail_cleanup; + } + + /* Flip a byte inside the sealed ciphertext, before the AEAD tag */ + pos = ctx.resp_hdr.len - 32; + ctx.resp_hdr.data[pos] ^= 0xFF; + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted a tampered identity block.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Cleartext md-only: rsp_tag echoes H(request) and is the sole gate */ +static int test_oap_cleartext_echo_tamper(void) +{ + struct oap_test_ctx ctx; + + memset(&test_cfg, 0, sizeof(test_cfg)); + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = NO_AUTH; + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + /* rsp_tag is the trailing field of an unsealed, unsigned response */ + ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF; + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted a tampered request echo.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Client rejects a response whose session ID does not match the request */ +static int test_oap_response_id_tamper(void) +{ + struct oap_test_ctx ctx; + + /* Cleartext md-only: no seal, so the ID check itself must reject. */ + memset(&test_cfg, 0, sizeof(test_cfg)); + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = NO_AUTH; + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + /* The session ID is the first field of the fixed header. */ + ctx.resp_hdr.data[0] ^= 0xFF; + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted a mismatched response ID.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_cleanup: @@ -1192,59 +2005,101 @@ int oap_test(int argc, (void) argv; ret |= test_oap_auth_init_fini(); + ret |= test_oap_replay_cap(); + ret |= test_oap_replay_generations(); #ifdef HAVE_OPENSSL ret |= test_oap_roundtrip_auth_only(); ret |= test_oap_roundtrip_kex_only(); ret |= test_oap_piggyback_data(); + ret |= test_oap_rekey_all(); + ret |= test_oap_rekey_badcache_all(); + ret |= test_oap_rekey_srv_badcache_all(); + ret |= test_oap_cli_rejects_rekey_no_kex(); + ret |= test_oap_srv_rejects_rekey_no_kex(); ret |= test_oap_roundtrip_all(); ret |= test_oap_roundtrip_md_all(); ret |= test_oap_corrupted_request(); ret |= test_oap_corrupted_response(); + ret |= test_oap_key_confirm_mismatch(); ret |= test_oap_truncated_request(); ret |= test_oap_inflated_length_field(); ret |= test_oap_deflated_length_field(); ret |= test_oap_nid_without_kex(); - ret |= test_oap_unsupported_nid(); + ret |= test_oap_unsupported_nid_undefined(); + ret |= test_oap_unsupported_nid_all(); ret |= test_oap_cipher_mismatch(); ret |= test_oap_srv_enc_cli_none(); ret |= test_oap_cli_enc_srv_none(); ret |= test_oap_cli_rejects_downgrade(); + ret |= test_oap_cli_rejects_suite_swap(); ret |= test_oap_srv_rejects_weak_kex(); - ret |= test_oap_outdated_packet(); - ret |= test_oap_future_packet(); + ret |= test_oap_ts_reject_all(); ret |= test_oap_replay_packet(); ret |= test_oap_missing_root_ca(); ret |= test_oap_server_name_mismatch(); + + ret |= test_oap_cli_requires_srv_auth(); + ret |= test_oap_srv_requires_cli_auth(); + ret |= test_oap_mutual_req_auth(); + + + ret |= test_oap_cli_rejects_md_mismatch(); + ret |= test_oap_srv_rejects_md_mismatch(); + + ret |= test_oap_server_cert_hidden(); + ret |= test_oap_sealed_tamper(); + ret |= test_oap_cleartext_echo_tamper(); + ret |= test_oap_response_id_tamper(); #else (void) test_oap_roundtrip_auth_only; (void) test_oap_roundtrip_kex_only; (void) test_oap_piggyback_data; + (void) test_oap_rekey_all; + (void) test_oap_rekey_badcache_all; + (void) test_oap_rekey_srv_badcache_all; + (void) test_oap_cli_rejects_rekey_no_kex; + (void) test_oap_srv_rejects_rekey_no_kex; (void) test_oap_roundtrip; (void) test_oap_roundtrip_all; (void) test_oap_roundtrip_md; (void) test_oap_roundtrip_md_all; (void) test_oap_corrupted_request; (void) test_oap_corrupted_response; + (void) test_oap_key_confirm_mismatch; (void) test_oap_truncated_request; (void) test_oap_inflated_length_field; (void) test_oap_deflated_length_field; (void) test_oap_nid_without_kex; (void) test_oap_unsupported_nid; + (void) test_oap_unsupported_nid_undefined; + (void) test_oap_unsupported_nid_all; (void) test_oap_cipher_mismatch; (void) test_oap_srv_enc_cli_none; (void) test_oap_cli_enc_srv_none; (void) test_oap_cli_rejects_downgrade; + (void) test_oap_cli_rejects_suite_swap; (void) test_oap_srv_rejects_weak_kex; - (void) test_oap_outdated_packet; - (void) test_oap_future_packet; + (void) test_oap_ts_reject_all; (void) test_oap_replay_packet; + (void) test_oap_replay_generations; (void) test_oap_missing_root_ca; (void) test_oap_server_name_mismatch; + (void) test_oap_cli_requires_srv_auth; + (void) test_oap_srv_requires_cli_auth; + (void) test_oap_mutual_req_auth; + (void) test_oap_cli_rejects_md_mismatch; + (void) test_oap_srv_rejects_md_mismatch; + (void) test_oap_server_cert_hidden; + (void) test_oap_sealed_tamper; + (void) test_oap_cleartext_echo_tamper; + (void) test_oap_response_id_tamper; + (void) test_oap_rekey; + (void) test_oap_rekey_badcache; ret = TEST_RC_SKIP; #endif diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c index 81b307ab..b9132b13 100644 --- a/src/irmd/oap/tests/oap_test_ml_dsa.c +++ b/src/irmd/oap/tests/oap_test_ml_dsa.c @@ -29,6 +29,7 @@ #include "config.h" #include <ouroboros/crypt.h> +#include <ouroboros/endian.h> #include <ouroboros/flow.h> #include <ouroboros/name.h> #include <ouroboros/random.h> @@ -36,6 +37,7 @@ #include <test/certs/ml_dsa.h> +#include "oap/hdr.h" #include "oap.h" #include "common.h" @@ -46,10 +48,12 @@ #include <openssl/evp.h> #endif -#define CLI_AUTH 1 -#define NO_CLI_AUTH 0 -#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP -#define SRV_ENCAP KEM_MODE_SERVER_ENCAP +#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP +#define SRV_ENCAP KEM_MODE_SERVER_ENCAP + +/* Wire constants for inspecting the encoded kex_len field. */ +#define OAP_KEX_LEN_OFFSET 32 +#define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 1 = client encaps */ extern const uint16_t kex_supported_nids[]; extern const uint16_t md_supported_nids[]; @@ -94,16 +98,16 @@ int mock_load_credentials(void ** pkp, return 0; } -int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp) +int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp) { #ifdef HAVE_OPENSSL struct sec_config local_cfg; ssize_t pk_len; (void) name; - (void) raw_fmt; + (void) cfg; /* * Uses reference counting. The caller will call @@ -147,7 +151,7 @@ int load_server_kem_keypair(const char * name, #else (void) name; - (void) raw_fmt; + (void) cfg; (void) pkp; return -1; #endif @@ -179,6 +183,7 @@ int load_server_kem_pk(const char * name, pk->data = malloc(test_kem_pk_len); if (pk->data == NULL) return -1; + memcpy(pk->data, test_kem_pk, test_kem_pk_len); pk->len = test_kem_pk_len; @@ -237,10 +242,89 @@ static int test_oap_roundtrip_auth_only(void) return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); } +/* Digest pin does not apply to PQC: the digest is intrinsic */ +static int test_oap_cli_md_pin_exempts_pqc(void) +{ + test_cfg_init(NID_undef, NID_undef, NID_undef, 0, NO_AUTH); + test_cfg.cli.md = NID_sha256; + + return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_srv_md_pin_exempts_pqc(void) +{ + test_cfg_init(NID_undef, NID_undef, NID_undef, 0, AUTH); + test_cfg.srv.md = NID_sha256; + + return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_rekey(bool srv_auth, + bool cli_auth) +{ + test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256, + 0, cli_auth); + test_cfg.srv.auth = srv_auth; + + return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml, + srv_auth, cli_auth); +} + +static int test_oap_rekey_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey(AUTH, NO_AUTH); + ret |= test_oap_rekey(AUTH, AUTH); + ret |= test_oap_rekey(NO_AUTH, AUTH); + ret |= test_oap_rekey(NO_AUTH, NO_AUTH); + + return ret; +} + +static int test_oap_rekey_badcache(bool cli_auth) +{ + test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256, + 0, cli_auth); + + return roundtrip_rekey_badcache(root_ca_crt_ml, im_ca_crt_ml, + cli_auth); +} + +static int test_oap_rekey_badcache_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey_badcache(NO_AUTH); + ret |= test_oap_rekey_badcache(AUTH); + + return ret; +} + +static int test_oap_rekey_srv_badcache(bool srv_auth) +{ + test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256, + 0, AUTH); + test_cfg.srv.auth = srv_auth; + + return roundtrip_rekey_srv_badcache(root_ca_crt_ml, im_ca_crt_ml, + srv_auth); +} + +static int test_oap_rekey_srv_badcache_all(void) +{ + int ret = 0; + + ret |= test_oap_rekey_srv_badcache(AUTH); + ret |= test_oap_rekey_srv_badcache(NO_AUTH); + + return ret; +} + static int test_oap_corrupted_request(void) { test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), - SRV_ENCAP, CLI_AUTH); + SRV_ENCAP, AUTH); return corrupted_request(root_ca_crt_ml, im_ca_crt_ml); } @@ -248,7 +332,7 @@ static int test_oap_corrupted_request(void) static int test_oap_corrupted_response(void) { test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), - SRV_ENCAP, NO_CLI_AUTH); + SRV_ENCAP, NO_AUTH); return corrupted_response(root_ca_crt_ml, im_ca_crt_ml); } @@ -256,7 +340,7 @@ static int test_oap_corrupted_response(void) static int test_oap_truncated_request(void) { test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), - SRV_ENCAP, NO_CLI_AUTH); + SRV_ENCAP, NO_AUTH); return truncated_request(root_ca_crt_ml, im_ca_crt_ml); } @@ -269,7 +353,7 @@ static int test_oap_roundtrip_kem(int kex, const char * mode_str = kem_mode == CLI_ENCAP ? "cli" : "srv"; test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(), - kem_mode, NO_CLI_AUTH); + kem_mode, NO_AUTH); TEST_START("(%s, %s encaps)", kex_str, mode_str); @@ -332,6 +416,231 @@ static int test_oap_roundtrip_kem_all(void) return ret; } +/* Re-key over a KEM KEX: forced ephemeral server-encap + cert-drop cache. */ +static int test_oap_rekey_kem(int kex, + int kem_mode) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(kex); + const char * mode_str = "srv"; + uint8_t key0[SYMMKEYSZ]; + + if (kem_mode == CLI_ENCAP) + mode_str = "cli"; + + test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(), + kem_mode, NO_AUTH); + + TEST_START("(%s, %s encaps)", kex_str, mode_str); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Initial client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Initial server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Initial client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Initial keys do not match.\n"); + goto fail_cleanup; + } + + if (ctx.cli_crt.len == 0) { + printf("Server cert was not cached for re-key.\n"); + goto fail_cleanup; + } + + memcpy(key0, ctx.cli.key, SYMMKEYSZ); + + freebuf(ctx.req_hdr); + freebuf(ctx.resp_hdr); + freebuf(ctx.data); + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Re-key client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Re-key server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Re-key client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Re-key keys do not match.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) { + printf("Re-key did not produce a fresh key.\n"); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS("(%s, %s encaps)", kex_str, mode_str); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL("(%s, %s encaps)", kex_str, mode_str); + return TEST_RC_FAIL; +} + +static int test_oap_rekey_kem_all(void) +{ + int ret = 0; + int i; + + for (i = 0; kex_supported_nids[i] != NID_undef; i++) { + const char * algo = kex_nid_to_str(kex_supported_nids[i]); + + if (!IS_KEM_ALGORITHM(algo)) + continue; + + ret |= test_oap_rekey_kem(kex_supported_nids[i], SRV_ENCAP); + ret |= test_oap_rekey_kem(kex_supported_nids[i], CLI_ENCAP); + } + + return ret; +} + +/* + * Client-encap bakes the KDF into the ciphertext, so the server cannot + * upgrade it: a client KDF weaker than the server floor must be rejected. + */ +static int test_oap_kem_kdf_floor(int kex) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(kex); + + test_cfg_init(kex, NID_aes_256_gcm, NID_sha256, + CLI_ENCAP, NO_AUTH); + test_cfg.srv.kdf = NID_sha512; + test_cfg.cli.kdf = NID_sha256; + + TEST_START("(%s)", kex_str); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted a client KDF below its floor.\n"); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS("(%s)", kex_str); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL("(%s)", kex_str); + return TEST_RC_FAIL; +} + +/* + * A client-encap flow re-keys to ephemeral server-encap, so it keeps + * forward secrecy. The re-key request must advertise server-encap + * (kex_len Role bit clear) rather than re-using client encapsulation. + */ +static int test_oap_rekey_kem_forcing(int kex) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(kex); + uint16_t kex_len; + + test_cfg_init(kex, NID_aes_256_gcm, NID_sha256, + CLI_ENCAP, NO_AUTH); + + TEST_START("(%s)", kex_str); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Initial client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Initial server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Initial client complete failed.\n"); + goto fail_cleanup; + } + + freebuf(ctx.req_hdr); + freebuf(ctx.resp_hdr); + freebuf(ctx.data); + + ctx.rekey = true; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Re-key client prepare failed.\n"); + goto fail_cleanup; + } + + memcpy(&kex_len, ctx.req_hdr.data + OAP_KEX_LEN_OFFSET, + sizeof(kex_len)); + kex_len = ntoh16(kex_len); + + if (kex_len & OAP_KEX_ROLE_BIT) { + printf("Re-key did not force server-encap KEX.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Re-key server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Re-key client complete failed.\n"); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS("(%s)", kex_str); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL("(%s)", kex_str); + return TEST_RC_FAIL; +} + static int test_oap_kem_srv_uncfg(int kex) { struct oap_test_ctx ctx; @@ -412,6 +721,89 @@ static int test_oap_kem_srv_uncfg_all(void) return ret; } +/* Server must reject a raw kex payload tagged with a bad NID */ +static int test_oap_kem_bad_nid_tag(uint16_t bad_nid) +{ + struct oap_test_ctx ctx; + + test_cfg_init(NID_X25519MLKEM768, NID_aes_256_gcm, + get_random_kdf(), SRV_ENCAP, NO_AUTH); + + TEST_START("(%u)", bad_nid); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* NO_AUTH request: raw kex starts after the fixed header */ + oap_kex_set_nid(ctx.req_hdr.data + OAP_HDR_MIN_SIZE, bad_nid); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted bad NID tag %u.\n", bad_nid); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS("(%u)", bad_nid); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL("(%u)", bad_nid); + return TEST_RC_FAIL; +} + +/* Client encap must be rejected when the server has no KEX config */ +static int test_oap_kem_cli_encap_srv_uncfg(void) +{ + struct oap_test_ctx ctx; + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: auth only, no KEX configured */ + + test_cfg.srv.auth = true; + + /* Client: requests KEM with client-side encapsulation */ + test_cfg.cli.kex = NID_MLKEM768; + test_cfg.cli.cipher = NID_aes_256_gcm; + test_cfg.cli.kdf = get_random_kdf(); + test_cfg.cli.kem_mode = CLI_ENCAP; + test_cfg.cli.auth = false; + + TEST_START(); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted client encap without KEX.\n"); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int oap_test_ml_dsa(int argc, char **argv) { @@ -420,22 +812,48 @@ int oap_test_ml_dsa(int argc, (void) argc; (void) argv; -#ifdef HAVE_OPENSSL_ML_KEM +#ifdef HAVE_ML ret |= test_oap_roundtrip_auth_only(); + ret |= test_oap_cli_md_pin_exempts_pqc(); + ret |= test_oap_srv_md_pin_exempts_pqc(); ret |= test_oap_roundtrip_kem_all(); ret |= test_oap_kem_srv_uncfg_all(); + ret |= test_oap_kem_cli_encap_srv_uncfg(); + ret |= test_oap_kem_bad_nid_tag(NID_X25519); /* known, not hybrid */ + ret |= test_oap_kem_bad_nid_tag(0xFFFF); /* unknown */ ret |= test_oap_corrupted_request(); ret |= test_oap_corrupted_response(); ret |= test_oap_truncated_request(); + + ret |= test_oap_rekey_all(); + ret |= test_oap_rekey_badcache_all(); + ret |= test_oap_rekey_srv_badcache_all(); + ret |= test_oap_rekey_kem_all(); + ret |= test_oap_kem_kdf_floor(NID_MLKEM768); + ret |= test_oap_rekey_kem_forcing(NID_MLKEM768); #else (void) test_oap_roundtrip_auth_only; + (void) test_oap_cli_md_pin_exempts_pqc; + (void) test_oap_srv_md_pin_exempts_pqc; + (void) test_oap_rekey; + (void) test_oap_rekey_all; + (void) test_oap_rekey_badcache; + (void) test_oap_rekey_badcache_all; + (void) test_oap_rekey_srv_badcache; + (void) test_oap_rekey_srv_badcache_all; (void) test_oap_roundtrip_kem; (void) test_oap_roundtrip_kem_all; + (void) test_oap_rekey_kem; + (void) test_oap_rekey_kem_all; + (void) test_oap_kem_kdf_floor; + (void) test_oap_rekey_kem_forcing; (void) test_oap_kem_srv_uncfg; (void) test_oap_kem_srv_uncfg_all; + (void) test_oap_kem_cli_encap_srv_uncfg; + (void) test_oap_kem_bad_nid_tag; (void) test_oap_corrupted_request; (void) test_oap_corrupted_response; (void) test_oap_truncated_request; diff --git a/src/irmd/reg/flow.c b/src/irmd/reg/flow.c index 5c709dea..61e2a14b 100644 --- a/src/irmd/reg/flow.c +++ b/src/irmd/reg/flow.c @@ -24,6 +24,7 @@ #define OUROBOROS_PREFIX "reg/flow" +#include <ouroboros/crypt.h> #include <ouroboros/logs.h> #include "flow.h" @@ -32,6 +33,7 @@ #include <errno.h> #include <stdbool.h> #include <stdlib.h> +#include <string.h> struct reg_flow * reg_flow_create(const struct flow_info * info) { @@ -68,10 +70,12 @@ static void destroy_rbuffs(struct reg_flow * flow) { if (flow->n_rb != NULL) ssm_rbuff_destroy(flow->n_rb); + flow->n_rb = NULL; if (flow->n_1_rb != NULL) ssm_rbuff_destroy(flow->n_1_rb); + flow->n_1_rb = NULL; } @@ -79,6 +83,11 @@ void reg_flow_destroy(struct reg_flow * flow) { assert(flow != NULL); + if (flow->rk.pending_seed != NULL) + crypt_secure_free(flow->rk.pending_seed, SYMMKEYSZ); + + freebuf(flow->rk.peer_crt); + switch(flow->info.state) { case FLOW_ACCEPT_PENDING: clrbuf(flow->req_data); @@ -117,6 +126,9 @@ static int create_rbuffs(struct reg_flow * flow, assert(flow->n_1_rb == NULL); flow->info.n_1_pid = info->n_1_pid; + if (flow->poa) + return 0; + flow->n_1_rb = ssm_rbuff_create(info->n_1_pid, info->id); if (flow->n_1_rb == NULL) goto fail_n_1_rb; @@ -160,8 +172,9 @@ int reg_flow_update(struct reg_flow * flow, assert(flow->info.n_pid != 0); assert(info->mpl != 0); - flow->info.mpl = info->mpl; - flow->info.mtu = info->mtu; + flow->info.mpl = info->mpl; + flow->info.mtu = info->mtu; + flow->info.max_rtt = info->max_rtt; if (flow->info.state == FLOW_ALLOC_PENDING) break; diff --git a/src/irmd/reg/flow.h b/src/irmd/reg/flow.h index 9a4046d3..c7021a0f 100644 --- a/src/irmd/reg/flow.h +++ b/src/irmd/reg/flow.h @@ -48,6 +48,24 @@ struct reg_flow { char name[NAME_SIZE + 1]; bool direct; + bool poa; /* transport is a point of attachment */ + void * oap_ctx; /* key exchange, prepare -> complete */ + + /* Tier-2 re-key state (encrypted flows only) */ + struct { + bool encrypted; /* flow carries a cipher */ + uint8_t epoch; /* last epoch installed by app */ + bool initiator; /* OAP initiator (role 0) */ + bool in_flight; /* a re-key is in progress */ + bool req_queued; /* a peer REQ is in the inbox */ + bool resp_queued; /* a peer RESP is in the inbox */ + uint8_t * pending_seed; /* secure heap; NULL until set */ + uint8_t pending_epoch; + bool pending_initiator; /* pending seed: oap_cli side */ + bool has_pending; /* new seed awaits app pull */ + uint8_t pulled; /* direct: per-app pull mask */ + buffer_t peer_crt; /* peer cert DER, cached at HS */ + } rk; struct ssm_rbuff * n_rb; struct ssm_rbuff * n_1_rb; diff --git a/src/irmd/reg/name.c b/src/irmd/reg/name.c index 61a328ec..08426033 100644 --- a/src/irmd/reg/name.c +++ b/src/irmd/reg/name.c @@ -1,4 +1,3 @@ - /* * Ouroboros - Copyright (C) 2016 - 2026 * @@ -157,6 +156,8 @@ static struct prog_entry * __reg_name_get_prog(const struct reg_name * name, llist_for_each(p, &name->progs) { struct prog_entry * entry; entry = list_entry(p, struct prog_entry, next); + assert(entry->exec != NULL); + assert(entry->exec[0] != NULL); if (strcmp(entry->exec[0], prog) == 0) return entry; } diff --git a/src/irmd/reg/proc.c b/src/irmd/reg/proc.c index 8a7e24c9..94ac6b8f 100644 --- a/src/irmd/reg/proc.c +++ b/src/irmd/reg/proc.c @@ -119,6 +119,7 @@ static struct name_entry * __reg_proc_get_name(const struct reg_proc * proc, llist_for_each(p, &proc->names) { struct name_entry * entry; entry = list_entry(p, struct name_entry, next); + assert(entry->name != NULL); if (strcmp(entry->name, name) == 0) return entry; } @@ -140,7 +141,7 @@ int reg_proc_add_name(struct reg_proc * proc, } entry->name = strdup(name); - if (entry == NULL) { + if (entry->name == NULL) { log_err("Failed to strdup name."); goto fail_name; } diff --git a/src/irmd/reg/prog.c b/src/irmd/reg/prog.c index 2d7f9f8d..1e977c89 100644 --- a/src/irmd/reg/prog.c +++ b/src/irmd/reg/prog.c @@ -107,6 +107,7 @@ static struct name_entry * __reg_prog_get_name(const struct reg_prog * prog, llist_for_each(p, &prog->names) { struct name_entry * entry; entry = list_entry(p, struct name_entry, next); + assert(entry->name != NULL); if (strcmp(entry->name, name) == 0) return entry; } @@ -128,7 +129,7 @@ int reg_prog_add_name(struct reg_prog * prog, } entry->name = strdup(name); - if (entry == NULL) { + if (entry->name == NULL) { log_err("Failed to strdup name."); goto fail_name; } diff --git a/src/irmd/reg/reg.c b/src/irmd/reg/reg.c index 365064e5..e19566d4 100644 --- a/src/irmd/reg/reg.c +++ b/src/irmd/reg/reg.c @@ -25,6 +25,7 @@ The IPC Resource Manager - Registry #define OUROBOROS_PREFIX "reg" #include <ouroboros/bitmap.h> +#include <ouroboros/crypt.h> #include <ouroboros/errno.h> #include <ouroboros/list.h> #include <ouroboros/logs.h> @@ -871,6 +872,7 @@ int reg_list_ipcps(ipcp_list_msg_t *** ipcps) fail: while (i-- > 0) ipcp_list_msg__free_unpacked((*ipcps)[i], NULL); + free(*ipcps); fail_malloc: pthread_mutex_unlock(®.mtx); @@ -1032,6 +1034,20 @@ int reg_get_name_for_flow_id(char * buf, return f == NULL ? -ENOENT : 0; } +void reg_set_name_for_flow_id(const char * name, + int flow_id) +{ + struct reg_flow * f; + + pthread_mutex_lock(®.mtx); + + f = __reg_get_flow(flow_id); + if (f != NULL) + strcpy(f->name, name); + + pthread_mutex_unlock(®.mtx); +} + int reg_list_names(name_info_msg_t *** names) { struct list_head * p; @@ -1076,6 +1092,7 @@ int reg_list_names(name_info_msg_t *** names) fail: while (i-- > 0) name_info_msg__free_unpacked((*names)[i], NULL); + free(*names); fail_malloc: pthread_mutex_unlock(®.mtx); @@ -2102,6 +2119,582 @@ bool reg_flow_is_direct(int flow_id) return ret; } +bool reg_flow_is_poa(int flow_id) +{ + struct reg_flow * flow; + bool ret; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + + ret = flow != NULL && flow->poa; + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +int reg_flow_set_poa(int flow_id) +{ + struct reg_flow * flow; + int ret = -1; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + flow->poa = true; + ret = 0; + } + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +int reg_flow_set_oap_ctx(int flow_id, + void * ctx) +{ + struct reg_flow * flow; + int ret = -1; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + flow->oap_ctx = ctx; + ret = 0; + } + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +void * reg_flow_take_oap_ctx(int flow_id) +{ + struct reg_flow * flow; + void * ctx = NULL; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + ctx = flow->oap_ctx; + flow->oap_ctx = NULL; + } + + pthread_mutex_unlock(®.mtx); + + return ctx; +} + +void reg_flow_set_rekey(int flow_id, + bool initiator, + buffer_t peer_crt) +{ + struct reg_flow * flow; + uint8_t * crt = NULL; + + /* Copy the cert outside the lock; publish it with rk.encrypted. */ + if (peer_crt.len > 0) { + crt = malloc(peer_crt.len); + if (crt != NULL) + memcpy(crt, peer_crt.data, peer_crt.len); + else + log_warn("Failed to cache peer cert for re-key."); + } + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + flow->rk.encrypted = true; + flow->rk.initiator = initiator; + flow->rk.epoch = 0; + if (crt != NULL) { + freebuf(flow->rk.peer_crt); + flow->rk.peer_crt.data = crt; + flow->rk.peer_crt.len = peer_crt.len; + crt = NULL; + } + } + + pthread_mutex_unlock(®.mtx); + + free(crt); +} + +int reg_flow_get_peer_crt(int flow_id, + buffer_t * crt) +{ + struct reg_flow * flow; + int ret = -ENOENT; + + assert(crt != NULL); + + clrbuf(*crt); + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL && flow->rk.peer_crt.len > 0) { + crt->data = malloc(flow->rk.peer_crt.len); + if (crt->data == NULL) { + ret = -ENOMEM; + } else { + memcpy(crt->data, flow->rk.peer_crt.data, + flow->rk.peer_crt.len); + crt->len = flow->rk.peer_crt.len; + ret = 0; + } + } + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +int reg_flow_get_epoch(int flow_id) +{ + struct reg_flow * flow; + int epoch = -1; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL && flow->rk.encrypted) + epoch = flow->rk.epoch; + + pthread_mutex_unlock(®.mtx); + + return epoch; +} + +bool reg_flow_rekey_pending(int flow_id) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = flow->rk.has_pending; + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +pid_t reg_flow_get_n_1_pid(int flow_id) +{ + struct reg_flow * flow; + pid_t pid = -1; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + pid = flow->info.n_1_pid; + + pthread_mutex_unlock(®.mtx); + + return pid; +} + +int reg_flow_snapshot_rekey_due(struct rekey_info * snap, + int max) +{ + struct list_head * p; + int n = 0; + + pthread_mutex_lock(®.mtx); + + llist_for_each(p, ®.flows) { + struct reg_flow * f; + + if (n == max) + break; + + f = list_entry(p, struct reg_flow, next); + + if (f->info.state != FLOW_ALLOCATED) + continue; + + if (!f->rk.encrypted) + continue; + + /* Direct flows have no IPCP initiator; either side drives. */ + if (!f->direct && !f->rk.initiator) + continue; + + if (f->rk.in_flight || f->rk.has_pending) + continue; + + f->rk.in_flight = true; + + snap[n].flow_id = f->info.id; + snap[n].n_pid = f->info.n_pid; + snap[n].n_1_pid = f->info.n_1_pid; + snap[n].epoch = f->rk.epoch; + snap[n].direct = f->direct; + strcpy(snap[n].name, f->name); + ++n; + } + + pthread_mutex_unlock(®.mtx); + + return n; +} + +void reg_flow_clear_in_flight(int flow_id) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + flow->rk.in_flight = false; + + pthread_mutex_unlock(®.mtx); +} + +/* Test-and-set the in-flight latch; refuse if a re-key is already active. */ +bool reg_flow_rekey_begin(int flow_id) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL && flow->rk.encrypted) { + if (!flow->rk.in_flight && !flow->rk.has_pending) { + flow->rk.in_flight = true; + ret = true; + } + } + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +/* Initiator yields the responder role while driving its own exchange. */ +bool reg_flow_rekey_should_yield(int flow_id) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = flow->rk.initiator && flow->rk.in_flight; + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +int reg_flow_store_pending(int flow_id, + const uint8_t * seed, + uint8_t epoch, + bool initiator) +{ + struct reg_flow * flow; + int ret = -ENOENT; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + /* Exchange done: release the latch regardless of parking. */ + flow->rk.in_flight = false; + + if (flow->rk.pending_seed == NULL) + flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ); + + if (flow->rk.pending_seed != NULL) { + memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ); + flow->rk.pending_epoch = epoch; + flow->rk.pending_initiator = initiator; + flow->rk.has_pending = true; + /* Doorbell raised only after the seed is parked. */ + if (flow->n_rb != NULL) + ssm_rbuff_set_flags(flow->n_rb, RB_REKEY); + ret = 0; + } else { + ret = -ENOMEM; + } + } + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +/* Direct re-key: which of the two local apps has pulled the seed. */ +#define RK_N_PID 0x1 /* acceptor (n_pid) pulled the seed */ +#define RK_N_1_PID 0x2 /* allocator (n_1_pid) pulled the seed */ +#define RK_PID_MASK (RK_N_PID | RK_N_1_PID) + +/* + * Park a single re-key seed for a direct flow and ring BOTH apps' + * doorbells. The seed is the one shared secret; each app pulls it once + * (reg_flow_take_pending), so it is held until both have taken it. + */ +int reg_flow_store_pending_direct(int flow_id, + const uint8_t * seed, + uint8_t epoch) +{ + struct reg_flow * flow; + int ret = -ENOENT; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow == NULL) + goto out; + + /* Exchange done: release the latch regardless of parking. */ + flow->rk.in_flight = false; + + if (flow->rk.pending_seed == NULL) + flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ); + + if (flow->rk.pending_seed == NULL) { + ret = -ENOMEM; + goto out; + } + + memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ); + flow->rk.pending_epoch = epoch; + flow->rk.has_pending = true; + flow->rk.pulled = 0; + + /* A departed peer never pulls; treat its side as already done. */ + if (flow->info.n_pid <= 0) + flow->rk.pulled |= RK_N_PID; + + if (flow->info.n_1_pid <= 0) + flow->rk.pulled |= RK_N_1_PID; + + if (flow->n_rb != NULL && !(flow->rk.pulled & RK_N_PID)) + ssm_rbuff_set_flags(flow->n_rb, RB_REKEY); + + if (flow->n_1_rb != NULL && !(flow->rk.pulled & RK_N_1_PID)) + ssm_rbuff_set_flags(flow->n_1_rb, RB_REKEY); + + ret = 0; + out: + pthread_mutex_unlock(®.mtx); + + return ret; +} + +/* A caller may act on a flow if it is privileged or owns the flow. */ +static bool uid_may_access(uid_t caller, + uid_t owner) +{ + return is_ouroboros_member_uid(caller) || caller == owner; +} + +/* + * Caller holds reg.mtx. The direct seed is shared by both apps, so the + * per-app initiator role is resolved from the verified caller pid (the + * allocator is n_1_pid), and the seed is held until both have pulled. + */ +static void __take_pending_direct(struct reg_flow * flow, + pid_t cpid, + uint8_t * seed, + uint8_t * epoch, + bool * initiator) +{ + bool allocator; + + allocator = cpid == flow->info.n_1_pid; + + memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ); + *epoch = flow->rk.pending_epoch; + *initiator = allocator; + flow->rk.epoch = flow->rk.pending_epoch; + + if (allocator) { + flow->rk.pulled |= RK_N_1_PID; + if (flow->n_1_rb != NULL) + ssm_rbuff_clr_flags(flow->n_1_rb, RB_REKEY); + } else { + flow->rk.pulled |= RK_N_PID; + if (flow->n_rb != NULL) + ssm_rbuff_clr_flags(flow->n_rb, RB_REKEY); + } + + if ((flow->rk.pulled & RK_PID_MASK) != RK_PID_MASK) + return; + + flow->rk.has_pending = false; + flow->rk.pulled = 0; + crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ); +} + +int reg_flow_take_pending(int flow_id, + uid_t uid, + pid_t cpid, + uint8_t * seed, + uint8_t * epoch, + bool * initiator) +{ + struct reg_flow * flow; + int ret = -ENOENT; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow == NULL || !flow->rk.has_pending) + goto out; + + if (!uid_may_access(uid, flow->info.uid)) { + ret = -EPERM; + goto out; + } + + if (flow->direct) { + __take_pending_direct(flow, cpid, seed, epoch, initiator); + ret = 0; + goto out; + } + + memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ); + *epoch = flow->rk.pending_epoch; + *initiator = flow->rk.pending_initiator; + flow->rk.epoch = flow->rk.pending_epoch; + flow->rk.has_pending = false; + crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ); + if (flow->n_rb != NULL) + ssm_rbuff_clr_flags(flow->n_rb, RB_REKEY); + + ret = 0; + out: + pthread_mutex_unlock(®.mtx); + + return ret; +} + +/* + * Admit a peer-driven re-key arrival before a worker event is allocated: + * the flow must exist, carry a cipher, and the update must come from its + * own lower IPCP. Coalesces to one queued REQ and one queued RESP per flow + * so a flooding peer cannot grow the inbox without bound. + */ +bool reg_flow_rekey_arr_admit(int flow_id, + pid_t n_1_pid, + bool is_req) +{ + struct reg_flow * flow; + bool admit = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL && flow->rk.encrypted + && flow->info.n_1_pid == n_1_pid) { + if (is_req && !flow->rk.req_queued) { + flow->rk.req_queued = true; + admit = true; + } else if (!is_req && flow->rk.in_flight + && !flow->rk.resp_queued) { + flow->rk.resp_queued = true; + admit = true; + } + } + + pthread_mutex_unlock(®.mtx); + + return admit; +} + +void reg_flow_rekey_arr_done(int flow_id, + bool is_req) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + if (is_req) + flow->rk.req_queued = false; + else + flow->rk.resp_queued = false; + } + + pthread_mutex_unlock(®.mtx); +} + +bool reg_flow_owned_by(int flow_id, + uid_t uid) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = uid_may_access(uid, flow->info.uid); + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +/* Caller holds reg.mtx. */ +static void __notify_proc(pid_t pid, + int flow_id, + int event) +{ + struct reg_proc * proc; + + proc = __reg_get_proc(pid); + if (proc != NULL) + ssm_flow_set_notify(proc->set, flow_id, event); +} + +void reg_notify_flow(int flow_id, + int event) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + __notify_proc(flow->info.n_pid, flow_id, event); + + pthread_mutex_unlock(®.mtx); +} + +/* Wake both PoAs of a direct flow (acceptor and allocator). */ +void reg_notify_flow_peers(int flow_id, + int event) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) { + __notify_proc(flow->info.n_pid, flow_id, event); + __notify_proc(flow->info.n_1_pid, flow_id, event); + } + + pthread_mutex_unlock(®.mtx); +} + int reg_respond_flow_direct(int flow_id, buffer_t * pbuf) { diff --git a/src/irmd/reg/reg.h b/src/irmd/reg/reg.h index 6b576471..6882532c 100644 --- a/src/irmd/reg/reg.h +++ b/src/irmd/reg/reg.h @@ -33,146 +33,222 @@ #include "pool.h" -int reg_init(void); +int reg_init(void); -void reg_clear(void); +void reg_clear(void); -void reg_fini(void); +void reg_fini(void); -int reg_create_flow(struct flow_info * info); +int reg_create_flow(struct flow_info * info); -int reg_destroy_flow(int flow_id); +int reg_destroy_flow(int flow_id); -bool reg_has_flow(int flow_id); +bool reg_has_flow(int flow_id); -int reg_create_proc(const struct proc_info * info); +int reg_create_proc(const struct proc_info * info); /* Use this for all processes, including ipcps */ -int reg_destroy_proc(pid_t pid); +int reg_destroy_proc(pid_t pid); -bool reg_has_proc(pid_t pid); +bool reg_has_proc(pid_t pid); -bool reg_is_proc_privileged(pid_t pid); +bool reg_is_proc_privileged(pid_t pid); -int reg_prepare_pool(uid_t uid, - gid_t gid); +int reg_prepare_pool(uid_t uid, + gid_t gid); -uid_t reg_get_proc_uid(pid_t pid); +uid_t reg_get_proc_uid(pid_t pid); -void reg_kill_all_proc(int signal); +void reg_kill_all_proc(int signal); -pid_t reg_get_dead_proc(void); +pid_t reg_get_dead_proc(void); -int reg_create_spawned(pid_t pid); +int reg_create_spawned(pid_t pid); -bool reg_has_spawned(pid_t pid); +bool reg_has_spawned(pid_t pid); -void reg_kill_all_spawned(int signal); +void reg_kill_all_spawned(int signal); -int reg_first_spawned(void); +int reg_first_spawned(void); -int reg_bind_proc(const char * name, - pid_t proc); +int reg_bind_proc(const char * name, + pid_t proc); -int reg_unbind_proc(const char * name, - pid_t proc); +int reg_unbind_proc(const char * name, + pid_t proc); -int reg_create_ipcp(const struct ipcp_info * info); +int reg_create_ipcp(const struct ipcp_info * info); -bool reg_has_ipcp(pid_t pid); +bool reg_has_ipcp(pid_t pid); -int reg_set_layer_for_ipcp(struct ipcp_info * info, - const struct layer_info * layer); +int reg_set_layer_for_ipcp(struct ipcp_info * info, + const struct layer_info * layer); -int reg_get_ipcp(struct ipcp_info * info, - struct layer_info * layer); +int reg_get_ipcp(struct ipcp_info * info, + struct layer_info * layer); -int reg_get_ipcp_by_layer(struct ipcp_info * info, - struct layer_info * layer); +int reg_get_ipcp_by_layer(struct ipcp_info * info, + struct layer_info * layer); /* TODO don't rely on protobuf here */ -int reg_list_ipcps(ipcp_list_msg_t *** msg); +int reg_list_ipcps(ipcp_list_msg_t *** msg); -int reg_create_name(const struct name_info * info); +int reg_create_name(const struct name_info * info); -int reg_destroy_name(const char * name); +int reg_destroy_name(const char * name); -bool reg_has_name(const char * name); +bool reg_has_name(const char * name); -int reg_get_name_info(const char * name, - struct name_info * info); +int reg_get_name_info(const char * name, + struct name_info * info); -int reg_get_name_for_hash(char * buf, - enum hash_algo algo, - const uint8_t * hash); +int reg_get_name_for_hash(char * buf, + enum hash_algo algo, + const uint8_t * hash); -int reg_get_name_for_flow_id(char * buf, - int flow_id); +int reg_get_name_for_flow_id(char * buf, + int flow_id); + +void reg_set_name_for_flow_id(const char * name, + int flow_id); /* TODO don't rely on protobuf here */ -int reg_list_names(name_info_msg_t *** names); +int reg_list_names(name_info_msg_t *** names); + +int reg_create_prog(const struct prog_info * info); + +int reg_destroy_prog(const char * name); -int reg_create_prog(const struct prog_info * info); +bool reg_has_prog(const char * name); -int reg_destroy_prog(const char * name); +int reg_get_exec(const char * name, + char *** exec); -bool reg_has_prog(const char * name); +int reg_bind_prog(const char * name, + char ** exec, + uint8_t flags); -int reg_get_exec(const char * name, - char *** exec); +int reg_unbind_prog(const char * name, + const char * prog); -int reg_bind_prog(const char * name, - char ** exec, - uint8_t flags); +int reg_prepare_flow_alloc(struct flow_info * info); -int reg_unbind_prog(const char * name, - const char * prog); +int reg_wait_flow_allocated(struct flow_info * info, + buffer_t * pbuf, + const struct timespec * abstime); -int reg_prepare_flow_alloc(struct flow_info * info); +int reg_respond_alloc(struct flow_info * info, + buffer_t * pbuf, + int response); -int reg_wait_flow_allocated(struct flow_info * info, +int reg_prepare_flow_accept(struct flow_info * info); + +int reg_wait_flow_accepted(struct flow_info * info, buffer_t * pbuf, const struct timespec * abstime); -int reg_respond_alloc(struct flow_info * info, - buffer_t * pbuf, - int response); +int reg_wait_flow_accepting(const char * name, + const struct timespec * abstime); -int reg_prepare_flow_accept(struct flow_info * info); +int reg_respond_accept(struct flow_info * info, + buffer_t * pbuf); -int reg_wait_flow_accepted(struct flow_info * info, - buffer_t * pbuf, - const struct timespec * abstime); +int reg_prepare_flow_direct(struct flow_info * info, + buffer_t * pbuf, + uid_t alloc_uid); -int reg_wait_flow_accepting(const char * name, - const struct timespec * abstime); +int reg_respond_flow_direct(int flow_id, + buffer_t * pbuf); + +int reg_wait_flow_direct(int flow_id, + buffer_t * pbuf, + const struct timespec * abstime); + +bool reg_flow_is_direct(int flow_id); + +bool reg_flow_is_poa(int flow_id); + +int reg_flow_set_poa(int flow_id); + +int reg_flow_set_oap_ctx(int flow_id, + void * ctx); + +void * reg_flow_take_oap_ctx(int flow_id); + +/* Per-flow snapshot for the re-key timer */ +struct rekey_info { + int flow_id; + pid_t n_pid; + pid_t n_1_pid; + char name[NAME_SIZE + 1]; + uint8_t epoch; + bool direct; +}; + +void reg_flow_set_rekey(int flow_id, + bool initiator, + buffer_t peer_crt); + +int reg_flow_get_peer_crt(int flow_id, + buffer_t * crt); + +int reg_flow_get_epoch(int flow_id); + +bool reg_flow_rekey_pending(int flow_id); + +pid_t reg_flow_get_n_1_pid(int flow_id); + +int reg_flow_snapshot_rekey_due(struct rekey_info * snap, + int max); + +void reg_flow_clear_in_flight(int flow_id); + +bool reg_flow_rekey_begin(int flow_id); + +bool reg_flow_rekey_should_yield(int flow_id); + +int reg_flow_store_pending(int flow_id, + const uint8_t * seed, + uint8_t epoch, + bool initiator); + +int reg_flow_store_pending_direct(int flow_id, + const uint8_t * seed, + uint8_t epoch); + +int reg_flow_take_pending(int flow_id, + uid_t uid, + pid_t cpid, + uint8_t * seed, + uint8_t * epoch, + bool * initiator); -int reg_respond_accept(struct flow_info * info, - buffer_t * pbuf); +bool reg_flow_rekey_arr_admit(int flow_id, + pid_t n_1_pid, + bool is_req); -int reg_prepare_flow_direct(struct flow_info * info, - buffer_t * pbuf, - uid_t alloc_uid); +void reg_flow_rekey_arr_done(int flow_id, + bool is_req); -int reg_respond_flow_direct(int flow_id, - buffer_t * pbuf); +bool reg_flow_owned_by(int flow_id, + uid_t uid); -int reg_wait_flow_direct(int flow_id, - buffer_t * pbuf, - const struct timespec * abstime); +void reg_notify_flow(int flow_id, + int event); -bool reg_flow_is_direct(int flow_id); +void reg_notify_flow_peers(int flow_id, + int event); -void reg_dealloc_flow(struct flow_info * info); +void reg_dealloc_flow(struct flow_info * info); -void reg_dealloc_flow_resp(struct flow_info * info); +void reg_dealloc_flow_resp(struct flow_info * info); -int reg_wait_proc(pid_t pid, - const struct timespec * abstime); +int reg_wait_proc(pid_t pid, + const struct timespec * abstime); -int reg_wait_ipcp_boot(struct ipcp_info * ipcp, - const struct timespec * abstime); +int reg_wait_ipcp_boot(struct ipcp_info * ipcp, + const struct timespec * abstime); -int reg_respond_ipcp(const struct ipcp_info * info); +int reg_respond_ipcp(const struct ipcp_info * info); #endif /* OUROBOROS_IRMD_REG_H */ diff --git a/src/irmd/reg/tests/reg_test.c b/src/irmd/reg/tests/reg_test.c index 0b1014f9..ab57241c 100644 --- a/src/irmd/reg/tests/reg_test.c +++ b/src/irmd/reg/tests/reg_test.c @@ -746,7 +746,7 @@ static int test_reg_direct_flow_success(void) reg_dealloc_flow(&info); if (info.state != FLOW_DEALLOC_PENDING) { - printf("Same endpoint dealloc changed state.\n"); + printf("Same PoA dealloc changed state.\n"); goto fail; } @@ -771,6 +771,167 @@ static int test_reg_direct_flow_success(void) return TEST_RC_FAIL; } +/* + * Direct-flow re-key: one shared seed is parked for both local apps. The + * per-app initiator role is resolved from the verified caller pid (the + * allocator is n_1_pid), and the seed is held until both have pulled it. + */ +static int test_reg_direct_flow_rekey(void) +{ + pthread_t thr; + struct timespec abstime; + struct timespec timeo = TIMESPEC_INIT_S(1); + buffer_t rbuf = BUF_INIT; + buffer_t rsp; + buffer_t no_crt = BUF_INIT; + struct direct_alloc_info dai; + uint8_t seed[SYMMKEYSZ]; + uint8_t out[SYMMKEYSZ]; + uint8_t epoch; + bool initiator; + size_t i; + + struct flow_info info = { + .n_pid = TEST_PID, + .qs = qos_raw + }; + + TEST_START(); + + for (i = 0; i < SYMMKEYSZ; ++i) + seed[i] = (uint8_t) i; + + clock_gettime(PTHREAD_COND_CLOCK, &abstime); + + ts_add(&abstime, &timeo, &abstime); + + if (reg_init() < 0) { + printf("Failed to init registry.\n"); + goto fail; + } + + if (reg_create_flow(&info) < 0) { + printf("Failed to add flow.\n"); + goto fail; + } + + if (reg_prepare_flow_accept(&info) < 0) { + printf("Failed to prepare for accept.\n"); + goto fail; + } + + dai.info.id = info.id; + dai.info.n_1_pid = TEST_N_1_PID; + dai.info.mpl = TEST_MPL; + dai.info.qs = qos_msg; + dai.info.state = FLOW_ALLOCATED; + dai.rsp.len = 0; + dai.rsp.data = NULL; + dai.abstime = abstime; + + pthread_create(&thr, NULL, test_flow_alloc_direct, &dai); + + if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) { + printf("Flow accept failed.\n"); + pthread_join(thr, NULL); + goto fail; + } + + freebuf(rbuf); + + rsp.data = (uint8_t *) strdup(TEST_DATA2); + if (rsp.data == NULL) { + printf("Failed to strdup rsp data.\n"); + pthread_join(thr, NULL); + goto fail; + } + rsp.len = strlen(TEST_DATA2) + 1; + + if (reg_respond_flow_direct(info.id, &rsp) < 0) { + printf("Failed to respond direct.\n"); + freebuf(rsp); + pthread_join(thr, NULL); + goto fail; + } + + pthread_join(thr, NULL); + + freebuf(dai.rsp); + + if (!reg_flow_is_direct(info.id)) { + printf("Flow not marked direct.\n"); + goto fail; + } + + reg_flow_set_rekey(info.id, false, no_crt); + + if (reg_flow_store_pending_direct(info.id, seed, 5) < 0) { + printf("Failed to store pending direct seed.\n"); + goto fail; + } + + if (!reg_flow_rekey_pending(info.id)) { + printf("Seed not pending after store.\n"); + goto fail; + } + + /* Allocator (n_1_pid) pulls: initiator role, seed still held. */ + if (reg_flow_take_pending(info.id, 0, TEST_N_1_PID, out, + &epoch, &initiator) != 0) { + printf("Allocator failed to take pending seed.\n"); + goto fail; + } + + if (!initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) { + printf("Allocator got wrong seed/role/epoch.\n"); + goto fail; + } + + if (!reg_flow_rekey_pending(info.id)) { + printf("Seed cleared before both apps pulled.\n"); + goto fail; + } + + /* Acceptor (n_pid) pulls: responder role, seed now released. */ + if (reg_flow_take_pending(info.id, 0, TEST_PID, out, + &epoch, &initiator) != 0) { + printf("Acceptor failed to take pending seed.\n"); + goto fail; + } + + if (initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) { + printf("Acceptor got wrong seed/role/epoch.\n"); + goto fail; + } + + if (reg_flow_rekey_pending(info.id)) { + printf("Seed still pending after both pulled.\n"); + goto fail; + } + + if (reg_flow_get_epoch(info.id) != 5) { + printf("Flow epoch not advanced.\n"); + goto fail; + } + + info.n_pid = TEST_PID; + reg_dealloc_flow(&info); + + info.n_pid = TEST_N_1_PID; + reg_dealloc_flow(&info); + + reg_destroy_flow(info.id); + + reg_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + REG_TEST_FAIL(); + return TEST_RC_FAIL; +} + static int test_reg_flow(void) { int rc = 0; @@ -781,6 +942,7 @@ static int test_reg_flow(void) { rc |= test_reg_allocate_flow_fail(); rc |= test_reg_respond_alloc_duplicate(); rc |= test_reg_direct_flow_success(); + rc |= test_reg_direct_flow_rekey(); return rc; } @@ -875,6 +1037,7 @@ static int test_reg_list_ipcps(void) while (len-- > 0) ipcp_list_msg__free_unpacked(ipcps[len], NULL); + free(ipcps); for (i = 0; i < 10; i++) @@ -941,6 +1104,7 @@ static int test_insert_ipcps(void) while (len-- > 0) ipcp_list_msg__free_unpacked(ipcps[len], NULL); + free(ipcps); reg_clear(); @@ -1118,6 +1282,7 @@ static int test_reg_list_names(void) for (i = 0; i < len; i++) name_info_msg__free_unpacked(names[i], NULL); + free(names); for (i = 0; i < 10; i++) { diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 6cd3a8a4..48064ce4 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -22,6 +22,7 @@ set(SOURCE_FILES_COMMON crc/crc32.c crc/crc64.c crypt.c + crypt/keyrot.c hash.c lockfile.c logs.c @@ -92,6 +93,13 @@ if(HAVE_FUSE) target_link_libraries(ouroboros-common PRIVATE Fuse::Fuse) endif() +if(HAVE_LIBURCU) + target_link_libraries(ouroboros-common PRIVATE Urcu::Urcu) + # urcu headers require C99; override the global -std=c89 for these TUs. + set_source_files_properties(crypt/keyrot.c dev.c poa/addr.c poa/eth.c + poa/udp.c PROPERTIES COMPILE_OPTIONS "-std=gnu99") +endif() + install(TARGETS ouroboros-common EXPORT OuroborosTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) @@ -99,8 +107,14 @@ install(TARGETS ouroboros-common set(SOURCE_FILES_DEV cep.c dev.c + poa/addr.c + poa/udp.c ) +if(HAVE_ETH) + list(APPEND SOURCE_FILES_DEV poa/eth.c) +endif() + add_library(ouroboros-dev SHARED ${SOURCE_FILES_DEV} ${CEP_PROTO_SRCS}) @@ -123,6 +137,11 @@ target_include_directories(ouroboros-dev target_link_libraries(ouroboros-dev PUBLIC ouroboros-common) +if(HAVE_LIBURCU) + # dev.c calls urcu directly; do not rely on transitive linkage. + target_link_libraries(ouroboros-dev PRIVATE Urcu::Urcu) +endif() + install(TARGETS ouroboros-dev EXPORT OuroborosTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/lib/cap.c b/src/lib/cap.c new file mode 100644 index 00000000..f116bfb0 --- /dev/null +++ b/src/lib/cap.c @@ -0,0 +1,187 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Link capacity estimation + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +/* + * Link-capacity estimation by watching the egress queue drain. + * + * A saturated link drains its queue at exactly its capacity, so we + * estimate capacity by measuring the drain rate of the transmit + * queue toward an n-1 flow (the flow to the layer below) while that + * queue is backlogged. + * + * Sampling is lock-free and off the fast path: the queue depth is + * read only at enqueue time, concurrently by many sender threads. + * Each enqueue bumps relaxed counters (packets, bytes, empty-queue + * hits). At most once per CAP_T_MIN, one thread wins a try-lock and + * closes a measurement window. + * + * Over a window, byte conservation gives the bytes that drained: + * drained = queue at start (q0) + enqueued - queue now (q1) + * A window stays open until CAP_N_MIN packets' worth has drained, so + * its length self-scales with the link rate (~1 ms at 1 Gbit, ~19 ms + * at 10 Mbit). CAP_T_MAX discards a window that spanned a traffic gap. + * + * Only a backlogged link measures its own capacity, so a window + * whose ring ran mostly idle is discarded (a few empty samples, as + * from a token-bucket shaper, are tolerated). The drain rate feeds a + * max filter that jumps up at once but decays slowly, converging on + * the capacity from below. A window that touched an empty ring at + * either edge may have drained into downstream buffers faster than + * the wire, so it may only lower the estimate, never raise it. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif +#else +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + +#include "config.h" + +#include <ouroboros/atomics.h> +#include <ouroboros/time.h> + +#include "cap.h" + +#include <string.h> + +#define CAP_T_MIN (BILLION / 1000) /* min close spacing ~1 ms */ +#define CAP_T_MAX (1ULL << 27) /* voiding traffic gap ~134 ms */ +#define CAP_N_MIN 16 /* drained packets to close */ +#define CAP_DEC_SHFT 4 /* max-filter decay 1/16 */ +#define CAP_IDL_SHFT 3 /* idle tolerance 1/8 */ + +/* Busy-flag try-lock: test-and-set acquire, store release. */ +#define CAP_TAS(p) __atomic_exchange_n(p, 1, __ATOMIC_ACQUIRE) +#define CAP_REL(p) (__atomic_store_n(p, 0, __ATOMIC_RELEASE)) + +void cap_clear(struct cap_est * e) +{ + memset(e, 0, sizeof(*e)); +} + +uint64_t cap_rate(const struct cap_est * e) +{ + return LOAD_RELAXED(&e->est); +} + +/* Busy flag held; q1 is the caller's pre-write ring sample. */ +static void cap_close(struct cap_est * e, + uint64_t q1, + uint64_t now, + uint64_t gap) +{ + uint64_t pkt; /* current c_pkt snapshot */ + uint64_t byt; /* current c_byt snapshot */ + uint64_t idl; /* current c_idl snapshot */ + uint64_t dt; /* window duration (ns) */ + uint64_t enq; /* packets enqueued in window */ + uint64_t avg; /* mean packet size (bytes) */ + uint64_t r; /* window drain rate (bytes/s) */ + int64_t drained; /* bytes drained over window */ + + pkt = LOAD_RELAXED(&e->c_pkt); + byt = LOAD_RELAXED(&e->c_byt); + idl = LOAD_RELAXED(&e->c_idl); + + dt = now - e->t0; + enq = pkt - e->pkt0; + + drained = (int64_t) (e->q0 + (byt - e->byt0) - q1); + + if (e->t0 == 0 || enq == 0) + goto reopen; + + if (gap > CAP_T_MAX) + goto reopen; /* traffic stopped: window void */ + + avg = (byt - e->byt0) / enq; + if (drained < (int64_t) (CAP_N_MIN * avg)) + return; /* extend the window until enough drains */ + + if ((idl - e->idl0) << CAP_IDL_SHFT > enq) + goto reopen; /* mostly idle ring: not saturated */ + + r = (uint64_t) drained * MILLION / (dt / 1000); + if (r >= e->rate) { + if (e->q0 > 0 && q1 > 0) /* empty edge drains below */ + e->rate = r; + } else { + e->rate -= (e->rate - r) >> CAP_DEC_SHFT; + } + + STORE_RELAXED(&e->est, e->rate); + reopen: + e->t0 = now; + e->q0 = q1; + e->pkt0 = pkt; + e->byt0 = byt; + e->idl0 = idl; +} + +void cap_update_at(struct cap_est * e, + size_t qlen, + size_t len, + uint64_t now) +{ + uint64_t prev; + + FETCH_ADD_RELAXED(&e->c_pkt, 1); + FETCH_ADD_RELAXED(&e->c_byt, len); + + if (qlen == 0) + FETCH_ADD_RELAXED(&e->c_idl, 1); + + prev = LOAD_RELAXED(&e->t_last); + if (prev > now) + prev = now; /* a racing writer stamped ahead */ + + STORE_RELAXED(&e->t_last, now); + + if (now - LOAD_RELAXED(&e->t_gate) < CAP_T_MIN) + return; + + if (CAP_TAS(&e->busy) != 0) + return; + + if (now - e->t_gate >= CAP_T_MIN) { + cap_close(e, qlen, now, now - prev); + STORE_RELAXED(&e->t_gate, now); + } + + CAP_REL(&e->busy); +} + +void cap_update(struct cap_est * e, + size_t qlen, + size_t len) +{ + struct timespec now; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + cap_update_at(e, qlen, len, TS_TO_UINT64(now)); +} diff --git a/src/lib/cap.h b/src/lib/cap.h new file mode 100644 index 00000000..3d94d9a3 --- /dev/null +++ b/src/lib/cap.h @@ -0,0 +1,63 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Link capacity estimation + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#ifndef OUROBOROS_LIB_CAP_H +#define OUROBOROS_LIB_CAP_H + +#include <stddef.h> +#include <stdint.h> + +#define CAP_ALIGN 64 + +struct cap_est { + uint64_t c_pkt; /* total packets enqueued (relaxed) */ + uint64_t c_byt; /* total bytes enqueued (relaxed) */ + uint64_t c_idl; /* times ring seen empty (relaxed) */ + + uint64_t t_gate; /* last window close (ns) */ + uint64_t t_last; /* last update, to spot a gap (ns) */ + uint8_t busy; /* close in progress (try-lock) */ + + uint64_t t0; /* window start (ns), 0 = no window */ + uint64_t q0; /* ring occupancy at window start */ + uint64_t pkt0; /* c_pkt snapshot at window start */ + uint64_t byt0; /* c_byt snapshot at window start */ + uint64_t idl0; /* c_idl snapshot at window start */ + uint64_t rate; /* filtered drain rate (bytes/s) */ + + uint64_t est; /* published estimate (bytes/s) */ +} __attribute__((aligned(CAP_ALIGN))); + +void cap_clear(struct cap_est * e); + +void cap_update(struct cap_est * e, + size_t qlen, + size_t len); + +void cap_update_at(struct cap_est * e, + size_t qlen, + size_t len, + uint64_t now); + +uint64_t cap_rate(const struct cap_est * e); + +#endif /* OUROBOROS_LIB_CAP_H */ diff --git a/src/lib/config.h.in b/src/lib/config.h.in index 7124a974..38d6f768 100644 --- a/src/lib/config.h.in +++ b/src/lib/config.h.in @@ -33,11 +33,11 @@ #cmakedefine HAVE_LIBGCRYPT #cmakedefine HAVE_OPENSSL #ifdef HAVE_OPENSSL -#cmakedefine HAVE_OPENSSL_ML_KEM -#cmakedefine HAVE_OPENSSL_ML_DSA -#cmakedefine HAVE_OPENSSL_SLH_DSA +#cmakedefine HAVE_ML +#cmakedefine HAVE_SLH +#cmakedefine HAVE_OPENSSL_4_1 #define HAVE_ENCRYPTION -#define SECMEM_GUARD @SECMEM_GUARD@ +#define SECMEM_MINSIZE @SECMEM_MINSIZE@ #endif #define PROC_SECMEM_MAX @PROC_SECMEM_MAX@ @@ -49,6 +49,7 @@ #cmakedefine HAVE_PMULL #define SHM_LOCKFILE_NAME "@SHM_LOCKFILE_NAME@" +#define SSM_RBUFF_TXQ_DELAY @SSM_RBUFF_TXQ_DELAY@ /* ms */ #define FLOW_ALLOC_TIMEOUT @FLOW_ALLOC_TIMEOUT@ #define TPM_DEBUG_REPORT_INTERVAL @TPM_DEBUG_REPORT_INTERVAL@ @@ -70,6 +71,8 @@ #cmakedefine PROC_FLOW_STATS #endif +#cmakedefine HAVE_LIBURCU + #cmakedefine FRCT_DEBUG_STDOUT #define PTHREAD_COND_CLOCK @PTHREAD_COND_CLOCK@ @@ -78,6 +81,25 @@ #define PROC_RES_FDS @PROC_RES_FDS@ #define PROC_MAX_FQUEUES @PROC_MAX_FQUEUES@ +/* Flow endpoints */ +#define POA_MGMT_FRAME_SIZE @POA_MGMT_FRAME_SIZE@ +#define POA_MGMT_SND_TIMEO @POA_MGMT_SND_TIMEO@ +#define POA_MAX_POAS @POA_MAX_POAS@ +#define POA_UDP_MPL @POA_UDP_MPL@ +#define POA_UDP4_MTU @POA_UDP4_MTU@ +#define POA_UDP6_MTU @POA_UDP6_MTU@ +#define POA_UDP_RD_BUF @POA_UDP_RD_BUF@ +#cmakedefine HAVE_ETH +#cmakedefine HAVE_RAW_SOCKETS +#cmakedefine HAVE_BPF +#cmakedefine HAVE_NETMAP +#cmakedefine POA_ETH_QDISC_BYPASS +#define POA_ETH_MPL @POA_ETH_MPL@ +#define POA_ETH_SNDBUF @POA_ETH_SNDBUF@ +#define POA_ETH_RCVBUF @POA_ETH_RCVBUF@ +#define POA_ETH_LO_MTU @POA_ETH_LO_MTU@ +#define POA_ETH_RD_BUF @POA_ETH_RD_BUF@ + /* Default Delta-t parameters */ #cmakedefine FRCT_LINUX_RTT_ESTIMATOR #define DELT_A (@DELTA_T_ACK@) /* ms */ @@ -100,4 +122,9 @@ #define ACKQ_SLOTS (@ACK_WHEEL_SLOTS@) #define ACKQ_RES (@ACK_WHEEL_RESOLUTION@) /* 2^N ns */ -#define KEY_ROTATION_BIT (@KEY_ROTATION_BIT@) /* Bit for key rotation */ +#define KEY_LEAF_BITS (@KEY_LEAF_BITS@) /* pkts/leaf-key = 2^n */ +#define KEY_NODE_BITS (@KEY_NODE_BITS@) /* leaf-keys/node = 2^n */ +#define KEY_NODE_COUNT (@KEY_NODE_COUNT@) /* node keys/batch N */ +#define KEY_REKEY_WATERMARK (@KEY_REKEY_WATERMARK@) /* node-keys-left trig */ +#define KEY_REPLAY_WINDOW (@KEY_REPLAY_WINDOW@) /* rx replay win pkts */ +#define FLOW_WM_CHECK (1u << @KEY_REKEY_WM_CHECK_BITS@) /* wm chk/n wr */ diff --git a/src/lib/crypt.c b/src/lib/crypt.c index 71197f6e..8ca7084f 100644 --- a/src/lib/crypt.c +++ b/src/lib/crypt.c @@ -27,10 +27,14 @@ #include <config.h> #include <ouroboros/errno.h> +#include <ouroboros/pthread.h> #include <ouroboros/random.h> #include <ouroboros/crypt.h> +#include "crypt/keyrot.h" + #ifdef HAVE_OPENSSL +#include <openssl/crypto.h> #include <openssl/evp.h> #include "crypt/openssl.h" #endif @@ -50,18 +54,12 @@ static const struct nid_map cipher_nid_map[] = { {NID_aes_192_gcm, "aes-192-gcm"}, {NID_aes_256_gcm, "aes-256-gcm"}, {NID_chacha20_poly1305, "chacha20-poly1305"}, - {NID_aes_128_ctr, "aes-128-ctr"}, - {NID_aes_192_ctr, "aes-192-ctr"}, - {NID_aes_256_ctr, "aes-256-ctr"}, {NID_undef, NULL} }; /* Ordered in strength preference, lowest first */ const uint16_t crypt_supported_nids[] = { #ifdef HAVE_OPENSSL - NID_aes_128_ctr, - NID_aes_192_ctr, - NID_aes_256_ctr, NID_aes_128_gcm, NID_aes_192_gcm, NID_aes_256_gcm, @@ -71,39 +69,43 @@ const uint16_t crypt_supported_nids[] = { }; static const struct nid_map kex_nid_map[] = { - {NID_X9_62_prime256v1, "prime256v1"}, - {NID_secp384r1, "secp384r1"}, - {NID_secp521r1, "secp521r1"}, - {NID_X25519, "X25519"}, - {NID_X448, "X448"}, - {NID_ffdhe2048, "ffdhe2048"}, - {NID_ffdhe3072, "ffdhe3072"}, - {NID_ffdhe4096, "ffdhe4096"}, - {NID_MLKEM512, "ML-KEM-512"}, - {NID_MLKEM768, "ML-KEM-768"}, - {NID_MLKEM1024, "ML-KEM-1024"}, - {NID_X25519MLKEM768, "X25519MLKEM768"}, - {NID_X448MLKEM1024, "X448MLKEM1024"}, - {NID_undef, NULL} + {NID_X9_62_prime256v1, "prime256v1"}, + {NID_secp384r1, "secp384r1"}, + {NID_secp521r1, "secp521r1"}, + {NID_X25519, "X25519"}, + {NID_X448, "X448"}, + {NID_ffdhe2048, "ffdhe2048"}, + {NID_ffdhe3072, "ffdhe3072"}, + {NID_ffdhe4096, "ffdhe4096"}, + {NID_MLKEM512, "ML-KEM-512"}, + {NID_MLKEM768, "ML-KEM-768"}, + {NID_MLKEM1024, "ML-KEM-1024"}, + {NID_X25519MLKEM768, "X25519MLKEM768"}, + {NID_X448MLKEM1024, "X448MLKEM1024"}, + {NID_SecP256r1MLKEM768, "SecP256r1MLKEM768"}, + {NID_SecP384r1MLKEM1024, "SecP384r1MLKEM1024"}, + {NID_undef, NULL} }; -/* Ordered in strength preference, lowest first */ +/* Ordered in strength preference, lowest first (NIST SP 800-57 levels) */ const uint16_t kex_supported_nids[] = { #ifdef HAVE_OPENSSL - NID_ffdhe2048, - NID_X9_62_prime256v1, - NID_X25519, - NID_ffdhe3072, - NID_secp384r1, - NID_ffdhe4096, - NID_X448, - NID_secp521r1, -#ifdef HAVE_OPENSSL_ML_KEM - NID_MLKEM512, - NID_MLKEM768, - NID_MLKEM1024, - NID_X25519MLKEM768, - NID_X448MLKEM1024, + NID_ffdhe2048, /* FFDHE-2048, ~112-bit */ + NID_X9_62_prime256v1, /* ECDH P-256, 128-bit */ + NID_X25519, /* ECDH X25519, 128-bit */ + NID_ffdhe3072, /* FFDHE-3072, ~128-bit */ + NID_ffdhe4096, /* FFDHE-4096, ~152-bit */ + NID_secp384r1, /* ECDH P-384, 192-bit */ + NID_X448, /* ECDH X448, 224-bit */ + NID_secp521r1, /* ECDH P-521, 256-bit */ +#ifdef HAVE_ML + NID_MLKEM512, /* ML-KEM-512, PQC L1 (~AES-128) */ + NID_MLKEM768, /* ML-KEM-768, PQC L3 (~AES-192) */ + NID_MLKEM1024, /* ML-KEM-1024, PQC L5 (~AES-256) */ + NID_SecP256r1MLKEM768, /* P-256 + ML-KEM-768, PQC L3 */ + NID_X25519MLKEM768, /* X25519 + ML-KEM-768, PQC L3 */ + NID_SecP384r1MLKEM1024, /* P-384 + ML-KEM-1024, PQC L5 */ + NID_X448MLKEM1024, /* X448 + ML-KEM-1024, PQC L5 */ #endif #endif NID_undef @@ -137,11 +139,13 @@ const uint16_t md_supported_nids[] = { }; struct crypt_ctx { - void * ctx; /* Encryption context */ + struct keyrot * kr; /* backend-independent key rotation */ + void * cipher; /* backend AEAD cipher context */ }; struct auth_ctx { - void * store; + void * store; /* trusted anchors */ + void * chain; /* untrusted build-only interm */ }; static int parse_kex_value(const char * value, @@ -162,6 +166,7 @@ int parse_sec_config(struct sec_config * cfg, char * equals; char * key; char * value; + bool no_enc = false; assert(cfg != NULL); assert(fp != NULL); @@ -172,6 +177,7 @@ int parse_sec_config(struct sec_config * cfg, SET_KEX_KDF_NID(cfg, NID_sha256); SET_KEX_CIPHER_NID(cfg, NID_aes_256_gcm); SET_KEX_DIGEST_NID(cfg, NID_sha256); + /* a.req is seeded per-role by the caller; only auth= overrides it */ while (fgets(line, sizeof(line), fp) != NULL) { char * trimmed; @@ -180,12 +186,10 @@ int parse_sec_config(struct sec_config * cfg, if (line[0] == '#' || line[0] == '\n') continue; - /* Check for 'none' keyword */ + /* Bare 'none' keyword replaced by encryption=none */ trimmed = trim_whitespace(line); - if (strcmp(trimmed, "none") == 0) { - memset(cfg, 0, sizeof(*cfg)); - return 0; - } + if (strcmp(trimmed, "none") == 0) + return -EINVAL; /* Find the = separator */ equals = strchr(line, '='); @@ -221,12 +225,54 @@ int parse_sec_config(struct sec_config * cfg, } else { return -EINVAL; } + } else if (strcmp(key, "auth") == 0) { + if (strcmp(value, "required") == 0) { + cfg->a.req = true; + } else if (strcmp(value, "optional") == 0) { + cfg->a.req = false; + } else { + return -EINVAL; + } + } else if (strcmp(key, "cacert") == 0) { + if (strlen(value) >= sizeof(cfg->a.cacert)) + return -EINVAL; + strcpy(cfg->a.cacert, value); + } else if (strcmp(key, "encryption") == 0) { + if (strcmp(value, "none") != 0) + return -EINVAL; + no_enc = true; + } else { + return -EINVAL; } } + if (no_enc) { + /* Digest stays: it belongs to the auth axis */ + CLEAR_KEX_ALGO(cfg); + CLEAR_KEX_KDF(cfg); + CLEAR_KEX_CIPHER(cfg); + } + return 0; } +/* + * Not in header, but non-static for unit testing. Without a crypto + * backend a present security config is refused. + */ +int crypt_load_sec_config(struct sec_config * cfg, + FILE * fp) +{ + assert(cfg != NULL); + assert(fp != NULL); + +#ifndef HAVE_OPENSSL + return -ENOTSUP; +#endif + + return parse_sec_config(cfg, fp); +} + /* Parse key exchange config from file */ int load_sec_config_file(struct sec_config * cfg, const char * path) @@ -239,12 +285,19 @@ int load_sec_config_file(struct sec_config * cfg, fp = fopen(path, "r"); if (fp == NULL) { - /* File doesn't exist - disable encryption */ - CLEAR_KEX_ALGO(cfg); - return 0; + /* Absent config disables encryption; other errors fail */ + if (errno == ENOENT) { + CLEAR_KEX_ALGO(cfg); + return 0; + } + return -errno; } - ret = parse_sec_config(cfg, fp); + pthread_cleanup_push(__cleanup_fclose, fp); + + ret = crypt_load_sec_config(cfg, fp); + + pthread_cleanup_pop(0); fclose(fp); @@ -329,14 +382,16 @@ ssize_t kex_kem_encap(buffer_t pk, #endif } -ssize_t kex_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf, - uint8_t * s) +ssize_t kex_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf, + uint8_t * s) { #ifdef HAVE_OPENSSL - return openssl_kem_encap_raw(pk, ct, kdf, s); + return openssl_kem_encap_raw(algo, pk, ct, kdf, s); #else + (void) algo; (void) pk; (void) ct; (void) kdf; @@ -378,19 +433,6 @@ int kex_get_algo_from_pk_der(buffer_t pk, #endif } -int kex_get_algo_from_pk_raw(buffer_t pk, - char * algo) -{ -#ifdef HAVE_OPENSSL - return openssl_get_algo_from_pk_raw(pk, algo); -#else - (void) pk; - algo[0] = '\0'; - - return -ECRYPT; -#endif -} - int kex_validate_algo(const char * algo) { if (algo == NULL) @@ -498,6 +540,11 @@ int kex_validate_nid(int nid) return -ENOTSUP; } +bool kex_nid_is_hybrid(uint16_t nid) +{ + return nid >= NID_HYBRID_KEM_MIN && nid <= NID_HYBRID_KEM_MAX; +} + const char * md_nid_to_str(uint16_t nid) { const struct nid_map * p; @@ -592,19 +639,71 @@ int crypt_kex_rank(int nid) return -1; } -/* Hash length now returned by md_digest() */ +/* AEAD primitive: 1:1 backend wrappers used by the data path below. */ +static int crypt_seal(void * cipher, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + uint8_t * out, + uint8_t * tag) +{ +#ifdef HAVE_OPENSSL + return openssl_seal(cipher, key, nonce, aad, in, out, tag); +#else + (void) cipher; + (void) key; + (void) nonce; + (void) aad; + (void) in; + (void) out; + (void) tag; -int crypt_encrypt(struct crypt_ctx * ctx, - buffer_t in, - buffer_t * out) + return -ECRYPT; +#endif +} + +static int crypt_open(void * cipher, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + const uint8_t * tag, + buffer_t * out) { - assert(ctx != NULL); - assert(ctx->ctx != NULL); +#ifdef HAVE_OPENSSL + return openssl_open(cipher, key, nonce, aad, in, tag, out); +#else + (void) cipher; + (void) key; + (void) nonce; + (void) aad; + (void) in; + (void) tag; + (void) out; + + return -ECRYPT; +#endif +} + +int crypt_oneshot_seal(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out) +{ + assert(key != NULL); + assert(nonce != NULL); + assert(out != NULL); #ifdef HAVE_OPENSSL - return openssl_encrypt(ctx->ctx, in, out); + return openssl_oneshot_seal(nid, key, nonce, aad, in, out); #else - (void) ctx; + (void) nid; + (void) key; + (void) nonce; + (void) aad; (void) in; (void) out; @@ -612,17 +711,24 @@ int crypt_encrypt(struct crypt_ctx * ctx, #endif } -int crypt_decrypt(struct crypt_ctx * ctx, - buffer_t in, - buffer_t * out) +int crypt_oneshot_open(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out) { - assert(ctx != NULL); - assert(ctx->ctx != NULL); + assert(key != NULL); + assert(nonce != NULL); + assert(out != NULL); #ifdef HAVE_OPENSSL - return openssl_decrypt(ctx->ctx, in, out); + return openssl_oneshot_open(nid, key, nonce, aad, in, out); #else - (void) ctx; + (void) nid; + (void) key; + (void) nonce; + (void) aad; (void) in; (void) out; @@ -630,8 +736,122 @@ int crypt_decrypt(struct crypt_ctx * ctx, #endif } +/* + * Data-path encrypt: rotate the key, frame selector ‖ ct ‖ tag, seal. + * Backend-agnostic: composed from keyrot_*, crypt_seal and crypt_get_tagsz. + */ +int crypt_encrypt(struct crypt_ctx * ctx, + buffer_t in, + buffer_t * out) +{ + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * key; + uint8_t * ct; + buffer_t aad; + int tagsz; + int out_sz; + + assert(ctx != NULL); + assert(ctx->kr != NULL); + + tagsz = crypt_get_tagsz(ctx); + if (tagsz < 0) + return -ECRYPT; + + out->data = malloc(KR_SELECTOR_LEN + in.len + (size_t) tagsz); + if (out->data == NULL) + goto fail_malloc; + + ct = out->data + KR_SELECTOR_LEN; + + /* keyrot writes the selector into the wire header (== AAD). */ + if (keyrot_tx_next(ctx->kr, out->data, &key, nonce) != 0) + goto fail_encrypt; + + aad.data = out->data; + aad.len = KR_SELECTOR_LEN; + + out_sz = crypt_seal(ctx->cipher, key, nonce, aad, in, ct, ct + in.len); + if (out_sz < 0) + goto fail_encrypt; + + out->len = KR_SELECTOR_LEN + (size_t) out_sz + (size_t) tagsz; + + return 0; + fail_encrypt: + free(out->data); + fail_malloc: + clrbuf(*out); + return -ECRYPT; +} + +/* + * Data-path decrypt: look up the rotated key from the selector, open, and + * commit the replay window only after the tag verifies. + */ +int crypt_decrypt(struct crypt_ctx * ctx, + buffer_t in, + buffer_t * out) +{ + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * key; + const uint8_t * tag; + struct kr_rx rx; + buffer_t aad; + buffer_t ct; + int tagsz; + int in_sz; + + assert(ctx != NULL); + assert(ctx->kr != NULL); + + tagsz = crypt_get_tagsz(ctx); + if (tagsz < 0) + return -ECRYPT; + + if (in.len < (size_t) (KR_SELECTOR_LEN + tagsz)) + return -ECRYPT; + + if (keyrot_rx_lookup(ctx->kr, in.data, &key, nonce, &rx) != 0) + return -ECRYPT; + + in_sz = (int) in.len - KR_SELECTOR_LEN - tagsz; + + /* +1 keeps malloc(0) defined for an empty (zero-length) frame. */ + out->data = malloc((size_t) in_sz + 1); + if (out->data == NULL) + goto fail_malloc; + + aad.data = in.data; + aad.len = KR_SELECTOR_LEN; + + ct.data = in.data + KR_SELECTOR_LEN; + ct.len = (size_t) in_sz; + + tag = in.data + KR_SELECTOR_LEN + in_sz; + + if (crypt_open(ctx->cipher, key, nonce, aad, ct, tag, out) < 0) + goto fail_decrypt; + + /* Commit replay state only after the tag verifies. */ + if (keyrot_rx_commit(ctx->kr, &rx) != 0) + goto fail_decrypt; + + return 0; + fail_decrypt: + free(out->data); + fail_malloc: + clrbuf(*out); + return -ECRYPT; +} + struct crypt_ctx * crypt_create_ctx(struct crypt_sk * sk) { +#ifndef HAVE_OPENSSL + (void) sk; + + return NULL; /* nothing to seal with */ +#else struct crypt_ctx * crypt; if (crypt_validate_nid(sk->nid) != 0) @@ -643,18 +863,23 @@ struct crypt_ctx * crypt_create_ctx(struct crypt_sk * sk) memset(crypt, 0, sizeof(*crypt)); -#ifdef HAVE_OPENSSL - crypt->ctx = openssl_crypt_create_ctx(sk); - if (crypt->ctx == NULL) - goto fail_ctx; -#endif + crypt->kr = keyrot_create(sk->key, sk->epoch, sk->role); + if (crypt->kr == NULL) + goto fail_kr; + + crypt->cipher = openssl_crypt_create_ctx(sk); + if (crypt->cipher == NULL) + goto fail_cipher; + return crypt; -#ifdef HAVE_OPENSSL - fail_ctx: + + fail_cipher: + keyrot_destroy(crypt->kr); + fail_kr: free(crypt); -#endif fail_crypt: return NULL; +#endif } void crypt_destroy_ctx(struct crypt_ctx * crypt) @@ -662,43 +887,76 @@ void crypt_destroy_ctx(struct crypt_ctx * crypt) if (crypt == NULL) return; + keyrot_destroy(crypt->kr); #ifdef HAVE_OPENSSL - assert(crypt->ctx != NULL); - openssl_crypt_destroy_ctx(crypt->ctx); -#else - assert(crypt->ctx == NULL); + openssl_crypt_destroy_ctx(crypt->cipher); #endif free(crypt); } -int crypt_get_ivsz(struct crypt_ctx * ctx) +int crypt_get_headsz(struct crypt_ctx * ctx) { - if (ctx == NULL) - return -EINVAL; + assert(ctx != NULL); + assert(ctx->kr != NULL); -#ifdef HAVE_OPENSSL - assert(ctx->ctx != NULL); - return openssl_crypt_get_ivsz(ctx->ctx); -#else - assert(ctx->ctx == NULL); - return -ENOTSUP; -#endif + (void) ctx; /* validated only; header size is a constant */ + + return KR_SELECTOR_LEN; +} + +int crypt_rekey(struct crypt_ctx * ctx, + struct crypt_sk * sk) +{ + int ret; + + assert(ctx != NULL); + assert(sk != NULL); + assert(ctx->kr != NULL); + + ret = keyrot_rekey(ctx->kr, sk->key, sk->epoch); + if (ret == -EREPLAY) + return -EREPLAY; + + return ret == 0 ? 0 : -ECRYPT; } int crypt_get_tagsz(struct crypt_ctx * ctx) { - if (ctx == NULL) - return -EINVAL; + assert(ctx != NULL); + assert(ctx->cipher != NULL); #ifdef HAVE_OPENSSL - assert(ctx->ctx != NULL); - return openssl_crypt_get_tagsz(ctx->ctx); + return openssl_crypt_get_tagsz(ctx->cipher); #else - assert(ctx->ctx == NULL); + (void) ctx; return -ENOTSUP; #endif } +int crypt_nodes_left(struct crypt_ctx * ctx) +{ + assert(ctx != NULL); + assert(ctx->kr != NULL); + + return (int) keyrot_tx_nodes_left(ctx->kr); +} + +int crypt_peer_synced(struct crypt_ctx * ctx) +{ + assert(ctx != NULL); + assert(ctx->kr != NULL); + + return keyrot_peer_switched(ctx->kr) ? 1 : 0; +} + +void crypt_tx_promote(struct crypt_ctx * ctx) +{ + assert(ctx != NULL); + assert(ctx->kr != NULL); + + keyrot_tx_promote(ctx->kr); +} + int crypt_load_privkey_file(const char * path, void ** key) { @@ -709,7 +967,7 @@ int crypt_load_privkey_file(const char * path, #else (void) path; - return 0; + return -ENOTSUP; #endif } @@ -723,7 +981,7 @@ int crypt_load_privkey_str(const char * str, #else (void) str; - return 0; + return -ENOTSUP; #endif } @@ -737,7 +995,7 @@ int crypt_load_pubkey_str(const char * str, #else (void) str; - return 0; + return -ENOTSUP; #endif } @@ -751,7 +1009,7 @@ int crypt_load_pubkey_file(const char * path, #else (void) path; - return 0; + return -ENOTSUP; #endif } @@ -772,14 +1030,16 @@ int crypt_load_pubkey_file_to_der(const char * path, } int crypt_load_pubkey_raw_file(const char * path, + const char * algo, buffer_t * buf) { assert(buf != NULL); #ifdef HAVE_OPENSSL - return openssl_load_pubkey_raw_file(path, buf); + return openssl_load_pubkey_raw_file(path, algo, buf); #else (void) path; + (void) algo; buf->data = NULL; buf->len = 0; @@ -788,19 +1048,40 @@ int crypt_load_pubkey_raw_file(const char * path, } int crypt_load_privkey_raw_file(const char * path, + const char * algo, void ** key) { *key = NULL; #ifdef HAVE_OPENSSL - return openssl_load_privkey_raw_file(path, key); + return openssl_load_privkey_raw_file(path, algo, key); #else (void) path; + (void) algo; return 0; #endif } +int crypt_ct_cmp(const void * a, + const void * b, + size_t len) +{ +#ifdef HAVE_OPENSSL + return CRYPTO_memcmp(a, b, len); +#else + const volatile uint8_t * pa = a; + const volatile uint8_t * pb = b; + uint8_t d = 0; + size_t i; + + for (i = 0; i < len; i++) + d |= pa[i] ^ pb[i]; + + return d != 0; +#endif +} + int crypt_cmp_key(const void * key1, const void * key2) { @@ -937,7 +1218,7 @@ int crypt_check_crt_name(void * crt, (void) crt; (void) name; - return 0; + return -ENOTSUP; #endif } @@ -967,9 +1248,15 @@ struct auth_ctx * auth_create_ctx(void) ctx->store = openssl_auth_create_store(); if (ctx->store == NULL) goto fail_store; + + ctx->chain = openssl_auth_create_chain(); + if (ctx->chain == NULL) + goto fail_chain; #endif return ctx; #ifdef HAVE_OPENSSL + fail_chain: + openssl_auth_destroy_store(ctx->store); fail_store: free(ctx); #endif @@ -982,6 +1269,7 @@ void auth_destroy_ctx(struct auth_ctx * ctx) if (ctx == NULL) return; #ifdef HAVE_OPENSSL + openssl_auth_destroy_chain(ctx->chain); openssl_auth_destroy_store(ctx->store); #endif free(ctx); @@ -1003,16 +1291,58 @@ int auth_add_crt_to_store(struct auth_ctx * ctx, #endif } +int auth_add_crt_to_chain(struct auth_ctx * ctx, + void * crt) +{ + assert(ctx != NULL); + assert(crt != NULL); + +#ifdef HAVE_OPENSSL + return openssl_auth_add_crt_to_chain(ctx->chain, crt); +#else + (void) ctx; + (void) crt; + + return 0; +#endif +} + int auth_verify_crt(struct auth_ctx * ctx, void * crt) { #ifdef HAVE_OPENSSL - return openssl_verify_crt(ctx->store, crt); + return openssl_verify_crt(ctx->store, ctx->chain, crt); #else (void) ctx; (void) crt; - return 0; + return -ENOTSUP; +#endif +} + +int auth_verify_crt_pin(struct auth_ctx * ctx, + void * crt, + void * pin) +{ +#ifdef HAVE_OPENSSL + return openssl_verify_crt_pin(ctx->store, ctx->chain, crt, pin); +#else + (void) ctx; + (void) crt; + (void) pin; + + return -ENOTSUP; +#endif +} + +bool crypt_pk_requires_md(const void * pk) +{ +#ifdef HAVE_OPENSSL + return openssl_pk_requires_md((const EVP_PKEY *) pk); +#else + (void) pk; + + return false; #endif } @@ -1048,7 +1378,7 @@ int auth_verify_sig(void * pk, (void) msg; (void) sig; - return 0; + return -ENOTSUP; #endif } @@ -1077,10 +1407,25 @@ ssize_t md_len(int md_nid) #endif } +int crypt_hkdf_expand(buffer_t key, + buffer_t info, + buffer_t out) +{ +#ifdef HAVE_OPENSSL + return openssl_hkdf_expand(key, info, out) == 0 ? 0 : -ECRYPT; +#else + (void) key; + (void) info; + (void) out; + + return -ECRYPT; +#endif +} + int crypt_secure_malloc_init(size_t max) { #ifdef HAVE_OPENSSL - return openssl_secure_malloc_init(max, SECMEM_GUARD); + return openssl_secure_malloc_init(max, SECMEM_MINSIZE); #else (void) max; return 0; diff --git a/src/lib/crypt/keyrot.c b/src/lib/crypt/keyrot.c new file mode 100644 index 00000000..e98df356 --- /dev/null +++ b/src/lib/crypt/keyrot.c @@ -0,0 +1,775 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Data-plane key-rotation schedule (node/leaf keys, selector) + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include <config.h> + +#include <ouroboros/atomics.h> +#include <ouroboros/crypt.h> +#include <ouroboros/errno.h> +#include <ouroboros/pthread.h> +#include <ouroboros/rcu.h> + +#include "crypt/keyrot.h" + +#include <assert.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +/* + * Per-flow keys are addressed by (epoch, node, leaf) and derived as: + * root = per-batch HKDF PRK from the OAP exchange, wiped once expanded + * nodes = HKDF-Expand(root, "o7s-keyrot-node") -> KEY_NODE_COUNT keys + * leaf = HKDF-Expand(node, "o7s-keyrot-leaf"|dir|leaf) -> AEAD key + * The epoch is a small wrapping counter, carried in the selector, that picks + * the live batch; a Tier-2 OAP re-key advances it. The "dir" byte forks the + * leaf keys per direction. + * + * Concurrency: cur/prev batch pointers are published by a re-key and read on + * the data path under an rcu_guard (lock-free RCU with liburcu, else a per- + * keyrot rwlock). The per-batch TX counter is atomic, so the (epoch, counter) + * nonce is unique without serialising TX. Leaf caches are THREAD-LOCAL (an app + * writer and the FRCT retransmit timer never share cache state), keyed on a + * global batch id and direct-mapped. + */ + +#define KR_WITHIN_BITS (KEY_LEAF_BITS + KEY_NODE_BITS) +#define KR_WITHIN_MASK (((uint64_t) 1 << KR_WITHIN_BITS) - 1) +#define KR_N (KEY_NODE_COUNT) +#define KR_LEAVES (1u << KEY_NODE_BITS) +#define KR_BATCH_MAX ((uint64_t) KR_N << KR_WITHIN_BITS) +#define KR_NODES_SZ ((size_t) KR_N * SYMMKEYSZ) +#define KR_TCACHE_WAYS 16 /* per-thread cache slots per direction (pow2) */ +#define KR_EPOCHS 16 /* 4-bit wire epoch: gens before wrap */ + +#define KR_RP_WORDS (KEY_REPLAY_WINDOW / 64) /* pow2; RFC 6479 bitmap */ +#define KR_RP_SHIFT 6 +#define KR_RP_MASK 63 +#define KR_RP_WINDOW (KEY_REPLAY_WINDOW - 64) /* reserve 1 slack word */ + +static const char kr_node_label[] = "o7s-keyrot-node"; +static const char kr_leaf_label[] = "o7s-keyrot-leaf"; + +struct kr_batch { + uint64_t id; /* process-global, unique; cache key (no ABA) */ + uint8_t epoch; /* 4-bit wire selector */ + uint8_t * nodes; /* KR_NODES_SZ in secure heap; NULL if empty */ + uint64_t tx_ctr; /* atomic; per-batch so nonces never collide */ + + struct { /* RFC 6479-like anti-replay window */ + uint64_t last; /* highest accepted ctr + 1 */ + uint64_t bits[KR_RP_WORDS]; + pthread_mutex_t mtx; + } rp; +}; + +struct kr_keycache { + uint8_t * key; /* SYMMKEYSZ, points into the per-thread slab */ + uint64_t id; /* batch the cached key belongs to */ + uint16_t node; + uint8_t leaf; + uint8_t dir; + bool valid; +}; + +struct keyrot { + struct kr_batch * cur; /* published; read on data path */ + struct kr_batch * prev; /* NULL = none */ + struct rcu_guard guard; /* re-key vs readers */ + uint8_t role; + uint8_t tx_epoch; /* epoch TX currently stamps */ + bool peer_switched; /* peer is on the cur epoch */ +}; + +/* Per-thread leaf-key caches, freed by the thread-exit destructor. */ +struct kr_tcache { + struct kr_keycache tx[KR_TCACHE_WAYS]; + struct kr_keycache rx[KR_TCACHE_WAYS]; + uint8_t * slab; /* 2*KR_TCACHE_WAYS*SYMMKEYSZ secure heap */ +}; + +static struct { + uint64_t next_id; /* batch-id allocator (atomic) */ + pthread_key_t tcache_key; /* per-thread leaf-key caches */ + pthread_once_t tcache_once; +} kr_g = { 0, 0, PTHREAD_ONCE_INIT }; + +static void kr_tcache_free(void * p) +{ + struct kr_tcache * t = p; + + if (t == NULL) + return; + + crypt_secure_free(t->slab, 2 * KR_TCACHE_WAYS * SYMMKEYSZ); + free(t); +} + +static void kr_tcache_init(void) +{ + pthread_key_create(&kr_g.tcache_key, kr_tcache_free); +} + +static struct kr_tcache * kr_tcache_get(void) +{ + struct kr_tcache * t; + size_t i; + + pthread_once(&kr_g.tcache_once, kr_tcache_init); + + t = pthread_getspecific(kr_g.tcache_key); + if (t != NULL) + return t; + + t = malloc(sizeof(*t)); + if (t == NULL) + goto fail_alloc; + + memset(t, 0, sizeof(*t)); + + t->slab = crypt_secure_malloc(2 * KR_TCACHE_WAYS * SYMMKEYSZ); + if (t->slab == NULL) + goto fail_slab; + + for (i = 0; i < KR_TCACHE_WAYS; i++) { + t->tx[i].key = t->slab + i * SYMMKEYSZ; + t->rx[i].key = t->slab + (KR_TCACHE_WAYS + i) * SYMMKEYSZ; + } + + if (pthread_setspecific(kr_g.tcache_key, t) != 0) + goto fail_set; + + return t; + + fail_set: + crypt_secure_free(t->slab, 2 * KR_TCACHE_WAYS * SYMMKEYSZ); + fail_slab: + free(t); + fail_alloc: + return NULL; +} + +static uint8_t * kr_expand_nodes(const uint8_t * root) +{ + uint8_t * nodes; + buffer_t prk; + buffer_t info; + buffer_t okm; + + nodes = crypt_secure_malloc(KR_NODES_SZ); + if (nodes == NULL) + return NULL; + + prk.len = SYMMKEYSZ; + prk.data = (uint8_t *) root; + info.len = sizeof(kr_node_label) - 1; + info.data = (uint8_t *) kr_node_label; + okm.len = KR_NODES_SZ; + okm.data = nodes; + + if (crypt_hkdf_expand(prk, info, okm) != 0) + goto fail_expand; + + return nodes; + + fail_expand: + crypt_secure_free(nodes, KR_NODES_SZ); + return NULL; +} + +static int kr_leaf_key(const uint8_t * node, + uint8_t leaf, + uint8_t dir, + uint8_t * out) +{ + uint8_t info_buf[sizeof(kr_leaf_label) - 1 + 2]; + buffer_t prk; + buffer_t info; + buffer_t okm; + size_t n = sizeof(kr_leaf_label) - 1; + + memcpy(info_buf, kr_leaf_label, n); + info_buf[n] = dir; + info_buf[n + 1] = leaf; + + prk.len = SYMMKEYSZ; + prk.data = (uint8_t *) node; + info.len = n + 2; + info.data = info_buf; + okm.len = SYMMKEYSZ; + okm.data = out; + + return crypt_hkdf_expand(prk, info, okm); +} + +static __inline__ bool kr_kc_hit(const struct kr_keycache * kc, + const struct kr_batch * b, + uint16_t node, + uint8_t leaf, + uint8_t dir) +{ + if (!kc->valid) + return false; + + if (kc->id != b->id) + return false; + + if (kc->node != node) + return false; + + if (kc->leaf != leaf) + return false; + + return kc->dir == dir; +} + +/* Fetch the leaf key; derive into the (direct-mapped) slot on a miss. */ +static const uint8_t * kr_kc_get(struct kr_keycache * cache, + const struct kr_batch * b, + uint16_t node, + uint8_t leaf, + uint8_t dir) +{ + struct kr_keycache * kc; + uint8_t * nkey; + + kc = &cache[b->id & (KR_TCACHE_WAYS - 1)]; + + if (kr_kc_hit(kc, b, node, leaf, dir)) + return kc->key; + + nkey = b->nodes + (size_t) node * SYMMKEYSZ; + if (kr_leaf_key(nkey, leaf, dir, kc->key) != 0) + return NULL; + + kc->valid = true; + kc->id = b->id; + kc->node = node; + kc->leaf = leaf; + kc->dir = dir; + + return kc->key; +} + +static void kr_sel_enc(uint8_t epoch, + uint16_t node, + uint32_t seq, + uint8_t sel[KR_SELECTOR_LEN]) +{ + sel[0] = (uint8_t) ((epoch << 4) | ((node >> 8) & 0x0F)); + sel[1] = (uint8_t) (node & 0xFF); + sel[2] = (uint8_t) (seq >> 24); + sel[3] = (uint8_t) (seq >> 16); + sel[4] = (uint8_t) (seq >> 8); + sel[5] = (uint8_t) (seq); +} + +static void kr_sel_dec(const uint8_t sel[KR_SELECTOR_LEN], + uint8_t * epoch, + uint16_t * node, + uint32_t * seq) +{ + *epoch = (uint8_t) (sel[0] >> 4); + *node = (uint16_t) (((sel[0] & 0x0F) << 8) | sel[1]); + *seq = ((uint32_t) sel[2] << 24) | ((uint32_t) sel[3] << 16) | + ((uint32_t) sel[4] << 8) | (uint32_t) sel[5]; +} + +static uint64_t kr_ctr(uint16_t node, + uint32_t seq) +{ + return ((uint64_t) node << KR_WITHIN_BITS) | + ((uint64_t) seq & KR_WITHIN_MASK); +} + +static void kr_nonce(uint64_t ctr, + uint8_t * nonce) +{ + size_t i; + + memset(nonce, 0, KR_NONCE_LEN); + + /* ctr big-endian in the low 8 bytes; high bytes stay zero */ + for (i = 0; i < 8; i++) + nonce[i] = (uint8_t) (ctr >> (56 - 8 * i)); +} + +static struct kr_batch * kr_batch_create(uint8_t epoch, + const uint8_t * root) +{ + struct kr_batch * b; + + b = malloc(sizeof(*b)); + if (b == NULL) + goto fail_alloc; + + b->nodes = kr_expand_nodes(root); + if (b->nodes == NULL) + goto fail_nodes; + + b->id = FETCH_ADD_RELAXED(&kr_g.next_id, 1); + b->epoch = epoch; + b->tx_ctr = 0; + if (pthread_mutex_init(&b->rp.mtx, NULL) != 0) + goto fail_lock; + + b->rp.last = 0; + memset(b->rp.bits, 0, sizeof(b->rp.bits)); + + return b; + + fail_lock: + crypt_secure_free(b->nodes, KR_NODES_SZ); + free(b); + return NULL; + fail_nodes: + free(b); + fail_alloc: + return NULL; +} + +static void kr_batch_free(struct kr_batch * b) +{ + if (b == NULL) + return; + + pthread_mutex_destroy(&b->rp.mtx); + crypt_secure_free(b->nodes, KR_NODES_SZ); + free(b); +} + +/* + * RFC 6479 anti-replay window keyed on the per-batch counter, with + * seq = ctr + 1 so 0 means "nothing accepted yet". Returns 0 if the + * packet is fresh (and records it), -1 on a replay or a too-old ctr. + */ +static int kr_rp_commit(struct kr_batch * b, + uint64_t ctr) +{ + uint64_t seq; + uint64_t idx; + uint64_t cur; + uint64_t diff; + + seq = ctr + 1; + + pthread_mutex_lock(&b->rp.mtx); + + if (seq > b->rp.last) { + idx = seq >> KR_RP_SHIFT; + cur = b->rp.last >> KR_RP_SHIFT; + diff = idx - cur; + if (diff > KR_RP_WORDS) + diff = KR_RP_WORDS; + + while (diff-- > 0) { + cur++; + b->rp.bits[cur & (KR_RP_WORDS - 1)] = 0; + } + + b->rp.bits[idx & (KR_RP_WORDS - 1)] |= + (uint64_t) 1 << (seq & KR_RP_MASK); + b->rp.last = seq; + goto finish; + } + + if (b->rp.last - seq >= KR_RP_WINDOW) + goto fail; + + idx = seq >> KR_RP_SHIFT; + if (b->rp.bits[idx & (KR_RP_WORDS - 1)] + & ((uint64_t) 1 << (seq & KR_RP_MASK))) + goto fail; + + b->rp.bits[idx & (KR_RP_WORDS - 1)] |= + (uint64_t) 1 << (seq & KR_RP_MASK); + finish: + pthread_mutex_unlock(&b->rp.mtx); + + return 0; + fail: + pthread_mutex_unlock(&b->rp.mtx); + + return -1; +} + +struct keyrot * keyrot_create(const uint8_t * root, + uint8_t epoch, + uint8_t role) +{ + struct keyrot * kr; + + assert(root != NULL); + assert(role <= 1); + + if (epoch >= KR_EPOCHS) + goto fail_kr; + + kr = malloc(sizeof(*kr)); + if (kr == NULL) + goto fail_kr; + + memset(kr, 0, sizeof(*kr)); + + kr->role = role; + kr->tx_epoch = epoch; + kr->peer_switched = true; + kr->prev = NULL; + + kr->cur = kr_batch_create(epoch, root); + if (kr->cur == NULL) + goto fail_cur; + + if (rcu_guard_init(&kr->guard)) + goto fail_guard; + + return kr; + + fail_guard: + kr_batch_free(kr->cur); + fail_cur: + free(kr); + fail_kr: + return NULL; +} + +void keyrot_destroy(struct keyrot * kr) +{ + if (kr == NULL) + return; + + /* Wait out any in-flight reader before freeing batches. */ + rcu_drain(&kr->guard); + + kr_batch_free(kr->cur); + kr_batch_free(kr->prev); + + rcu_guard_fini(&kr->guard); + + free(kr); +} + +/* A dup live epoch shadows straggler RX; epoch is peer-driven. */ +static struct kr_batch * kr_live_batch(struct kr_batch * cur, + struct kr_batch * prev, + uint8_t epoch) +{ + if (epoch == cur->epoch) + return cur; + + if (prev != NULL && epoch == prev->epoch) + return prev; + + return NULL; +} + +int keyrot_rekey(struct keyrot * kr, + const uint8_t * root, + uint8_t epoch) +{ + struct kr_batch * nb; + struct kr_batch * old_prev; + struct kr_batch * cur; + struct kr_batch * prev; + struct kr_batch * live; + int ret; + + assert(kr != NULL); + assert(root != NULL); + + if (epoch >= KR_EPOCHS) + return -1; + + nb = kr_batch_create(epoch, root); + if (nb == NULL) + return -1; + + rcu_wrlock(&kr->guard); + + cur = rcu_deref(kr->cur); + prev = rcu_deref(kr->prev); + + live = kr_live_batch(cur, prev, epoch); + if (live != NULL) { + /* The first node key identifies the root. */ + if (crypt_ct_cmp(live->nodes, nb->nodes, SYMMKEYSZ) == 0) + ret = -EREPLAY; + else + ret = -1; + rcu_wrunlock(&kr->guard); + kr_batch_free(nb); + return ret; + } + + old_prev = kr->prev; + rcu_assign(kr->prev, kr->cur); + rcu_publish(nb); + rcu_assign(kr->cur, nb); + + /* TX keeps the old epoch until the peer is seen on the new one. */ + STORE_RELEASE(&kr->peer_switched, false); + + rcu_wrunlock(&kr->guard); + + /* old_prev is unreachable now; reclaim past any live reader. */ + rcu_reclaim(&kr->guard); + kr_batch_free(old_prev); + + return 0; +} + +void keyrot_tx_promote(struct keyrot * kr) +{ + assert(kr != NULL); + + /* Serialise with keyrot_rekey so tx_epoch tracks a consistent cur. */ + rcu_wrlock(&kr->guard); + STORE_RELAXED(&kr->tx_epoch, rcu_deref(kr->cur)->epoch); + rcu_wrunlock(&kr->guard); +} + +int keyrot_tx_next(struct keyrot * kr, + uint8_t sel[KR_SELECTOR_LEN], + const uint8_t ** key, + uint8_t nonce[KR_NONCE_LEN]) +{ + struct kr_tcache * tc; + struct kr_batch * cur; + struct kr_batch * prev; + struct kr_batch * b; + uint64_t ctr; + uint16_t node; + uint8_t leaf; + uint8_t txe; + uint8_t epoch; + uint32_t seq; + const uint8_t * k; + + assert(kr != NULL); + assert(key != NULL); + + tc = kr_tcache_get(); + if (tc == NULL) + return -1; + + rcu_rdlock(&kr->guard); + + cur = rcu_deref(kr->cur); + prev = rcu_deref(kr->prev); + rcu_consume(cur); + rcu_consume(prev); + txe = LOAD_RELAXED(&kr->tx_epoch); + + if (cur->epoch == txe) + b = cur; + else if (prev != NULL && prev->epoch == txe) + b = prev; + else + b = NULL; + + if (b == NULL) { + rcu_rdunlock(&kr->guard); + return -1; /* tx_epoch batch gone; next promote resyncs */ + } + + /* Slot reserved even if exhausted; tx_nodes_left clamps the count. */ + ctr = FETCH_ADD_RELAXED(&b->tx_ctr, 1); + if (ctr >= KR_BATCH_MAX) { + rcu_rdunlock(&kr->guard); + return -1; /* batch exhausted */ + } + + node = (uint16_t) (ctr >> KR_WITHIN_BITS); + leaf = (uint8_t) ((ctr >> KEY_LEAF_BITS) & (KR_LEAVES - 1)); + seq = (uint32_t) (ctr & KR_WITHIN_MASK); + epoch = b->epoch; + + k = kr_kc_get(tc->tx, b, node, leaf, kr->role); + + rcu_rdunlock(&kr->guard); + + if (k == NULL) + return -1; + + kr_sel_enc(epoch, node, seq, sel); + kr_nonce(ctr, nonce); + + *key = k; + + return 0; +} + +int keyrot_rx_lookup(struct keyrot * kr, + const uint8_t sel[KR_SELECTOR_LEN], + const uint8_t ** key, + uint8_t nonce[KR_NONCE_LEN], + struct kr_rx * rx) +{ + struct kr_tcache * tc; + struct kr_batch * cur; + struct kr_batch * prev; + struct kr_batch * b; + uint8_t epoch; + uint16_t node; + uint32_t seq; + uint64_t ctr; + uint8_t leaf; + const uint8_t * k; + + assert(kr != NULL); + assert(key != NULL); + + kr_sel_dec(sel, &epoch, &node, &seq); + + if (node >= KR_N) + return -1; + + tc = kr_tcache_get(); + if (tc == NULL) + return -1; + + rcu_rdlock(&kr->guard); + + cur = rcu_deref(kr->cur); + prev = rcu_deref(kr->prev); + rcu_consume(cur); + rcu_consume(prev); + + if (epoch == cur->epoch) { + b = cur; + } else if (prev != NULL && epoch == prev->epoch) { + b = prev; + } else { + rcu_rdunlock(&kr->guard); + return -1; /* unknown epoch */ + } + + ctr = kr_ctr(node, seq); + leaf = (uint8_t) ((ctr >> KEY_LEAF_BITS) & (KR_LEAVES - 1)); + + /* peer's tx direction */ + k = kr_kc_get(tc->rx, b, node, leaf, (uint8_t) (kr->role ^ 1)); + + rx->id = b->id; + rx->ctr = ctr; + + rcu_rdunlock(&kr->guard); + + if (k == NULL) + return -1; + + kr_nonce(ctr, nonce); + + *key = k; + + return 0; +} + +/* + * Commit a packet that authenticated under the batch keyrot_rx_lookup + * selected. Re-finds that batch by id (epoch may have advanced) and, + * if still resident, advances the replay window and records that the + * peer is on the current batch. Runs only post-AEAD so a forged or + * replayed packet can mutate no receiver state. Returns -1 on replay. + */ +int keyrot_rx_commit(struct keyrot * kr, + const struct kr_rx * rx) +{ + struct kr_batch * cur; + struct kr_batch * prev; + struct kr_batch * b; + int rc; + + assert(kr != NULL); + assert(rx != NULL); + + rcu_rdlock(&kr->guard); + + cur = rcu_deref(kr->cur); + prev = rcu_deref(kr->prev); + rcu_consume(cur); + rcu_consume(prev); + + if (cur->id == rx->id) + b = cur; + else if (prev != NULL && prev->id == rx->id) + b = prev; + else + b = NULL; + + if (b == NULL) { + rcu_rdunlock(&kr->guard); + return 0; /* batch evicted post-auth; nothing to protect */ + } + + rc = kr_rp_commit(b, rx->ctr); + if (rc == 0 && b == cur) + STORE_RELEASE(&kr->peer_switched, true); + + rcu_rdunlock(&kr->guard); + + return rc; +} + +bool keyrot_peer_switched(const struct keyrot * kr) +{ + assert(kr != NULL); + + return LOAD_ACQUIRE(&kr->peer_switched); +} + +unsigned keyrot_tx_nodes_left(struct keyrot * kr) +{ + struct kr_batch * cur; + struct kr_batch * prev; + struct kr_batch * b; + uint64_t ctr; + unsigned used; + uint8_t txe; + + assert(kr != NULL); + + rcu_rdlock(&kr->guard); + cur = rcu_deref(kr->cur); + prev = rcu_deref(kr->prev); + rcu_consume(cur); + rcu_consume(prev); + txe = LOAD_RELAXED(&kr->tx_epoch); + + if (cur->epoch == txe) + b = cur; + else if (prev != NULL && prev->epoch == txe) + b = prev; + else + b = NULL; + + ctr = b != NULL ? LOAD_RELAXED(&b->tx_ctr) : KR_BATCH_MAX; + rcu_rdunlock(&kr->guard); + + used = (unsigned) (ctr >> KR_WITHIN_BITS); + if (used >= KR_N) + return 0; + + return KR_N - used; +} diff --git a/src/lib/crypt/keyrot.h b/src/lib/crypt/keyrot.h new file mode 100644 index 00000000..6a598f76 --- /dev/null +++ b/src/lib/crypt/keyrot.h @@ -0,0 +1,74 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Data-plane key-rotation schedule (node/leaf keys, selector) + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#ifndef OUROBOROS_LIB_CRYPT_KEYROT_H +#define OUROBOROS_LIB_CRYPT_KEYROT_H + +#include <ouroboros/crypt.h> /* SYMMKEYSZ, NONCESZ */ + +#include <stdbool.h> +#include <stdint.h> + +#define KR_SELECTOR_LEN 6 +#define KR_NONCE_LEN NONCESZ + +struct keyrot; + +struct kr_rx { + uint64_t id; /* batch id of the matched epoch */ + uint64_t ctr; /* packet counter for replay check */ +}; + +struct keyrot * keyrot_create(const uint8_t * root, + uint8_t epoch, + uint8_t role); + +void keyrot_destroy(struct keyrot * kr); + +int keyrot_rekey(struct keyrot * kr, + const uint8_t * root, + uint8_t epoch); + +/* Promote TX to the installed (new) batch once the peer is on it. */ +void keyrot_tx_promote(struct keyrot * kr); + +int keyrot_tx_next(struct keyrot * kr, + uint8_t sel[KR_SELECTOR_LEN], + const uint8_t ** key, + uint8_t nonce[KR_NONCE_LEN]); + +int keyrot_rx_lookup(struct keyrot * kr, + const uint8_t sel[KR_SELECTOR_LEN], + const uint8_t ** key, + uint8_t nonce[KR_NONCE_LEN], + struct kr_rx * rx); + +/* Commit an authenticated packet: replay window + peer-switched. */ +int keyrot_rx_commit(struct keyrot * kr, + const struct kr_rx * rx); + +/* True once an RX packet under the current batch has been observed. */ +bool keyrot_peer_switched(const struct keyrot * kr); + +unsigned keyrot_tx_nodes_left(struct keyrot * kr); + +#endif /* OUROBOROS_LIB_CRYPT_KEYROT_H */ diff --git a/src/lib/crypt/openssl.c b/src/lib/crypt/openssl.c index 5916e3cb..9c488b9d 100644 --- a/src/lib/crypt/openssl.c +++ b/src/lib/crypt/openssl.c @@ -30,11 +30,14 @@ #include <ouroboros/errno.h> #include <ouroboros/crypt.h> #include <ouroboros/hash.h> +#include <ouroboros/name.h> +#include <ouroboros/pthread.h> #include <ouroboros/random.h> #include <ouroboros/utils.h> #include <openssl/evp.h> #include <openssl/bio.h> +#include <openssl/crypto.h> #include <openssl/ec.h> #include <openssl/err.h> #include <openssl/kdf.h> @@ -45,6 +48,7 @@ #include <openssl/x509_vfy.h> #include <assert.h> +#include <limits.h> #include <stdio.h> #define IS_EC_GROUP(str) (strcmp(str, "EC") == 0) @@ -52,29 +56,43 @@ #define HKDF_INFO_DHE "o7s-ossl-dhe" #define HKDF_INFO_ENCAP "o7s-ossl-encap" -#define HKDF_INFO_ROTATION "o7s-key-rotation" #define HKDF_SALT_LEN 32 /* SHA-256 output size */ +#define AEAD_NONCE_LEN 12 /* 96-bit deterministic IV (SP 800-38D) */ +#define AEAD_TAG_LEN 16 /* 128-bit AEAD authentication tag */ +/* + * Holds only the cipher identity, which is fixed at creation. A flow's + * context is sealed and opened by several threads at once, so nothing + * here may carry per-packet state. + */ struct ossl_crypt_ctx { - EVP_CIPHER_CTX * evp_ctx; const EVP_CIPHER * cipher; - int ivsz; int tagsz; +}; + +/* + * Per-thread AEAD context. A key covers 2^KEY_LEAF_BITS packets, so + * the key schedule is worth keeping between them; only the nonce + * changes. Thread-local, so concurrent sealers share nothing, and a + * miss costs no more than a full install. + */ +struct ossl_aead { + EVP_CIPHER_CTX * evp; + const EVP_CIPHER * cipher; /* NULL when the state is unusable */ + uint8_t key[SYMMKEYSZ]; + size_t keylen; +}; - struct { - uint8_t * cur; /* current key */ - uint8_t * prv; /* rotated key */ - } keys; - - struct { - uint32_t cntr; /* counter */ - uint32_t mask; /* phase mask */ - uint32_t age; /* counter within epoch */ - uint8_t phase; /* current key phase */ - uint8_t salt[HKDF_SALT_LEN]; - } rot; /* rotation logic */ +struct ossl_aead_tls { + struct ossl_aead seal; + struct ossl_aead open; }; +static struct { + pthread_key_t key; + pthread_once_t once; +} aead_g = { 0, PTHREAD_ONCE_INIT }; + struct kdf_info { buffer_t secret; int nid; @@ -83,17 +101,6 @@ struct kdf_info { buffer_t key; }; -/* Key rotation macros */ -#define HAS_PHASE_BIT_TOGGLED(ctx) \ - (((ctx)->rot.cntr & (ctx)->rot.mask) != \ - (((ctx)->rot.cntr - 1) & (ctx)->rot.mask)) - -#define HAS_GRACE_EXPIRED(ctx) \ - ((ctx)->rot.age >= ((ctx)->rot.mask >> 1)) - -#define ROTATION_TOO_RECENT(ctx) \ - ((ctx)->rot.age < ((ctx)->rot.mask - ((ctx)->rot.mask >> 2))) - /* Convert hash NID to OpenSSL digest name string for HKDF */ static const char * hash_nid_to_digest_name(int nid) { @@ -102,11 +109,11 @@ static const char * hash_nid_to_digest_name(int nid) md = EVP_get_digestbynid(nid); if (md == NULL) - return "SHA256"; /* fallback to SHA-256 */ + return NULL; name = EVP_MD_get0_name(md); if (name == NULL) - return "SHA256"; /* fallback to SHA-256 */ + return NULL; return name; } @@ -144,21 +151,20 @@ static int get_pk_bytes_from_key(EVP_PKEY * key, } /* Derive salt from public key bytes by hashing them */ -static int derive_salt_from_pk_bytes(buffer_t pk, - uint8_t * salt, - size_t salt_len) +static int derive_salt_from_pk_bytes(buffer_t pk, + buffer_t salt) { uint8_t hash[EVP_MAX_MD_SIZE]; unsigned hash_len; assert(pk.data != NULL); - assert(salt != NULL); + assert(salt.data != NULL); if (EVP_Digest(pk.data, pk.len, hash, &hash_len, EVP_sha256(), NULL) != 1) goto fail_digest; - memcpy(salt, hash, salt_len < hash_len ? salt_len : hash_len); + memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len); return 0; fail_digest: @@ -166,10 +172,9 @@ static int derive_salt_from_pk_bytes(buffer_t pk, } /* Derive salt from two public key byte buffers (DHE) in canonical order */ -static int derive_salt_from_pk_bytes_dhe(buffer_t local, - buffer_t remote, - uint8_t * salt, - size_t salt_len) +static int derive_salt_from_pk_bytes_dhe(buffer_t local, + buffer_t remote, + buffer_t salt) { uint8_t * concat; size_t concat_len; @@ -180,7 +185,7 @@ static int derive_salt_from_pk_bytes_dhe(buffer_t local, assert(local.data != NULL); assert(remote.data != NULL); - assert(salt != NULL); + assert(salt.data != NULL); concat_len = local.len + remote.len; concat = OPENSSL_malloc(concat_len); @@ -204,7 +209,7 @@ static int derive_salt_from_pk_bytes_dhe(buffer_t local, OPENSSL_free(concat); - memcpy(salt, hash, salt_len < hash_len ? salt_len : hash_len); + memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len); return 0; fail_digest: @@ -225,6 +230,8 @@ static int derive_key_hkdf(struct kdf_info * ki) int idx; digest = hash_nid_to_digest_name(ki->nid); + if (digest == NULL) + goto fail_fetch; kdf = EVP_KDF_fetch(NULL, "HKDF", NULL); if (kdf == NULL) @@ -258,117 +265,258 @@ static int derive_key_hkdf(struct kdf_info * ki) return -ECRYPT; } -/* Key rotation helper functions implementation */ -static int should_rotate_key_rx(struct ossl_crypt_ctx * ctx, - uint8_t rx_phase) +int openssl_hkdf_expand(buffer_t key, + buffer_t info, + buffer_t out) { - assert(ctx != NULL); + EVP_KDF * kdf; + EVP_KDF_CTX * kctx; + OSSL_PARAM params[5]; + int mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + int idx = 0; + int ret = -1; + + kdf = EVP_KDF_fetch(NULL, "HKDF", NULL); + if (kdf == NULL) + goto fail_fetch; + + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) + goto fail_ctx; - /* Phase must have changed */ - if (rx_phase == ctx->rot.phase) - return 0; + params[idx++] = OSSL_PARAM_construct_utf8_string( + "digest", (char *) "SHA256", 0); + params[idx++] = OSSL_PARAM_construct_int("mode", &mode); + params[idx++] = OSSL_PARAM_construct_octet_string( + "key", key.data, key.len); + params[idx++] = OSSL_PARAM_construct_octet_string( + "info", info.data, info.len); + params[idx] = OSSL_PARAM_construct_end(); - if (ROTATION_TOO_RECENT(ctx)) - return 0; + if (EVP_KDF_derive(kctx, out.data, out.len, params) == 1) + ret = 0; - return 1; + EVP_KDF_CTX_free(kctx); + fail_ctx: + EVP_KDF_free(kdf); + fail_fetch: + return ret; } -static int rotate_key(struct ossl_crypt_ctx * ctx) +static void aead_tls_free(void * p) { - struct kdf_info ki; - uint8_t * tmp; + struct ossl_aead_tls * t = p; + if (t == NULL) + return; - assert(ctx != NULL); + EVP_CIPHER_CTX_free(t->seal.evp); + EVP_CIPHER_CTX_free(t->open.evp); - /* Swap keys - move current to prev */ - tmp = ctx->keys.prv; - ctx->keys.prv = ctx->keys.cur; + crypt_secure_clear(t->seal.key, SYMMKEYSZ); + crypt_secure_clear(t->open.key, SYMMKEYSZ); - if (tmp != NULL) { - /* Reuse old prev_key memory for new key */ - ctx->keys.cur = tmp; - } else { - /* First rotation - allocate new memory */ - ctx->keys.cur = OPENSSL_secure_malloc(SYMMKEYSZ); - if (ctx->keys.cur == NULL) - return -ECRYPT; + free(t); +} + +static void aead_tls_init(void) +{ + pthread_key_create(&aead_g.key, aead_tls_free); +} + +static struct ossl_aead_tls * aead_tls_get(void) +{ + struct ossl_aead_tls * t; + + pthread_once(&aead_g.once, aead_tls_init); + + t = pthread_getspecific(aead_g.key); + if (t != NULL) + return t; + + t = malloc(sizeof(*t)); + if (t == NULL) + return NULL; + + memset(t, 0, sizeof(*t)); + + if (pthread_setspecific(aead_g.key, t) != 0) { + free(t); + return NULL; } - /* Derive new key from previous key using HKDF */ - ki.secret.data = ctx->keys.prv; - ki.secret.len = SYMMKEYSZ; - ki.nid = NID_sha256; - ki.salt.data = ctx->rot.salt; - ki.salt.len = HKDF_SALT_LEN; - ki.info.data = (uint8_t *) HKDF_INFO_ROTATION; - ki.info.len = strlen(HKDF_INFO_ROTATION); - ki.key.data = ctx->keys.cur; - ki.key.len = SYMMKEYSZ; + return t; +} - if (derive_key_hkdf(&ki) != 0) - return -ECRYPT; +/* Install cipher and key; the nonce is set per packet by the caller. */ +static int aead_install(EVP_CIPHER_CTX * evp, + const EVP_CIPHER * cipher, + const uint8_t * key, + bool enc) +{ + EVP_CIPHER_CTX_reset(evp); + + if (enc) { + if (EVP_EncryptInit_ex(evp, cipher, NULL, NULL, NULL) != 1) + return -1; + } else { + if (EVP_DecryptInit_ex(evp, cipher, NULL, NULL, NULL) != 1) + return -1; + } - ctx->rot.age = 0; - ctx->rot.phase = !ctx->rot.phase; + /* Pin the AEAD nonce to 96 bits (SP 800-38D deterministic IV). */ + if (EVP_CIPHER_CTX_ctrl(evp, EVP_CTRL_AEAD_SET_IVLEN, + AEAD_NONCE_LEN, NULL) != 1) + return -1; + + if (enc) { + if (EVP_EncryptInit_ex(evp, NULL, NULL, key, NULL) != 1) + return -1; + } else { + if (EVP_DecryptInit_ex(evp, NULL, NULL, key, NULL) != 1) + return -1; + } return 0; } -static void cleanup_old_key(struct ossl_crypt_ctx * ctx) +/* This thread's context for cipher/key, ready to take a nonce. */ +static EVP_CIPHER_CTX * aead_ctx(struct ossl_aead * a, + const EVP_CIPHER * cipher, + const uint8_t * key, + bool enc) +{ + int keylen; + + keylen = EVP_CIPHER_get_key_length(cipher); + if (keylen <= 0 || (size_t) keylen > SYMMKEYSZ) + return NULL; + + /* Compare the bytes: a cache slot can be reused for a new key. */ + if (a->cipher == cipher && a->keylen == (size_t) keylen + && CRYPTO_memcmp(a->key, key, a->keylen) == 0) + return a->evp; + + if (a->evp == NULL) { + a->evp = EVP_CIPHER_CTX_new(); + if (a->evp == NULL) + return NULL; + } + + a->cipher = NULL; + if (aead_install(a->evp, cipher, key, enc) < 0) + return NULL; + + memcpy(a->key, key, (size_t) keylen); + + a->keylen = (size_t) keylen; + a->cipher = cipher; + + return a->evp; +} + +/* AEAD seal: encrypt in with key/nonce, bind aad, append tag */ +int openssl_seal(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + uint8_t * out, + uint8_t * tag) { + struct ossl_aead_tls * tls; + EVP_CIPHER_CTX * evp; + int out_sz; + int tmp_sz; + assert(ctx != NULL); + assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */ - if (ctx->keys.prv == NULL) - return; + tls = aead_tls_get(); + if (tls == NULL) + goto fail; - if (!HAS_GRACE_EXPIRED(ctx)) - return; + evp = aead_ctx(&tls->seal, ctx->cipher, key, true); + if (evp == NULL) + goto fail; + + if (EVP_EncryptInit_ex(evp, NULL, NULL, NULL, nonce) != 1) + goto fail_evp; + + if (EVP_EncryptUpdate(evp, NULL, &tmp_sz, aad.data, (int) aad.len) != 1) + goto fail_evp; + + if (EVP_EncryptUpdate(evp, out, &out_sz, in.data, (int) in.len) != 1) + goto fail_evp; + + if (EVP_EncryptFinal_ex(evp, out + out_sz, &tmp_sz) != 1) + goto fail_evp; + + out_sz += tmp_sz; + + if (EVP_CIPHER_CTX_ctrl(evp, EVP_CTRL_AEAD_GET_TAG, + ctx->tagsz, tag) != 1) + goto fail_evp; - OPENSSL_secure_clear_free(ctx->keys.prv, SYMMKEYSZ); - ctx->keys.prv = NULL; + return out_sz; + fail_evp: + tls->seal.cipher = NULL; /* state unknown; install afresh */ + fail: + return -1; } -static int try_decrypt(struct ossl_crypt_ctx * ctx, - uint8_t * key, - uint8_t * iv, - uint8_t * input, - int in_sz, - uint8_t * out, - int * out_sz) +/* AEAD open: decrypt in with key/nonce, verify aad and tag */ +int openssl_open(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + const uint8_t * tag, + buffer_t * out) { - uint8_t * tag; - int tmp_sz; - int ret; + struct ossl_aead_tls * tls; + EVP_CIPHER_CTX * evp; + int out_sz; + int tmp_sz; - tag = input + in_sz; + assert(ctx != NULL); + assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */ - EVP_CIPHER_CTX_reset(ctx->evp_ctx); + tls = aead_tls_get(); + if (tls == NULL) + goto fail; - ret = EVP_DecryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL, key, iv); - if (ret != 1) - return -1; + evp = aead_ctx(&tls->open, ctx->cipher, key, false); + if (evp == NULL) + goto fail; - if (ctx->tagsz > 0) { - ret = EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_TAG, - ctx->tagsz, tag); - if (ret != 1) - return -1; - } + if (EVP_DecryptInit_ex(evp, NULL, NULL, NULL, nonce) != 1) + goto fail_evp; - ret = EVP_DecryptUpdate(ctx->evp_ctx, out, &tmp_sz, input, in_sz); - if (ret != 1) - return -1; + if (EVP_CIPHER_CTX_ctrl(evp, EVP_CTRL_AEAD_SET_TAG, + ctx->tagsz, (void *) tag) != 1) + goto fail_evp; - *out_sz = tmp_sz; + if (EVP_DecryptUpdate(evp, NULL, &tmp_sz, aad.data, (int) aad.len) != 1) + goto fail_evp; - ret = EVP_DecryptFinal_ex(ctx->evp_ctx, out + tmp_sz, &tmp_sz); - if (ret != 1) - return -1; + if (EVP_DecryptUpdate(evp, out->data, &out_sz, + in.data, (int) in.len) != 1) + goto fail_evp; - *out_sz += tmp_sz; + /* A failed verify leaves defined state; keep the key cached. */ + if (EVP_DecryptFinal_ex(evp, out->data + out_sz, &tmp_sz) != 1) + goto fail_verify; - return 0; + out_sz += tmp_sz; + + out->len = (size_t) out_sz; + + return out_sz; + fail_evp: + tls->open.cipher = NULL; /* state unknown; install afresh */ + fail_verify: + fail: + return -1; } /* @@ -396,11 +544,14 @@ static int __openssl_dhe_derive(EVP_PKEY * pkp, ret = i2d_PUBKEY(pkp, &local_pk.data); if (ret <= 0) goto fail_local; + local_pk.len = (size_t) ret; + ki.salt.len = HKDF_SALT_LEN; + ki.salt.data = salt_buf; + /* Derive salt from both public keys */ - if (derive_salt_from_pk_bytes_dhe(local_pk, remote_pk, salt_buf, - HKDF_SALT_LEN) < 0) + if (derive_salt_from_pk_bytes_dhe(local_pk, remote_pk, ki.salt) < 0) goto fail_salt; ctx = EVP_PKEY_CTX_new(pkp, NULL); @@ -437,13 +588,11 @@ static int __openssl_dhe_derive(EVP_PKEY * pkp, ki.info.data = (uint8_t *) HKDF_INFO_DHE; ki.key.len = SYMMKEYSZ; ki.key.data = s; - ki.salt.len = HKDF_SALT_LEN; - ki.salt.data = salt_buf; /* Derive symmetric key from shared secret using HKDF */ ret = derive_key_hkdf(&ki); - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); EVP_PKEY_CTX_free(ctx); OPENSSL_free(local_pk.data); @@ -452,7 +601,7 @@ static int __openssl_dhe_derive(EVP_PKEY * pkp, return 0; fail_derive: - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); fail_ctx: EVP_PKEY_CTX_free(ctx); fail_salt: @@ -573,23 +722,6 @@ static int __openssl_kem_gen_key(const char * algo, return -ECRYPT; } -/* Determine hybrid KEM algorithm from raw key/ciphertext length */ -static const char * __openssl_hybrid_algo_from_len(size_t len) -{ - switch(len) { - case X25519MLKEM768_PKSZ: - return "X25519MLKEM768"; - case X25519MLKEM768_CTSZ: - return "X25519MLKEM768"; - case X448MLKEM1024_PKSZ: - return "X448MLKEM1024"; - default: - break; - } - - return NULL; -} - static int __openssl_kex_gen_key(const char * algo, EVP_PKEY ** kp) { @@ -624,14 +756,22 @@ ssize_t openssl_pkp_create(const char * algo, if (raw.len == 0) goto fail_pubkey; + if (raw.len > CRYPT_KEY_BUFSZ) { + OPENSSL_free(raw.data); + goto fail_pubkey; + } + memcpy(pk, raw.data, raw.len); OPENSSL_free(raw.data); return (ssize_t) raw.len; } else { /* DER encode standard algorithms */ + len = i2d_PUBKEY(*pkp, NULL); /* pre-flight length */ + if (len < 0 || len > CRYPT_KEY_BUFSZ) + goto fail_pubkey; + pos = pk; /* i2d_PUBKEY increments the ptr, don't use pk! */ - len = i2d_PUBKEY(*pkp, &pos); - if (len < 0) + if (i2d_PUBKEY(*pkp, &pos) < 0) goto fail_pubkey; return len; @@ -692,7 +832,7 @@ static ssize_t __openssl_kem_encap(EVP_PKEY * pub, /* Derive symmetric key from shared secret using HKDF */ ret = derive_key_hkdf(&ki); - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); EVP_PKEY_CTX_free(ctx); if (ret != 0) @@ -701,7 +841,7 @@ static ssize_t __openssl_kem_encap(EVP_PKEY * pub, return (ssize_t) ct_len; fail_secret: - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); fail_encap: EVP_PKEY_CTX_free(ctx); fail_ctx: @@ -717,13 +857,17 @@ ssize_t openssl_kem_encap(buffer_t pk, EVP_PKEY * pub; uint8_t * pos; uint8_t salt[HKDF_SALT_LEN]; + buffer_t salt_b; ssize_t ret; assert(pk.data != NULL); assert(ct != NULL); assert(s != NULL); - if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0) + salt_b.len = HKDF_SALT_LEN; + salt_b.data = salt; + + if (derive_salt_from_pk_bytes(pk, salt_b) < 0) goto fail_salt; pos = pk.data; @@ -740,26 +884,27 @@ ssize_t openssl_kem_encap(buffer_t pk, return -ECRYPT; } -/* Hybrid KEM encapsulation: raw-encoded public key */ -ssize_t openssl_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf, - uint8_t * s) +/* Hybrid KEM encapsulation: NID-tagged raw-encoded public key */ +ssize_t openssl_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf, + uint8_t * s) { - EVP_PKEY * pub; - const char * algo; - uint8_t salt[HKDF_SALT_LEN]; - ssize_t ret; + EVP_PKEY * pub; + uint8_t salt[HKDF_SALT_LEN]; + buffer_t salt_b; + ssize_t ret; + assert(algo != NULL); assert(pk.data != NULL); assert(ct != NULL); assert(s != NULL); - if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0) - goto fail_salt; + salt_b.len = HKDF_SALT_LEN; + salt_b.data = salt; - algo = __openssl_hybrid_algo_from_len(pk.len); - if (algo == NULL) + if (derive_salt_from_pk_bytes(pk, salt_b) < 0) goto fail_salt; pub = EVP_PKEY_new_raw_public_key_ex(NULL, algo, NULL, @@ -789,12 +934,16 @@ int openssl_kem_decap(EVP_PKEY * priv, size_t secret_len; int ret; uint8_t salt[HKDF_SALT_LEN]; + buffer_t salt_b; /* Extract public key bytes from private key */ if (get_pk_bytes_from_key(priv, &pk) < 0) goto fail_pk; - if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0) + salt_b.len = HKDF_SALT_LEN; + salt_b.data = salt; + + if (derive_salt_from_pk_bytes(pk, salt_b) < 0) goto fail_salt; ctx = EVP_PKEY_CTX_new(priv, NULL); @@ -833,7 +982,7 @@ int openssl_kem_decap(EVP_PKEY * priv, /* Derive symmetric key from shared secret using HKDF */ ret = derive_key_hkdf(&ki); - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); EVP_PKEY_CTX_free(ctx); OPENSSL_free(pk.data); @@ -843,7 +992,7 @@ int openssl_kem_decap(EVP_PKEY * priv, return 0; fail_secret: - OPENSSL_free(secret); + OPENSSL_clear_free(secret, secret_len); fail_ctx: EVP_PKEY_CTX_free(ctx); fail_salt: @@ -857,13 +1006,14 @@ void openssl_pkp_destroy(EVP_PKEY * pkp) EVP_PKEY_free(pkp); } -int __openssl_get_curve(EVP_PKEY * pub, - char * algo) +static int openssl_get_curve(EVP_PKEY * pub, + char * algo) { int ret; size_t len = KEX_ALGO_BUFSZ; ret = EVP_PKEY_get_utf8_string_param(pub, "group", algo, len, &len); + return ret == 1 ? 0 : -ECRYPT; } @@ -888,9 +1038,10 @@ int openssl_get_algo_from_pk_der(buffer_t pk, strcpy(algo, type_str); - if ((IS_EC_GROUP(algo) || IS_DH_GROUP(algo)) && - __openssl_get_curve(pub, algo) < 0) - goto fail_pub; + if (IS_EC_GROUP(algo) || IS_DH_GROUP(algo)) { + if (openssl_get_curve(pub, algo) < 0) + goto fail_pub; + } EVP_PKEY_free(pub); return 0; @@ -901,30 +1052,14 @@ int openssl_get_algo_from_pk_der(buffer_t pk, return -ECRYPT; } -int openssl_get_algo_from_pk_raw(buffer_t pk, - char * algo) -{ - const char * hybrid_algo; - - assert(pk.data != NULL); - assert(algo != NULL); - - hybrid_algo = __openssl_hybrid_algo_from_len(pk.len); - if (hybrid_algo == NULL) - return -ECRYPT; - - strcpy(algo, hybrid_algo); - - return 0; -} - int openssl_dhe_derive(EVP_PKEY * pkp, buffer_t pk, int kdf, uint8_t * s) { - uint8_t * pos; - EVP_PKEY * pub; + uint8_t * pos; + EVP_PKEY * pub; + const char * name; assert(pkp != NULL); assert(pk.data != NULL); @@ -936,6 +1071,11 @@ int openssl_dhe_derive(EVP_PKEY * pkp, if (pub == NULL) goto fail_decode; + /* A peer key of another type must not reach the derivation */ + name = EVP_PKEY_get0_type_name(pkp); + if (name == NULL || EVP_PKEY_is_a(pub, name) != 1) + goto fail_derive; + if (__openssl_dhe_derive(pkp, pub, pk, kdf, s) < 0) goto fail_derive; @@ -948,141 +1088,110 @@ int openssl_dhe_derive(EVP_PKEY * pkp, return -ECRYPT; } -int openssl_encrypt(struct ossl_crypt_ctx * ctx, - buffer_t in, - buffer_t * out) +/* Set up a fresh AEAD cipher ctx for nid: reject non-AEAD / oversized IV. */ +static int ossl_cipher_ctx_init(struct ossl_crypt_ctx * ctx, + int nid) { - uint8_t * ptr; - uint8_t * iv; - int in_sz; - int out_sz; - int tmp_sz; - int ret; - - assert(ctx != NULL); - - in_sz = (int) in.len; - - out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + \ - ctx->ivsz + ctx->tagsz); - if (out->data == NULL) - goto fail_malloc; - - iv = out->data; - ptr = out->data + ctx->ivsz; - - if (random_buffer(iv, ctx->ivsz) < 0) - goto fail_encrypt; - - /* Set IV bit 7 to current key phase (KEY_ROTATION_BIT of counter) */ - if (ctx->rot.cntr & ctx->rot.mask) - iv[0] |= 0x80; - else - iv[0] &= 0x7F; + ctx->cipher = EVP_get_cipherbynid(nid); + if (ctx->cipher == NULL) + return -1; - EVP_CIPHER_CTX_reset(ctx->evp_ctx); + /* IV must fit the NONCESZ nonce buffer. */ + if (EVP_CIPHER_get_iv_length(ctx->cipher) > NONCESZ) + return -1; - ret = EVP_EncryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL, - ctx->keys.cur, iv); - if (ret != 1) - goto fail_encrypt; + /* Authenticated encryption is mandatory; reject non-AEAD ciphers. */ + if ((EVP_CIPHER_flags(ctx->cipher) & EVP_CIPH_FLAG_AEAD_CIPHER) == 0) + return -1; - ret = EVP_EncryptUpdate(ctx->evp_ctx, ptr, &tmp_sz, in.data, in_sz); - if (ret != 1) - goto fail_encrypt; + ctx->tagsz = AEAD_TAG_LEN; - out_sz = tmp_sz; - ret = EVP_EncryptFinal_ex(ctx->evp_ctx, ptr + tmp_sz, &tmp_sz); - if (ret != 1) - goto fail_encrypt; + return 0; +} - out_sz += tmp_sz; +/* One-shot AEAD seal over an explicit key/nonce (no keyrot). out = ct ‖ tag. */ +int openssl_oneshot_seal(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out) +{ + struct ossl_crypt_ctx ctx; + int out_sz; - /* For AEAD ciphers, get and append the authentication tag */ - if (ctx->tagsz > 0) { - ret = EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_GET_TAG, - ctx->tagsz, ptr + out_sz); - if (ret != 1) - goto fail_encrypt; - out_sz += ctx->tagsz; - } + assert(key != NULL); + assert(nonce != NULL); + assert(out != NULL); - assert(out_sz >= in_sz); + memset(&ctx, 0, sizeof(ctx)); - out->len = (size_t) out_sz + ctx->ivsz; + if (ossl_cipher_ctx_init(&ctx, nid) < 0) + goto fail_cipher; - /* Increment packet counter and check for key rotation */ - ctx->rot.cntr++; - ctx->rot.age++; + out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + ctx.tagsz); + if (out->data == NULL) + goto fail_cipher; - if (HAS_PHASE_BIT_TOGGLED(ctx)) { - if (rotate_key(ctx) != 0) - goto fail_encrypt; - } + out_sz = openssl_seal(&ctx, key, nonce, aad, in, + out->data, out->data + in.len); + if (out_sz < 0) + goto fail_seal; - cleanup_old_key(ctx); + out->len = (size_t) out_sz + ctx.tagsz; return 0; - fail_encrypt: + + fail_seal: free(out->data); - fail_malloc: + fail_cipher: clrbuf(*out); return -ECRYPT; } -int openssl_decrypt(struct ossl_crypt_ctx * ctx, - buffer_t in, - buffer_t * out) +/* One-shot AEAD open; in = ct ‖ tag, verifies aad and tag. */ +int openssl_oneshot_open(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out) { - uint8_t * iv; - uint8_t * input; - uint8_t rx_phase; - int out_sz; - int in_sz; - - assert(ctx != NULL); + struct ossl_crypt_ctx ctx; + buffer_t ct; + const uint8_t * tag; + int in_sz; - in_sz = (int) in.len - ctx->ivsz; - if (in_sz < ctx->tagsz) - return -ECRYPT; - - in_sz -= ctx->tagsz; - - out->data = malloc(in_sz + EVP_MAX_BLOCK_LENGTH); - if (out->data == NULL) - goto fail_malloc; + assert(key != NULL); + assert(nonce != NULL); + assert(out != NULL); - iv = in.data; - input = in.data + ctx->ivsz; + memset(&ctx, 0, sizeof(ctx)); - /* Extract phase from IV bit 7 and check for key rotation */ - rx_phase = (iv[0] & 0x80) ? 1 : 0; + if (ossl_cipher_ctx_init(&ctx, nid) < 0) + goto fail_cipher; - if (should_rotate_key_rx(ctx, rx_phase)) { - if (rotate_key(ctx) != 0) - goto fail_decrypt; - } + if (in.len < (size_t) ctx.tagsz) + goto fail_cipher; - ctx->rot.cntr++; - ctx->rot.age++; + in_sz = (int) in.len - ctx.tagsz; - if (try_decrypt(ctx, ctx->keys.cur, iv, input, in_sz, out->data, - &out_sz) != 0) { - if (ctx->keys.prv == NULL) - goto fail_decrypt; - if (try_decrypt(ctx, ctx->keys.prv, iv, input, in_sz, - out->data, &out_sz) != 0) - goto fail_decrypt; - } + out->data = malloc((size_t) in_sz + EVP_MAX_BLOCK_LENGTH); + if (out->data == NULL) + goto fail_cipher; - assert(out_sz <= in_sz); + ct.data = in.data; + ct.len = (size_t) in_sz; + tag = in.data + in_sz; - out->len = (size_t) out_sz; + if (openssl_open(&ctx, key, nonce, aad, ct, tag, out) < 0) + goto fail_open; return 0; - fail_decrypt: + + fail_open: free(out->data); - fail_malloc: + fail_cipher: clrbuf(*out); return -ECRYPT; } @@ -1093,51 +1202,19 @@ struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk) assert(sk != NULL); assert(sk->key != NULL); - assert(sk->rot_bit > 0 && sk->rot_bit < 32); ctx = malloc(sizeof(*ctx)); if (ctx == NULL) - goto fail_malloc; + goto fail_malloc; memset(ctx, 0, sizeof(*ctx)); - ctx->keys.cur = OPENSSL_secure_malloc(SYMMKEYSZ); - if (ctx->keys.cur == NULL) - goto fail_key; - - memcpy(ctx->keys.cur, sk->key, SYMMKEYSZ); - - ctx->keys.prv = NULL; - - /* Derive rotation salt from initial shared secret */ - if (EVP_Digest(sk->key, SYMMKEYSZ, ctx->rot.salt, NULL, - EVP_sha256(), NULL) != 1) - goto fail_cipher; - - ctx->cipher = EVP_get_cipherbynid(sk->nid); - if (ctx->cipher == NULL) - goto fail_cipher; - - ctx->ivsz = EVP_CIPHER_iv_length(ctx->cipher); - - /* Set tag size for AEAD ciphers (GCM, CCM, OCB, ChaCha20-Poly1305) */ - if (EVP_CIPHER_flags(ctx->cipher) & EVP_CIPH_FLAG_AEAD_CIPHER) - ctx->tagsz = 16; /* Standard AEAD tag length (128 bits) */ - - ctx->rot.cntr = 0; - ctx->rot.mask = (1U << sk->rot_bit); - ctx->rot.age = 0; - ctx->rot.phase = 0; - - ctx->evp_ctx = EVP_CIPHER_CTX_new(); - if (ctx->evp_ctx == NULL) + if (ossl_cipher_ctx_init(ctx, sk->nid) < 0) goto fail_cipher; return ctx; fail_cipher: - OPENSSL_secure_clear_free(ctx->keys.cur, SYMMKEYSZ); - fail_key: free(ctx); fail_malloc: return NULL; @@ -1148,23 +1225,9 @@ void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx) if (ctx == NULL) return; - if (ctx->keys.cur != NULL) - OPENSSL_secure_clear_free(ctx->keys.cur, SYMMKEYSZ); - - if (ctx->keys.prv != NULL) - OPENSSL_secure_clear_free(ctx->keys.prv, SYMMKEYSZ); - - EVP_CIPHER_CTX_free(ctx->evp_ctx); free(ctx); } -int openssl_crypt_get_ivsz(struct ossl_crypt_ctx * ctx) -{ - assert(ctx != NULL); - - return ctx->ivsz; -} - int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx) { assert(ctx != NULL); @@ -1184,7 +1247,12 @@ int openssl_load_crt_file(const char * path, if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + xcrt = PEM_read_X509(fp, NULL, NULL, NULL); + + pthread_cleanup_pop(false); + if (xcrt == NULL) goto fail_crt; @@ -1200,35 +1268,58 @@ int openssl_load_crt_file(const char * path, return -1; } -int openssl_load_crt_str(const char * str, - void ** crt) +static void * rd_crt_bio(BIO * bio) +{ + return PEM_read_bio_X509(bio, NULL, NULL, NULL); +} + +static void * rd_privkey_bio(BIO * bio) +{ + return PEM_read_bio_PrivateKey(bio, NULL, NULL, ""); +} + +static void * rd_pubkey_bio(BIO * bio) +{ + return PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); +} + +/* Decode a PEM object from an in-memory string via rd. */ +static int load_pem_str(const char * str, + void * (* rd)(BIO *), + void ** out) { BIO * bio; - X509 * xcrt; + void * obj; bio = BIO_new(BIO_s_mem()); if (bio == NULL) goto fail_bio; if (BIO_write(bio, str, strlen(str)) < 0) - goto fail_crt; + goto fail_obj; - xcrt = PEM_read_bio_X509(bio, NULL, NULL, NULL); - if (xcrt == NULL) - goto fail_crt; + obj = rd(bio); + if (obj == NULL) + goto fail_obj; BIO_free(bio); - *crt = (void *) xcrt; + *out = obj; return 0; - fail_crt: + fail_obj: BIO_free(bio); fail_bio: - *crt = NULL; + *out = NULL; return -1; } +int openssl_load_crt_str(const char * str, + void ** crt) +{ + return load_pem_str(str, rd_crt_bio, crt); +} + int openssl_load_crt_der(buffer_t buf, void ** crt) { @@ -1288,7 +1379,12 @@ int openssl_load_privkey_file(const char * path, if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + pkey = PEM_read_PrivateKey(fp, NULL, NULL, ""); + + pthread_cleanup_pop(false); + if (pkey == NULL) goto fail_key; @@ -1307,30 +1403,7 @@ int openssl_load_privkey_file(const char * path, int openssl_load_privkey_str(const char * str, void ** key) { - BIO * bio; - EVP_PKEY * pkey; - - bio = BIO_new(BIO_s_mem()); - if (bio == NULL) - goto fail_bio; - - if (BIO_write(bio, str, strlen(str)) < 0) - goto fail_key; - - pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); - if (pkey == NULL) - goto fail_key; - - BIO_free(bio); - - *key = (void *) pkey; - - return 0; - fail_key: - BIO_free(bio); - fail_bio: - *key = NULL; - return -1; + return load_pem_str(str, rd_privkey_bio, key); } int openssl_load_pubkey_file(const char * path, @@ -1343,7 +1416,12 @@ int openssl_load_pubkey_file(const char * path, if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + pkey = PEM_read_PUBKEY(fp, NULL, NULL, NULL); + + pthread_cleanup_pop(false); + if (pkey == NULL) goto fail_key; @@ -1375,7 +1453,12 @@ int openssl_load_pubkey_file_to_der(const char * path, if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + pkey = PEM_read_PUBKEY(fp, NULL, NULL, NULL); + + pthread_cleanup_pop(false); + if (pkey == NULL) goto fail_key; @@ -1402,56 +1485,45 @@ int openssl_load_pubkey_file_to_der(const char * path, int openssl_load_pubkey_str(const char * str, void ** key) { - BIO * bio; - EVP_PKEY * pkey; - - bio = BIO_new(BIO_s_mem()); - if (bio == NULL) - goto fail_bio; - - if (BIO_write(bio, str, strlen(str)) < 0) - goto fail_key; - - pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); - if (pkey == NULL) - goto fail_key; - - BIO_free(bio); - - *key = (void *) pkey; - - return 0; - fail_key: - BIO_free(bio); - fail_bio: - *key = NULL; - return -1; + return load_pem_str(str, rd_pubkey_bio, key); } int openssl_load_pubkey_raw_file(const char * path, + const char * algo, buffer_t * buf) { - FILE * fp; - uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; - size_t bytes_read; - const char * algo; + FILE * fp; + uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; + size_t bytes_read; + EVP_PKEY * chk; assert(path != NULL); + assert(algo != NULL); assert(buf != NULL); fp = fopen(path, "rb"); if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + bytes_read = fread(tmp_buf, 1, CRYPT_KEY_BUFSZ, fp); - if (bytes_read == 0) + + pthread_cleanup_pop(false); + + /* A full buffer means the file was truncated */ + if (bytes_read == 0 || bytes_read == CRYPT_KEY_BUFSZ) goto fail_read; - /* Validate that this is a known hybrid KEM format */ - algo = __openssl_hybrid_algo_from_len(bytes_read); - if (algo == NULL) + /* Trial import: reject bad keys at load time */ + chk = EVP_PKEY_new_raw_public_key_ex(NULL, algo, NULL, + tmp_buf, bytes_read); + + if (chk == NULL) goto fail_read; + EVP_PKEY_free(chk); + buf->data = malloc(bytes_read); if (buf->data == NULL) goto fail_malloc; @@ -1470,44 +1542,38 @@ int openssl_load_pubkey_raw_file(const char * path, return -1; } -/* Determine hybrid KEM algorithm from raw private key length */ -static const char * __openssl_hybrid_algo_from_sk_len(size_t len) +/* Wipe the raw-key staging buffer if a cancel aborts the read. */ +static void __cleanse_key_buf(void * o) { - switch(len) { - case X25519MLKEM768_SKSZ: - return "X25519MLKEM768"; - case X448MLKEM1024_SKSZ: - return "X448MLKEM1024"; - default: - break; - } - - return NULL; + OPENSSL_cleanse(o, CRYPT_KEY_BUFSZ); } int openssl_load_privkey_raw_file(const char * path, + const char * algo, void ** key) { - FILE * fp; - uint8_t tmp_buf[4096]; - size_t bytes_read; - const char * algo; - EVP_PKEY * pkey; + FILE * fp; + uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; + size_t bytes_read; + EVP_PKEY * pkey; assert(path != NULL); + assert(algo != NULL); assert(key != NULL); fp = fopen(path, "rb"); if (fp == NULL) goto fail_file; + pthread_cleanup_push(__cleanup_fclose, fp); + pthread_cleanup_push(__cleanse_key_buf, tmp_buf); + bytes_read = fread(tmp_buf, 1, sizeof(tmp_buf), fp); - if (bytes_read == 0) - goto fail_read; - /* Determine algorithm from key size */ - algo = __openssl_hybrid_algo_from_sk_len(bytes_read); - if (algo == NULL) + pthread_cleanup_pop(false); + pthread_cleanup_pop(false); + + if (bytes_read == 0) goto fail_read; pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algo, NULL, @@ -1549,68 +1615,88 @@ void openssl_free_key(EVP_PKEY * key) EVP_PKEY_free(key); } +/* ASN1_STRING_length is deprecated in OpenSSL 4.1, and returns size_t */ +static int ossl_asn1_str_len(const ASN1_STRING * val) +{ +#ifdef HAVE_OPENSSL_4_1 + size_t len; + + len = ASN1_STRING_get_length(val); + + return len > INT_MAX ? -1 : (int) len; +#else + return ASN1_STRING_length(val); +#endif +} + int openssl_check_crt_name(void * crt, const char * name) { - char * subj; - char * cn; - X509 * xcrt; + const unsigned char * cn; + const ASN1_STRING * val; + const X509_NAME * nm; + int idx; + int len; - xcrt = (X509 *) crt; + nm = X509_get_subject_name((X509 *) crt); + if (nm == NULL) + return -1; - subj = X509_NAME_oneline(X509_get_subject_name(xcrt), NULL, 0); - if (subj == NULL) - goto fail_subj; + idx = X509_NAME_get_index_by_NID(nm, NID_commonName, -1); + if (idx < 0) + return -1; - cn = strstr(subj, "CN="); - if (cn == NULL) - goto fail_cn; + val = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(nm, idx)); + cn = ASN1_STRING_get0_data(val); + len = ossl_asn1_str_len(val); - if (strcmp(cn + 3, name) != 0) - goto fail_cn; + if (len < 0 || (size_t) len != strlen(name)) + return -1; - free(subj); + if (memchr(cn, '\0', (size_t) len) != NULL) + return -1; + + if (memcmp(cn, name, (size_t) len) != 0) + return -1; return 0; - fail_cn: - free(subj); - fail_subj: - return -1; } int openssl_get_crt_name(void * crt, char * name) { - char * subj; - char * cn; - char * end; - X509 * xcrt; + const unsigned char * cn; + const ASN1_STRING * val; + const X509_NAME * nm; + int idx; + int len; - xcrt = (X509 *) crt; + nm = X509_get_subject_name((X509 *) crt); + if (nm == NULL) + return -1; + + idx = X509_NAME_get_index_by_NID(nm, NID_commonName, -1); + if (idx < 0) + return -1; - subj = X509_NAME_oneline(X509_get_subject_name(xcrt), NULL, 0); - if (subj == NULL) - goto fail_subj; + val = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(nm, idx)); + cn = ASN1_STRING_get0_data(val); + len = ossl_asn1_str_len(val); - cn = strstr(subj, "CN="); - if (cn == NULL) - goto fail_cn; + if (len < 0) + return -1; - cn += 3; /* Skip "CN=" */ + if ((size_t) len > NAME_SIZE) + return -ENAME; - /* Find end of CN (comma or slash for next field) */ - end = strpbrk(cn, ",/"); - if (end != NULL) - *end = '\0'; + /* Reject an embedded NUL that would truncate the parsed name. */ + if (memchr(cn, '\0', (size_t) len) != NULL) + return -1; - strcpy(name, cn); - free(subj); + memcpy(name, cn, (size_t) len); + name[len] = '\0'; return 0; - fail_cn: - free(subj); - fail_subj: - return -1; } int openssl_crt_str(const void * crt, @@ -1695,12 +1781,43 @@ int openssl_auth_add_crt_to_store(void * store, return ret == 1 ? 0 : -1; } -int openssl_verify_crt(void * store, - void * crt) +void * openssl_auth_create_chain(void) +{ + return sk_X509_new_null(); +} + +void openssl_auth_destroy_chain(void * chain) +{ + sk_X509_pop_free((STACK_OF(X509) *) chain, X509_free); +} + +int openssl_auth_add_crt_to_chain(void * chain, + void * crt) +{ + if (X509_up_ref((X509 *) crt) != 1) + goto fail_ref; + + if (sk_X509_push((STACK_OF(X509) *) chain, (X509 *) crt) == 0) + goto fail_push; + + return 0; + fail_push: + X509_free((X509 *) crt); + fail_ref: + return -1; +} + +int openssl_verify_crt_pin(void * store, + void * untrusted, + void * crt, + void * pin) { X509_STORE_CTX * ctx; X509_STORE * _store; X509* _crt; + STACK_OF(X509) * chain; + int i; + int n; int ret; _store = (X509_STORE *) store; @@ -1710,7 +1827,8 @@ int openssl_verify_crt(void * store, if (ctx == NULL) goto fail_store_ctx; - ret = X509_STORE_CTX_init(ctx, _store, _crt, NULL); + ret = X509_STORE_CTX_init(ctx, _store, _crt, + (STACK_OF(X509) *) untrusted); if (ret != 1) goto fail_ca; @@ -1718,13 +1836,39 @@ int openssl_verify_crt(void * store, if (ret != 1) goto fail_ca; + /* Peer cert only verifies a signature; gate on sig KU, not role. */ + if ((X509_get_key_usage(_crt) & KU_DIGITAL_SIGNATURE) == 0) + goto fail_ca; + + if (pin != NULL) { + chain = X509_STORE_CTX_get0_chain(ctx); + if (chain == NULL) + goto fail_ca; + n = sk_X509_num(chain); + for (i = 1; i < n; i++) /* Skip the leaf */ + if (X509_cmp(sk_X509_value(chain, i), pin) == 0) + break; + if (i == n) + goto fail_pin; + } + X509_STORE_CTX_free(ctx); return 0; + fail_pin: + X509_STORE_CTX_free(ctx); + return -ENOENT; fail_ca: X509_STORE_CTX_free(ctx); fail_store_ctx: - return -1; + return -EAUTH; +} + +int openssl_verify_crt(void * store, + void * untrusted, + void * crt) +{ + return openssl_verify_crt_pin(store, untrusted, crt, NULL); } static const EVP_MD * select_md(EVP_PKEY * pkey, @@ -1739,6 +1883,12 @@ static const EVP_MD * select_md(EVP_PKEY * pkey, return EVP_get_digestbynid(nid); } +bool openssl_pk_requires_md(const EVP_PKEY * pk) +{ + /* Provider-based (PQC) signatures have an intrinsic digest */ + return EVP_PKEY_get_id(pk) >= 0; +} + int openssl_sign(EVP_PKEY * pkp, int nid, buffer_t msg, @@ -1866,9 +2016,10 @@ void * openssl_secure_malloc(size_t size) return OPENSSL_secure_malloc(size); } -void openssl_secure_free(void * ptr) +void openssl_secure_free(void * ptr, + size_t size) { - OPENSSL_secure_free(ptr); + OPENSSL_secure_clear_free(ptr, size); } void openssl_secure_clear(void * ptr, @@ -1876,6 +2027,7 @@ void openssl_secure_clear(void * ptr, { OPENSSL_cleanse(ptr, size); } + void openssl_cleanup(void) { OPENSSL_cleanup(); diff --git a/src/lib/crypt/openssl.h b/src/lib/crypt/openssl.h index af285232..6857e39b 100644 --- a/src/lib/crypt/openssl.h +++ b/src/lib/crypt/openssl.h @@ -28,147 +28,185 @@ struct ossl_crypt_ctx; -ssize_t openssl_pkp_create(const char * algo, - EVP_PKEY ** pkp, - uint8_t * pk); +ssize_t openssl_pkp_create(const char * algo, + EVP_PKEY ** pkp, + uint8_t * pk); -void openssl_pkp_destroy(EVP_PKEY * pkp); +void openssl_pkp_destroy(EVP_PKEY * pkp); -int openssl_dhe_derive(EVP_PKEY * pkp, - buffer_t pk, - int kdf_nid, - uint8_t * s); +int openssl_dhe_derive(EVP_PKEY * pkp, + buffer_t pk, + int kdf_nid, + uint8_t * s); -ssize_t openssl_kem_encap(buffer_t pk, - uint8_t * ct, - int kdf_nid, - uint8_t * s); +ssize_t openssl_kem_encap(buffer_t pk, + uint8_t * ct, + int kdf_nid, + uint8_t * s); /* no X509 DER support yet for DHKEM public keys */ -ssize_t openssl_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf_nid, - uint8_t * s); +ssize_t openssl_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf_nid, + uint8_t * s); + +int openssl_kem_decap(EVP_PKEY * priv, + buffer_t ct, + int kdf_nid, + uint8_t * s); + +int openssl_get_algo_from_pk_der(buffer_t pk, + char * algo); + +int openssl_seal(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + uint8_t * out, + uint8_t * tag); + +int openssl_open(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + const uint8_t * tag, + buffer_t * out); + +int openssl_oneshot_seal(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out); + +int openssl_oneshot_open(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out); + +int openssl_hkdf_expand(buffer_t key, + buffer_t info, + buffer_t out); -int openssl_kem_decap(EVP_PKEY * priv, - buffer_t ct, - int kdf_nid, - uint8_t * s); +struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk); -int openssl_get_algo_from_pk_der(buffer_t pk, - char * algo); +void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx); -int openssl_get_algo_from_pk_raw(buffer_t pk, - char * algo); +int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx); -int openssl_encrypt(struct ossl_crypt_ctx * ctx, - buffer_t in, - buffer_t * out); +/* AUTHENTICATION */ +int openssl_load_crt_file(const char * path, + void ** crt); -int openssl_decrypt(struct ossl_crypt_ctx * ctx, - buffer_t in, - buffer_t * out); +int openssl_load_crt_str(const char * str, + void ** crt); -struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk); +int openssl_load_crt_der(buffer_t buf, + void ** crt); -void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx); +int openssl_get_pubkey_crt(void * crt, + void ** pk); -int openssl_crypt_get_ivsz(struct ossl_crypt_ctx * ctx); +void openssl_free_crt(void * crt); -int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx); +int openssl_load_privkey_file(const char * path, + void ** key); -/* AUTHENTICATION */ +int openssl_load_privkey_str(const char * str, + void ** key); -int openssl_load_crt_file(const char * path, - void ** crt); +int openssl_load_pubkey_file(const char * path, + void ** key); -int openssl_load_crt_str(const char * str, - void ** crt); +int openssl_load_pubkey_str(const char * str, + void ** key); +int openssl_load_pubkey_file_to_der(const char * path, + buffer_t * buf); +int openssl_load_pubkey_raw_file(const char * path, + const char * algo, + buffer_t * buf); -int openssl_load_crt_der(buffer_t buf, - void ** crt); +int openssl_load_privkey_raw_file(const char * path, + const char * algo, + void ** key); -int openssl_get_pubkey_crt(void * crt, - void ** pk); +int openssl_cmp_key(const EVP_PKEY * key1, + const EVP_PKEY * key2); -void openssl_free_crt(void * crt); +void openssl_free_key(EVP_PKEY * key); -int openssl_load_privkey_file(const char * path, - void ** key); +int openssl_check_crt_name(void * crt, + const char * name); -int openssl_load_privkey_str(const char * str, - void ** key); +int openssl_get_crt_name(void * crt, + char * name); -int openssl_load_pubkey_file(const char * path, - void ** key); +int openssl_crt_str(const void * crt, + char * str); -int openssl_load_pubkey_str(const char * str, - void ** key); -int openssl_load_pubkey_file_to_der(const char * path, +int openssl_crt_der(const void * crt, buffer_t * buf); -int openssl_load_pubkey_raw_file(const char * path, - buffer_t * buf); - -int openssl_load_privkey_raw_file(const char * path, - void ** key); - -int openssl_cmp_key(const EVP_PKEY * key1, - const EVP_PKEY * key2); -void openssl_free_key(EVP_PKEY * key); +void * openssl_auth_create_store(void); -int openssl_check_crt_name(void * crt, - const char * name); +void openssl_auth_destroy_store(void * store); -int openssl_get_crt_name(void * crt, - char * name); +int openssl_auth_add_crt_to_store(void * store, + void * crt); -int openssl_crt_str(const void * crt, - char * str); +void * openssl_auth_create_chain(void); -int openssl_crt_der(const void * crt, - buffer_t * buf); +void openssl_auth_destroy_chain(void * chain); -void * openssl_auth_create_store(void); +int openssl_auth_add_crt_to_chain(void * chain, + void * crt); -void openssl_auth_destroy_store(void * store); +int openssl_verify_crt(void * store, + void * untrusted, + void * crt); -int openssl_auth_add_crt_to_store(void * store, - void * crt); +int openssl_verify_crt_pin(void * store, + void * untrusted, + void * crt, + void * pin); -int openssl_verify_crt(void * store, - void * crt); +bool openssl_pk_requires_md(const EVP_PKEY * pk); -int openssl_sign(EVP_PKEY * pkp, - int md_nid, - buffer_t msg, - buffer_t * sig); +int openssl_sign(EVP_PKEY * pkp, + int md_nid, + buffer_t msg, + buffer_t * sig); -int openssl_verify_sig(EVP_PKEY * pk, - int md_nid, - buffer_t msg, - buffer_t sig); +int openssl_verify_sig(EVP_PKEY * pk, + int md_nid, + buffer_t msg, + buffer_t sig); -ssize_t openssl_md_digest(int md_nid, - buffer_t in, - uint8_t * out); +ssize_t openssl_md_digest(int md_nid, + buffer_t in, + uint8_t * out); -ssize_t openssl_md_len(int md_nid); +ssize_t openssl_md_len(int md_nid); /* Secure memory allocation */ -int openssl_secure_malloc_init(size_t max, - size_t guard); +int openssl_secure_malloc_init(size_t max, + size_t guard); -void openssl_secure_malloc_fini(void); +void openssl_secure_malloc_fini(void); -void * openssl_secure_malloc(size_t size); +void * openssl_secure_malloc(size_t size); -void openssl_secure_free(void * ptr, - size_t size); +void openssl_secure_free(void * ptr, + size_t size); -void openssl_secure_clear(void * ptr, - size_t size); +void openssl_secure_clear(void * ptr, + size_t size); -void openssl_cleanup(void); +void openssl_cleanup(void); #endif /* OUROBOROS_LIB_CRYPT_OPENSSL_H */ diff --git a/src/lib/dev.c b/src/lib/dev.c index ae0401b7..3b0363da 100644 --- a/src/lib/dev.c +++ b/src/lib/dev.c @@ -27,7 +27,11 @@ #endif #include "config.h" +#include "cap.h" #include "ssm.h" +#include "poa/poa.h" + +#define OUROBOROS_PREFIX "libouroboros" #include <ouroboros/atomics.h> #include <ouroboros/bitmap.h> @@ -45,6 +49,7 @@ #include <ouroboros/ipcp-dev.h> #include <ouroboros/list.h> #include <ouroboros/local-dev.h> +#include <ouroboros/logs.h> #include <ouroboros/np1_flow.h> #include <ouroboros/pthread.h> #include <ouroboros/random.h> @@ -56,6 +61,7 @@ #include <ouroboros/ssm_flow_set.h> #include <ouroboros/ssm_pool.h> #include <ouroboros/ssm_rbuff.h> +#include <ouroboros/time.h> #include <ouroboros/tw.h> #include <ouroboros/utils.h> @@ -63,8 +69,10 @@ #ifdef HAVE_LIBGCRYPT #include <gcrypt.h> #endif +#include <arpa/inet.h> #include <stdarg.h> #include <stdbool.h> +#include <stddef.h> #include <inttypes.h> #include <stdio.h> #include <stdlib.h> @@ -79,6 +87,7 @@ #define DONE_PART -2 #define CRCLEN (sizeof(uint32_t)) +#define FLOW_AVG_SHIFT 3 #define SECMEMSZ 16384 #define MSGBUFSZ 2048 @@ -98,8 +107,14 @@ struct flow { ssize_t part_idx; struct crypt_ctx * crypt; - int headsz; /* IV */ - int tailsz; /* Tag + CRC */ + int headsz; /* Selector */ + int tailsz; /* Tag + CRC */ + + struct timespec rk_grace; /* TX-promote deadline */ + struct timespec rk_attempt; /* Last re-key attempt */ + bool rk_wm_inflight; /* Re-key trigger in flight */ + uint32_t rk_wm_ctr; /* Throttles the consult */ + bool rk_initiator; /* OAP initiator this re-key */ struct timespec snd_act; struct timespec rcv_act; @@ -110,6 +125,14 @@ struct flow { struct timespec rcv_timeo; struct frcti * frcti; + + /* Mean written packet size (bytes), EWMA over the send path. */ + size_t mean_len; + + struct poa_flow * poa; /* NULL for shared memory flows */ + + /* Egress capacity estimator; armed by the IPCP, else NULL. */ + struct cap_est * cap; }; struct flow_set { @@ -133,6 +156,8 @@ struct { struct flow * flows; struct fmap * id_to_fd; + uint32_t max_rtt; /* IPCPs: declared layer RTT (ms) */ + pthread_mutex_t mtx; pthread_cond_t cond; @@ -261,7 +286,7 @@ static int spb_encrypt(struct flow * flow, in.len = ssm_pk_buff_len(spb); if (crypt_encrypt(flow->crypt, in, &out) < 0) - goto fail_encrypt; + return -ECRYPT; head = ssm_pk_buff_push(spb, flow->headsz); if (head == NULL) @@ -278,7 +303,7 @@ static int spb_encrypt(struct flow * flow, return 0; fail_alloc: freebuf(out); - fail_encrypt: + return -ECRYPT; } @@ -296,8 +321,7 @@ static int spb_decrypt(struct flow * flow, in.len = ssm_pk_buff_len(spb); if (crypt_decrypt(flow->crypt, in, &out) < 0) - return -ENOMEM; - + return -ECRYPT; head = ssm_pk_buff_pop(spb, flow->headsz) + flow->headsz; ssm_pk_buff_pop_tail(spb, flow->tailsz); @@ -342,8 +366,14 @@ static int crc_check(struct ssm_pk_buff * spb, size_t head_skip) { uint32_t crc; - uint8_t * head = ssm_pk_buff_head(spb) + head_skip; - uint8_t * tail = ssm_pk_buff_pop_tail(spb, CRCLEN); + uint8_t * head; + uint8_t * tail; + + if (ssm_pk_buff_len(spb) < head_skip + CRCLEN) + return 1; + + head = ssm_pk_buff_head(spb) + head_skip; + tail = ssm_pk_buff_pop_tail(spb, CRCLEN); mem_hash(HASH_CRC32, &crc, head, tail - head); @@ -353,10 +383,7 @@ static int crc_check(struct ssm_pk_buff * spb, /* FRCT included here so it can use proc and dev.c statics directly. */ #include "frct.c" -/* - * SACK / DATA carry trailer CRC32; HCS protects the headers on every - * FRCT packet. Decrypt before any check so plaintext is authoritative. - */ +/* Decrypt before any check so the plaintext is authoritative. */ static bool invalid_pkt(struct flow * flow, struct ssm_pk_buff * spb) { @@ -438,10 +465,6 @@ static void compute_wait_deadline(const struct timespec * dl, *out = *dl; } -/* - * proc.lock rdlock held across each iteration so flow_fini's wrlock - * waits for us to finish; FLOWDOWN already set means we exit promptly. - */ static void flow_drain_rx_nb(struct flow * flow) { ssize_t idx; @@ -509,10 +532,80 @@ static void flow_drain_rx_nb(struct flow * flow) } } -/* - * Wait clamped by caller deadline, next tw expiry, and TICTIME; - * a clamp-timeout means tw work is due, not caller-deadline. - */ +/* TX-promotion grace when the peer's install latency is unknown (raw). */ +#define REKEY_GRACE_MS 1000 + +/* Last-resort promote within N node-keys of exhaustion (< watermark). */ +#define REKEY_PROMOTE_FLOOR 1 + +/* Throttle re-key retries so a failed attempt can't storm the IRMd. */ +#define REKEY_BACKOFF_NS (250 * MILLION) + +/* proc.lock (rd) only guards teardown; crypt_rekey self-synchronises. */ +static void flow_rekey(struct flow * flow) +{ + struct flow_info info; + struct crypt_sk sk; + struct timespec now; + struct timespec intv; + time_t ms; + uint8_t key[SYMMKEYSZ]; + uint8_t buf[SOCK_BUF_SIZE]; + buffer_t msg = {SOCK_BUF_SIZE, buf}; + bool has_key; + bool initiator = false; + + pthread_rwlock_rdlock(&proc.lock); + if (flow->info.id < 0 || flow->crypt == NULL) { + pthread_rwlock_unlock(&proc.lock); + return; + } + + /* Back off so a failed attempt can't storm the IRMd per syscall. */ + clock_gettime(PTHREAD_COND_CLOCK, &now); + if (ts_diff_ns(&now, &flow->rk_attempt) < REKEY_BACKOFF_NS) { + pthread_rwlock_unlock(&proc.lock); + return; + } + + flow->rk_attempt = now; + info = flow->info; + pthread_rwlock_unlock(&proc.lock); + + if (flow_update__irm_req_ser(&msg, &info, false) < 0) + return; + + if (send_recv_msg(&msg) < 0) + return; + + sk.key = key; + if (flow_rekey__irm_result_des(&msg, &sk, &has_key, &initiator) < 0) + return; + + if (!has_key) + return; + + pthread_rwlock_rdlock(&proc.lock); + if (flow->info.id == info.id && flow->crypt != NULL) { + if (crypt_rekey(flow->crypt, &sk) == 0) { + flow->rk_initiator = initiator; + /* Hold TX on the old epoch until the peer installs. */ + ms = flow->info.mpl > 0 ? flow->info.mpl * 3 + : REKEY_GRACE_MS; + intv.tv_sec = ms / 1000; + intv.tv_nsec = (ms % 1000) * MILLION; + clock_gettime(PTHREAD_COND_CLOCK, &now); + ts_add(&now, &intv, &flow->rk_grace); + } + /* Re-arm the watermark even if the install was a no-op. */ + STORE_RELAXED(&flow->rk_wm_inflight, false); + } + pthread_rwlock_unlock(&proc.lock); + + crypt_secure_clear(key, SYMMKEYSZ); +} + +/* A clamp-timeout means tw work is due, not the caller deadline. */ static int flow_rx_one(struct flow * flow, struct timespec * abs) { @@ -533,7 +626,20 @@ static int flow_rx_one(struct flow * flow, return -EFLOWDOWN; } + /* Pull a parked re-key before re-blocking (idle reader). */ + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(rx_rb) & RB_REKEY)) { + pthread_rwlock_unlock(&proc.lock); + flow_rekey(flow); + continue; + } + + pthread_cleanup_push(__cleanup_rwlock_unlock, &proc.lock); + idx = ssm_rbuff_read_b(rx_rb, &wait_abs); + + pthread_cleanup_pop(false); + if (idx == -ETIMEDOUT) { pthread_rwlock_unlock(&proc.lock); if (deadline_passed(abs)) @@ -592,27 +698,26 @@ static void flow_clear(int fd) proc.flows[fd].info.id = -1; } -/* - * Set ACL_FLOWDOWN on rx/tx so any in-flight blocking reads or writes - * wake up and drop their proc.lock rdlock. Must run BEFORE flow_fini's - * wrlock, else the wrlock blocks on those rdlock holders and the - * in-flight calls never see the FLOWDOWN signal. - */ +/* Order before flow_fini's wrlock, which blocks on rdlock holders. */ static void flow_quiesce(int fd) { struct ssm_rbuff * rx_rb = proc.flows[fd].rx_rb; struct ssm_rbuff * tx_rb = proc.flows[fd].tx_rb; if (rx_rb != NULL) - ssm_rbuff_set_acl(rx_rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(rx_rb, RB_FLOWDOWN); + if (tx_rb != NULL) - ssm_rbuff_set_acl(tx_rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(tx_rb, RB_FLOWDOWN); } static void do_flow_fini(int fd) { assert(fd >= 0 && fd < PROC_MAX_FLOWS); + if (proc.flows[fd].poa != NULL) + poa_flow_detach(proc.flows[fd].poa); + if (proc.flows[fd].frcti != NULL) frcti_destroy(proc.flows[fd].frcti); @@ -636,6 +741,8 @@ static void do_flow_fini(int fd) crypt_destroy_ctx(proc.flows[fd].crypt); + free(proc.flows[fd].cap); + flow_clear(fd); } @@ -667,14 +774,21 @@ static __inline__ size_t flow_user_mtu(const struct flow * flow, return raw > hdr ? raw - hdr : 0; } +/* A PoA flow transmits on its own socket; it has no tx ring. */ static int flow_init(struct flow_info * info, struct crypt_sk * sk, - time_t rtt_hint) + time_t rtt_hint, + struct poa_flow * pf) { - struct timespec now; - struct flow * flow; - int fd; - int err = -ENOMEM; + struct timespec now; + struct timespec txq; + struct flow * flow; + struct ssm_rbuff * tx_rb = NULL; + int fd; + int err = -ENOMEM; + + if (info->id < 0 || info->id >= SYS_MAX_FLOWS) + return -EBADF; clock_gettime(PTHREAD_COND_CLOCK, &now); @@ -694,13 +808,17 @@ static int flow_init(struct flow_info * info, if (flow->rx_rb == NULL) goto fail_rx_rb; - flow->tx_rb = ssm_rbuff_open(info->n_1_pid, info->id); - if (flow->tx_rb == NULL) - goto fail_tx_rb; + if (pf == NULL) { + flow->tx_rb = ssm_rbuff_open(info->n_1_pid, info->id); + if (flow->tx_rb == NULL) + goto fail_tx_rb; - flow->set = ssm_flow_set_open(info->n_1_pid); - if (flow->set == NULL) - goto fail_set; + tx_rb = flow->tx_rb; + + flow->set = ssm_flow_set_open(info->n_1_pid); + if (flow->set == NULL) + goto fail_set; + } flow->oflags = FLOWFDEFAULT; flow->part_idx = NO_PART; @@ -709,13 +827,15 @@ static int flow_init(struct flow_info * info, flow->crypt = NULL; flow->headsz = 0; flow->tailsz = 0; + flow->poa = pf; if (IS_ENCRYPTED(sk)) { - sk->rot_bit = KEY_ROTATION_BIT; flow->crypt = crypt_create_ctx(sk); - if (flow->crypt == NULL) + if (flow->crypt == NULL) { + err = -ECRYPT; goto fail_crypt; - flow->headsz = crypt_get_ivsz(flow->crypt); + } + flow->headsz = crypt_get_headsz(flow->crypt); flow->tailsz = crypt_get_tagsz(flow->crypt); } @@ -725,7 +845,7 @@ static int flow_init(struct flow_info * info, uint32_t frct_mtu = flow_user_mtu(flow, info->mtu); flow->frcti = frcti_create(fd, DELT_A, DELT_R, - info->mpl, rtt_hint, + info->mpl, rtt_hint, info->max_rtt, info->qs, frct_mtu); if (flow->frcti == NULL) goto fail_frcti; @@ -733,18 +853,30 @@ static int flow_init(struct flow_info * info, proc.id_to_fd[info->id].fd = fd; + if (pf != NULL) + poa_flow_attach(pf, info->id, flow->rx_rb); + flow_set_state(&proc.id_to_fd[info->id], FLOW_ALLOCATED); pthread_rwlock_unlock(&proc.lock); + if (tx_rb != NULL) { + txq.tv_sec = SSM_RBUFF_TXQ_DELAY / 1000; + txq.tv_nsec = (SSM_RBUFF_TXQ_DELAY % 1000) * MILLION; + + ssm_rbuff_set_txq_target(tx_rb, &txq); + } + return fd; fail_frcti: crypt_destroy_ctx(flow->crypt); fail_crypt: - ssm_flow_set_close(flow->set); + if (flow->set != NULL) + ssm_flow_set_close(flow->set); fail_set: - ssm_rbuff_close(flow->tx_rb); + if (flow->tx_rb != NULL) + ssm_rbuff_close(flow->tx_rb); fail_tx_rb: ssm_rbuff_close(flow->rx_rb); fail_rx_rb: @@ -768,9 +900,10 @@ static void init(int argc, char ** argv, char ** envp) { - struct proc_info info; - char * prog = argv[0]; - int i; + struct proc_info info; + char * prog = argv[0]; + int i; + pthread_rwlockattr_t attr; #ifdef PROC_FLOW_STATS char procstr[32]; #endif @@ -855,7 +988,16 @@ static void init(int argc, goto fail_cond; } - if (pthread_rwlock_init(&proc.lock, NULL) < 0) { + /* Writer-preferred: FRCT readers must not starve flow accept. */ + if (pthread_rwlockattr_init(&attr) != 0) { + fprintf(stderr, "FATAL: Could not init rwlock attributes.\n"); + goto fail_rwlock_attr; + } +#if defined(__GLIBC__) + pthread_rwlockattr_setkind_np( + &attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP); +#endif + if (pthread_rwlock_init(&proc.lock, &attr) != 0) { fprintf(stderr, "FATAL: Could not initialize flow lock.\n"); goto fail_flow_lock; } @@ -885,6 +1027,8 @@ static void init(int argc, } } #endif + pthread_rwlockattr_destroy(&attr); + return; #if defined PROC_FLOW_STATS @@ -898,6 +1042,8 @@ static void init(int argc, fail_fqset: pthread_rwlock_destroy(&proc.lock); fail_flow_lock: + pthread_rwlockattr_destroy(&attr); + fail_rwlock_attr: pthread_cond_destroy(&proc.cond); fail_cond: pthread_mutex_destroy(&proc.mtx); @@ -981,14 +1127,20 @@ static void fini(void) __attribute__((section(INIT_SECTION))) __typeof__(init) * __init = init; __attribute__((section(FINI_SECTION))) __typeof__(fini) * __fini = fini; +/* + * A PoA flow is announced by its peer before the accept; from the + * reply on, the peer may transmit, so the flow must be able to + * receive. + */ int flow_accept(qosspec_t * qs, const struct timespec * timeo) { struct flow_info flow; - struct crypt_sk crypt; + struct crypt_sk crypt; uint8_t buf[SOCK_BUF_SIZE]; buffer_t msg = {SOCK_BUF_SIZE, buf}; uint8_t key[SYMMKEYSZ]; + struct poa_flow * pf; int fd; int err; @@ -1012,14 +1164,21 @@ int flow_accept(qosspec_t * qs, if (err < 0) return err; - crypt.key = key; + crypt.key = key; + crypt.epoch = 0; + crypt.role = CRYPT_ROLE_RESP; err = flow__irm_result_des(&msg, &flow, &crypt); if (err < 0) return err; - /* No RTT in accept; rtt_hint=0 bootstraps from first ACK. */ - fd = flow_init(&flow, &crypt, 0); + pf = poa_flow_take_pending(flow.id); + + fd = flow_init(&flow, &crypt, 0, pf); + if (fd >= 0) + poa_flow_ready(pf); + else if (pf != NULL) + poa_flow_detach(pf); crypt_secure_clear(key, SYMMKEYSZ); @@ -1067,13 +1226,15 @@ int flow_alloc(const char * dst, clock_gettime(PTHREAD_COND_CLOCK, &t1); - crypt.key = key; + crypt.key = key; + crypt.epoch = 0; + crypt.role = CRYPT_ROLE_INIT; err = flow__irm_result_des(&msg, &flow, &crypt); if (err < 0) return err; - fd = flow_init(&flow, &crypt, ts_diff_ns(&t1, &t0)); + fd = flow_init(&flow, &crypt, ts_diff_ns(&t1, &t0), NULL); crypt_secure_clear(key, SYMMKEYSZ); @@ -1106,13 +1267,15 @@ int flow_join(const char * dst, if (err < 0) return err; - crypt.key = key; + crypt.key = key; + crypt.epoch = 0; + crypt.role = CRYPT_ROLE_INIT; err = flow__irm_result_des(&msg, &flow, &crypt); if (err < 0) return err; - fd = flow_init(&flow, &crypt, 0); + fd = flow_init(&flow, &crypt, 0, NULL); crypt_secure_clear(key, SYMMKEYSZ); @@ -1152,6 +1315,8 @@ int flow_dealloc(int fd) pthread_rwlock_unlock(&proc.lock); + frcti_set_draining(flow->frcti); + flow_read(fd, buf, SOCK_BUF_SIZE); pthread_rwlock_rdlock(&proc.lock); @@ -1173,7 +1338,8 @@ int flow_dealloc(int fd) pthread_cleanup_push(__cleanup_rwlock_unlock, &proc.lock); - ssm_rbuff_fini(flow->tx_rb); + if (flow->tx_rb != NULL) + ssm_rbuff_fini(flow->tx_rb); pthread_cleanup_pop(true); @@ -1241,6 +1407,18 @@ int ipcp_flow_dealloc(int fd) return err; } +/* A settable delay is a normalised, non-negative timespec. */ +static bool delay_is_valid(const struct timespec * ts) +{ + if (ts->tv_sec < 0 || ts->tv_nsec < 0) + return false; + + if (ts->tv_nsec >= BILLION) + return false; + + return TS_TO_UINT64(*ts) <= SSM_RBUFF_TXQ_MAX_DELAY; +} + int fccntl(int fd, int cmd, ...) @@ -1251,8 +1429,6 @@ int fccntl(int fd, va_list l; struct timespec * timeo; qosspec_t * qs; - uint32_t rx_acl; - uint32_t tx_acl; size_t * qlen; struct flow * flow; uint16_t old_acc; @@ -1265,6 +1441,7 @@ int fccntl(int fd, time_t * rtop; int rc; bool emit_eos = false; + bool set_txq = false; if (fd < 0 || fd >= PROC_MAX_FLOWS) return -EBADF; @@ -1328,7 +1505,11 @@ int fccntl(int fd, break; case FLOWGTXQLEN: qlen = va_arg(l, size_t *); - *qlen = ssm_rbuff_queued(flow->tx_rb); + + if (flow->poa != NULL) + *qlen = poa_flow_qpkts(flow->poa); + else + *qlen = ssm_rbuff_queued(flow->tx_rb); break; case FLOWGMTU: maxp = va_arg(l, size_t *); @@ -1336,6 +1517,28 @@ int fccntl(int fd, goto einval; *maxp = flow_user_mtu(flow, flow->info.mtu); break; + case FLOWSTXQDLY: + timeo = va_arg(l, struct timespec *); + if (timeo == NULL) + goto einval; + + if (flow->tx_rb == NULL) + goto eperm; + + if (!delay_is_valid(timeo)) + goto einval; + + set_txq = true; + break; + case FLOWGTXQDLY: + timeo = va_arg(l, struct timespec *); + if (timeo == NULL) + goto einval; + + if (flow->tx_rb == NULL) + goto eperm; + ssm_rbuff_get_txq_target(flow->tx_rb, timeo); + break; case FLOWSFLAGS: old_acc = flow->oflags & FLOWFACCMODE; flow->oflags = va_arg(l, uint32_t); @@ -1348,31 +1551,28 @@ int fccntl(int fd, && flow->frcti != NULL) emit_eos = true; - rx_acl = ssm_rbuff_get_acl(flow->rx_rb); - tx_acl = ssm_rbuff_get_acl(flow->tx_rb); - /* Our flow write-only -> peer's read-only. */ + /* Our flow write-only -> peer's read-only; restore on RDWR. */ if (flow->oflags & FLOWFWRONLY) - rx_acl |= ACL_RDONLY; - if (flow->oflags & FLOWFRDWR) - rx_acl |= ACL_RDWR; + ssm_rbuff_clr_flags(flow->rx_rb, RB_WR); + else + ssm_rbuff_set_flags(flow->rx_rb, RB_WR); if (flow->oflags & FLOWFDOWN) { - rx_acl |= ACL_FLOWDOWN; - tx_acl |= ACL_FLOWDOWN; - ssm_flow_set_notify(flow->set, - flow->info.id, - FLOW_DOWN); + ssm_rbuff_set_flags(flow->rx_rb, RB_FLOWDOWN); + if (flow->tx_rb != NULL) + ssm_rbuff_set_flags(flow->tx_rb, RB_FLOWDOWN); + if (flow->set != NULL) + ssm_flow_set_notify(flow->set, flow->info.id, + FLOW_DOWN); } else { - rx_acl &= ~ACL_FLOWDOWN; - tx_acl &= ~ACL_FLOWDOWN; - ssm_flow_set_notify(flow->set, - flow->info.id, - FLOW_UP); + ssm_rbuff_clr_flags(flow->rx_rb, RB_FLOWDOWN); + if (flow->tx_rb != NULL) + ssm_rbuff_clr_flags(flow->tx_rb, RB_FLOWDOWN); + if (flow->set != NULL) + ssm_flow_set_notify(flow->set, flow->info.id, + FLOW_UP); } - ssm_rbuff_set_acl(flow->rx_rb, rx_acl); - ssm_rbuff_set_acl(flow->tx_rb, tx_acl); - break; case FLOWGFLAGS: fflags = va_arg(l, uint32_t *); @@ -1459,6 +1659,9 @@ int fccntl(int fd, if (emit_eos) frcti_fin_snd(flow->frcti); + if (set_txq) + ssm_rbuff_set_txq_target(flow->tx_rb, timeo); + va_end(l); return 0; @@ -1473,6 +1676,25 @@ int fccntl(int fd, return -EPERM; } +/* + * The ring counts slots, so the queue is only bytes if we know what a + * packet weighs. Ordered so the unsigned arithmetic cannot wrap. + */ +static void flow_mean_len_update(struct flow * flow, + size_t len) +{ + size_t avg = LOAD_RELAXED(&flow->mean_len); + + if (avg == 0) { + STORE_RELAXED(&flow->mean_len, len); + return; + } + + avg = avg + (len >> FLOW_AVG_SHIFT) - (avg >> FLOW_AVG_SHIFT); + + STORE_RELAXED(&flow->mean_len, avg == 0 ? 1 : avg); +} + static int flow_tx_spb(struct flow * flow, struct ssm_pk_buff * spb, uint16_t flags, @@ -1504,21 +1726,23 @@ static int flow_tx_spb(struct flow * flow, goto enomem; } + if (flow->poa != NULL) + return poa_flow_tx(flow->poa, spb, block, abstime); + + flow_mean_len_update(flow, ssm_pk_buff_len(spb)); + if (!block) ret = ssm_rbuff_write(flow->tx_rb, idx); else ret = ssm_rbuff_write_b(flow->tx_rb, idx, abstime); - if (ret < 0) { - ssm_pool_remove(proc.pool, idx); + if (ret < 0) return ret; - } ssm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT); return 0; enomem: - ssm_pool_remove(proc.pool, idx); return -ENOMEM; } @@ -1527,20 +1751,16 @@ static __inline__ uint16_t flow_frag_role(size_t i, size_t n) { if (n == 1) return FRCT_FR_SOLE; + if (i == 0) return FRCT_FR_FIRST; + if (i + 1 == n) return FRCT_FR_LAST; return FRCT_FR_MID; } -/* - * Stream-mode write: split buf into chunks of - * (frag_mtu - PCI - PCI_STREAM) bytes; each chunk goes through the - * normal tx path. frcti_snd injects the [start,end) extension and - * advances snd_byte_next under its wrlock. No FFGM/LFGM role bits. - */ static ssize_t flow_write_stream(struct flow * flow, const void * buf, size_t count, @@ -1581,8 +1801,10 @@ static ssize_t flow_write_stream(struct flow * flow, memcpy(ptr, src + off, clen); ret = flow_tx_spb(flow, spb, 0, block, dl); - if (ret < 0) + if (ret < 0) { + ssm_pool_remove(proc.pool, idx); return off > 0 ? (ssize_t) off : (ssize_t) ret; + } off += clen; } @@ -1614,6 +1836,7 @@ static ssize_t flow_write_frag(struct flow * flow, /* Guard the ceil-divide against size_t overflow. */ if (count > SIZE_MAX - frag_payload + 1) return -EMSGSIZE; + n = (count + frag_payload - 1) / frag_payload; /* SDU larger than the FC window can ever offer would deadlock. */ @@ -1648,9 +1871,9 @@ static ssize_t flow_write_frag(struct flow * flow, memcpy(ptr, src + off, clen); - ret = flow_tx_spb(flow, spb, flow_frag_role(i, n), - block, dl); + ret = flow_tx_spb(flow, spb, flow_frag_role(i, n), block, dl); if (ret < 0) { + ssm_pool_remove(proc.pool, idx); if (off > 0) STAT_BUMP(flow->frcti, sdu_snd_tx); return off > 0 ? (ssize_t) off : (ssize_t) ret; @@ -1662,6 +1885,91 @@ static ssize_t flow_write_frag(struct flow * flow, return (ssize_t) count; } +/* + * Initiator promotes on the install grace (it holds the key-confirm + * tag); responder waits for peer_synced. The near-exhaustion floor + * backstops both roles: the receiver selects the epoch by the wire + * selector, so promoting beats wedging TX on a spent keyring. + */ +static void flow_tx_promote(struct flow * flow) +{ + struct timespec now; + int nodes_left; + bool promote; + + if (flow->crypt == NULL) + return; + + if (flow->rk_grace.tv_sec == 0 && flow->rk_grace.tv_nsec == 0) + return; + + promote = crypt_peer_synced(flow->crypt); + + if (!promote && flow->rk_initiator) { + clock_gettime(PTHREAD_COND_CLOCK, &now); + promote = ts_diff_ns(&now, &flow->rk_grace) >= 0; + } + + if (!promote) { + nodes_left = crypt_nodes_left(flow->crypt); + promote = nodes_left >= 0 && nodes_left <= REKEY_PROMOTE_FLOOR; + } + + if (!promote) + return; + + crypt_tx_promote(flow->crypt); + flow->rk_grace.tv_sec = 0; + flow->rk_grace.tv_nsec = 0; +} + +/* The reply carries no key; the seed arrives later over RB_REKEY. */ +static int flow_rekey_trigger(struct flow * flow) +{ + struct flow_info info; + uint8_t buf[SOCK_BUF_SIZE]; + buffer_t msg = {SOCK_BUF_SIZE, buf}; + + pthread_rwlock_rdlock(&proc.lock); + if (flow->info.id < 0 || flow->crypt == NULL) { + pthread_rwlock_unlock(&proc.lock); + return -1; + } + info = flow->info; + pthread_rwlock_unlock(&proc.lock); + + if (flow_update__irm_req_ser(&msg, &info, true) < 0) + return -1; + + if (send_recv_msg(&msg) < 0) + return -1; + + return 0; +} + +static bool flow_wm_due(struct flow * flow) +{ + uint32_t tick; + + if (KEY_REKEY_WATERMARK == 0) + return false; + + if (flow->crypt == NULL) + return false; + + if (LOAD_RELAXED(&flow->rk_wm_inflight)) + return false; + + tick = FETCH_ADD_RELAXED(&flow->rk_wm_ctr, 1); + if ((tick & (FLOW_WM_CHECK - 1)) != 0) + return false; + + if (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY) + return false; + + return crypt_nodes_left(flow->crypt) <= KEY_REKEY_WATERMARK; +} + ssize_t flow_write(int fd, const void * buf, size_t count) @@ -1705,6 +2013,19 @@ ssize_t flow_write(int fd, if ((flags & FLOWFACCMODE) == FLOWFRDONLY) return -EPERM; + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY)) + flow_rekey(flow); + + flow_tx_promote(flow); + + /* Pre-empt TX key exhaustion; the timer is the backstop. */ + if (flow_wm_due(flow)) { + STORE_RELAXED(&flow->rk_wm_inflight, true); + if (flow_rekey_trigger(flow) < 0) + STORE_RELAXED(&flow->rk_wm_inflight, false); + } + tw_move_safe(); if (flow->frcti != NULL) { @@ -1736,8 +2057,12 @@ ssize_t flow_write(int fd, ret = flow_tx_spb(flow, spb, FRCT_FR_SOLE, !(flags & FLOWFWNOBLOCK), dl); + if (ret < 0) { + ssm_pool_remove(proc.pool, idx); + return (ssize_t) ret; + } - return ret < 0 ? (ssize_t) ret : (ssize_t) count; + return (ssize_t) count; } static ssize_t flow_rx_spb(struct flow * flow, @@ -1775,6 +2100,10 @@ static ssize_t raw_flow_read_pkt(struct flow * flow, ssize_t idx; while (true) { + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY)) + flow_rekey(flow); + if (!block) { idx = ssm_rbuff_read(flow->rx_rb); if (idx < 0) @@ -1908,6 +2237,13 @@ ssize_t flow_read(int fd, pthread_rwlock_unlock(&proc.lock); + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY)) + flow_rekey(flow); + + /* Advance TX off a stale epoch even on recv-mostly (ACK-only) flows. */ + flow_tx_promote(flow); + tw_move_safe(); idx = flow->part_idx; @@ -2092,6 +2428,18 @@ static int fqueue_filter(struct fqueue * fq) pthread_rwlock_rdlock(&proc.lock); while (fq->next < fq->fqsize) { + if (fq->fqueue[fq->next].event == FLOW_UPD) { + /* Re-key doorbell: pull internally, never surface. */ + fd = proc.id_to_fd[fq->fqueue[fq->next].flow_id].fd; + ++fq->next; + if (fd >= 0) { + pthread_rwlock_unlock(&proc.lock); + flow_rekey(&proc.flows[fd]); + pthread_rwlock_rdlock(&proc.lock); + } + continue; + } + if (fq->fqueue[fq->next].event != FLOW_PKT) { ret = 1; goto out; @@ -2224,7 +2572,8 @@ int np1_flow_alloc(pid_t n_pid, int flow_id) { struct flow_info flow; - struct crypt_sk crypt = { .nid = NID_undef, .key = NULL }; + struct crypt_sk crypt = { .nid = NID_undef, .key = NULL, + .epoch = 0, .role = CRYPT_ROLE_INIT }; memset(&flow, 0, sizeof(flow)); @@ -2235,7 +2584,7 @@ int np1_flow_alloc(pid_t n_pid, /* np1 flow: n_1_pid is the upper. */ flow.n_1_pid = n_pid; - return flow_init(&flow, &crypt, 0); + return flow_init(&flow, &crypt, 0, NULL); } int np1_flow_dealloc(int flow_id, @@ -2272,6 +2621,38 @@ int np1_flow_resp(int flow_id, return fd; } +int np1_flow_fd(int flow_id) +{ + int fd; + + if (flow_id < 0 || flow_id >= SYS_MAX_FLOWS) + return -1; + + pthread_rwlock_rdlock(&proc.lock); + + fd = proc.id_to_fd[flow_id].fd; + + pthread_rwlock_unlock(&proc.lock); + + return fd; +} + +int np1_flow_id(int fd) +{ + int flow_id; + + if (fd < 0 || fd >= PROC_MAX_FLOWS) + return -1; + + pthread_rwlock_rdlock(&proc.lock); + + flow_id = proc.flows[fd].info.id; + + pthread_rwlock_unlock(&proc.lock); + + return flow_id; +} + int ipcp_create_r(const struct ipcp_info * info) { uint8_t buf[SOCK_BUF_SIZE]; @@ -2288,6 +2669,12 @@ int ipcp_create_r(const struct ipcp_info * info) return irm__irm_result_des(&msg); } +/* Layer-wide bound for flow_info; set once before flows are served. */ +void ipcp_flow_set_max_rtt(uint32_t max_rtt) +{ + proc.max_rtt = max_rtt; +} + int ipcp_flow_req_arr(const buffer_t * dst, qosspec_t qs, time_t mpl, @@ -2309,6 +2696,7 @@ int ipcp_flow_req_arr(const buffer_t * dst, flow.qs = qs; flow.mpl = mpl; flow.mtu = mtu; + flow.max_rtt = proc.max_rtt; if (ipcp_flow_req_arr__irm_req_ser(&msg, dst, &flow, data) < 0) return -ENOMEM; @@ -2317,7 +2705,9 @@ int ipcp_flow_req_arr(const buffer_t * dst, if (err < 0) return err; - crypt.key = key; + crypt.key = key; + crypt.epoch = 0; + crypt.role = CRYPT_ROLE_INIT; err = flow__irm_result_des(&msg, &flow, &crypt); if (err < 0) @@ -2335,7 +2725,30 @@ int ipcp_flow_req_arr(const buffer_t * dst, crypt.nid = NID_undef; - return flow_init(&flow, &crypt, 0); + return flow_init(&flow, &crypt, 0, NULL); +} + +int ipcp_flow_update_arr(int flow_id, + const buffer_t * data) +{ + struct flow_info flow; + uint8_t buf[SOCK_BUF_SIZE]; + buffer_t msg = {SOCK_BUF_SIZE, buf}; + int err; + + memset(&flow, 0, sizeof(flow)); + + flow.id = flow_id; + flow.n_1_pid = getpid(); + + if (ipcp_flow_update_arr__irm_req_ser(&msg, &flow, data) < 0) + return -ENOMEM; + + err = send_recv_msg(&msg); + if (err < 0) + return err; + + return irm__irm_result_des(&msg); } int ipcp_flow_alloc_reply(int fd, @@ -2359,6 +2772,7 @@ int ipcp_flow_alloc_reply(int fd, flow.mpl = mpl; flow.mtu = mtu; + flow.max_rtt = proc.max_rtt; if (ipcp_flow_alloc_reply__irm_msg_ser(&msg, &flow, response, data) < 0) return -ENOMEM; @@ -2373,8 +2787,14 @@ int ipcp_flow_alloc_reply(int fd, int ipcp_flow_read(int fd, struct ssm_pk_buff ** spb) { - struct flow * flow; - ssize_t idx = -1; + struct flow * flow; + struct ssm_pk_buff * out; + uint8_t * ptr; + ssize_t idx = -1; + ssize_t fret; + size_t len; + size_t nfrags; + int ret; assert(fd >= 0 && fd < PROC_MAX_FLOWS); assert(spb); @@ -2383,37 +2803,101 @@ int ipcp_flow_read(int fd, pthread_rwlock_rdlock(&proc.lock); - assert(flow->info.id >= 0); + if (flow->info.id < 0) { + pthread_rwlock_unlock(&proc.lock); + return -ENOTALLOC; + } + + if (FRCTI_IS_STREAM(flow->frcti)) { + pthread_rwlock_unlock(&proc.lock); + return -ENOTSUP; + } + + pthread_rwlock_unlock(&proc.lock); + + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY)) + flow_rekey(flow); + + /* Advance TX off a stale epoch even on recv-mostly flows. */ + flow_tx_promote(flow); + + tw_move_safe(); + + pthread_rwlock_rdlock(&proc.lock); /* Raw flow: deliver the popped pkt directly (no FRCT rq). */ if (flow->frcti == NULL) { - pthread_rwlock_unlock(&proc.lock); idx = flow_rx_spb(flow, spb, false, NULL); + pthread_rwlock_unlock(&proc.lock); return idx < 0 ? (int) idx : 0; } while (!FRCTI_PDU_READY(flow->frcti)) { - pthread_rwlock_unlock(&proc.lock); - idx = flow_rx_spb(flow, spb, false, NULL); - if (idx < 0) + if (idx < 0) { + pthread_rwlock_unlock(&proc.lock); return idx; - - pthread_rwlock_rdlock(&proc.lock); + } FRCTI_RCV(flow->frcti, *spb); } pthread_rwlock_unlock(&proc.lock); + /* + * A hand-back of the fed spb would leave it double-owned by + * the reorder queue; frcti_consume is the only safe way to + * take it. A write can also complete a PDU, so PDU_READY may + * be true with no loop-local spb to fall back on anyway. + */ + + ret = FRCTI_PDU_INFO(flow->frcti, &len, &nfrags); + if (ret < 0) + return ret; + + /* + * Oversize (over frcti's own cap, or too big for any pool + * class): force frcti_consume's total > count drop branch + * now, so the run leaves the delivery edge instead of + * stalling every read after this one. + */ + if (len > frcti_get_max_rcv_sdu(flow->frcti)) { + (void) FRCTI_CONSUME(flow->frcti, NULL, 0); + return -EMSGSIZE; + } + + idx = ssm_pool_alloc_b(proc.pool, len, &ptr, &out, NULL); + if (idx < 0) { + if (idx == -EMSGSIZE) + (void) FRCTI_CONSUME(flow->frcti, NULL, 0); + return (int) idx; + } + + fret = FRCTI_CONSUME(flow->frcti, ptr, len); + if (fret < 0 || (size_t) fret != len) { + ssm_pool_remove(proc.pool, idx); + return fret < 0 ? (int) fret : -EIO; + } + + *spb = out; + return 0; } +/* + * Writes an spb to an IPCP-internal flow, splitting it over multiple + * FRCT fragments when it exceeds the flow's fragment payload cap. + * Consumes spb on success; on failure spb is left to the caller. + */ int ipcp_flow_write(int fd, struct ssm_pk_buff * spb) { - struct flow * flow; - int ret; + struct flow * flow; + int oflags; + size_t len; + ssize_t fret; + int ret; assert(fd >= 0 && fd < PROC_MAX_FLOWS); assert(spb); @@ -2432,8 +2916,54 @@ int ipcp_flow_write(int fd, return -EPERM; } + if (FRCTI_IS_STREAM(flow->frcti)) { + pthread_rwlock_unlock(&proc.lock); + return -ENOTSUP; + } + + oflags = flow->oflags; + pthread_rwlock_unlock(&proc.lock); + if (flow->crypt != NULL + && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY)) + flow_rekey(flow); + + flow_tx_promote(flow); + + /* Pre-empt TX key exhaustion; the timer is the backstop. */ + if (flow_wm_due(flow)) { + STORE_RELAXED(&flow->rk_wm_inflight, true); + + if (flow_rekey_trigger(flow) < 0) + STORE_RELAXED(&flow->rk_wm_inflight, false); + } + + tw_move_safe(); + + len = ssm_pk_buff_len(spb); + if (FRCTI_NEEDS_FRAG(flow->frcti, len)) { + fret = flow_write_frag(flow, ssm_pk_buff_head(spb), len, + oflags, NULL); + + if (fret < 0) + return (int) fret; + + /* Partial: flow_write_frag swallowed the real cause. */ + if (fret != (ssize_t) len) { + /* PoA flows have no tx_rb flag to consult. */ + if (flow->tx_rb != NULL + && (ssm_rbuff_get_flags(flow->tx_rb) + & RB_FLOWDOWN)) + return -EFLOWDOWN; + return -EIO; + } + + ipcp_spb_release(spb); + + return 0; + } + ret = flow_tx_spb(flow, spb, FRCT_FR_SOLE, true, NULL); return ret; @@ -2472,10 +3002,13 @@ int np1_flow_read(int fd, flow = &proc.flows[fd]; - assert(flow->info.id >= 0); - pthread_rwlock_rdlock(&proc.lock); + if (flow->info.id < 0) { + pthread_rwlock_unlock(&proc.lock); + return -ENOTALLOC; + } + off = ssm_rbuff_read(flow->rx_rb); if (off < 0) { pthread_rwlock_unlock(&proc.lock); @@ -2498,6 +3031,11 @@ int np1_flow_read(int fd, return 0; } +/* + * An N-1 flow gets no flow_write to advance its TX epoch off a rotated + * key. Promoting is local; a re-key request here would block on the + * IRMd. + */ int np1_flow_write(int fd, struct ssm_pk_buff * spb, struct ssm_pool * pool) @@ -2576,12 +3114,14 @@ int ipcp_flow_fini(int fd) return -1; } - ssm_rbuff_set_acl(proc.flows[fd].rx_rb, ACL_FLOWDOWN); - ssm_rbuff_set_acl(proc.flows[fd].tx_rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(proc.flows[fd].rx_rb, RB_FLOWDOWN); + + if (proc.flows[fd].tx_rb != NULL) + ssm_rbuff_set_flags(proc.flows[fd].tx_rb, RB_FLOWDOWN); - ssm_flow_set_notify(proc.flows[fd].set, - proc.flows[fd].info.id, - FLOW_DEALLOC); + if (proc.flows[fd].set != NULL) + ssm_flow_set_notify(proc.flows[fd].set, proc.flows[fd].info.id, + FLOW_DEALLOC); rx_rb = proc.flows[fd].rx_rb; @@ -2610,19 +3150,101 @@ int ipcp_flow_get_qoscube(int fd, return 0; } +/* Not a snapshot: two atomic loads; caller keeps the fd live. */ size_t ipcp_flow_queued(int fd) { - size_t q; + assert(fd >= 0 && fd < PROC_MAX_FLOWS); + assert(proc.flows[fd].info.id >= 0); - pthread_rwlock_rdlock(&proc.lock); + if (proc.flows[fd].poa != NULL) + return poa_flow_qlen(proc.flows[fd].poa); + + return ssm_rbuff_queued(proc.flows[fd].tx_rb) + * LOAD_RELAXED(&proc.flows[fd].mean_len); +} +size_t ipcp_flow_mean_len(int fd) +{ + assert(fd >= 0 && fd < PROC_MAX_FLOWS); assert(proc.flows[fd].info.id >= 0); - q = ssm_rbuff_queued(proc.flows[fd].tx_rb); + if (proc.flows[fd].poa != NULL) + return poa_flow_mean_len(proc.flows[fd].poa); - pthread_rwlock_unlock(&proc.lock); + return LOAD_RELAXED(&proc.flows[fd].mean_len); +} + +/* An update racing the arm seeds one bogus window; the filter absorbs. */ +int ipcp_flow_cap_arm(int fd) +{ + struct flow * flow; + struct cap_est * e; - return q; + assert(fd >= 0 && fd < PROC_MAX_FLOWS); + assert(proc.flows[fd].info.id >= 0); + + flow = &proc.flows[fd]; + if (flow->poa != NULL) { + cap_clear(poa_flow_cap_est(flow->poa)); + return 0; + } + + e = flow->cap; + if (e != NULL) { + cap_clear(e); + return 0; + } + + if (posix_memalign((void **) &e, CAP_ALIGN, sizeof(*e)) != 0) + return -ENOMEM; + + cap_clear(e); + + STORE_RELEASE(&flow->cap, e); + + return 0; +} + +void ipcp_flow_cap_update(int fd, + size_t qlen, + size_t len) +{ + struct flow * flow; + struct cap_est * e; + + assert(fd >= 0 && fd < PROC_MAX_FLOWS); + assert(proc.flows[fd].info.id >= 0); + + flow = &proc.flows[fd]; + if (flow->poa != NULL) { + cap_update(poa_flow_cap_est(flow->poa), qlen, len); + return; + } + + e = LOAD_ACQUIRE(&flow->cap); + if (e == NULL) + return; + + cap_update(e, qlen, len); +} + +uint64_t ipcp_flow_cap(int fd) +{ + struct flow * flow; + struct cap_est * e; + + assert(fd >= 0 && fd < PROC_MAX_FLOWS); + assert(proc.flows[fd].info.id >= 0); + + flow = &proc.flows[fd]; + if (flow->poa != NULL) + return cap_rate(poa_flow_cap_est(flow->poa)); + + e = LOAD_ACQUIRE(&flow->cap); + if (e == NULL) + return 0; + + return cap_rate(e); } int local_flow_transfer(int src_fd, @@ -2690,3 +3312,6 @@ int local_flow_transfer(int src_fd, return ret; } + +#include "cap.c" +#include "poa/poa.c" diff --git a/src/lib/frct.c b/src/lib/frct.c index 2e8955e3..ecec2543 100644 --- a/src/lib/frct.c +++ b/src/lib/frct.c @@ -25,16 +25,18 @@ #define DELT_RDV (100 * MILLION) /* ns */ #define MAX_RDV (1 * BILLION) /* ns */ -#define MAX_RTO_MUL 8 /* caps the RTO backoff shift */ +#define RXM_TRIES_SHIFT 5 /* >= 32 HoL tries within t_r */ +#define MAX_RTO_MUL 16 /* guard; rxm_backoff clamps */ #define MAX_TLP_PER_EP 2 /* RFC 8985 §7.3: up to 2 TLPs */ -#define INITIAL_RTO (1 * BILLION) /* RFC 6298 §2.1: 1 s default */ #define RTT_BOOT_NS (10 * MILLION) /* rtt_hint floor + initial mdev */ #define SRTT_FLOOR_NS 1000L /* 1 us; smoothed RTT floor */ #define MDEV_FLOOR_NS 100L /* 100 ns; mdev sanity floor */ #define RTT_CLAMP_MUL 16 /* probe sample cap = N * srtt */ #define MIN_RTT_WIN_NS (300ULL * BILLION) /* 5 min, Linux tcp default */ +#define MIN_RTT_SLOTS 3 /* windowed-min sample slots */ #define NACK_COOLDOWN_NS (100 * MILLION) /* pre-DRF NACK cooldown */ #define FRCT_TX_TIMEO_NS (250 * 1000) /* tx ring write deadline */ +#define RTT_LOUD_NS (500 * MILLION) /* diagnostic sample threshold */ #define ACK_DELAY_NS (2ULL * TICTIME) /* delayed-ACK fire delay */ #define FRCT "frct" @@ -49,11 +51,13 @@ #define SACK_MIN_GAP_NS (250u * 1000u) /* 250 us SACK gap */ #define MIN_REORDER_NS (250u * 1000u) /* 250 us RACK floor */ #define SACK_RXM_MAX 32 /* Cap on retransmits staged from single SACK.*/ -#define DUP_THRESH 3 /* RFC 8985 §6.2 step 2.2 SACK count gate. */ +#define DUP_THRESH 3 /* RFC 8985 §6.2 step 4 SACK count gate. */ +/* Repair budget: burst cap on SACK-driven retransmits (tokens). */ +#define RXM_BUDGET_MAX (2 * SACK_RXM_MAX) -/* RFC 8985 §7.2 RACK reorder-window scaling cap. */ +/* RFC 8985 §6.2 RACK reorder-window scaling cap. */ #define REO_WND_MULT_MAX 20 -/* RFC 8985 §7.2 step 5: round trips of no DSACK before halving. */ +/* RFC 8985 §6.2: fresh-ACKed seqnos before decaying the scale. */ #define REO_DECAY_PKTS 16 /* DSACK seqno sanity: reject reports older/farther than one rcv window. */ #define MAX_DSACK_LAG RQ_SIZE @@ -186,13 +190,17 @@ struct frcti_stat { size_t rxm_dup_rcv; /* RXM dups (peer already had it) */ size_t rxm_sack; /* SACK-mechanism retransmits */ size_t rxm_rack; /* RACK-driven retransmits */ - size_t rxm_dupthresh; /* DupThresh-driven retransmits */ + size_t rxm_zero_reo; /* repairs at zero reorder wnd */ size_t rxm_nack; /* NACK-pulled retransmits */ size_t rxm_due_count; /* rxm_due entries (pre-bail) */ size_t rxm_due_acked; /* bail: seqno < snd_lwe */ size_t rxm_due_unowned; /* bail: slot.rxm replaced */ size_t rxm_due_aged; /* bail: r->t0 + t_r < now */ size_t rxm_due_defer; /* bail: non-HoL, deferred to HoL */ + size_t rxm_hol_gone; /* defers with no rxm at HoL slot */ + size_t rxm_fast_skip; /* SACK skips: slot has FAST_RXM */ + size_t rxm_fast_stuck; /* those skips with age > rto */ + size_t rxm_no_budget; /* SACKs cut short: no repair token*/ size_t rxm_arm_fail; /* rxm_arm: malloc failed */ size_t rxm_cancel; /* entries cancelled at teardown */ size_t rxm_tx_dead; /* RXM tx into terminal flow */ @@ -302,6 +310,11 @@ struct frct_cr { uint64_t inact; /* Inactivity threshold (ns) */ }; +struct rtt_min { + time_t v; /* measured RTT (ns) */ + uint64_t t; /* when it was measured (ns) */ +}; + struct frcti { /* IMM: set once in frcti_create; read-only thereafter. */ int fd; @@ -322,18 +335,17 @@ struct frcti { struct frct_cr rcv_cr; /* RTT/RACK estimator */ - time_t srtt; /* smoothed RTT */ - time_t mdev; /* mean deviation */ - time_t min_rtt; /* RACK base, ns */ - uint64_t t_min_rtt; /* min_rtt last set */ - time_t rto; /* retransmit TO */ - time_t rto_min; /* RTO floor (ns) */ - uint8_t rto_mul; /* RTO backoff bits */ - uint32_t rtt_lwe; /* RTT-sample fence */ - uint64_t t_rcv_rtt; /* last RTT feed */ - uint64_t t_snd_probe; /* last probe sent */ - uint64_t t_latest_ack; /* RACK.fack snd-ts */ - uint32_t probe_id_next; + time_t srtt; /* smoothed RTT */ + time_t mdev; /* mean deviation */ + struct rtt_min min_rtt[MIN_RTT_SLOTS]; + time_t rto; /* retransmit TO */ + time_t rto_min; /* RTO floor (ns) */ + uint8_t rto_mul; /* RTO backoff bits */ + uint32_t rtt_lwe; /* RTT-sample fence */ + uint64_t t_rcv_rtt; /* last RTT feed */ + uint64_t t_snd_probe; /* last probe sent */ + uint64_t t_latest_ack; /* RACK.fack snd-ts */ + uint32_t probe_id_next; struct { uint32_t id; uint64_t ts; /* ts_to_ns send */ @@ -342,6 +354,7 @@ struct frcti { /* rcv reassembly */ size_t max_rcv_sdu; /* max reasm bytes */ + bool draining; /* dealloc drain */ uint8_t * rcv_ring; /* lazy alloc */ size_t rcv_ring_sz; /* power of 2 */ uint32_t ring_seq_cap; /* ring/per_pkt */ @@ -366,12 +379,13 @@ struct frcti { uint32_t dsack_seqno; bool dsack_valid; - /* RFC 8985 §7.2 RACK reorder-window scaling. */ + /* RFC 8985 §6.2 RACK reorder-window scaling. */ uint8_t reo_wnd_mult; /* REO_WND_MULT_MAX */ uint32_t dsack_lwe_snap; /* lwe @ last DSACK */ uint64_t t_last_reo_widen; /* once-per-RTT */ uint32_t dup_thresh; /* RFC 8985 */ + uint32_t rxm_budget; /* repair tokens */ uint32_t tlp_high_seq; /* §7.3: 0 = none */ uint8_t tlp_count; /* §7.3 per-episode */ uint64_t t_nack; @@ -465,7 +479,7 @@ static int frct_rib_read(const char * path, s.srtt = frcti->srtt; s.mdev = frcti->mdev; s.rto = frcti->rto; - s.min_rtt = frcti->min_rtt; + s.min_rtt = frcti->min_rtt[0].v; s.snd_cr = frcti->snd_cr; s.rcv_cr = frcti->rcv_cr; s.stat = frcti->stat; @@ -494,7 +508,7 @@ static int frct_rib_read(const char * path, " duplicates received: %20zu\n" "RXM (SACK mechanism) sent: %20zu\n" "RXM (RACK-driven) sent: %20zu\n" - "RXM (DupThresh-driven) sent: %20zu\n" + "RXM (zero reorder wnd) sent: %20zu\n" "RXM (NACK-driven) sent: %20zu\n" "ACK packets sent: %20zu\n" "Delayed-ACK timer fires: %20zu\n" @@ -549,6 +563,10 @@ static int frct_rib_read(const char * path, " bail (unowned): %20zu\n" " bail (aged): %20zu\n" " bail (defer): %20zu\n" + " defer, no rxm at HoL: %20zu\n" + " skip (fast-rxm set): %20zu\n" + " skip (stuck past rto): %20zu\n" + " skip (no repair budget): %20zu\n" "RXM-arm malloc failures: %20zu\n" "RXM cancels (teardown): %20zu\n" "RXM tx into dead flow: %20zu\n" @@ -570,7 +588,7 @@ static int frct_rib_read(const char * path, (long long)(now_ns - s.rcv_cr.act), s.rcv_cr.seqno, s.stat.rxm_rto, s.stat.rxm_rcv, s.stat.rxm_dup_rcv, - s.stat.rxm_sack, s.stat.rxm_rack, s.stat.rxm_dupthresh, + s.stat.rxm_sack, s.stat.rxm_rack, s.stat.rxm_zero_reo, s.stat.rxm_nack, s.stat.ack_snd, s.stat.ack_fire, s.stat.ack_supp_seqno, s.stat.ack_supp_inact, @@ -597,6 +615,9 @@ static int frct_rib_read(const char * path, s.stat.rxm_due_count, s.stat.rxm_due_acked, s.stat.rxm_due_unowned, s.stat.rxm_due_aged, s.stat.rxm_due_defer, + s.stat.rxm_hol_gone, + s.stat.rxm_fast_skip, s.stat.rxm_fast_stuck, + s.stat.rxm_no_budget, s.stat.rxm_arm_fail, s.stat.rxm_cancel, s.stat.rxm_tx_dead, s.stat.tx_drop, @@ -689,15 +710,15 @@ static __inline__ bool same_epoch_drf(uint32_t seq, /* * RACK reorder window R (RFC 8985 §6.2): * R = MIN(reo_wnd_mult * RACK.min_RTT / 4, SRTT) - * reo_wnd_mult scales on D-SACK evidence of under-tolerance (§7.2). + * reo_wnd_mult scales on D-SACK evidence of under-tolerance (§6.2). * Fall back to srtt when no min_rtt sample exists yet; MIN_REORDER_NS * floor guards collapse below the timer-tick resolution. */ static __inline__ uint64_t rack_reorder_window(struct frcti * frcti) { uint64_t mult = frcti->reo_wnd_mult > 0 ? frcti->reo_wnd_mult : 1; - uint64_t base = frcti->min_rtt > 0 ? (uint64_t) frcti->min_rtt - : (uint64_t) frcti->srtt; + time_t min = frcti->min_rtt[0].v; + uint64_t base = min > 0 ? (uint64_t) min : (uint64_t) frcti->srtt; uint64_t R = mult * (base / 4); R = MAX(R, (uint64_t) MIN_REORDER_NS); @@ -706,6 +727,24 @@ static __inline__ uint64_t rack_reorder_window(struct frcti * frcti) return R; } +/* + * RFC 8985 §6.2 RACK_update_reo_wnd(): as long as no reordering has + * been observed, a repair episode or DupThresh SACKs above the head + * drop the reordering tolerance to zero. This removes the tolerance + * only; the RACK time test still gates every repair. + */ +static __inline__ uint64_t rack_reo_wnd(struct frcti * frcti, + uint64_t R) +{ + if (frcti->reo_wnd_mult > 1) + return R; + + if (frcti->in_recovery || frcti->dup_thresh >= DUP_THRESH) + return 0; + + return R; +} + static __inline__ int frct_spb_reserve(size_t len, struct ssm_pk_buff ** spb) { @@ -822,7 +861,9 @@ static void frct_tx_drop_bump(struct frcti * frcti, STAT_BUMP(frcti, tx_drop_other); } -static int frct_tx(struct frcti * frcti, struct ssm_pk_buff * spb) +static int frct_tx(struct frcti * frcti, + struct ssm_pk_buff * spb, + bool prio) { struct flow * f = frcti_to_flow(frcti); const struct frct_pci * pci; @@ -849,16 +890,33 @@ static int frct_tx(struct frcti * frcti, struct ssm_pk_buff * spb) if (spb_encrypt(f, spb) < 0) goto fail; - idx = ssm_pk_buff_get_off(spb); - - /* DATA blocks; control times out so a full ring can't stall wheel. */ + /* Control times out so a full queue cannot stall the wheel. */ if (!(flags & FRCT_DATA)) { clock_gettime(PTHREAD_COND_CLOCK, &now); ts_add(&now, &intv, &deadline); + dl = &deadline; } - ret = ssm_rbuff_write_b(f->tx_rb, idx, dl); + if (f->poa != NULL) { + ret = poa_flow_tx(f->poa, spb, true, dl); + if (ret < 0) + goto fail; + + return 0; + } + + idx = ssm_pk_buff_get_off(spb); + + /* + * The peer is already waiting on a retransmission, so it skips + * the occupancy limit and never waits: the timer that sent it + * must not block, and the r-timer retries what does not fit. + */ + if (prio) + ret = ssm_rbuff_write_prio(f->tx_rb, idx); + else + ret = ssm_rbuff_write_b(f->tx_rb, idx, dl); if (ret < 0) goto fail; @@ -878,10 +936,10 @@ static void frct_mark_flow_down(struct frcti * frcti) struct flow * f = frcti_to_flow(frcti); if (f->rx_rb != NULL) - ssm_rbuff_set_acl(f->rx_rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(f->rx_rb, RB_FLOWDOWN); if (f->tx_rb != NULL) - ssm_rbuff_set_acl(f->tx_rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(f->tx_rb, RB_FLOWDOWN); } __attribute__((cold)) @@ -890,7 +948,7 @@ static void frct_mark_peer_dead(struct frcti * frcti) struct flow * f = frcti_to_flow(frcti); if (f->rx_rb != NULL) - ssm_rbuff_set_acl(f->rx_rb, ACL_FLOWPEER); + ssm_rbuff_set_flags(f->rx_rb, RB_FLOWPEER); if (proc.fqset != NULL) ssm_flow_set_notify(proc.fqset, f->info.id, FLOW_PEER); @@ -950,14 +1008,30 @@ static void frcti_pkt_snd(struct frcti * frcti, frct_hcs_set(pci, false); - frct_tx(frcti, spb); + frct_tx(frcti, spb, false); +} + +/* Restart the window from a single sample. */ +static __inline__ void min_rtt_seed(struct frcti * frcti, + time_t mrtt, + uint64_t now_ns) +{ + size_t i; + + for (i = 0; i < MIN_RTT_SLOTS; i++) { + frcti->min_rtt[i].v = mrtt; + frcti->min_rtt[i].t = now_ns; + } } /* RTO floor scales with srtt; hard floor rto_min guards sub-ms RTT. */ static void rtt_init(struct frcti * frcti, - time_t rtt_hint) + time_t rtt_hint, + uint32_t max_rtt, + uint64_t now_ns) { time_t floor; + time_t cap; if (rtt_hint > 0) { rtt_hint = MAX(rtt_hint, (time_t) RTT_BOOT_NS); @@ -965,42 +1039,85 @@ static void rtt_init(struct frcti * frcti, frcti->mdev = rtt_hint >> 3; floor = MAX(frcti->rto_min, 2 * frcti->srtt); frcti->rto = MAX(floor, rtt_hint + (frcti->mdev << MDEV_MUL)); - frcti->min_rtt = rtt_hint; + + min_rtt_seed(frcti, rtt_hint, now_ns); } else { - /* Boot from first ACK. */ + /* Boot from first ACK; declared max path RTT caps RTO. */ + cap = (time_t) (frcti->t_r >> RXM_TRIES_SHIFT); + + if (max_rtt > 0) + cap = MIN(cap, (time_t) max_rtt * 2 * MILLION); frcti->srtt = 0; frcti->mdev = RTT_BOOT_NS; - frcti->rto = MAX((time_t) INITIAL_RTO, frcti->rto_min); - frcti->min_rtt = 0; + frcti->rto = MAX(cap, frcti->rto_min); + + min_rtt_seed(frcti, 0, now_ns); } frcti->rto_mul = 0; } -/* RFC 8985 §6.2: replace min_RTT on unset, smaller sample, or expiry. */ -static __inline__ bool min_rtt_stale(struct frcti * frcti, - time_t mrtt, - uint64_t now_ns) +/* Promote the runners-up as the window slides past each slot. */ +static __inline__ void min_rtt_subwin(struct frcti * frcti, + const struct rtt_min * val) { - if (frcti->min_rtt == 0) - return true; + struct rtt_min * s = frcti->min_rtt; + int64_t dt = ts_age_ns(val->t, s[0].t); + int64_t win = (int64_t) MIN_RTT_WIN_NS; - if (mrtt < frcti->min_rtt) - return true; + /* A clock step or an out-of-order stamp: hold the window. */ + if (dt < 0) + return; - return ts_aged_ns(now_ns, frcti->t_min_rtt, MIN_RTT_WIN_NS); + if (dt > win) { + /* Slot 0 fell out; slot 1 may be stale in turn. */ + s[0] = s[1]; + s[1] = s[2]; + s[2] = *val; + if (ts_aged_ns(val->t, s[0].t, MIN_RTT_WIN_NS)) { + s[0] = s[1]; + s[1] = s[2]; + s[2] = *val; + } + } else if (s[1].t == s[0].t && dt > win / 4) { + s[2] = s[1] = *val; + } else if (s[2].t == s[1].t && dt > win / 2) { + s[2] = *val; + } } -/* Linux-style windowed-min refresh of RACK.min_RTT. */ +/* + * Windowed minimum of RACK.min_RTT over MIN_RTT_WIN_NS, after Linux + * lib/minmax.c. Slots 1 and 2 hold minima over the trailing 3/4 and + * 1/2 of the window, so when slot 0 ages out the estimate drops back + * to a true minimum over what remains rather than to a spot sample. + */ static __inline__ void min_rtt_update(struct frcti * frcti, time_t mrtt, uint64_t now_ns) { - if (!min_rtt_stale(frcti, mrtt, now_ns)) + struct rtt_min * s = frcti->min_rtt; + struct rtt_min val; + + if (mrtt <= 0) + return; + + val.v = mrtt; + val.t = now_ns; + + /* New min, unseeded, or nothing left in the window. */ + if (s[0].v == 0 || mrtt <= s[0].v + || ts_aged_ns(now_ns, s[2].t, MIN_RTT_WIN_NS)) { + min_rtt_seed(frcti, mrtt, now_ns); return; + } + + if (mrtt <= s[1].v) + s[1] = s[2] = val; + else if (mrtt <= s[2].v) + s[2] = val; - frcti->min_rtt = mrtt; - frcti->t_min_rtt = now_ns; + min_rtt_subwin(frcti, &val); } static void rtt_update(struct frcti * frcti, @@ -1035,8 +1152,15 @@ static void rtt_update(struct frcti * frcti, floor = MAX(frcti->rto_min, 2 * frcti->srtt); rto = MAX(floor, frcti->srtt + (frcti->mdev << MDEV_MUL)); + /* FIXME: align with t_r; an rto that spans it retries nothing. */ STORE_RELEASE(&frcti->rto, rto); STORE_RELEASE(&frcti->rto_mul, 0); + + /* Diagnostic: a sample this large is not a path RTT. */ + if (mrtt > RTT_LOUD_NS) + log_warn("RTT sample %lld ms, srtt %lld ms on fd %d.", + (long long) mrtt / MILLION, + (long long) frcti->srtt / MILLION, frcti->fd); } /* Fill probes[pos], return new probe_id; 0 on entropy failure. Wrlock. */ @@ -1111,7 +1235,7 @@ static void frcti_rttp_snd(struct frcti * frcti, rttp->echo_id = hton32(echo_id); memcpy(rttp->nonce, nonce, sizeof(rttp->nonce)); - frct_tx(frcti, spb); + frct_tx(frcti, spb, false); } struct rxm_entry { @@ -1124,33 +1248,6 @@ struct rxm_entry { uint8_t pkt[]; /* flexible — sized at alloc time */ }; -static struct rxm_entry * rxm_entry_create(struct frcti * frcti, - uint32_t seqno, - const struct ssm_pk_buff * spb) -{ - struct rxm_entry * r; - struct timespec now; - size_t len = ssm_pk_buff_len(spb); - - r = malloc(sizeof(*r) + len); - if (r == NULL) { - STAT_BUMP(frcti, rxm_arm_fail); - return NULL; - } - - memcpy(r->pkt, ssm_pk_buff_head(spb), len); - r->len = len; - r->frcti = frcti; - r->seqno = seqno; - - clock_gettime(PTHREAD_COND_CLOCK, &now); - r->t0 = TS_TO_UINT64(now); - - tw_init_entry(&r->tw); - - return r; -} - static void rxm_entry_destroy(struct rxm_entry * r) { free(r); @@ -1164,6 +1261,29 @@ static bool rxm_still_owned(struct frcti * frcti, } /* + * Backoff clamped to a fixed fraction of t_r, so the ladder always + * leaves room for 1 << RXM_TRIES_SHIFT tries inside the flow's life + * whatever t_r is. Never returns less than the RTO estimate itself: + * on a path whose RTT is large against t_r that many tries do not + * fit, and retrying faster than the estimate only duplicates. + */ +static uint64_t rxm_backoff(struct frcti * frcti, + time_t rto, + uint8_t rto_mul) +{ + uint64_t cap = frcti->t_r >> RXM_TRIES_SHIFT; + + if (cap < (uint64_t) rto) + return (uint64_t) rto; + + /* Compare before shifting; the product can overflow at large t_r. */ + if (rto_mul >= 64 || (uint64_t) rto > (cap >> rto_mul)) + return cap; + + return (uint64_t) rto << rto_mul; +} + +/* * All in-flight slots share the HoL backoff; otherwise non-HoL timers * cycle at base RTO and storm the wire while HoL is still backing off. */ @@ -1173,7 +1293,7 @@ static uint64_t rxm_next_deadline(struct frcti * frcti, time_t rto = LOAD_RELAXED(&frcti->rto); uint8_t rto_mul = LOAD_RELAXED(&frcti->rto_mul); - return now_ns + ((uint64_t) rto << rto_mul); + return now_ns + rxm_backoff(frcti, rto, rto_mul); } /* Copy pkt, set FRCT_RXM, refresh ackno, re-seal HCS. */ @@ -1238,7 +1358,7 @@ static void rxm_snd(struct frcti * frcti, if (seqno == snd_lwe && frcti->rto_mul < MAX_RTO_MUL) STORE_RELEASE(&frcti->rto_mul, frcti->rto_mul + 1); - /* RFC 8985 §7.2 step 4: RTO on HoL resets RACK reo scaling. */ + /* RFC 8985 §6.3: RTO on HoL resets RACK reo scaling. */ if (seqno == snd_lwe) frcti->reo_wnd_mult = 1; @@ -1251,7 +1371,7 @@ static void rxm_snd(struct frcti * frcti, return; /* ETIMEDOUT/ENOMEM: let r-timer drive teardown. */ - ret = frct_tx(frcti, spb); + ret = frct_tx(frcti, spb, true); if (ret == -EFLOWDOWN || ret == -ENOTALLOC) STAT_BUMP(frcti, rxm_tx_dead); } @@ -1287,6 +1407,24 @@ static void rxm_due(void * arg) /* R-timer expired: peer unreachable. */ if (RXM_AGED_OUT(r->t0, now_ns, frcti->t_r)) { STAT_BUMP(frcti, rxm_due_aged); + log_warn("Flow down: rxm seq=%u aged out (hol=%u) " + "age_ms=%llu t_r_ms=%llu rto_ms=%llu mul=%u " + "ack_age_ms=%lld hol_rxm=%s hol_flags=0x%x " + "budget=%u tlp_hi=%u tlp_n=%u on fd %d.", + r->seqno, snd_lwe, + (unsigned long long)(now_ns - r->t0) / MILLION, + (unsigned long long) frcti->t_r / MILLION, + (unsigned long long) LOAD_RELAXED(&frcti->rto) + / MILLION, + (unsigned) LOAD_RELAXED(&frcti->rto_mul), + (long long)(now_ns - frcti->t_latest_ack) / MILLION, + LOAD_ACQUIRE(&frcti->snd_slots[RQ_SLOT(snd_lwe)].rxm) + == NULL ? "none" : "live", + (unsigned) frcti->snd_slots[RQ_SLOT(snd_lwe)].flags, + (unsigned) frcti->rxm_budget, + frcti->tlp_high_seq, + (unsigned) frcti->tlp_count, + frcti->fd); frct_mark_flow_down(frcti); goto cleanup; } @@ -1294,8 +1432,11 @@ static void rxm_due(void * arg) /* HoL-only retx; defer at base rto so HoL transitions react. */ if (r->seqno != snd_lwe) { STAT_BUMP(frcti, rxm_due_defer); - tw_post(&r->tw, now_ns + LOAD_RELAXED(&frcti->rto), - rxm_due, r); + + if (LOAD_ACQUIRE(&frcti->snd_slots[RQ_SLOT(snd_lwe)].rxm) + == NULL) + STAT_BUMP(frcti, rxm_hol_gone); + tw_post(&r->tw, now_ns + LOAD_RELAXED(&frcti->rto), rxm_due, r); return; } @@ -1324,33 +1465,56 @@ static void rxm_due(void * arg) rxm_entry_destroy(r); } -static int rxm_arm(struct frcti * frcti, - uint32_t seqno, - const struct ssm_pk_buff * spb) +/* Pre-allocate rxm entry so frcti_snd can fail before committing seqno. */ +static struct rxm_entry * rxm_alloc(struct frcti * frcti, + size_t pkt_len) { struct rxm_entry * r; - time_t rto; - uint8_t rto_mul; - uint64_t deadline; - r = rxm_entry_create(frcti, seqno, spb); - if (r == NULL) - return -ENOMEM; + r = malloc(sizeof(*r) + pkt_len); + if (r == NULL) { + STAT_BUMP(frcti, rxm_arm_fail); + return NULL; + } + + r->frcti = frcti; + tw_init_entry(&r->tw); + + return r; +} + +static void rxm_arm(struct frcti * frcti, + uint32_t seqno, + struct rxm_entry * r, + const struct ssm_pk_buff * spb) +{ + struct timespec now; + time_t rto; + uint8_t rto_mul; + uint64_t deadline; + size_t len = ssm_pk_buff_len(spb); + + memcpy(r->pkt, ssm_pk_buff_head(spb), len); + r->len = len; + r->seqno = seqno; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + r->t0 = TS_TO_UINT64(now); rto = LOAD_RELAXED(&frcti->rto); rto_mul = LOAD_RELAXED(&frcti->rto_mul); - deadline = r->t0 + ((uint64_t) rto << rto_mul); + deadline = r->t0 + rxm_backoff(frcti, rto, rto_mul); pthread_rwlock_wrlock(&frcti->lock); + assert(before(seqno, frcti->snd_cr.lwe + RQ_SIZE)); + list_add_tail(&r->next, &frcti->rxm_list); STORE_RELEASE(&frcti->snd_slots[RQ_SLOT(seqno)].rxm, r); pthread_rwlock_unlock(&frcti->lock); tw_post(&r->tw, deadline, rxm_due, r); - - return 0; } static void rxm_cancel_all(struct frcti * frcti) @@ -1475,7 +1639,7 @@ static void frcti_sack_snd(struct frcti * frcti, for (i = 0; i < sa->n; ++i) sack_block_put(buf.data, i, sa->blocks[i][0], sa->blocks[i][1]); - frct_tx(frcti, spb); + frct_tx(frcti, spb, false); } static void ack_snd(struct frcti * frcti, @@ -1652,6 +1816,8 @@ static void ka_snd(struct frcti * frcti) snd_idle = ts_age_ns(now_ns, LOAD_RELAXED(&frcti->snd_cr.act)); if (rcv_idle > timeo_ns) { + log_warn("Peer dead: rcv idle %lld ms on fd %d.", + (long long) rcv_idle / MILLION, frcti->fd); frct_mark_peer_dead(frcti); return; } @@ -1674,7 +1840,7 @@ static void ka_snd(struct frcti * frcti) frct_hcs_set(pci, false); STAT_BUMP(frcti, ka_snd); - frct_tx(frcti, spb); + frct_tx(frcti, spb, false); ka_arm(frcti); } @@ -1806,6 +1972,7 @@ struct frcti * frcti_create(int fd, uint64_t r, uint64_t mpl, time_t rtt_hint, + uint32_t max_rtt, qosspec_t qs, uint32_t mtu) { @@ -1861,6 +2028,7 @@ struct frcti * frcti_create(int fd, / SACK_BLOCK_SIZE; if (bb > SACK_MAX_BLOCKS) bb = SACK_MAX_BLOCKS; + frcti->sack_n_max = (uint16_t) bb; frcti->max_rcv_sdu = FRCT_MAX_SDU; @@ -1874,8 +2042,8 @@ struct frcti * frcti_create(int fd, } frcti->rto_min = (time_t) MAX(RTO_MIN, 1ULL << RXMQ_RES); - rtt_init(frcti, rtt_hint); - frcti->t_min_rtt = now_ns; + + rtt_init(frcti, rtt_hint, max_rtt, now_ns); frcti->probe_id_next = 1; frcti->t_rcv_rtt = now_ns; frcti->t_snd_probe = now_ns; @@ -1892,6 +2060,7 @@ struct frcti * frcti_create(int fd, frcti->in_recovery = false; frcti->recovery_high = 0; frcti->rack_fired_lwe = 0; + frcti->rxm_budget = RXM_BUDGET_MAX; tw_init_entry(&frcti->ack_tw); tw_init_entry(&frcti->ka_tw); @@ -1952,10 +2121,14 @@ void frcti_destroy(struct frcti * frcti) printf("[FRCT teardown] pid=%d fd=%d " "sdu_snd=%zu sdu_reasm=%zu sdu_sole=%zu " "frag_snd=%zu frag_rcv=%zu frag_drop=%zu " - "rxm_rto=%zu rxm_sack=%zu rxm_dup=%zu " + "rxm_rto=%zu rxm_sack=%zu rxm_rack=%zu rxm_zreo=%zu " "rxm_due=%zu acked=%zu unowned=%zu aged=%zu defer=%zu " + "hol_gone=%zu " + "fast_skip=%zu fast_stuck=%zu no_budget=%zu " "cancel=%zu arm_fail=%zu inflight=%u " "nack_snd=%zu nack_rcv=%zu inact_drop=%zu " + "tlp_snd=%zu sack_snd=%zu sack_rcv=%zu ack_supp=%zu " + "out_rcv=%zu rqo_rcv=%zu dup_rcv=%zu rxm_dup_rcv=%zu " "drf_rebase=%zu rq_released=%zu\n", (int) getpid(), frcti->fd, frcti->stat.sdu_snd_frag, frcti->stat.sdu_reasm, @@ -1963,14 +2136,21 @@ void frcti_destroy(struct frcti * frcti) frcti->stat.frag_snd, frcti->stat.frag_rcv, frcti->stat.frag_drop, frcti->stat.rxm_rto, frcti->stat.rxm_sack, - frcti->stat.rxm_dupthresh, + frcti->stat.rxm_rack, frcti->stat.rxm_zero_reo, frcti->stat.rxm_due_count, frcti->stat.rxm_due_acked, frcti->stat.rxm_due_unowned, frcti->stat.rxm_due_aged, frcti->stat.rxm_due_defer, + frcti->stat.rxm_hol_gone, + frcti->stat.rxm_fast_skip, frcti->stat.rxm_fast_stuck, + frcti->stat.rxm_no_budget, frcti->stat.rxm_cancel, frcti->stat.rxm_arm_fail, frcti->snd_cr.seqno - frcti->snd_cr.lwe, frcti->stat.nack_snd, frcti->stat.nack_rcv, frcti->stat.inact_drop, + frcti->stat.tlp_snd, frcti->stat.sack_snd, + frcti->stat.sack_rcv, frcti->stat.ack_supp_seqno, + frcti->stat.out_rcv, frcti->stat.rqo_rcv, + frcti->stat.dup_rcv, frcti->stat.rxm_dup_rcv, frcti->stat.drf_rebase, frcti->stat.rq_released); #endif @@ -2042,6 +2222,19 @@ int frcti_set_max_rcv_sdu(struct frcti * frcti, return 0; } +/* Dealloc drain discards SDUs by design; don't count them as drops. */ +static void frcti_set_draining(struct frcti * frcti) +{ + if (frcti == NULL) + return; + + pthread_rwlock_wrlock(&frcti->lock); + + frcti->draining = true; + + pthread_rwlock_unlock(&frcti->lock); +} + size_t frcti_get_rcv_ring_sz(struct frcti * frcti) { size_t ret; @@ -2066,6 +2259,7 @@ int frcti_set_rcv_ring_sz(struct frcti * frcti, if (!frcti->stream) return -ENOTSUP; + if (!stream_ring_sz_ok(frcti, n)) return -EINVAL; @@ -2135,6 +2329,7 @@ static void sack_rxm_snd(struct frcti * frcti, { struct ssm_pk_buff * spb; const struct frct_pci * pci; + struct rxm_entry * rxm; uint32_t rcv_lwe; uint32_t seqno; int ret; @@ -2148,14 +2343,15 @@ static void sack_rxm_snd(struct frcti * frcti, pci = (const struct frct_pci *) ssm_pk_buff_head(spb); seqno = ntoh32(pci->seqno); - /* Register fresh rxm before send; old entry self-cleans. */ - if (rxm_arm(frcti, seqno, spb) < 0) { + rxm = rxm_alloc(frcti, ssm_pk_buff_len(spb)); + if (rxm == NULL) { frct_spb_release(spb); return; } + rxm_arm(frcti, seqno, rxm, spb); STAT_BUMP(frcti, rxm_sack); - ret = frct_tx(frcti, spb); + ret = frct_tx(frcti, spb, true); if (ret == -EFLOWDOWN || ret == -ENOTALLOC) STAT_BUMP(frcti, rxm_tx_dead); } @@ -2174,7 +2370,7 @@ static int fast_rxm_send(struct frcti * frcti, if (spb == NULL) return 0; - return frct_tx(frcti, spb); + return frct_tx(frcti, spb, true); } /* PCI bytes survive head_release at receive; just rewind the pointer. */ @@ -2629,14 +2825,16 @@ static ssize_t frcti_consume(struct frcti * frcti, goto unlock; } if (st == FRAG_DROP) { - STAT_ADD(frcti, frag_drop, n); + if (!frcti->draining) + STAT_ADD(frcti, frag_drop, n); frag_drop(frcti, n); continue; } /* FRAG_DELIVER */ total = frag_total_len(frcti, n, &overflow); if (overflow || total > frcti->max_rcv_sdu || total > count) { - STAT_ADD(frcti, frag_drop, n); + if (!frcti->draining) + STAT_ADD(frcti, frag_drop, n); frag_drop(frcti, n); ret = -EMSGSIZE; goto unlock; @@ -2685,6 +2883,49 @@ static bool frcti_pdu_ready(struct frcti * frcti) return ready; } +/* + * Size a ready SDU before consuming it: *len is the total byte + * count, *nfrags the fragment count. 0 on success, -EAGAIN if no + * complete SDU is ready (includes the stream and overflow cases). + */ +static int frcti_pdu_info(struct frcti * frcti, + size_t * len, + size_t * nfrags) +{ + size_t count; + bool overflow; + int ret; + + assert(frcti); + + pthread_rwlock_rdlock(&frcti->lock); + + if (frcti->stream) { + ret = -EAGAIN; + goto unlock; + } + + if (frag_run_inspect(frcti, &count) != FRAG_DELIVER) { + ret = -EAGAIN; + goto unlock; + } + + *len = frag_total_len(frcti, count, &overflow); + + if (overflow) { + ret = -EAGAIN; + goto unlock; + } + + *nfrags = count; + ret = 0; + + unlock: + pthread_rwlock_unlock(&frcti->lock); + + return ret; +} + /* No srtt yet: probe at the cold-probe cadence to seed it. */ #define PROBE_DUE_COLD(frcti, now_ns) \ ((now_ns) - (frcti)->t_snd_probe > (uint64_t) RTTP_COLD_NS) @@ -2932,9 +3173,6 @@ static void tlp_due(void * arg) if (frcti->snd_cr.seqno == frcti->snd_cr.lwe) goto unlock; - if (!before(frcti->snd_cr.seqno, frcti->snd_cr.rwe)) - goto unlock; /* FC-blocked: RDV handles it. */ - /* RFC 8985 §7.3: one outstanding probe, MAX_TLP_PER_EP per ep. */ if (frcti->tlp_high_seq != 0) goto unlock; @@ -2957,8 +3195,8 @@ static void tlp_due(void * arg) goto unlock; /* Cap: if HoL RTO is due, let rxm_due fire instead. */ - rto_at = rxm->t0 + ((uint64_t) frcti->rto - << LOAD_RELAXED(&frcti->rto_mul)); + rto_at = rxm->t0 + rxm_backoff(frcti, frcti->rto, + LOAD_RELAXED(&frcti->rto_mul)); if (rto_at <= now_ns) goto unlock; @@ -2967,10 +3205,10 @@ static void tlp_due(void * arg) memcpy(pkt_copy, rxm->pkt, rxm->len); pkt_len = rxm->len; frcti->snd_slots[hp].time = now_ns; - frcti->snd_slots[hp].flags |= SND_TLP | SND_FAST_RXM; + frcti->snd_slots[hp].flags |= SND_TLP; frcti->rtt_lwe = frcti->snd_cr.lwe + 1; - /* §7.3 outstanding-probe marker; ack_rcv/rxm_snd clear. */ - frcti->tlp_high_seq = frcti->snd_cr.seqno; + /* Probe is the HoL: any cum-ACK resolves the episode. */ + frcti->tlp_high_seq = frcti->snd_cr.lwe + 1; frcti->tlp_count++; STAT_BUMP(frcti, tlp_snd); } @@ -3000,8 +3238,10 @@ static int tlp_arm(struct frcti * frcti) /* §7.3: one outstanding probe, MAX_TLP_PER_EP per recovery ep. */ if (LOAD_RELAXED(&frcti->tlp_high_seq) != 0) return 0; + if (LOAD_RELAXED(&frcti->tlp_count) >= MAX_TLP_PER_EP) return 0; + if (__atomic_test_and_set(&frcti->tlp_pending, __ATOMIC_RELAXED)) return 0; @@ -3084,15 +3324,20 @@ static bool rtt_sample_eligible(struct frcti * frcti, { if (flags & FRCT_RXM) return false; + if (frcti->snd_slots[p].flags & (SND_RTX | SND_TLP)) return false; + if (LOAD_ACQUIRE(&frcti->snd_slots[p].rxm) == NULL) return false; + if (before(lwe, frcti->rtt_lwe)) return false; + /* Don't seed srtt from a cum-ACK; let probes seed. */ if (frcti->srtt == 0) return false; + return true; } @@ -3109,19 +3354,21 @@ static void fast_rxm_consider(struct frcti * frcti, struct snd_slot * slot; size_t hp; uint64_t R; - bool rack_ok; + uint64_t reo; + int64_t age; hp = RQ_SLOT(frcti->snd_cr.lwe); slot = &frcti->snd_slots[hp]; rxm = LOAD_ACQUIRE(&slot->rxm); R = rack_reorder_window(frcti); + reo = rack_reo_wnd(frcti, R); if (RXM_SLOT_EMPTY(rxm)) return; - /* RFC 8985 §6.2: time-based RACK OR DupThresh count. */ - rack_ok = (int64_t)(frcti->t_latest_ack - slot->time) > (int64_t) R; - if (!rack_ok && frcti->dup_thresh < DUP_THRESH) + /* RFC 8985 §6.2: last transmission older than the latest ack + reo. */ + age = (int64_t)(frcti->t_latest_ack - slot->time); + if (age <= (int64_t) reo) return; /* HoL aged past t_r; let rxm_due tear the flow down. */ @@ -3142,10 +3389,11 @@ static void fast_rxm_consider(struct frcti * frcti, memcpy(pending->fast_rxm.data, rxm->pkt, rxm->len); slot->flags |= SND_RTX | SND_FAST_RXM; frcti->rtt_lwe = frcti->snd_cr.lwe + 1; - if (rack_ok) + + if (age > (int64_t) R) STAT_BUMP(frcti, rxm_rack); else - STAT_BUMP(frcti, rxm_dupthresh); + STAT_BUMP(frcti, rxm_zero_reo); } /* Caller holds wrlock; RACK fast retransmit queued in pending. */ @@ -3158,6 +3406,7 @@ static void frcti_ack_rcv(struct frcti * frcti, { uint32_t ackno; uint32_t lwe; + uint64_t t_ack; size_t p; size_t fresh; @@ -3182,16 +3431,24 @@ static void frcti_ack_rcv(struct frcti * frcti, STORE_RELEASE(&frcti->snd_cr.lwe, ackno); + /* Packet conservation: one repair token per seqno that left. */ + frcti->rxm_budget += ackno - lwe; + + if (frcti->rxm_budget > RXM_BUDGET_MAX) + frcti->rxm_budget = RXM_BUDGET_MAX; + /* §7.3: cum-ACK past the probed seqno resolves the TLP. */ if (frcti->tlp_high_seq != 0 - && !before(ackno, frcti->tlp_high_seq)) + && !before(ackno, frcti->tlp_high_seq)) { frcti->tlp_high_seq = 0; + frcti->tlp_count = 0; + } /* §7.3: end the probe episode once inflight drains. */ if (ackno == frcti->snd_cr.seqno) frcti->tlp_count = 0; - /* RFC 8985 §7.2: halve mult per REO_DECAY_PKTS fresh-ACK'd seqnos. */ + /* RFC 8985 §6.2: halve mult per REO_DECAY_PKTS fresh-ACK'd seqnos. */ fresh = ackno - frcti->dsack_lwe_snap; if (frcti->reo_wnd_mult > 1 && fresh >= REO_DECAY_PKTS) { uint8_t half = frcti->reo_wnd_mult >> 1; @@ -3199,8 +3456,15 @@ static void frcti_ack_rcv(struct frcti * frcti, frcti->dsack_lwe_snap = ackno; } - /* RFC 8985: latest cum-ACKed send-time (slot of ackno-1). */ - frcti->t_latest_ack = frcti->snd_slots[RQ_SLOT(ackno - 1)].time; + /* + * RFC 8985 §6.2 RACK_sent_after: RACK.xmit_ts only ever moves + * forward. A cum-ACK covers older seqnos than the SACK blocks + * that raised it, so assigning here would drop it back and + * wedge the loss test for every hole above the cum-ACK. + */ + t_ack = frcti->snd_slots[RQ_SLOT(ackno - 1)].time; + if (t_ack > frcti->t_latest_ack) + frcti->t_latest_ack = t_ack; /* RFC 8985: SACK-above-lwe count is per-recovery-episode. */ frcti->dup_thresh = 0; @@ -3228,10 +3492,12 @@ static void frcti_ack_rcv(struct frcti * frcti, static uint32_t sack_mark_blocks(struct frcti * frcti, const uint8_t * payload, uint16_t n, - uint32_t * newly_marked) + uint32_t * newly_marked, + uint64_t now_ns) { uint32_t hi_sacked = frcti->snd_cr.lwe; uint32_t marked = 0; + uint64_t rtt_t = 0; /* freshest send time worth timing */ uint16_t i; for (i = 0; i < n; ++i) { @@ -3254,10 +3520,14 @@ static uint32_t sack_mark_blocks(struct frcti * frcti, for (k = s; before(k, e); ++k) { size_t kp = RQ_SLOT(k); uint64_t t_k; + uint8_t f_k; if (clamped && k == frcti->snd_cr.lwe) continue; if (LOAD_ACQUIRE(&frcti->snd_slots[kp].rxm) == NULL) continue; + + f_k = frcti->snd_slots[kp].flags; + STORE_RELEASE(&frcti->snd_slots[kp].rxm, NULL); frcti->snd_slots[kp].flags = 0; marked++; @@ -3265,12 +3535,38 @@ static uint32_t sack_mark_blocks(struct frcti * frcti, t_k = frcti->snd_slots[kp].time; if (t_k > frcti->t_latest_ack) frcti->t_latest_ack = t_k; + + /* Karn: a retransmitted seqno times nothing. */ + if (f_k & (SND_RTX | SND_TLP | SND_FAST_RXM)) + continue; + + if (before(k, frcti->rtt_lwe)) + continue; + + if (t_k > rtt_t) + rtt_t = t_k; } if (after(e, hi_sacked)) hi_sacked = e; } + /* + * One sample per SACK, off the freshest packet it confirms. + * A hole keeps every seqno out of the cum-ACK path, so this + * is the only estimator input while one is open. Seeding is + * still left to the probes. + */ + if (rtt_t > 0 && frcti->srtt != 0) { + int64_t mrtt = ts_age_ns(now_ns, rtt_t); + + if (mrtt > 0) { + rtt_update(frcti, (time_t) mrtt, now_ns); + + frcti->t_rcv_rtt = now_ns; + } + } + *newly_marked = marked; return hi_sacked; } @@ -3281,9 +3577,9 @@ static void sack_queue_rxm(struct frcti * frcti, uint64_t now_ns, struct pending * pending) { - uint64_t R = rack_reorder_window(frcti); + uint64_t R = rack_reorder_window(frcti); + uint64_t reo = rack_reo_wnd(frcti, R); uint32_t k; - bool rack_ok; for (k = frcti->snd_cr.lwe; before(k, hi_sacked); ++k) { struct rxm_entry * rxm; @@ -3299,22 +3595,40 @@ static void sack_queue_rxm(struct frcti * frcti, if (rxm == NULL) continue; - if (frcti->snd_slots[kp].flags & SND_FAST_RXM) - continue; + /* Repairs are ACK-clocked; RTO/HoL cover a dry bucket. */ + if (frcti->rxm_budget == 0) { + STAT_BUMP(frcti, rxm_no_budget); + break; + } + + /* + * A fast-retx outstanding past the reorder window is + * presumed lost in turn; clear the flag so RACK can + * repair it again. The rack_ok test below still needs + * an ack for a later packet, so this cannot storm. + */ + if (frcti->snd_slots[kp].flags & SND_FAST_RXM) { + if (!ts_aged_ns(now_ns, frcti->snd_slots[kp].time, R)) { + STAT_BUMP(frcti, rxm_fast_skip); + continue; + } + + STAT_BUMP(frcti, rxm_fast_stuck); + frcti->snd_slots[kp].flags &= ~SND_FAST_RXM; + } if (RXM_AGED_OUT(rxm->t0, now_ns, frcti->t_r)) continue; rack_age = frcti->t_latest_ack - frcti->snd_slots[kp].time; - /* RFC 8985 §6.2: time-based RACK OR DupThresh count. */ - rack_ok = (int64_t) rack_age > (int64_t) R; - if (!rack_ok && frcti->dup_thresh < DUP_THRESH) + /* RFC 8985 §6.2: last transmission older than latest + reo. */ + if ((int64_t) rack_age <= (int64_t) reo) continue; - if (rack_ok) + if ((int64_t) rack_age > (int64_t) R) STAT_BUMP(frcti, rxm_rack); else - STAT_BUMP(frcti, rxm_dupthresh); + STAT_BUMP(frcti, rxm_zero_reo); pending->sack_rxm[cnt].data = malloc(rxm->len); if (pending->sack_rxm[cnt].data == NULL) @@ -3323,6 +3637,7 @@ static void sack_queue_rxm(struct frcti * frcti, pending->sack_rxm[cnt].len = rxm->len; memcpy(pending->sack_rxm[cnt].data, rxm->pkt, rxm->len); pending->sack_rxm_cnt++; + frcti->rxm_budget--; /* NULL slot so the original timer self-cleans. */ STORE_RELEASE(&frcti->snd_slots[kp].rxm, NULL); frcti->snd_slots[kp].time = now_ns; @@ -3376,7 +3691,7 @@ static bool sack_is_dsack(struct frcti * frcti, return false; } -/* RFC 8985 §7.2: grow reo_wnd_mult on DSACK; at most once per RTT. */ +/* RFC 8985 §6.2: grow reo_wnd_mult on DSACK; at most once per RTT. */ static __inline__ void reo_wnd_on_dsack(struct frcti * frcti, uint64_t now_ns) { @@ -3433,9 +3748,15 @@ static void frcti_sack_rcv(struct frcti * frcti, recovery_enter(frcti); marked = 0; - hi_sacked = sack_mark_blocks(frcti, pkt.data, n, &marked); + hi_sacked = sack_mark_blocks(frcti, pkt.data, n, &marked, now_ns); frcti->dup_thresh += marked; + /* Packet conservation: a newly SACKed seqno also left the wire. */ + frcti->rxm_budget += marked; + + if (frcti->rxm_budget > RXM_BUDGET_MAX) + frcti->rxm_budget = RXM_BUDGET_MAX; + if (after(hi_sacked, frcti->snd_cr.lwe)) sack_queue_rxm(frcti, hi_sacked, now_ns, pending); } @@ -3476,7 +3797,7 @@ static void frcti_nack_snd(struct frcti * frcti, frct_hcs_set(pci, false); - frct_tx(frcti, spb); + frct_tx(frcti, spb, false); } enum frct_act { @@ -3586,13 +3907,10 @@ static bool sack_check(struct frcti * frcti, n = dsack_consume(frcti, out->blocks); if (n == 1) out->dsack = true; + n += sack_blocks_build(frcti, out->blocks + n, frcti->sack_n_max - n); - if (!out->dsack - && rcv_cr->lwe == frcti->sack_lwe && n == frcti->sack_n) - return false; - out->n = n; out->ack = rcv_cr->lwe; out->rwe = frcti_advert_rwe(frcti); @@ -3648,6 +3966,7 @@ static void seqno_rotate(struct frcti * frcti, if (!ts_aged_ns(now_ns, snd_cr->act, snd_cr->inact)) return; + /* Idle-on-wire ≠idle e2e: don't orphan in-flight rxm. */ if (snd_cr->seqno != snd_cr->lwe) return; @@ -3673,6 +3992,7 @@ static int frcti_snd(struct frcti * frcti, struct timespec now; struct frct_cr * snd_cr; struct frct_cr * rcv_cr; + struct rxm_entry * rxm = NULL; uint32_t seqno; uint16_t pci_flags = 0; bool rtx; @@ -3699,10 +4019,16 @@ static int frcti_snd(struct frcti * frcti, if (pci == NULL) return -ENOMEM; - memset(pci, 0, FRCT_PCILEN); + /* Pre-allocate rxm so alloc fail can't orphan a seqno. */ + if (snd_cr->cflags & FRCTFRTX) { + rxm = rxm_alloc(frcti, ssm_pk_buff_len(spb)); + if (rxm == NULL) { + ssm_pk_buff_pop(spb, frcti_data_hdr_len(frcti)); + return -ENOMEM; + } + } - if (frcti->stream) - spci = FRCT_SPCI(pci); + memset(pci, 0, FRCT_PCILEN); clock_gettime(PTHREAD_COND_CLOCK, &now); now_ns = TS_TO_UINT64(now); @@ -3719,6 +4045,8 @@ static int frcti_snd(struct frcti * frcti, STAT_BUMP(frcti, frag_snd); if (frcti->stream) { + spci = FRCT_SPCI(pci); + if (flags & FRCT_FIN) pci_flags |= FRCT_FIN; @@ -3773,13 +4101,23 @@ static int frcti_snd(struct frcti * frcti, frcti_rttp_snd(frcti, probe_id, 0, probe_nonce); if (rtx) { - rxm_arm(frcti, seqno, spb); + assert(rxm != NULL); + rxm_arm(frcti, seqno, rxm, spb); tlp_arm(frcti); } return 0; } +/* Stream FIN is armed for rxm; needs to be in window. */ +static __inline__ bool stream_fin_blocked(struct frcti * frcti) +{ + if (!frcti->stream) + return false; + + return !before(frcti->snd_cr.seqno, frcti->snd_cr.lwe + RQ_SIZE); +} + /* * Stream: 0-byte FRCT_FIN DATA so peer's flow_read returns 0 at this * byte. Msg: control packet with FRCT_FIN flag, snd_cr.seqno carried @@ -3797,6 +4135,13 @@ static void frcti_fin_snd(struct frcti * frcti) pthread_rwlock_wrlock(&frcti->lock); already = frcti->snd_fin_sent; + + /* Defer before committing snd_fin_sent; linger loop retries. */ + if (!already && stream_fin_blocked(frcti)) { + pthread_rwlock_unlock(&frcti->lock); + return; + } + frcti->snd_fin_sent = true; fin_seqno = frcti->snd_cr.seqno; @@ -3824,7 +4169,7 @@ static void frcti_fin_snd(struct frcti * frcti) return; } - if (frct_tx(frcti, spb) < 0) + if (frct_tx(frcti, spb, false) < 0) return; pthread_rwlock_wrlock(&frcti->lock); @@ -4154,6 +4499,9 @@ static void frcti_rcv(struct frcti * frcti, #define FRCTI_PDU_READY(frcti) \ ((frcti) != NULL && frcti_pdu_ready(frcti)) +#define FRCTI_PDU_INFO(frcti, len, nfrags) \ + ((frcti) == NULL ? -EAGAIN : frcti_pdu_info((frcti), (len), (nfrags))) + #define FRCTI_CONSUME(frcti, buf, count) \ ((frcti) == NULL ? (ssize_t) -EAGAIN \ : (frcti)->stream \ diff --git a/src/lib/hash.c b/src/lib/hash.c index 62bbf2b8..903474df 100644 --- a/src/lib/hash.c +++ b/src/lib/hash.c @@ -74,8 +74,10 @@ uint16_t hash_len(enum hash_algo algo) { if (algo == HASH_CRC8) return CRC8_HASH_LEN; + if (algo == HASH_CRC16) return CRC16_HASH_LEN; + if (algo == HASH_CRC64) return CRC64_HASH_LEN; #ifdef HAVE_LIBGCRYPT @@ -101,6 +103,7 @@ void mem_hash(enum hash_algo algo, *(uint8_t *) dst = crc; return; } + if (algo == HASH_CRC16) { uint16_t crc = 0; @@ -108,6 +111,7 @@ void mem_hash(enum hash_algo algo, *(uint16_t *) dst = htobe16(crc); return; } + if (algo == HASH_CRC64) { uint64_t crc = 0; @@ -162,3 +166,14 @@ void str_hash(enum hash_algo algo, { return mem_hash(algo, dst, (const uint8_t *) str, strlen(str)); } + +uint64_t hash_mix64(uint64_t key) +{ + key ^= key >> 33; + key *= 0xff51afd7ed558ccdULL; + key ^= key >> 33; + key *= 0xc4ceb9fe1a85ec53ULL; + key ^= key >> 33; + + return key; +} diff --git a/src/lib/irm.c b/src/lib/irm.c index c62701aa..5d53841f 100644 --- a/src/lib/irm.c +++ b/src/lib/irm.c @@ -118,6 +118,8 @@ int irm_bootstrap_ipcp(pid_t pid, msg.has_pid = true; msg.pid = pid; msg.conf = ipcp_config_s_to_msg(conf); + if (msg.conf == NULL) + return -ENOMEM; recv_msg = send_recv_irm_msg(&msg); ipcp_config_msg__free_unpacked(msg.conf, NULL); @@ -135,10 +137,11 @@ int irm_bootstrap_ipcp(pid_t pid, return ret; } -int irm_connect_ipcp(pid_t pid, - const char * dst, - const char * component, - qosspec_t qs) +int irm_connect_ipcp(pid_t pid, + const char * dst, + const char * component, + qosspec_t qs, + const struct poa_addr * addr) { irm_msg_t msg = IRM_MSG__INIT; irm_msg_t * recv_msg; @@ -152,7 +155,18 @@ int irm_connect_ipcp(pid_t pid, msg.pid = pid; msg.qosspec = qos_spec_s_to_msg(&qs); + if (addr != NULL) { + msg.peer = poa_addr_s_to_msg(addr); + if (msg.peer == NULL) { + qosspec_msg__free_unpacked(msg.qosspec, NULL); + return -ENOMEM; + } + } + recv_msg = send_recv_irm_msg(&msg); + + if (msg.peer != NULL) + poa_addr_msg__free_unpacked(msg.peer, NULL); qosspec_msg__free_unpacked(msg.qosspec, NULL); if (recv_msg == NULL) @@ -245,8 +259,99 @@ ssize_t irm_list_ipcps(struct ipcp_list_info ** ipcps) return nr; } -int irm_enroll_ipcp(pid_t pid, - const char * dst) +static int irm_attach_detach(pid_t pid, + const struct poa_spec * poa, + int code) +{ + irm_msg_t msg = IRM_MSG__INIT; + irm_msg_t * recv_msg; + int ret; + + if (pid == -1 || poa == NULL) + return -EINVAL; + + msg.code = code; + msg.has_pid = true; + msg.pid = pid; + msg.poa = poa_spec_s_to_msg(poa); + if (msg.poa == NULL) + return -EINVAL; + + recv_msg = send_recv_irm_msg(&msg); + + poa_spec_msg__free_unpacked(msg.poa, NULL); + + if (recv_msg == NULL) + return -EIRMD; + + if (!recv_msg->has_result) { + irm_msg__free_unpacked(recv_msg, NULL); + return -EIRMD; + } + + ret = recv_msg->result; + + irm_msg__free_unpacked(recv_msg, NULL); + + return ret; +} + +ssize_t irm_list_poas(pid_t pid, + struct poa_spec ** poas) +{ + irm_msg_t msg = IRM_MSG__INIT; + irm_msg_t * recv_msg; + size_t nr; + size_t i; + + if (pid == -1 || poas == NULL) + return -EINVAL; + + *poas = NULL; + + msg.code = IRM_MSG_CODE__IRM_LIST_POAS; + msg.has_pid = true; + msg.pid = pid; + + recv_msg = send_recv_irm_msg(&msg); + if (recv_msg == NULL) + return -EIRMD; + + nr = recv_msg->n_poas; + if (nr == 0) { + irm_msg__free_unpacked(recv_msg, NULL); + return 0; + } + + *poas = malloc(nr * sizeof(**poas)); + if (*poas == NULL) { + irm_msg__free_unpacked(recv_msg, NULL); + return -ENOMEM; + } + + for (i = 0; i < nr; i++) + (*poas)[i] = poa_spec_msg_to_s(recv_msg->poas[i]); + + irm_msg__free_unpacked(recv_msg, NULL); + + return (ssize_t) nr; +} + +int irm_attach_ipcp(pid_t pid, + const struct poa_spec * poa) +{ + return irm_attach_detach(pid, poa, IRM_MSG_CODE__IRM_ATTACH_IPCP); +} + +int irm_detach_ipcp(pid_t pid, + const struct poa_spec * poa) +{ + return irm_attach_detach(pid, poa, IRM_MSG_CODE__IRM_DETACH_IPCP); +} + +int irm_enroll_ipcp(pid_t pid, + const char * dst, + const struct poa_addr * addr) { irm_msg_t msg = IRM_MSG__INIT; irm_msg_t * recv_msg = NULL; @@ -260,7 +365,17 @@ int irm_enroll_ipcp(pid_t pid, msg.pid = pid; msg.dst = (char *) dst; + if (addr != NULL) { + msg.peer = poa_addr_s_to_msg(addr); + if (msg.peer == NULL) + return -ENOMEM; + } + recv_msg = send_recv_irm_msg(&msg); + + if (msg.peer != NULL) + poa_addr_msg__free_unpacked(msg.peer, NULL); + if (recv_msg == NULL) return -EIRMD; diff --git a/src/lib/pb/ipcp.proto b/src/lib/pb/ipcp.proto index 406b8d9c..298a15e2 100644 --- a/src/lib/pb/ipcp.proto +++ b/src/lib/pb/ipcp.proto @@ -39,6 +39,10 @@ enum ipcp_msg_code { IPCP_CONNECT = 10; IPCP_DISCONNECT = 11; IPCP_REPLY = 12; + IPCP_FLOW_UPDATE = 13; + IPCP_ATTACH = 14; + IPCP_DETACH = 15; + IPCP_LIST_POAS = 16; } message ipcp_msg { @@ -57,4 +61,8 @@ message ipcp_msg { optional sint32 mpl = 13; /* MPL in ms. */ optional int32 result = 14; optional uint32 uid = 15; /* 0 = GSPP, >0 = PUP uid */ + optional poa_addr_msg peer = 16; /* peer PoA address */ + optional bool is_poa = 17; /* flow rides a PoA */ + optional poa_spec_msg poa = 18; /* PoA to attach */ + repeated poa_spec_msg poas = 19; /* PoAs attached */ } diff --git a/src/lib/pb/ipcp_config.proto b/src/lib/pb/ipcp_config.proto index eac4da37..f6d313ac 100644 --- a/src/lib/pb/ipcp_config.proto +++ b/src/lib/pb/ipcp_config.proto @@ -42,6 +42,7 @@ message dt_config_msg { required uint32 eid_size = 2; required uint32 max_ttl = 3; required routing_config_msg routing = 4; + optional uint32 max_rtt = 5; } message dir_dht_config_msg { @@ -65,29 +66,32 @@ message uni_config_msg { required uint32 cong_avoid = 4; } -message eth_config_msg { +message eth_poa_msg { required string dev = 1; required uint32 ethertype = 2; + optional bytes mac = 3; } -message udp4_config_msg { - required uint32 ip_addr = 1; - required uint32 port = 2; - required uint32 dns_addr = 3; /* set to 0 if unused */ +message udp4_poa_msg { + required uint32 ip_addr = 1; + required uint32 port = 2; } -message udp6_config_msg { - required bytes ip_addr = 1; - required uint32 port = 2; - required bytes dns_addr = 3; /* set to NULL if unused */ +message udp6_poa_msg { + required bytes ip_addr = 1; + required uint32 port = 2; } +/* Exactly one field is set; presence is the PoA type. */ +message poa_spec_msg { + optional udp4_poa_msg udp4 = 1; + optional udp6_poa_msg udp6 = 2; + optional eth_poa_msg eth = 3; +} + message ipcp_config_msg { required layer_info_msg layer_info = 1; required uint32 ipcp_type = 2; optional uni_config_msg unicast = 3; - optional udp4_config_msg udp4 = 4; - optional udp6_config_msg udp6 = 5; - optional eth_config_msg eth = 6; } diff --git a/src/lib/pb/irm.proto b/src/lib/pb/irm.proto index 5de860a5..8f594fa3 100644 --- a/src/lib/pb/irm.proto +++ b/src/lib/pb/irm.proto @@ -53,6 +53,14 @@ enum irm_msg_code { IPCP_FLOW_REQ_ARR = 25; IPCP_FLOW_ALLOC_REPLY = 26; IRM_REPLY = 27; + IRM_FLOW_UPDATE = 28; + IPCP_FLOW_UPDATE_ARR = 29; + IRM_POA_FLOW_ALLOC = 30; + IRM_POA_FLOW_ALLOC_R = 31; + IPCP_POA_FLOW_REQ_ARR = 32; + IRM_ATTACH_IPCP = 33; + IRM_DETACH_IPCP = 34; + IRM_LIST_POAS = 35; } message timespec_msg { @@ -96,4 +104,10 @@ message irm_msg { optional sint32 result = 25; optional bytes sym_key = 26; /* symmetric encryption key */ optional sint32 cipher_nid = 27; /* cipher NID */ + optional uint32 generation = 28; /* re-key batch generation */ + optional bool rekey = 29; /* re-key watermark trigger */ + optional bool rk_initiator = 30; /* re-key proof-holder side */ + optional poa_addr_msg peer = 32; /* peer PoA address */ + optional poa_spec_msg poa = 33; /* PoA to attach */ + repeated poa_spec_msg poas = 34; /* PoAs attached */ } diff --git a/src/lib/pb/model.proto b/src/lib/pb/model.proto index 4c1564a5..f3e85c33 100644 --- a/src/lib/pb/model.proto +++ b/src/lib/pb/model.proto @@ -42,6 +42,7 @@ message flow_info_msg { required qosspec_msg qos = 6; required uint32 uid = 7; required uint32 mtu = 8; /* Layer MTU (bytes). */ + required uint32 max_rtt = 9; /* Max path RTT (ms). */ } message name_info_msg { @@ -58,6 +59,19 @@ message layer_info_msg { required uint32 dir_hash_algo = 2; } +/* Address of a flow PoA peer. */ +message poa_addr_msg { + required uint32 type = 1; /* enum poa_type */ + optional uint32 ip4 = 2; + optional bytes ip6 = 3; + optional uint32 port = 4; + optional bytes mac = 5; + optional uint32 ethertype = 6; + optional string dev = 7; /* our device, eth only */ + optional string hostname = 8; /* IRMd resolves, UDP only */ + optional bytes src_mac = 9; /* our MAC, eth only */ +} + message ipcp_info_msg { required uint32 type = 1; required string name = 2; diff --git a/src/lib/poa/addr.c b/src/lib/poa/addr.c new file mode 100644 index 00000000..d8811244 --- /dev/null +++ b/src/lib/poa/addr.c @@ -0,0 +1,142 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Points of attachment (PoA) - addresses and management messages + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#ifndef _DEFAULT_SOURCE /* Test include source */ +#define _DEFAULT_SOURCE +#endif +#endif + +#include "config.h" + +#include <ouroboros/endian.h> +#include <ouroboros/errno.h> + +#include "poa.h" + +#include <arpa/inet.h> +#include <stdio.h> +#include <string.h> + +int poa_addr_cmp(const struct poa_addr * a, + const struct poa_addr * b) +{ + if (a->type != b->type) + return 1; + + switch (a->type) { + case POA_UDP4: + if (a->udp4.port != b->udp4.port) + return 1; + return memcmp(&a->udp4.ip_addr, &b->udp4.ip_addr, + sizeof(a->udp4.ip_addr)); + case POA_UDP6: + if (a->udp6.port != b->udp6.port) + return 1; + return memcmp(&a->udp6.ip_addr, &b->udp6.ip_addr, + sizeof(a->udp6.ip_addr)); + case POA_ETH: + if (a->eth.dst.ethertype != b->eth.dst.ethertype) + return 1; + return memcmp(a->eth.dst.mac, b->eth.dst.mac, POA_MAC_SIZE); + default: + return 1; + } +} + +/* Display/RIB entry name, e.g. "udp4.<ip>.<port>". */ +int poa_addr_name(const struct poa_addr * a, + char * buf, + size_t len) +{ + char ip[INET6_ADDRSTRLEN]; + int ret; + + switch (a->type) { + case POA_UDP4: + if (inet_ntop(AF_INET, &a->udp4.ip_addr, + ip, sizeof(ip)) == NULL) + return -EINVAL; + ret = snprintf(buf, len, "udp4.%s.%u", ip, a->udp4.port); + break; + case POA_UDP6: + if (inet_ntop(AF_INET6, &a->udp6.ip_addr, + ip, sizeof(ip)) == NULL) + return -EINVAL; + ret = snprintf(buf, len, "udp6.%s.%u", ip, a->udp6.port); + break; + case POA_ETH: + ret = snprintf(buf, len, "eth.%s.%04X", + a->eth.src.dev, a->eth.src.ethertype); + break; + default: + return -EINVAL; + } + + if (ret < 0 || (size_t) ret >= len) + return -EMSGSIZE; + + return 0; +} + +void poa_mgmt_msg_ser(struct poa_mgmt_msg * msg, + uint8_t code, + uint32_t s_eid, + uint32_t d_eid, + qosspec_t qs, + int response, + size_t data_len) +{ + memset(msg, 0, sizeof(*msg)); + + msg->code = code; + msg->s_eid = hton32(s_eid); + msg->d_eid = hton32(d_eid); + msg->response = hton32(response); + msg->data_len = hton16((uint16_t) data_len); + + if (code != POA_FLOW_REQ) + return; + + msg->bandwidth = hton64(qs.bandwidth); + msg->delay = hton32(qs.delay); + msg->loss = hton32(qs.loss); + msg->ber = hton32(qs.ber); + msg->max_gap = hton32(qs.max_gap); + msg->timeout = hton32(qs.timeout); + msg->availability = qs.availability; + msg->service = qs.service; +} + +void poa_mgmt_msg_qos(const struct poa_mgmt_msg * msg, + qosspec_t * qs) +{ + qs->bandwidth = ntoh64(msg->bandwidth); + qs->delay = ntoh32(msg->delay); + qs->loss = ntoh32(msg->loss); + qs->ber = ntoh32(msg->ber); + qs->max_gap = ntoh32(msg->max_gap); + qs->timeout = ntoh32(msg->timeout); + qs->availability = msg->availability; + qs->service = msg->service; +} + diff --git a/src/lib/poa/eth.c b/src/lib/poa/eth.c new file mode 100644 index 00000000..93ccbdfe --- /dev/null +++ b/src/lib/poa/eth.c @@ -0,0 +1,2011 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Points of attachment (PoA) - Ethernet transport + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#if defined(__APPLE__) +#define _BSD_SOURCE +#define _DARWIN_C_SOURCE +#elif defined(__FreeBSD__) +#define __BSD_VISIBLE 1 +#elif defined(__linux__) || defined(__CYGWIN__) +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif +#else +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + +#include "config.h" + +#define OUROBOROS_PREFIX "poa-eth" + +#include <ouroboros/endian.h> +#include <ouroboros/errno.h> +#include <ouroboros/hash.h> +#include <ouroboros/logs.h> +#include <ouroboros/pthread.h> +#include <ouroboros/random.h> +#include <ouroboros/sockets.h> +#include <ouroboros/time.h> + +#include "poa.h" + +#ifdef HAVE_RAW_SOCKETS +#include <net/if.h> +#include <netinet/in.h> +#include <linux/if_ether.h> +#include <linux/if_packet.h> +#include <linux/netlink.h> +#include <linux/gen_stats.h> +#include <linux/pkt_sched.h> +#include <linux/rtnetlink.h> +#include <linux/sockios.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <ifaddrs.h> +#elif defined(HAVE_BPF) +#include <net/bpf.h> +#include <net/if.h> +#include <net/if_dl.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <fcntl.h> +#include <ifaddrs.h> +#elif defined(HAVE_NETMAP) +#define NETMAP_WITH_LIBS +#include <net/netmap_user.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include <poll.h> +#ifndef __linux__ +#include <net/if_dl.h> +#include <ifaddrs.h> +#endif +#endif + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define ETH_TYPE_LEN_SIZE sizeof(uint16_t) +#define ETH_HDR_SIZE (2 * POA_MAC_SIZE + ETH_TYPE_LEN_SIZE) +#define POA_HDR_SIZE (sizeof(struct poa_hdr)) +#define ETH_HDR_TOT_SIZE (ETH_HDR_SIZE + POA_HDR_SIZE) + +#define BPF_DEV_MAX 256 /* /dev/bpfN units to try */ +/* Wait for the link to come back before reading it again. */ +#define ETH_DOWN_TIMEO 100 /* ms */ +/* Budget for a qdisc dump; the send path must not wait on netlink. */ +#define POA_QDISC_TIMEO 5 /* ms */ + +#ifndef ETH_MAX_MTU /* In if_ether.h as of Linux 4.10. */ +#define ETH_MAX_MTU 0xFFFFU +#endif + +struct poa_hdr { + uint16_t eid; + uint16_t len; + uint8_t hcs; +} __attribute__((packed)); + +struct eth_hdr { + uint8_t dst[POA_MAC_SIZE]; + uint8_t src[POA_MAC_SIZE]; + uint16_t ethertype; + struct poa_hdr poa; +} __attribute__((packed)); + + +struct eth_priv { + struct poa * poa; + + int s_fd; /* raw socket or bpf device */ + uint16_t ethertype; /* network order */ + uint8_t hw_addr[POA_MAC_SIZE]; + uint32_t mtu; /* device payload capacity */ + int ifindex; /* link id; 0 where unknown */ + + pthread_t reader; + bool running; +#ifdef HAVE_RAW_SOCKETS + struct sockaddr_ll device; +#elif defined(HAVE_BPF) + size_t blen; /* bpf device buffer size */ +#elif defined(HAVE_NETMAP) + struct nm_desc * nmd; + struct pollfd poll_in; + struct pollfd poll_out; +#endif + /* The kernel zeroes its counters on read, so accumulate. */ + size_t kern_rcv; + size_t kern_drp; +#ifdef HAVE_RAW_SOCKETS + /* Qdisc depth: netlink descriptor, its try-lock and cache. */ + int nl_fd; + uint8_t nl_busy; + size_t nl_pkt; + size_t nl_byt; + uint64_t nl_time; +#endif +}; + +struct eth_query { + struct list_head next; + uint8_t hash[POA_QUERY_HLEN]; + /* The resolve's constraints; replies must satisfy them. */ + uint16_t ethertype; + char c_dev[DEV_NAME_SIZE + 1]; + uint8_t c_mac[POA_MAC_SIZE]; + bool replied; + char dev[DEV_NAME_SIZE + 1]; + uint8_t src_mac[POA_MAC_SIZE]; + uint8_t mac[POA_MAC_SIZE]; + uint16_t r_ethertype; +}; + +static struct { + pthread_once_t once; + + struct llist pending; + pthread_mutex_t mtx; + pthread_cond_t cond; +} queries = { .once = PTHREAD_ONCE_INIT }; + +static void eth_hdr_ser(const struct eth_priv * priv, + struct eth_hdr * hdr, + const uint8_t * dst, + uint32_t eid, + size_t len) +{ + memcpy(hdr->dst, dst, POA_MAC_SIZE); + memcpy(hdr->src, priv->hw_addr, POA_MAC_SIZE); + + hdr->ethertype = priv->ethertype; + hdr->poa.eid = htons((uint16_t) eid); + hdr->poa.len = htons((uint16_t) len); + mem_hash(HASH_CRC8, &hdr->poa.hcs, + (uint8_t *) &hdr->poa.eid, 2 * sizeof(uint16_t)); +} + +/* Oriented from us to the sender: our PoA, then their MAC. */ +static void frame_to_addr(const struct eth_priv * priv, + const struct eth_hdr * hdr, + struct poa_addr * addr) +{ + memset(addr, 0, sizeof(*addr)); + + addr->type = priv->poa->type; + + addr->eth.src = priv->poa->local.eth.src; + + memcpy(addr->eth.dst.mac, hdr->src, POA_MAC_SIZE); + + addr->eth.dst.ethertype = ntohs(priv->ethertype); +} + +static bool frame_is_for_us(const struct eth_priv * priv, + const uint8_t * dst) +{ + static const uint8_t bc[POA_MAC_SIZE] = + { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + + if (memcmp(dst, priv->hw_addr, POA_MAC_SIZE) == 0) + return true; + + return memcmp(dst, bc, POA_MAC_SIZE) == 0; +} + +/* + * Parse and validate a frame; on success *eid and *plen locate the + * payload. Errors are silent: anyone can spray an interface. + */ +static int frame_parse(const struct eth_priv * priv, + const uint8_t * buf, + size_t len, + uint32_t * eid, + size_t * plen) +{ + const struct eth_hdr * hdr = (const struct eth_hdr *) buf; + uint8_t hcs; + + if (len < ETH_HDR_TOT_SIZE) + return -1; + + if (!frame_is_for_us(priv, hdr->dst)) + return -1; + + if (hdr->ethertype != priv->ethertype) + return -1; + + mem_hash(HASH_CRC8, &hcs, + (const uint8_t *) &hdr->poa.eid, 2 * sizeof(uint16_t)); + + if (hcs != hdr->poa.hcs) + return -1; + + *eid = ntohs(hdr->poa.eid); + + *plen = ntohs(hdr->poa.len); + if (*plen > len - ETH_HDR_TOT_SIZE) + return -1; + + return 0; +} + +#define ETH_QUERY_TIMEO 1900 /* ms total budget */ +#define ETH_QUERY_RETRIES 3 /* retransmits, 4 attempts total */ + +/* A resolve in progress; a reply fills every matching entry. */ +static void queries_init(void) +{ + pthread_condattr_t cattr; + + llist_init(&queries.pending); + + pthread_mutex_init(&queries.mtx, NULL); + + pthread_condattr_init(&cattr); +#ifndef __APPLE__ + pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); +#endif + pthread_cond_init(&queries.cond, &cattr); + pthread_condattr_destroy(&cattr); +} + +static void eth_query_cleanup(void * o) +{ + struct eth_query * q = (struct eth_query *) o; + + pthread_mutex_lock(&queries.mtx); + llist_del(&q->next, &queries.pending); + pthread_mutex_unlock(&queries.mtx); +} + +static void eth_query_arr(struct poa * poa, + const struct poa_addr * src, + const uint8_t * hash) +{ + uint8_t buf[sizeof(struct poa_mgmt_msg) + + POA_QUERY_HLEN]; + struct poa_mgmt_msg * msg = (struct poa_mgmt_msg *) buf; + + if (!poa_has_name(hash)) + return; + + poa_mgmt_msg_ser(msg, POA_NAME_REPLY, 0, 0, qos_raw, 0, POA_QUERY_HLEN); + + memcpy(buf + sizeof(*msg), hash, POA_QUERY_HLEN); + + if (poa->ops->poa_send_mgmt(poa, src, buf, sizeof(buf)) < 0) + return; /* the requester retransmits */ + + POA_STAT_BUMP(poa, rep_tx); +} + +static bool mac_is_zero(const uint8_t * mac) +{ + static const uint8_t zero[POA_MAC_SIZE] = { 0 }; + + return memcmp(mac, zero, POA_MAC_SIZE) == 0; +} + +/* Assign a random MAC on loopback interfaces (zero MAC). */ +static int eth_dev_mac(uint8_t * mac) +{ + if (!mac_is_zero(mac)) + return 0; + + if (random_buffer(mac, POA_MAC_SIZE) < 0) + return -1; + + mac[0] = (mac[0] | 0x02) & 0xFE; + + return 0; +} + +static bool eth_query_is_match(const struct eth_query * q, + const struct poa * poa) +{ + if (q->ethertype != 0 && + q->ethertype != poa->local.eth.src.ethertype) + return false; + + if (q->c_dev[0] != '\0' && + strcmp(q->c_dev, poa->local.eth.src.dev) != 0) + return false; + + if (mac_is_zero(q->c_mac)) + return true; + + return memcmp(q->c_mac, poa->local.eth.src.mac, POA_MAC_SIZE) == 0; +} + +static void eth_reply_arr(struct poa * poa, + const struct poa_addr * src, + const uint8_t * hash) +{ + struct list_head * p; + + pthread_once(&queries.once, queries_init); + + pthread_mutex_lock(&queries.mtx); + + list_for_each(p, &queries.pending.list) { + struct eth_query * q; + + q = list_entry(p, struct eth_query, next); + if (q->replied || memcmp(q->hash, hash, POA_QUERY_HLEN) != 0) + continue; + + if (!eth_query_is_match(q, poa)) + continue; + + memcpy(q->mac, src->eth.dst.mac, POA_MAC_SIZE); + memcpy(q->src_mac, src->eth.src.mac, POA_MAC_SIZE); + strcpy(q->dev, src->eth.src.dev); + + q->r_ethertype = poa->local.eth.src.ethertype; + + q->replied = true; + } + + pthread_cond_broadcast(&queries.cond); + + pthread_mutex_unlock(&queries.mtx); +} + +/* Name query frames are handled in the transport. */ +static void eth_rx_mgmt(struct poa * poa, + const struct poa_addr * src, + const uint8_t * buf, + size_t len) +{ + const struct poa_mgmt_msg * msg; + const uint8_t * hash; + + msg = (const struct poa_mgmt_msg *) buf; + if (len < sizeof(*msg)) { + poa_rx_mgmt(poa, src, buf, len); + return; + } + + if (msg->code != POA_NAME_QUERY && msg->code != POA_NAME_REPLY) { + poa_rx_mgmt(poa, src, buf, len); + return; + } + + if (ntoh16(msg->data_len) != POA_QUERY_HLEN) + return; /* malformed */ + + if (len < sizeof(*msg) + POA_QUERY_HLEN) + return; /* malformed */ + + hash = buf + sizeof(*msg); + + if (msg->code == POA_NAME_QUERY) { + POA_STAT_BUMP(poa, qry_rx); + eth_query_arr(poa, src, hash); + } else { + POA_STAT_BUMP(poa, rep_rx); + eth_reply_arr(poa, src, hash); + } +} + +static int eth_query_send(const uint8_t * hash, + const struct poa_addr * addr) +{ + uint8_t buf[sizeof(struct poa_mgmt_msg) + + POA_QUERY_HLEN]; + struct poa_mgmt_msg * msg = (struct poa_mgmt_msg *) buf; + struct poa_addr bcast; + + bcast = *addr; + + memset(bcast.eth.dst.mac, 0xff, POA_MAC_SIZE); + + poa_mgmt_msg_ser(msg, POA_NAME_QUERY, 0, 0, qos_raw, 0, POA_QUERY_HLEN); + + memcpy(buf + sizeof(*msg), hash, POA_QUERY_HLEN); + + return poa_bcast_mgmt(&bcast, buf, sizeof(buf)); +} + +/* Complete addr for dst by broadcast query; the poa_query op. */ +static int eth_query(const char * dst, + const struct timespec * timeo, + struct poa_addr * addr) +{ + struct timespec dflt = TIMESPEC_INIT_MS(ETH_QUERY_TIMEO); + struct timespec rintv = TIMESPEC_INIT_MS(ETH_QUERY_TIMEO + / (ETH_QUERY_RETRIES + 1)); + struct eth_query q; + struct timespec abstime; + struct timespec now; + struct timespec dl; + time_t rintv_ns = (time_t) TS_TO_UINT64(rintv); + time_t diff; + uint8_t hash[POA_QUERY_HLEN]; + int n; + int err = -ETIMEDOUT; + + if (strlen(addr->eth.src.dev) > DEV_NAME_SIZE) + return -EINVAL; + + addr->eth.src.ethertype = addr->eth.dst.ethertype; + + /* The destination is set; a zero ethertype cannot be sent. */ + if (!mac_is_zero(addr->eth.dst.mac)) + return addr->eth.dst.ethertype < 0x0600 ? -EINVAL : 0; + + str_hash(HASH_SHA3_256, hash, dst); + + pthread_once(&queries.once, queries_init); + + memset(&q, 0, sizeof(q)); + memcpy(q.hash, hash, POA_QUERY_HLEN); + + q.ethertype = addr->eth.dst.ethertype; + + memcpy(q.c_mac, addr->eth.src.mac, POA_MAC_SIZE); + strcpy(q.c_dev, addr->eth.src.dev); + + pthread_mutex_lock(&queries.mtx); + llist_add(&q.next, &queries.pending); + pthread_mutex_unlock(&queries.mtx); + + pthread_cleanup_push(eth_query_cleanup, &q); + + clock_gettime(PTHREAD_COND_CLOCK, &now); + ts_add(&now, timeo != NULL ? timeo : &dflt, &abstime); + + while (ts_diff_ns(&now, &abstime) < 0) { + n = eth_query_send(hash, addr); + if (n < 0) { + err = n; + break; + } + + if (n == 0) { + err = -EPERM; + break; + } + + ts_add(&now, &rintv, &dl); + + if (ts_diff_ns(&dl, &abstime) > 0) + dl = abstime; + + pthread_mutex_lock(&queries.mtx); + + pthread_cleanup_push(__cleanup_mutex_unlock, &queries.mtx); + + while (!q.replied) { + if (pthread_cond_timedwait(&queries.cond, &queries.mtx, + &dl) == ETIMEDOUT) + break; + } + + if (q.replied) { + memcpy(addr->eth.dst.mac, q.mac, POA_MAC_SIZE); + memcpy(addr->eth.src.mac, q.src_mac, POA_MAC_SIZE); + strcpy(addr->eth.src.dev, q.dev); + + addr->eth.dst.ethertype = q.r_ethertype; + addr->eth.src.ethertype = q.r_ethertype; + + err = 0; + } + + pthread_cleanup_pop(true); + + if (err == 0) + break; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + diff = ts_diff_ns(&now, &abstime); + if (diff > -rintv_ns) /* skip the runt attempt */ + break; + } + + pthread_cleanup_pop(true); + + return err; +} + +static void eth_spec(const struct poa * poa, + struct poa_spec * spec) +{ + spec->type = poa->type; + spec->eth = poa->local.eth.src; +} + +static bool eth_has_id(const struct poa * poa, + const struct poa_spec * spec) +{ + if (strnlen(spec->eth.dev, sizeof(spec->eth.dev)) > DEV_NAME_SIZE) + return false; + + if (strcmp(poa->local.eth.src.dev, spec->eth.dev) != 0) + return false; + + return poa->local.eth.src.ethertype == spec->eth.ethertype; +} + +/* Our end of the link; an unnamed one matches any. */ +static bool eth_is_src(const struct poa * poa, + const struct eth_poa * src) +{ + if (src->dev[0] != '\0' && + strcmp(poa->local.eth.src.dev, src->dev) != 0) + return false; + + if (mac_is_zero(src->mac)) + return true; + + return memcmp(poa->local.eth.src.mac, src->mac, + POA_MAC_SIZE) == 0; +} + +/* An ethertype of 0 in dst matches any, for broadcast queries. */ +static bool eth_match(const struct poa * poa, + const struct poa_addr * dst) +{ + uint16_t et = dst->eth.dst.ethertype; + + if (et != 0 && et != poa->local.eth.src.ethertype) + return false; + + return eth_is_src(poa, &dst->eth.src); +} + +static bool eth_link_match(const struct poa * poa, + int id) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + return priv->ifindex == id; +} + +#ifdef HAVE_RAW_SOCKETS + +/* MSG_DONTWAIT: the reader blocks on this socket. */ +static int eth_sendv(struct eth_priv * priv, + const uint8_t * dst, + uint32_t eid, + const uint8_t * body, + size_t len, + bool block, + const struct timespec * abstime) +{ + struct eth_hdr hdr; + struct msghdr msg; + struct iovec iov[2]; + int ret; + + if (len > priv->mtu - POA_HDR_SIZE) + return -EMSGSIZE; + + eth_hdr_ser(priv, &hdr, dst, eid, len); + + iov[0].iov_base = &hdr; + iov[0].iov_len = ETH_HDR_TOT_SIZE; + iov[1].iov_base = (void *) body; + iov[1].iov_len = len; + + memset(&msg, 0, sizeof(msg)); + + msg.msg_name = &priv->device; + msg.msg_namelen = sizeof(priv->device); + msg.msg_iov = iov; + msg.msg_iovlen = len > 0 ? 2 : 1; + while (sendmsg(priv->s_fd, &msg, MSG_DONTWAIT) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + return -EIO; + + if (!block) + return -EAGAIN; + + ret = poa_wait_out(priv->s_fd, abstime); + if (ret < 0) + return ret; + } + + return 0; +} + +static void * eth_reader(void * o) +{ + struct poa * poa = (struct poa *) o; + struct eth_priv * priv = (struct eth_priv *) poa->priv; + struct timespec down = TIMESPEC_INIT_MS(ETH_DOWN_TIMEO); + uint8_t * buf; + size_t bufsz; + + bufsz = ETH_HDR_SIZE + priv->mtu; + + buf = malloc(bufsz); + if (buf == NULL) + return (void *) -1; + + pthread_cleanup_push(free, buf); + + while (true) { + struct ssm_pk_buff * spb; + struct sockaddr_ll from; + socklen_t flen; + struct poa_addr src; + ssize_t n; + uint32_t eid; + size_t plen; + const uint8_t * body; + + flen = sizeof(from); + + n = recvfrom(priv->s_fd, buf, bufsz, 0, + (struct sockaddr *) &from, &flen); + + if (n < 0) { + if (errno == EINTR) + continue; + + POA_STAT_BUMP(poa, rcv_fail); + + if (errno == ENETDOWN) { + nanosleep(&down, NULL); + continue; + } + + log_err("Reader on %s stopped: %s.", + poa->local.eth.src.dev, + strerror(errno)); + break; + } + + if (from.sll_pkttype == PACKET_OUTGOING) + continue; + + if (frame_parse(priv, buf, (size_t) n, &eid, &plen) < 0) + continue; + + body = buf + ETH_HDR_TOT_SIZE; + + if (eid == POA_MGMT_EID) { + frame_to_addr(priv, (struct eth_hdr *) buf, &src); + eth_rx_mgmt(poa, &src, body, plen); + continue; + } + + if (poa_spb_reserve(&spb, plen) < 0) { + POA_STAT_BUMP(poa, buf_fail); + continue; + } + + memcpy(ssm_pk_buff_head(spb), body, plen); + + poa_rx_pkt(poa, eid, spb); + } + + pthread_cleanup_pop(true); + + return (void *) 0; +} + +/* + * One netlink socket for the whole subsystem: RTMGRP_LINK delivers the + * events of every interface anyway, so a socket per PoA only added + * discards. + */ +int poa_monitor_open(void) +{ + struct sockaddr_nl sa; + int fd; + + memset(&sa, 0, sizeof(sa)); + + sa.nl_family = AF_NETLINK; + sa.nl_groups = RTMGRP_LINK; + + fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (fd < 0) + return -1; + + if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) { + close(fd); + return -1; + } + + return fd; +} + +void poa_monitor_read(int fd) +{ + uint8_t buf[4096]; + struct nlmsghdr * h; + ssize_t n; + + n = recv(fd, buf, sizeof(buf), 0); + if (n < 0) + return; + + for (h = (struct nlmsghdr *) buf; + NLMSG_OK(h, (unsigned int) n); + h = NLMSG_NEXT(h, n)) { + struct ifinfomsg * ifi; + unsigned int usable; + bool up; + size_t cnt; + + if (h->nlmsg_type == NLMSG_DONE) + break; + + if (h->nlmsg_type != RTM_NEWLINK) + continue; + + ifi = NLMSG_DATA(h); + + usable = ifi->ifi_flags & (IFF_UP | IFF_RUNNING); + up = usable == (IFF_UP | IFF_RUNNING); + + cnt = poa_link_updown(ifi->ifi_index, up); + if (cnt > 0) + log_info("Link %d %s, %zu flows.", ifi->ifi_index, + up ? "up" : "down", cnt); + } +} + +static int eth_dev_info(const char * dev, + int * idx, + uint8_t * mac, + uint32_t * mtu) +{ + struct ifreq ifr; + int fd; + + if (strlen(dev) >= IFNAMSIZ) + return -EINVAL; + + *idx = if_nametoindex(dev); + if (*idx == 0) { + log_err("Failed to find device %s.", dev); + return -ENODEV; + } + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return -EIO; + + memset(&ifr, 0, sizeof(ifr)); + strcpy(ifr.ifr_name, dev); + + if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) { + log_err("Failed to get hardware address of %s.", dev); + goto fail_ioctl; + } + + memcpy(mac, ifr.ifr_hwaddr.sa_data, POA_MAC_SIZE); + + if (ioctl(fd, SIOCGIFMTU, &ifr) < 0) { + log_err("Failed to get MTU of %s.", dev); + goto fail_ioctl; + } + + close(fd); + + if (eth_dev_mac(mac) < 0) + return -EIO; + + *mtu = MIN(MIN(ETH_MAX_MTU, POA_ETH_RD_BUF), (uint32_t) ifr.ifr_mtu); + if (memcmp(dev, "lo", 2) == 0 && *mtu > POA_ETH_LO_MTU) + *mtu = POA_ETH_LO_MTU; + + return 0; + + fail_ioctl: + close(fd); + return -EIO; +} + +/* SO_RCVBUFFORCE bypasses rmem_max; SO_RCVBUF is the fallback. */ +static void eth_set_rcvbuf(int fd, + int rcvbuf) +{ + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, + &rcvbuf, sizeof(rcvbuf)) == 0) + return; + + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) + log_info("Failed to set SO_RCVBUF to %d.", rcvbuf); +} + +/* The send buffer holds at least one frame, or sendmsg cannot fit a PDU. */ +static int eth_socket(struct eth_priv * priv, + int idx, + const uint8_t * mac) +{ + int sndbuf; + int rcvbuf; +#ifdef POA_ETH_QDISC_BYPASS + int bypass = 1; +#endif + + memset(&priv->device, 0, sizeof(priv->device)); + + priv->device.sll_ifindex = idx; + priv->device.sll_family = AF_PACKET; + priv->device.sll_halen = POA_MAC_SIZE; + priv->device.sll_protocol = htons(ETH_P_ALL); + + memcpy(priv->device.sll_addr, mac, POA_MAC_SIZE); + memcpy(priv->hw_addr, mac, POA_MAC_SIZE); + + priv->s_fd = socket(AF_PACKET, SOCK_RAW, priv->ethertype); + if (priv->s_fd < 0) { + log_err("Failed to create socket: %s.", strerror(errno)); + return -1; + } + +#ifdef POA_ETH_QDISC_BYPASS + if (setsockopt(priv->s_fd, SOL_PACKET, PACKET_QDISC_BYPASS, + &bypass, sizeof(bypass)) < 0) + log_info("Qdisc bypass not supported."); +#endif + + sndbuf = POA_ETH_SNDBUF; + if (sndbuf > 0) { + sndbuf = MAX(sndbuf, (int) (ETH_HDR_SIZE + priv->mtu)); + + if (setsockopt(priv->s_fd, SOL_SOCKET, SO_SNDBUF, + &sndbuf, sizeof(sndbuf)) < 0) + log_info("Failed to set SO_SNDBUF to %d.", sndbuf); + } + + rcvbuf = POA_ETH_RCVBUF; + if (rcvbuf > 0) + eth_set_rcvbuf(priv->s_fd, rcvbuf); + + if (bind(priv->s_fd, (struct sockaddr *) &priv->device, + sizeof(priv->device)) < 0) { + log_err("Failed to bind socket to %d.", idx); + close(priv->s_fd); + return -1; + } + + return 0; +} + +/* + * Pull qlen and backlog from the nested TCA_STATS2. The top-level + * TCA_STATS shares TCA_STATS_QUEUE's id but carries a wholly + * different struct, so descend first; never match on id alone. + */ +static void eth_qdisc_parse(struct rtattr * rta, + size_t rlen, + size_t * byt, + size_t * pkt) +{ + struct gnet_stats_queue q; + struct rtattr * in; + size_t ilen; + + for (; RTA_OK(rta, rlen); rta = RTA_NEXT(rta, rlen)) { + if (rta->rta_type != TCA_STATS2) + continue; + + in = (struct rtattr *) RTA_DATA(rta); + ilen = RTA_PAYLOAD(rta); + for (; RTA_OK(in, ilen); in = RTA_NEXT(in, ilen)) { + if (in->rta_type != TCA_STATS_QUEUE) + continue; + + if (RTA_PAYLOAD(in) < sizeof(q)) + continue; + + memcpy(&q, RTA_DATA(in), sizeof(q)); + + *byt = q.backlog; + *pkt = q.qlen; + } + } +} + +/* + * Egress backlog of the device's root qdisc, in bytes and packets, + * as the kernel queues them. The caller owns the netlink descriptor + * and serialises the query. An early end of dump reports failure. + */ +static int eth_qdisc_backlog(int fd, + int ifindex, + size_t * byt, + size_t * pkt) +{ + struct { + struct nlmsghdr nh; + struct tcmsg tc; + } req; + struct nlmsghdr * nh; + struct rtattr * rta; + struct tcmsg * tc; + char buf[16384]; + ssize_t len; + int ret = -1; + + if (fd < 0) + goto fail; + + memset(&req, 0, sizeof(req)); + + req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(req.tc)); + req.nh.nlmsg_type = RTM_GETQDISC; + req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + req.nh.nlmsg_seq = 1; + req.tc.tcm_family = AF_UNSPEC; + req.tc.tcm_ifindex = ifindex; + if (send(fd, &req, req.nh.nlmsg_len, 0) < 0) + goto fail; + + *byt = 0; + *pkt = 0; + + while ((len = recv(fd, buf, sizeof(buf), 0)) > 0) { + nh = (struct nlmsghdr *) buf; + for (; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) { + if (nh->nlmsg_type == NLMSG_DONE) + goto done; + + if (nh->nlmsg_type == NLMSG_ERROR) + goto fail; + + if (nh->nlmsg_type != RTM_NEWQDISC) + continue; + + tc = (struct tcmsg *) NLMSG_DATA(nh); + if (tc->tcm_ifindex != ifindex) + continue; + + if (tc->tcm_parent != TC_H_ROOT) + continue; + + rta = (struct rtattr *) + ((char *) tc + NLMSG_ALIGN(sizeof(*tc))); + + eth_qdisc_parse(rta, nh->nlmsg_len + - NLMSG_LENGTH(sizeof(*tc)), + byt, pkt); + } + } + + goto fail; /* early end: a zero would read as empty */ + done: + ret = 0; + fail: + return ret; +} + +/* + * A netlink descriptor for the qdisc query. SO_RCVTIMEO bounds the + * dump: this is read from the send path, and a reply that never + * arrives must not park a sender thread. + */ +static int eth_qdisc_open(void) +{ + struct sockaddr_nl sa; + struct timeval tv = TIMEVAL_INIT_MS(POA_QDISC_TIMEO); + int fd; + + fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (fd < 0) + return -1; + + memset(&sa, 0, sizeof(sa)); + + sa.nl_family = AF_NETLINK; + if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) + goto fail; + + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) + goto fail; + + return fd; + fail: + close(fd); + + return -1; +} + +/* + * Queue depth in packets, straight from the qdisc. Gated like the + * transport's own depth and skipped when another sender is already + * asking, so the send path never waits on netlink. + */ +static int eth_qpkts(struct poa * poa, + size_t * pkts, + size_t * byts) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + struct timespec now; + uint64_t ns; + size_t byt; + size_t pkt; + + if (priv->nl_fd < 0) + return -1; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + ns = TS_TO_UINT64(now); + if (ns - LOAD_RELAXED(&priv->nl_time) < POA_QLEN_GATE) + goto cached; + + if (__atomic_exchange_n(&priv->nl_busy, 1, __ATOMIC_ACQUIRE) != 0) + goto cached; + + if (eth_qdisc_backlog(priv->nl_fd, priv->ifindex, &byt, &pkt) == 0) { + STORE_RELAXED(&priv->nl_pkt, pkt); + STORE_RELAXED(&priv->nl_byt, byt); + STORE_RELAXED(&priv->nl_time, ns); + } + + __atomic_store_n(&priv->nl_busy, 0, __ATOMIC_RELEASE); + cached: + if (LOAD_RELAXED(&priv->nl_time) == 0) /* nothing measured yet */ + return -1; + + *pkts = LOAD_RELAXED(&priv->nl_pkt); + *byts = LOAD_RELAXED(&priv->nl_byt); + + return 0; +} + +static int eth_attach(struct poa * poa, + const struct poa_spec * spec) +{ + const struct eth_poa * c = &spec->eth; + struct eth_priv * priv; + uint8_t mac[POA_MAC_SIZE]; + uint32_t mtu; + int idx; + int err; + + if (strnlen(c->dev, sizeof(c->dev)) > DEV_NAME_SIZE) + return -EINVAL; + + if (c->ethertype < 0x0600 || c->ethertype == 0xFFFF) { + log_err("Invalid Ethertype 0x%x.", c->ethertype); + return -EINVAL; + } + + priv = malloc(sizeof(*priv)); + if (priv == NULL) + return -ENOMEM; + + memset(priv, 0, sizeof(*priv)); + + priv->poa = poa; + priv->nl_fd = eth_qdisc_open(); /* optional; socket fallback */ + + priv->ethertype = htons(c->ethertype); + + err = eth_dev_info(c->dev, &idx, mac, &mtu); + if (err < 0) + goto fail_conf; + + priv->mtu = mtu; + if (eth_socket(priv, idx, mac) < 0) + goto fail_conf; + + poa->priv = priv; + poa->local.type = poa->type; + poa->local.eth.src.ethertype = c->ethertype; + priv->ifindex = idx; + + memcpy(poa->local.eth.src.mac, mac, POA_MAC_SIZE); + strcpy(poa->local.eth.src.dev, c->dev); + + return 0; + + fail_conf: + if (priv->nl_fd >= 0) + close(priv->nl_fd); + + free(priv); + + return -EIO; +} + +static void eth_detach(struct poa * poa) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + if (priv == NULL) + return; + + close(priv->s_fd); + + if (priv->nl_fd >= 0) + close(priv->nl_fd); + + free(priv); + + poa->priv = NULL; +} + +static uint32_t eth_mtu(struct poa * poa, + const struct poa_addr * dst) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + (void) dst; + + return priv->mtu - POA_HDR_SIZE; +} + +/* All flows on the PoA share the socket, so this is aggregate. */ +static size_t eth_qlen(struct poa * poa) +{ +#ifdef SIOCOUTQ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + int qlen; + + qlen = 0; + if (ioctl(priv->s_fd, SIOCOUTQ, &qlen) < 0) + return 0; + + return (size_t) qlen; +#else + (void) poa; + + return 0; +#endif +} + +/* + * PACKET_STATISTICS zeroes the kernel counters on read, so totals + * accumulate here; relaxed atomics allow concurrent RIB reads. + * The qdisc depth rides the sender's guarded path: one dump at a time. + */ +static int eth_rib(struct poa * poa, + char * buf, + size_t len) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + struct tpacket_stats ts; + socklen_t optlen; + size_t sndbuf = 0; + size_t rcvbuf = 0; + size_t qd_byt; + size_t qd_pkt; + int val; + int size; + + optlen = sizeof(val); + if (getsockopt(priv->s_fd, SOL_SOCKET, SO_SNDBUF, &val, &optlen) == 0) + sndbuf = (size_t) val; + + optlen = sizeof(val); + if (getsockopt(priv->s_fd, SOL_SOCKET, SO_RCVBUF, &val, &optlen) == 0) + rcvbuf = (size_t) val; + + optlen = sizeof(ts); + + if (getsockopt(priv->s_fd, SOL_PACKET, PACKET_STATISTICS, + &ts, &optlen) == 0) { + FETCH_ADD_RELAXED(&priv->kern_rcv, ts.tp_packets); + FETCH_ADD_RELAXED(&priv->kern_drp, ts.tp_drops); + } + + if (eth_qpkts(poa, &qd_pkt, &qd_byt) < 0) { + qd_byt = 0; + qd_pkt = 0; + } + + size = snprintf(buf, len, + "Socket sndbuf (bytes): %zu\n" + "Socket rcvbuf (bytes): %zu\n" + "Kernel packets received: %zu\n" + "Kernel packets dropped: %zu\n" + "Qdisc backlog (bytes): %zu\n" + "Qdisc backlog (packets): %zu\n", + sndbuf, rcvbuf, + LOAD_RELAXED(&priv->kern_rcv), + LOAD_RELAXED(&priv->kern_drp), + qd_byt, qd_pkt); + if (size < 0 || (size_t) size >= len) + return -1; + + return size; +} + +#elif defined(HAVE_BPF) + +/* + * BSD and macOS reach the link layer through a cloned /dev/bpf + * device, bound to an interface with BIOCSETIF. One device per PoA. + */ + +static int eth_sendv(struct eth_priv * priv, + const uint8_t * dst, + uint32_t eid, + const uint8_t * body, + size_t len, + bool block, + const struct timespec * abstime) +{ + struct eth_hdr hdr; + struct iovec iov[2]; + int n; + int ret; + + if (len > priv->mtu - POA_HDR_SIZE) + return -EMSGSIZE; + + eth_hdr_ser(priv, &hdr, dst, eid, len); + + iov[0].iov_base = &hdr; + iov[0].iov_len = ETH_HDR_TOT_SIZE; + iov[1].iov_base = (void *) body; + iov[1].iov_len = len; + n = len > 0 ? 2 : 1; + while (writev(priv->s_fd, iov, n) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + return -EIO; + + if (!block) + return -EAGAIN; + + ret = poa_wait_out(priv->s_fd, abstime); + if (ret < 0) + return ret; + } + + return 0; +} + +/* One read yields a batch of BPF_WORDALIGN'ed frames; walk all of them. */ +static void * eth_reader(void * o) +{ + struct poa * poa = (struct poa *) o; + struct eth_priv * priv = (struct eth_priv *) poa->priv; + uint8_t * buf; + + buf = malloc(priv->blen); + if (buf == NULL) + return (void *) -1; + + pthread_cleanup_push(free, buf); + + while (true) { + uint8_t * p; + uint8_t * end; + ssize_t n; + + n = read(priv->s_fd, buf, priv->blen); + if (n < 0) { + if (errno == EINTR) + continue; + + POA_STAT_BUMP(poa, rcv_fail); + break; + } + + p = buf; + end = buf + n; + while (p + sizeof(struct bpf_hdr) <= end) { + struct bpf_hdr * bh = (struct bpf_hdr *) p; + struct ssm_pk_buff * spb; + struct poa_addr src; + const uint8_t * frame; + const uint8_t * body; + uint32_t eid; + size_t plen; + + frame = p + bh->bh_hdrlen; + if (frame + bh->bh_caplen > end) + break; + + if (frame_parse(priv, frame, bh->bh_caplen, + &eid, &plen) < 0) + goto next; + + body = frame + ETH_HDR_TOT_SIZE; + + if (eid == POA_MGMT_EID) { + frame_to_addr(priv, + (const struct eth_hdr *) frame, + &src); + eth_rx_mgmt(poa, &src, body, plen); + goto next; + } + + if (poa_spb_reserve(&spb, plen) < 0) { + POA_STAT_BUMP(poa, buf_fail); + goto next; + } + + memcpy(ssm_pk_buff_head(spb), body, plen); + + poa_rx_pkt(poa, eid, spb); + next: + p += BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen); + } + } + + pthread_cleanup_pop(true); + + return (void *) 0; +} + +static int eth_dev_info(const char * dev, + uint8_t * mac, + uint32_t * mtu) +{ + struct ifaddrs * ifas; + struct ifaddrs * ifa; + struct ifreq ifr; + int fd; + int found = 0; + + if (getifaddrs(&ifas) < 0) { + log_err("Failed to list interfaces."); + return -1; + } + + for (ifa = ifas; ifa != NULL; ifa = ifa->ifa_next) { + struct sockaddr_dl * dl; + + if (ifa->ifa_addr == NULL) + continue; + + if (ifa->ifa_addr->sa_family != AF_LINK) + continue; + + if (strcmp(ifa->ifa_name, dev) != 0) + continue; + + dl = (struct sockaddr_dl *) ifa->ifa_addr; + if (dl->sdl_alen != POA_MAC_SIZE) + continue; + + memcpy(mac, LLADDR(dl), POA_MAC_SIZE); + + found = 1; + break; + } + + freeifaddrs(ifas); + + if (!found) { + log_err("No such device: %s.", dev); + return -1; + } + + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return -1; + + memset(&ifr, 0, sizeof(ifr)); + + strcpy(ifr.ifr_name, dev); + + if (ioctl(fd, SIOCGIFMTU, &ifr) < 0) { + log_err("Failed to get MTU of %s.", dev); + close(fd); + return -1; + } + + close(fd); + + if (eth_dev_mac(mac) < 0) + return -1; + + *mtu = MIN(MIN(ETH_MAX_MTU, POA_ETH_RD_BUF), (uint32_t) ifr.ifr_mtu); + if (memcmp(dev, "lo", 2) == 0 && *mtu > POA_ETH_LO_MTU) + *mtu = POA_ETH_LO_MTU; + + return 0; +} + +static int eth_bpf_open(void) +{ + char dev[32]; + size_t i; + + for (i = 0; i < BPF_DEV_MAX; ++i) { + int fd; + + sprintf(dev, "/dev/bpf%zu", i); + + fd = open(dev, O_RDWR); + if (fd >= 0) + return fd; + } + + return -1; +} + +/* + * BIOCSHDRCMPLT: we fill in the source address ourselves. + * BIOCSSEESENT: our own egress must not come back at us. + * BIOCIMMEDIATE: deliver on arrival, do not wait for a full buffer. + */ +static int eth_bpf_setup(struct eth_priv * priv, + const char * dev) +{ + struct ifreq ifr; + int enable = 1; + int disable = 0; + int blen = 0; + + memset(&ifr, 0, sizeof(ifr)); + + strcpy(ifr.ifr_name, dev); + + if (ioctl(priv->s_fd, BIOCSETIF, &ifr) < 0) { + log_err("Failed to bind bpf device to %s.", dev); + return -1; + } + + if (ioctl(priv->s_fd, BIOCGBLEN, &blen) < 0 || blen <= 0) { + log_err("Failed to get the bpf buffer length."); + return -1; + } + + priv->blen = (size_t) blen; + if (ioctl(priv->s_fd, BIOCSHDRCMPLT, &enable) < 0) { + log_err("Failed to set BIOCSHDRCMPLT."); + return -1; + } + + if (ioctl(priv->s_fd, BIOCSSEESENT, &disable) < 0) { + log_err("Failed to set BIOCSSEESENT."); + return -1; + } + + if (ioctl(priv->s_fd, BIOCIMMEDIATE, &enable) < 0) { + log_err("Failed to set BIOCIMMEDIATE."); + return -1; + } + + return 0; +} + +static int eth_attach(struct poa * poa, + const struct poa_spec * spec) +{ + const struct eth_poa * c = &spec->eth; + struct eth_priv * priv; + uint8_t mac[POA_MAC_SIZE]; + uint32_t mtu; + + if (strnlen(c->dev, sizeof(c->dev)) > DEV_NAME_SIZE) + return -EINVAL; + + if (c->ethertype < 0x0600 || c->ethertype == 0xFFFF) { + log_err("Invalid Ethertype 0x%x.", c->ethertype); + return -EINVAL; + } + + priv = malloc(sizeof(*priv)); + if (priv == NULL) + return -ENOMEM; + + memset(priv, 0, sizeof(*priv)); + + priv->poa = poa; + priv->s_fd = -1; + + priv->ethertype = htons(c->ethertype); + + if (eth_dev_info(c->dev, mac, &mtu) < 0) + goto fail_conf; + + priv->mtu = mtu; + + memcpy(priv->hw_addr, mac, POA_MAC_SIZE); + + priv->s_fd = eth_bpf_open(); + if (priv->s_fd < 0) { + log_err("Failed to open a bpf device."); + goto fail_conf; + } + + if (eth_bpf_setup(priv, c->dev) < 0) + goto fail_device; + + poa->priv = priv; + poa->local.type = poa->type; + poa->local.eth.src.ethertype = c->ethertype; + + memcpy(poa->local.eth.src.mac, mac, POA_MAC_SIZE); + strcpy(poa->local.eth.src.dev, c->dev); + + log_info("Using Berkeley Packet Filter on %s.", c->dev); + + return 0; + + fail_device: + close(priv->s_fd); + fail_conf: + free(priv); + + return -EIO; +} + +static void eth_detach(struct poa * poa) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + if (priv == NULL) + return; + + close(priv->s_fd); + + free(priv); + + poa->priv = NULL; +} + +static uint32_t eth_mtu(struct poa * poa, + const struct poa_addr * dst) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + (void) dst; + + return priv->mtu - POA_HDR_SIZE; +} + +/* A bpf device has no send queue to report; mb-ECN cannot mark here. */ +static size_t eth_qlen(struct poa * poa) +{ + (void) poa; + + return 0; +} + +/* The bpf device buffer is all the receive queue there is. */ +static int eth_rib(struct poa * poa, + char * buf, + size_t len) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + struct bpf_stat bs; + int size; + + if (ioctl(priv->s_fd, BIOCGSTATS, &bs) == 0) { + FETCH_ADD_RELAXED(&priv->kern_rcv, bs.bs_recv); + FETCH_ADD_RELAXED(&priv->kern_drp, bs.bs_drop); + } + + size = snprintf(buf, len, + "Socket rcvbuf (bytes): %zu\n" + "Kernel packets received: %zu\n" + "Kernel packets dropped: %zu\n", + priv->blen, + LOAD_RELAXED(&priv->kern_rcv), + LOAD_RELAXED(&priv->kern_drp)); + if (size < 0 || (size_t) size >= len) + return -1; + + return size; +} + +#elif defined(HAVE_NETMAP) + +/* + * netmap gives one PoA the whole port: reads copy out of the NIC + * ring, writes inject into it. + */ + +/* nm_inject takes one contiguous frame, so the header is copied in. */ +static int eth_sendv(struct eth_priv * priv, + const uint8_t * dst, + uint32_t eid, + const uint8_t * body, + size_t len, + bool block, + const struct timespec * abstime) +{ + uint8_t * frame; + size_t flen; + int ret; + + if (len > priv->mtu - POA_HDR_SIZE) + return -EMSGSIZE; + + flen = ETH_HDR_TOT_SIZE + len; + + frame = malloc(flen); + if (frame == NULL) + return -ENOMEM; + + eth_hdr_ser(priv, (struct eth_hdr *) frame, dst, eid, len); + + if (len > 0) + memcpy(frame + ETH_HDR_TOT_SIZE, body, len); + + if (block) + ret = poa_wait_out(priv->poll_out.fd, abstime); + else + ret = poll(&priv->poll_out, 1, 0) > 0 ? 0 : -EAGAIN; + + if (ret < 0) + goto fail; + + ret = nm_inject(priv->nmd, frame, flen) == (int) flen ? 0 : -EIO; + fail: + free(frame); + + return ret; +} + +/* A slot stays owned by the ring, so each frame is copied out. */ +static void * eth_reader(void * o) +{ + struct poa * poa = (struct poa *) o; + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + while (true) { + struct ssm_pk_buff * spb; + struct poa_addr src; + struct nm_pkthdr hdr; + const uint8_t * frame; + const uint8_t * body; + uint32_t eid; + size_t plen; + + if (poll(&priv->poll_in, 1, -1) < 0) { + if (errno == EINTR) + continue; + + POA_STAT_BUMP(poa, rcv_fail); + break; + } + + if (priv->poll_in.revents == 0) + continue; + + frame = nm_nextpkt(priv->nmd, &hdr); + if (frame == NULL) + continue; + + if (frame_parse(priv, frame, hdr.len, &eid, &plen) < 0) + continue; + + body = frame + ETH_HDR_TOT_SIZE; + + if (eid == POA_MGMT_EID) { + frame_to_addr(priv, (const struct eth_hdr *) frame, + &src); + eth_rx_mgmt(poa, &src, body, plen); + continue; + } + + if (poa_spb_reserve(&spb, plen) < 0) { + POA_STAT_BUMP(poa, buf_fail); + continue; + } + + memcpy(ssm_pk_buff_head(spb), body, plen); + + poa_rx_pkt(poa, eid, spb); + } + + return (void *) 0; +} + +static int eth_dev_info(const char * dev, + uint8_t * mac, + uint32_t * mtu) +{ + struct ifreq ifr; +#ifndef __linux__ + struct ifaddrs * ifas; + struct ifaddrs * ifa; + int found = 0; +#endif + int fd; + + if (strlen(dev) >= IFNAMSIZ) + return -EINVAL; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return -EIO; + + memset(&ifr, 0, sizeof(ifr)); + + strcpy(ifr.ifr_name, dev); + +#ifdef __linux__ + if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) { + log_err("Failed to get hardware address of %s.", dev); + goto fail_ioctl; + } + + memcpy(mac, ifr.ifr_hwaddr.sa_data, POA_MAC_SIZE); +#else + if (getifaddrs(&ifas) < 0) + goto fail_ioctl; + + for (ifa = ifas; ifa != NULL; ifa = ifa->ifa_next) { + struct sockaddr_dl * dl; + + if (ifa->ifa_addr == NULL) + continue; + + if (ifa->ifa_addr->sa_family != AF_LINK) + continue; + + if (strcmp(ifa->ifa_name, dev) != 0) + continue; + + dl = (struct sockaddr_dl *) ifa->ifa_addr; + if (dl->sdl_alen != POA_MAC_SIZE) + continue; + + memcpy(mac, LLADDR(dl), POA_MAC_SIZE); + + found = 1; + break; + } + + freeifaddrs(ifas); + + if (!found) { + log_err("No hardware address for %s.", dev); + goto fail_ioctl; + } +#endif + if (ioctl(fd, SIOCGIFMTU, &ifr) < 0) { + log_err("Failed to get MTU of %s.", dev); + goto fail_ioctl; + } + + close(fd); + + if (eth_dev_mac(mac) < 0) + return -1; + + *mtu = MIN(MIN(ETH_MAX_MTU, POA_ETH_RD_BUF), (uint32_t) ifr.ifr_mtu); + if (memcmp(dev, "lo", 2) == 0 && *mtu > POA_ETH_LO_MTU) + *mtu = POA_ETH_LO_MTU; + + return 0; + + fail_ioctl: + close(fd); + + return -EIO; +} + +static int eth_nm_open(struct eth_priv * priv, + const char * dev) +{ + char ifn[IFNAMSIZ + sizeof("netmap:")]; + + strcpy(ifn, "netmap:"); + strcat(ifn, dev); + + priv->nmd = nm_open(ifn, NULL, 0, NULL); + if (priv->nmd == NULL) { + log_err("Failed to open netmap device for %s.", dev); + return -1; + } + + memset(&priv->poll_in, 0, sizeof(priv->poll_in)); + memset(&priv->poll_out, 0, sizeof(priv->poll_out)); + + priv->poll_in.fd = NETMAP_FD(priv->nmd); + priv->poll_in.events = POLLIN; + priv->poll_out.fd = NETMAP_FD(priv->nmd); + priv->poll_out.events = POLLOUT; + + return 0; +} + +static int eth_attach(struct poa * poa, + const struct poa_spec * spec) +{ + const struct eth_poa * c = &spec->eth; + struct eth_priv * priv; + uint8_t mac[POA_MAC_SIZE]; + uint32_t mtu; + + if (strnlen(c->dev, sizeof(c->dev)) > DEV_NAME_SIZE) + return -EINVAL; + + if (c->ethertype < 0x0600 || c->ethertype == 0xFFFF) { + log_err("Invalid Ethertype 0x%x.", c->ethertype); + return -EINVAL; + } + + priv = malloc(sizeof(*priv)); + if (priv == NULL) + return -ENOMEM; + + memset(priv, 0, sizeof(*priv)); + + priv->poa = poa; + + priv->ethertype = htons(c->ethertype); + + if (eth_dev_info(c->dev, mac, &mtu) < 0) + goto fail_conf; + + priv->mtu = mtu; + + memcpy(priv->hw_addr, mac, POA_MAC_SIZE); + + if (eth_nm_open(priv, c->dev) < 0) + goto fail_conf; + + poa->priv = priv; + poa->local.type = poa->type; + poa->local.eth.src.ethertype = c->ethertype; + + memcpy(poa->local.eth.src.mac, mac, POA_MAC_SIZE); + strcpy(poa->local.eth.src.dev, c->dev); + + log_info("Using netmap on %s.", c->dev); + + return 0; + + fail_conf: + free(priv); + + return -EIO; +} + +static void eth_detach(struct poa * poa) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + if (priv == NULL) + return; + + nm_close(priv->nmd); + + free(priv); + + poa->priv = NULL; +} + +static uint32_t eth_mtu(struct poa * poa, + const struct poa_addr * dst) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + (void) dst; + + return priv->mtu - POA_HDR_SIZE; +} + +/* The ring is drained by the NIC; there is no queue to report. */ +static size_t eth_qlen(struct poa * poa) +{ + (void) poa; + + return 0; +} + +#endif /* HAVE_RAW_SOCKETS */ + +#ifndef HAVE_RAW_SOCKETS + +/* Only netlink reports link events; no other backend has a monitor. */ +int poa_monitor_open(void) +{ + return -1; +} + +void poa_monitor_read(int fd) +{ + (void) fd; +} + +#endif + +/* One reader per socket, so a flow cannot be reordered on receive. */ +static int eth_start(struct poa * poa) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + if (pthread_create(&priv->reader, NULL, eth_reader, poa) != 0) + return -1; + + priv->running = true; + + return 0; +} + +static void eth_stop(struct poa * poa) +{ + struct eth_priv * priv = (struct eth_priv *) poa->priv; + + if (!priv->running) + return; + + pthread_cancel(priv->reader); + pthread_join(priv->reader, NULL); + + priv->running = false; +} + +static int eth_send(struct poa * poa, + const struct poa_addr * dst, + uint32_t eid, + struct ssm_pk_buff * spb, + bool block, + const struct timespec * abstime) +{ + return eth_sendv((struct eth_priv *) poa->priv, dst->eth.dst.mac, eid, + ssm_pk_buff_head(spb), ssm_pk_buff_len(spb), + block, abstime); +} + +static int eth_send_mgmt(struct poa * poa, + const struct poa_addr * dst, + const uint8_t * buf, + size_t len) +{ + struct timespec timeo = TIMESPEC_INIT_MS(POA_MGMT_SND_TIMEO); + struct timespec abstime; + + clock_gettime(PTHREAD_COND_CLOCK, &abstime); + ts_add(&abstime, &timeo, &abstime); + + return eth_sendv((struct eth_priv *) poa->priv, dst->eth.dst.mac, + POA_MGMT_EID, buf, len, true, &abstime); +} + +const struct poa_ops eth_poa_ops = { + .poa_attach = eth_attach, + .poa_detach = eth_detach, + .poa_start = eth_start, + .poa_stop = eth_stop, + .poa_send = eth_send, + .poa_send_mgmt = eth_send_mgmt, + .poa_query = eth_query, + .poa_mtu = eth_mtu, + .poa_qlen = eth_qlen, +#ifdef HAVE_RAW_SOCKETS + .poa_qpkts = eth_qpkts, +#endif +#ifndef HAVE_NETMAP + .poa_rib = eth_rib, +#endif + .poa_spec = eth_spec, + .poa_has_id = eth_has_id, + .poa_match = eth_match, + .poa_link_match = eth_link_match, + .mpl = POA_ETH_MPL +}; diff --git a/src/lib/poa/poa.c b/src/lib/poa/poa.c new file mode 100644 index 00000000..1fd91660 --- /dev/null +++ b/src/lib/poa/poa.c @@ -0,0 +1,2498 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Points of attachment (PoA) - transport independent core + * + * Included by dev.c; uses dev.c statics (proc, flow_init, ...). + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#define POA_MAX_EIDS PROC_MAX_FLOWS +#define POA_ALLOC_TIMEO 10000 /* ms, overall FLOW_REQ deadline */ +#define POA_RETRY_TIMEO 300 /* ms, FLOW_REQ retransmit period */ +/* Must fit a certificate chain: post-quantum ones are large. */ +#define POA_MGMT_BUFSZ POA_MGMT_FRAME_SIZE +#define POA_MGMT_QMAX 64 /* queued management frames per PoA */ +#define POA_PEND_TIMEO 10 /* s, reap a request that never completes */ +#define POA_SWEEP_TIMEO 1000 /* ms, sweep interval */ +#define POA_SWEEP_MAX 16 /* requests reaped per sweep */ +#define POA_DEFER_MAX 64 /* replies waiting for their flow id */ + +/* EWMA over 8 samples. */ +#define POA_AVG_SHIFT 3 +/* Queue cost is sampled every 64th packet: qlen is a syscall. */ +#define POA_COST_MASK 63 +/* Reuse a qlen read for this long; the mark moves on doublings. */ + +#define POA_RIB "poa" +/* Fits the RIB labels below with 20-digit counters. */ +#define POA_RIB_STRLEN 2048 + +enum poa_flow_state { + POA_FLOW_NULL = 0, + POA_FLOW_PENDING, + POA_FLOW_ALLOCATED, + POA_FLOW_DEAD +}; + +enum poa_state { + POA_NULL = 0, + POA_INIT, /* poa_init(); this process may attach */ + POA_RUNNING, /* the threads are up */ + POA_OPERATIONAL /* bootstrapped or enrolled in a layer */ +}; + +struct poa_flow { + struct poa * poa; + + int flow_id; + uint32_t eid; + uint32_t r_eid; + struct poa_addr r_addr; + + enum poa_flow_state state; + + struct ssm_rbuff * rx_rb; + + struct list_head pend; /* on poa->pending while unanswered */ + struct timespec t0; + + /* Our answer, kept to re-send when a request is repeated. */ + bool answered; + bool answer_sent; + + /* Handed to a caller that will attach it; not the sweeper's. */ + bool claimed; + int answer; + buffer_t answer_data; + + /* Handshake rendezvous with the reader thread. */ + pthread_mutex_t mtx; + pthread_cond_t cond; + bool replied; + bool pending; + int response; + buffer_t resp_data; +}; + +/* + * An answer can be ready before the flow it answers has an id, and + * the caller must not be kept waiting for one: it answers to the IRMd, + * which gives up long before we would. + */ +struct poa_deferred { + struct list_head next; + struct timespec t0; + int flow_id; + int response; + buffer_t data; +}; + +struct poa_mgmt_frame { + struct list_head next; + struct poa * poa; + struct poa_addr src; + size_t len; + uint8_t buf[POA_MGMT_BUFSZ]; +}; + +/* + * A detach is performed by the management thread, so that it cannot + * run alongside the accept side. The caller waits for the result. + */ +struct poa_detach_req { + struct list_head next; + struct poa_spec spec; + int result; + bool done; +}; + +static struct { + struct list_head list; + + struct poa_flow * id_to_pf[SYS_MAX_FLOWS]; + + struct llist deferred; + + pthread_mutex_t mtx; /* guards id_to_pf */ + pthread_cond_t cond; + + /* One management thread and one link monitor for all PoAs. */ + struct llist mgmt_frames; + struct list_head detach; + pthread_mutex_t mgmt_mtx; + pthread_cond_t mgmt_cond; /* work for the handler */ + pthread_cond_t done_cond; /* a detach has completed */ + pthread_t mgmt_handler; + bool mgmt_stop; + + pthread_t monitor; + int mon_fd; + + enum poa_state state; + + /* Hashes of the names this process answers queries for. */ + uint8_t name_hash[POA_QUERY_HLEN]; + uint8_t layer_hash[POA_QUERY_HLEN]; + + pthread_rwlock_t lock; /* the PoAs and their flows */ +} poas; + +static int mgmt_send(struct poa * poa, + const struct poa_addr * dst, + uint8_t code, + uint32_t s_eid, + uint32_t d_eid, + qosspec_t qs, + int response, + const buffer_t * data) +{ + uint8_t buf[POA_MGMT_BUFSZ]; + struct poa_mgmt_msg * msg = (struct poa_mgmt_msg *) buf; + size_t len; + + len = sizeof(*msg); + if (data != NULL && data->len > 0) { + if (len + data->len > sizeof(buf)) + return -EMSGSIZE; + memcpy(buf + len, data->data, data->len); + + len += data->len; + } + + poa_mgmt_msg_ser(msg, code, s_eid, d_eid, qs, response, + data != NULL ? data->len : 0); + + POA_STAT_BUMP(poa, mgmt_tx); + + return poa->ops->poa_send_mgmt(poa, dst, buf, len); +} + +/* Caller holds poas.lock for writing. */ +static struct poa_flow * pf_create(struct poa * poa, + const struct poa_addr * r_addr) +{ + struct poa_flow * pf; + pthread_condattr_t cattr; + int eid; + + pf = malloc(sizeof(*pf)); + if (pf == NULL) + goto fail_malloc; + + memset(pf, 0, sizeof(*pf)); + + if (pthread_mutex_init(&pf->mtx, NULL) != 0) + goto fail_mtx; + + if (pthread_condattr_init(&cattr) != 0) + goto fail_cond; +#ifndef __APPLE__ + pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); +#endif + if (pthread_cond_init(&pf->cond, &cattr) != 0) { + pthread_condattr_destroy(&cattr); + goto fail_cond; + } + + pthread_condattr_destroy(&cattr); + + eid = bmp_allocate(poa->eids); + if (!bmp_is_id_valid(poa->eids, eid)) + goto fail_eid; + + pf->poa = poa; + pf->eid = (uint32_t) eid; + pf->flow_id = -1; + pf->state = POA_FLOW_PENDING; + pf->r_addr = *r_addr; + + clock_gettime(PTHREAD_COND_CLOCK, &pf->t0); + + rcu_wrlock(&poa->guard); + + rcu_publish(pf); + rcu_assign(poa->eid_to_pf[eid], pf); + + rcu_wrunlock(&poa->guard); + + list_add_tail(&pf->pend, &poa->flows); + + POA_STAT_BUMP(poa, n_flows); + + return pf; + + fail_eid: + pthread_cond_destroy(&pf->cond); + fail_cond: + pthread_mutex_destroy(&pf->mtx); + fail_mtx: + free(pf); + fail_malloc: + return NULL; +} + +/* Caller holds poas.mtx. */ +static void deferred_purge(int flow_id) +{ + struct list_head * p; + struct list_head * h; + + list_for_each_safe(p, h, &poas.deferred.list) { + struct poa_deferred * d; + + d = list_entry(p, struct poa_deferred, next); + if (d->flow_id != flow_id) + continue; + + llist_del(&d->next, &poas.deferred); + freebuf(d->data); + free(d); + } +} + +/* The id may have moved to a newer flow; only its owner clears it. */ +static void pf_destroy(struct poa_flow * pf) +{ + struct poa * poa = pf->poa; + + pthread_rwlock_wrlock(&poas.lock); + + rcu_wrlock(&poa->guard); + + rcu_assign(poa->eid_to_pf[pf->eid], NULL); + rcu_assign(pf->rx_rb, NULL); + + rcu_wrunlock(&poa->guard); + + bmp_release(poa->eids, pf->eid); + + if (!list_is_empty(&pf->pend)) { + list_del(&pf->pend); + POA_STAT_SUB(poa, n_flows, 1); + } + + pthread_rwlock_unlock(&poas.lock); + + pthread_mutex_lock(&poas.mtx); + + if (pf->flow_id >= 0 && poas.id_to_pf[pf->flow_id] == pf) { + poas.id_to_pf[pf->flow_id] = NULL; + + deferred_purge(pf->flow_id); + } + + pthread_mutex_unlock(&poas.mtx); + + rcu_reclaim(&poa->guard); /* a receive may still hold pf */ + + freebuf(pf->resp_data); + freebuf(pf->answer_data); + + pthread_cond_destroy(&pf->cond); + pthread_mutex_destroy(&pf->mtx); + + free(pf); +} + +/* + * Keeps the answer for a repeat, then sends it. An accept must wait + * until the flow can receive; a refusal needs no receiver. + */ +static int pf_answer(struct poa_flow * pf, + int response, + const buffer_t * data) +{ + int err; + + pthread_rwlock_wrlock(&poas.lock); + + freebuf(pf->answer_data); + + if (data != NULL && data->len > 0) { + pf->answer_data.data = malloc(data->len); + if (pf->answer_data.data != NULL) { + memcpy(pf->answer_data.data, data->data, data->len); + + pf->answer_data.len = data->len; + } + } + + pf->answer = response; + pf->answered = true; + if (response == 0 && pf->state != POA_FLOW_ALLOCATED) { + pthread_rwlock_unlock(&poas.lock); + return 0; + } + + pf->answer_sent = true; + + pthread_rwlock_unlock(&poas.lock); + + err = mgmt_send(pf->poa, &pf->r_addr, POA_FLOW_REPLY, pf->eid, + pf->r_eid, qos_raw, response, data); + + if (err == -ETIMEDOUT || err == -EAGAIN) + err = 0; /* stored; a repeat request resends it */ + + return err; +} + +/* Takes an answer left for a flow that had no id yet. */ +static struct poa_deferred * deferred_take(int flow_id) +{ + struct list_head * p; + struct list_head * h; + + list_for_each_safe(p, h, &poas.deferred.list) { + struct poa_deferred * d; + + d = list_entry(p, struct poa_deferred, next); + if (d->flow_id != flow_id) + continue; + + llist_del(&d->next, &poas.deferred); + + return d; + } + + return NULL; +} + +/* Publishes the flow_id so an answer can find this flow. */ +static void pf_set_flow_id(struct poa_flow * pf, + int flow_id) +{ + struct poa_deferred * d; + + pthread_mutex_lock(&poas.mtx); + + pf->flow_id = flow_id; + poas.id_to_pf[flow_id] = pf; + + d = deferred_take(flow_id); + + pthread_cond_broadcast(&poas.cond); + + pthread_mutex_unlock(&poas.mtx); + + if (d != NULL) { + pf_answer(pf, d->response, &d->data); + freebuf(d->data); + free(d); + } +} + +/* + * Between the request arriving and the accept returning, the flow has + * an id but no fd yet; flow_init claims the PoA here. + */ +static void pf_set_pending(struct poa_flow * pf) +{ + pthread_mutex_lock(&poas.mtx); + + pf->pending = true; + + pthread_mutex_unlock(&poas.mtx); +} + +static void pf_clr_pending(struct poa_flow * pf) +{ + pthread_mutex_lock(&poas.mtx); + + pf->pending = false; + + pthread_mutex_unlock(&poas.mtx); +} + +/* A process that attaches no PoA has nothing pending. */ +struct poa_flow * poa_flow_take_pending(int flow_id) +{ + struct poa_flow * pf; + + if (poas.state == POA_NULL) + return NULL; + + if (flow_id < 0 || flow_id >= SYS_MAX_FLOWS) + return NULL; + + pthread_mutex_lock(&poas.mtx); + + pf = poas.id_to_pf[flow_id]; + if (pf != NULL && pf->pending) + pf->pending = false; + else + pf = NULL; + + pthread_mutex_unlock(&poas.mtx); + + return pf; +} + +static struct poa_flow * pf_get(int flow_id) +{ + struct poa_flow * pf; + + if (flow_id < 0 || flow_id >= SYS_MAX_FLOWS) + return NULL; + + pthread_mutex_lock(&poas.mtx); + + pf = poas.id_to_pf[flow_id]; + + pthread_mutex_unlock(&poas.mtx); + + return pf; +} + +#ifdef PROC_FLOW_STATS + +/* Caller holds poas.lock. */ +static struct poa * poa_by_rib_name(const char * name) +{ + struct list_head * p; + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (strcmp(poa->name, name) == 0) + return poa; + } + + return NULL; +} + +static int poa_rib_read(const char * path, + char * buf, + size_t len) +{ + struct poa * poa; + const char * entry; + size_t qlen; + size_t avg; + size_t cost; + int size; + int ret; + + entry = strstr(path, RIB_SEPARATOR) + 1; + + if (len < POA_RIB_STRLEN) + return -1; + + pthread_rwlock_rdlock(&poas.lock); + + poa = poa_by_rib_name(entry); + if (poa == NULL) + goto fail; + + qlen = poa->ops->poa_qlen(poa); + avg = poa->avg_len; + cost = poa->avg_len + poa->avg_ovh; + + size = snprintf(buf, len, + "Active flows: %zu\n" + "Packets received: %zu\n" + "Bytes received: %zu\n" + "Packets sent: %zu\n" + "Bytes sent: %zu\n" + "Management frames rcvd: %zu\n" + "Management frames sent: %zu\n" + "Bad EID packets: %zu\n" + "Delivery (N+1) failures: %zu\n" + "Buffer alloc failures: %zu\n" + "Packet read failures: %zu\n" + "Packet send failures: %zu\n" + "Name queries sent: %zu\n" + "Name queries received: %zu\n" + "Name replies sent: %zu\n" + "Name replies received: %zu\n" + "Queued (transport): %zu\n" + "Queued (packets): %zu\n" + "Mean packet size (bytes): %zu\n" + "Mean packet cost: %zu\n", + POA_STAT_LOAD(poa, n_flows), + POA_STAT_LOAD(poa, rx_pkts), + POA_STAT_LOAD(poa, rx_bytes), + POA_STAT_LOAD(poa, tx_pkts), + POA_STAT_LOAD(poa, tx_bytes), + POA_STAT_LOAD(poa, mgmt_rx), + POA_STAT_LOAD(poa, mgmt_tx), + POA_STAT_LOAD(poa, bad_eid), + POA_STAT_LOAD(poa, dlv_fail), + POA_STAT_LOAD(poa, buf_fail), + POA_STAT_LOAD(poa, rcv_fail), + POA_STAT_LOAD(poa, snd_fail), + POA_STAT_LOAD(poa, qry_tx), + POA_STAT_LOAD(poa, qry_rx), + POA_STAT_LOAD(poa, rep_tx), + POA_STAT_LOAD(poa, rep_rx), + qlen, cost > 0 ? qlen / cost : 0, avg, cost); + if (size < 0 || (size_t) size >= len) + goto fail; + + if (poa->ops->poa_rib != NULL) { + ret = poa->ops->poa_rib(poa, buf + size, len - size); + if (ret < 0) + goto fail; + + size += ret; + } + + pthread_rwlock_unlock(&poas.lock); + + return size; + + fail: + pthread_rwlock_unlock(&poas.lock); + + return -1; +} + +static int poa_rib_readdir(char *** buf) +{ + struct list_head * p; + size_t n = 0; + int idx = 0; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) + ++n; + + if (n == 0) { + *buf = NULL; + goto no_poas; + } + + *buf = malloc(sizeof(**buf) * n); + if (*buf == NULL) + goto fail_entries; + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + (*buf)[idx] = malloc(strlen(poa->name) + 1); + if ((*buf)[idx] == NULL) + goto fail_entry; + + strcpy((*buf)[idx++], poa->name); + } + no_poas: + pthread_rwlock_unlock(&poas.lock); + + return idx; + + fail_entry: + while (idx-- > 0) + free((*buf)[idx]); + + free(*buf); + fail_entries: + pthread_rwlock_unlock(&poas.lock); + + return -ENOMEM; +} + +static int poa_rib_getattr(const char * path, + struct rib_attr * attr) +{ + (void) path; + + attr->size = POA_RIB_STRLEN; + attr->mtime = 0; + + return 0; +} + +static struct rib_ops poa_r_ops = { + .read = poa_rib_read, + .readdir = poa_rib_readdir, + .getattr = poa_rib_getattr +}; + +#endif /* PROC_FLOW_STATS */ + +int poa_init(const char * name) +{ + pthread_condattr_t cattr; + + assert(name != NULL); + + memset(&poas, 0, sizeof(poas)); + + str_hash(HASH_SHA3_256, poas.name_hash, name); + + poas.mon_fd = -1; + if (pthread_mutex_init(&poas.mtx, NULL) != 0) + goto fail_mtx; + + if (pthread_cond_init(&poas.cond, NULL) != 0) + goto fail_cond; + + if (pthread_mutex_init(&poas.mgmt_mtx, NULL) != 0) + goto fail_mgmt_mtx; + + if (pthread_condattr_init(&cattr) != 0) + goto fail_cattr; +#ifndef __APPLE__ + pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); +#endif + if (pthread_cond_init(&poas.mgmt_cond, &cattr) != 0) { + pthread_condattr_destroy(&cattr); + goto fail_cattr; + } + + pthread_condattr_destroy(&cattr); + + if (pthread_cond_init(&poas.done_cond, NULL) != 0) + goto fail_done_cond; + + if (pthread_rwlock_init(&poas.lock, NULL) != 0) + goto fail_lock; + + list_head_init(&poas.list); + llist_init(&poas.deferred); + llist_init(&poas.mgmt_frames); + list_head_init(&poas.detach); + + poas.state = POA_INIT; + +#ifdef PROC_FLOW_STATS + if (rib_reg(POA_RIB, &poa_r_ops) < 0) + goto fail_rib; +#endif + return 0; + +#ifdef PROC_FLOW_STATS + fail_rib: + pthread_rwlock_destroy(&poas.lock); +#endif + + fail_lock: + pthread_cond_destroy(&poas.done_cond); + fail_done_cond: + pthread_cond_destroy(&poas.mgmt_cond); + fail_cattr: + pthread_mutex_destroy(&poas.mgmt_mtx); + fail_mgmt_mtx: + pthread_cond_destroy(&poas.cond); + fail_cond: + pthread_mutex_destroy(&poas.mtx); + fail_mtx: + return -1; +} + +int poa_set_layer(const char * layer) +{ + if (layer == NULL) + return -EINVAL; + + pthread_rwlock_wrlock(&poas.lock); + + str_hash(HASH_SHA3_256, poas.layer_hash, layer); + + poas.state = POA_OPERATIONAL; + + pthread_rwlock_unlock(&poas.lock); + + return 0; +} + +bool poa_has_name(const uint8_t * hash) +{ + bool match = false; + + pthread_rwlock_rdlock(&poas.lock); + + if (poas.state >= POA_INIT) + match = memcmp(hash, poas.name_hash, POA_QUERY_HLEN) == 0; + + if (!match && poas.state >= POA_OPERATIONAL) + match = memcmp(hash, poas.layer_hash, POA_QUERY_HLEN) == 0; + + pthread_rwlock_unlock(&poas.lock); + + return match; +} + +int poa_spb_reserve(struct ssm_pk_buff ** spb, + size_t len) +{ + return ssm_pool_alloc_b(proc.pool, len, NULL, spb, NULL) < 0 ? -1 : 0; +} + +void poa_spb_release(struct ssm_pk_buff * spb) +{ + ssm_pool_remove(proc.pool, ssm_pk_buff_get_off(spb)); +} + +void poa_rx_pkt(struct poa * poa, + uint32_t eid, + struct ssm_pk_buff * spb) +{ + struct poa_flow * pf; + struct ssm_rbuff * rx_rb; + size_t len; + + len = ssm_pk_buff_len(spb); /* the ring write takes it over */ + + if (eid >= poa->n_eids) { + POA_STAT_BUMP(poa, bad_eid); + poa_spb_release(spb); + return; + } + + rcu_rdlock(&poa->guard); + + pf = rcu_deref(poa->eid_to_pf[eid]); + + rcu_consume(pf); + + if (pf == NULL) + goto fail_eid; + + rx_rb = rcu_deref(pf->rx_rb); + + rcu_consume(rx_rb); + + if (rx_rb == NULL) + goto fail_eid; + + if (ssm_rbuff_write(rx_rb, ssm_pk_buff_get_off(spb)) < 0) { + POA_STAT_BUMP(poa, dlv_fail); + rcu_rdunlock(&poa->guard); + poa_spb_release(spb); + return; + } + + POA_STAT_BUMP(poa, rx_pkts); + POA_STAT_ADD(poa, rx_bytes, len); + + ssm_flow_set_notify(proc.fqset, pf->flow_id, FLOW_PKT); + + rcu_rdunlock(&poa->guard); + + return; + + fail_eid: + POA_STAT_BUMP(poa, bad_eid); + + rcu_rdunlock(&poa->guard); + + poa_spb_release(spb); +} + +static int poa_flow_req_arr(struct poa_flow * pf, + qosspec_t qs, + uint32_t mtu, + const buffer_t * data) +{ + struct flow_info flow; + uint8_t buf[SOCK_BUF_SIZE]; + buffer_t msg = {SOCK_BUF_SIZE, buf}; + buffer_t out = BUF_INIT; + int err; + + memset(&flow, 0, sizeof(flow)); + + flow.n_pid = getpid(); + flow.n_1_pid = getpid(); + flow.qs = qs; + flow.mpl = pf->poa->mpl; + flow.mtu = mtu; + if (ipcp_poa_flow_req_arr__irm_req_ser(&msg, &flow, data) < 0) + return -ENOMEM; + + pf_set_pending(pf); + + err = send_recv_msg(&msg); + if (err < 0) + goto fail; + + err = poa_flow__irm_result_des(&msg, &flow, &out); + if (err < 0) + goto fail; + + freebuf(out); + + if (flow.id < 0 || flow.id >= SYS_MAX_FLOWS) { + err = -EBADF; + goto fail; + } + + pf_set_flow_id(pf, flow.id); + + return 0; + fail: + pf_clr_pending(pf); + return err; +} + +static void handle_flow_req(struct poa * poa, + const struct poa_addr * src, + const struct poa_mgmt_msg * msg, + const uint8_t * data, + size_t data_len) +{ + struct list_head * p; + struct poa_flow * pf = NULL; + qosspec_t qs; + buffer_t buf; + buffer_t answer_data = BUF_INIT; + uint32_t r_eid; + uint32_t mtu; + uint32_t eid = 0; + int answer = 0; + bool found = false; + bool served = false; + + r_eid = ntoh32(msg->s_eid); + + poa_mgmt_msg_qos(msg, &qs); + + pthread_rwlock_wrlock(&poas.lock); + + list_for_each(p, &poa->flows) { + pf = list_entry(p, struct poa_flow, pend); + if (pf->r_eid != r_eid || poa_addr_cmp(&pf->r_addr, src) != 0) + continue; + + answer = pf->answer; + eid = pf->eid; + served = true; + + found = pf->answer_sent; + if (found && pf->answer_data.len > 0) { + answer_data.data = malloc(pf->answer_data.len); + if (answer_data.data != NULL) { + memcpy(answer_data.data, pf->answer_data.data, + pf->answer_data.len); + answer_data.len = pf->answer_data.len; + } + } + break; + } + + if (!served) { + pf = pf_create(poa, src); + if (pf != NULL) + pf->r_eid = r_eid; + } + + pthread_rwlock_unlock(&poas.lock); + + if (found) { + mgmt_send(poa, src, POA_FLOW_REPLY, eid, r_eid, qos_raw, answer, + answer_data.len > 0 ? &answer_data : NULL); + freebuf(answer_data); + } + + if (served || pf == NULL) + return; + + buf.len = data_len; + buf.data = (uint8_t *) data; + mtu = poa->ops->poa_mtu(poa, src); + if (poa_flow_req_arr(pf, qs, mtu, &buf) < 0) { + mgmt_send(poa, src, POA_FLOW_REPLY, pf->eid, r_eid, qos_raw, + -1, NULL); + pf_destroy(pf); + } +} + +static void handle_flow_reply(struct poa * poa, + const struct poa_addr * src, + const struct poa_mgmt_msg * msg, + const uint8_t * data, + size_t data_len) +{ + struct poa_flow * pf; + uint32_t eid; + + eid = ntoh32(msg->d_eid); + + pthread_rwlock_rdlock(&poas.lock); + + pf = eid < poa->n_eids ? poa->eid_to_pf[eid] : NULL; + if (pf == NULL || pf->state != POA_FLOW_PENDING) { + pthread_rwlock_unlock(&poas.lock); + return; + } + + if (poa_addr_cmp(&pf->r_addr, src) != 0) { + pthread_rwlock_unlock(&poas.lock); + return; + } + + pthread_mutex_lock(&pf->mtx); + + if (pf->replied) { + pthread_mutex_unlock(&pf->mtx); + pthread_rwlock_unlock(&poas.lock); + return; + } + + if (data_len > 0) { + pf->resp_data.data = malloc(data_len); + if (pf->resp_data.data != NULL) { + memcpy(pf->resp_data.data, data, data_len); + + pf->resp_data.len = data_len; + } + } + + pf->r_eid = ntoh32(msg->s_eid); + pf->response = ntoh32(msg->response); + pf->replied = true; + + pthread_cond_broadcast(&pf->cond); + + pthread_mutex_unlock(&pf->mtx); + + pthread_rwlock_unlock(&poas.lock); +} + +static void handle_flow_update(struct poa * poa, + const struct poa_addr * src, + const struct poa_mgmt_msg * msg, + const uint8_t * data, + size_t data_len) +{ + struct poa_flow * pf; + buffer_t buf; + uint32_t eid; + + eid = ntoh32(msg->d_eid); + + pthread_rwlock_rdlock(&poas.lock); + + pf = eid < poa->n_eids ? poa->eid_to_pf[eid] : NULL; + if (pf == NULL || pf->state != POA_FLOW_ALLOCATED) { + pthread_rwlock_unlock(&poas.lock); + return; + } + + if (poa_addr_cmp(&pf->r_addr, src) != 0) { + pthread_rwlock_unlock(&poas.lock); + return; + } + + eid = (uint32_t) pf->flow_id; + + pthread_rwlock_unlock(&poas.lock); + + buf.len = data_len; + buf.data = (uint8_t *) data; + + ipcp_flow_update_arr((int) eid, &buf); +} + +static void mgmt_frame_handle(struct poa_mgmt_frame * frame) +{ + const struct poa_mgmt_msg * msg; + const uint8_t * data; + size_t data_len; + + msg = (const struct poa_mgmt_msg *) frame->buf; + if (frame->len < sizeof(*msg)) + return; + + data_len = ntoh16(msg->data_len); + if (data_len > frame->len - sizeof(*msg)) + return; + + data = frame->buf + sizeof(*msg); + + switch (msg->code) { + case POA_FLOW_REQ: + handle_flow_req(frame->poa, &frame->src, msg, data, data_len); + break; + case POA_FLOW_REPLY: + handle_flow_reply(frame->poa, &frame->src, msg, data, data_len); + break; + case POA_FLOW_UPDATE: + handle_flow_update(frame->poa, &frame->src, msg, data, + data_len); + break; + default: + break; + } +} + +static bool pf_steal(struct poa_flow * pf) +{ + bool stolen = false; + + pthread_mutex_lock(&poas.mtx); + + if (pf->pending) { + pf->pending = false; + poas.id_to_pf[pf->flow_id] = NULL; + + deferred_purge(pf->flow_id); + + stolen = true; + } + + pthread_mutex_unlock(&poas.mtx); + + return stolen; +} + +static void sweep_pending(void) +{ + struct poa_flow * dead[POA_SWEEP_MAX]; + struct list_head * p; + struct list_head * q; + struct timespec now; + size_t n = 0; + size_t i; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + list_for_each(q, &poa->flows) { + struct poa_flow * pf; + + if (n == POA_SWEEP_MAX) + break; + + pf = list_entry(q, struct poa_flow, pend); + if (pf->state != POA_FLOW_PENDING || pf->claimed) + continue; + + if (now.tv_sec - pf->t0.tv_sec < POA_PEND_TIMEO) + continue; + + if (pf->flow_id >= 0 && !pf_steal(pf)) + continue; + + dead[n++] = pf; + } + + if (n == POA_SWEEP_MAX) + break; + } + + pthread_rwlock_unlock(&poas.lock); + + for (i = 0; i < n; ++i) + pf_destroy(dead[i]); + + pthread_mutex_lock(&poas.mtx); + + list_for_each_safe(p, q, &poas.deferred.list) { + struct poa_deferred * d; + + d = list_entry(p, struct poa_deferred, next); + if (now.tv_sec - d->t0.tv_sec < POA_PEND_TIMEO) + continue; + + llist_del(&d->next, &poas.deferred); + freebuf(d->data); + free(d); + } + + pthread_mutex_unlock(&poas.mtx); +} + +void poa_rx_mgmt(struct poa * poa, + const struct poa_addr * src, + const uint8_t * buf, + size_t len) +{ + struct poa_mgmt_frame * frame; + + if (len < sizeof(struct poa_mgmt_msg) || len > POA_MGMT_BUFSZ) + return; + + POA_STAT_BUMP(poa, mgmt_rx); + + frame = malloc(offsetof(struct poa_mgmt_frame, buf) + len); + if (frame == NULL) + return; + + frame->poa = poa; + frame->src = *src; + frame->len = len; + + memcpy(frame->buf, buf, len); + + pthread_mutex_lock(&poas.mgmt_mtx); + + if (poa->n_mgmt >= POA_MGMT_QMAX) { + pthread_mutex_unlock(&poas.mgmt_mtx); + free(frame); + return; + } + + ++poa->n_mgmt; + + llist_add_tail(&frame->next, &poas.mgmt_frames); + + pthread_cond_signal(&poas.mgmt_cond); + + pthread_mutex_unlock(&poas.mgmt_mtx); +} + +static void mgmt_frames_purge(const struct poa * poa) +{ + struct list_head * p; + struct list_head * h; + + pthread_mutex_lock(&poas.mgmt_mtx); + + list_for_each_safe(p, h, &poas.mgmt_frames.list) { + struct poa_mgmt_frame * frame; + + frame = list_entry(p, struct poa_mgmt_frame, next); + if (frame->poa != poa) + continue; + + --frame->poa->n_mgmt; + + llist_del(&frame->next, &poas.mgmt_frames); + + free(frame); + } + + pthread_mutex_unlock(&poas.mgmt_mtx); +} + +static bool poa_has_id(const struct poa * poa, + const struct poa_spec * spec) +{ + if (poa->type != spec->type) + return false; + + return poa->ops->poa_has_id(poa, spec); +} + +/* + * The PoA carrying dst, as the transport judges it. -EPERM if none + * matches, -EINVAL if several do. Caller holds poas.lock. + */ +static int poa_lookup(const struct poa_addr * dst, + struct poa ** out) +{ + struct list_head * p; + struct poa * found = NULL; + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->type != dst->type) + continue; + + if (!poa->ops->poa_match(poa, dst)) + continue; + + if (found != NULL) /* nothing given, two candidates */ + return -EINVAL; + + found = poa; + } + + if (found == NULL) + return -EPERM; + + *out = found; + + return 0; +} + +/* Sends are deadlined, bounding the lock hold on a full queue. */ +int poa_bcast_mgmt(const struct poa_addr * dst, + const uint8_t * buf, + size_t len) +{ + struct list_head * p; + int n = 0; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->type != dst->type) + continue; + + if (!poa->ops->poa_match(poa, dst)) + continue; + + if (poa->ops->poa_send_mgmt(poa, dst, buf, len) < 0) + continue; + + /* All management broadcasts are name queries. */ + POA_STAT_BUMP(poa, qry_tx); + ++n; + } + + pthread_rwlock_unlock(&poas.lock); + + return n; +} + +static bool deadline_is_malformed(const struct timespec * timeo) +{ + if (timeo == NULL) + return false; + + if (timeo->tv_sec < 0 || timeo->tv_nsec < 0) + return true; + + return timeo->tv_nsec >= BILLION; +} + +/* + * Complete addr for dst on any backend that can query. The ops are + * collected under poas.lock but called outside it: a query blocks up + * to its deadline and takes the lock again to broadcast. The tables + * are static, so nothing dangles; a struct poa cannot be carried + * across the unlock. The deadline applies per backend. + */ +int poa_query(const char * dst, + const struct timespec * timeo, + struct poa_addr * addr) +{ + const struct poa_ops * cand[POA_MAX_POAS]; + enum poa_type type[POA_MAX_POAS]; + struct list_head * p; + size_t n = 0; + size_t i; + int err = -ENOTSUP; + + if (dst == NULL || addr == NULL) + return -EINVAL; + + if (deadline_is_malformed(timeo)) + return -EINVAL; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->ops->poa_query == NULL) + continue; + + for (i = 0; i < n && cand[i] != poa->ops; i++) + ; + if (i < n) + continue; + + /* One type per backend: eth. Revisit if that changes. */ + cand[n] = poa->ops; + type[n++] = poa->type; + } + + pthread_rwlock_unlock(&poas.lock); + + for (i = 0; i < n; i++) { + memset(addr, 0, sizeof(*addr)); + + addr->type = type[i]; + + err = cand[i]->poa_query(dst, timeo, addr); + if (err == 0) + return 0; + } + + return err; +} + +static int poa_check(const struct poa_addr * dst) +{ + struct poa * poa; + int err; + + pthread_rwlock_rdlock(&poas.lock); + + err = poa_lookup(dst, &poa); + + pthread_rwlock_unlock(&poas.lock); + + return err; +} + +static int poa_alloc(const struct poa_addr * dst, + qosspec_t qs, + const buffer_t * req, + buffer_t * resp, + struct poa_flow ** pf_out, + uint32_t * mtu, + const struct timespec * timeo) +{ + struct timespec dflt = TIMESPEC_INIT_MS(POA_ALLOC_TIMEO); + struct timespec rintv = TIMESPEC_INIT_MS(POA_RETRY_TIMEO); + struct poa_flow * pf; + struct poa * poa; + struct timespec abstime; + struct timespec now; + struct timespec retry; + int err; + + pthread_rwlock_wrlock(&poas.lock); + + err = poa_lookup(dst, &poa); + if (err < 0) { + pthread_rwlock_unlock(&poas.lock); + return err; + } + + pf = pf_create(poa, dst); + if (pf == NULL) { + pthread_rwlock_unlock(&poas.lock); + return -ENOMEM; + } + + pf->claimed = true; + + pthread_rwlock_unlock(&poas.lock); + + clock_gettime(PTHREAD_COND_CLOCK, &abstime); + ts_add(&abstime, timeo != NULL ? timeo : &dflt, &abstime); + + pthread_mutex_lock(&pf->mtx); + + while (!pf->replied) { + pthread_mutex_unlock(&pf->mtx); + + err = mgmt_send(poa, dst, POA_FLOW_REQ, pf->eid, 0, qs, 0, req); + + pthread_mutex_lock(&pf->mtx); + + if (err < 0 && err != -ETIMEDOUT && err != -EAGAIN) { + err = -EIO; + goto fail; + } + + if (pf->replied) + break; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + ts_add(&now, &rintv, &retry); + + if (ts_diff_ns(&retry, &abstime) > 0) + retry = abstime; + + pthread_cond_timedwait(&pf->cond, &pf->mtx, &retry); + + if (pf->replied) + break; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + if (ts_diff_ns(&now, &abstime) >= 0) { + err = -ETIMEDOUT; + goto fail; + } + } + + if (pf->response != 0) { + err = -ECONNREFUSED; + goto fail; + } + + *resp = pf->resp_data; + pf->resp_data.len = 0; + pf->resp_data.data = NULL; + + pthread_mutex_unlock(&pf->mtx); + + *mtu = poa->ops->poa_mtu(poa, dst); + *pf_out = pf; + + return 0; + + fail: + pthread_mutex_unlock(&pf->mtx); + pf_destroy(pf); + return err; +} + +static void poa_alloc_fail(struct poa_flow * pf) +{ + pf_destroy(pf); +} + +void poa_flow_attach(struct poa_flow * pf, + int flow_id, + struct ssm_rbuff * rx_rb) +{ + struct poa * poa = pf->poa; + + if (pf->flow_id != flow_id) + pf_set_flow_id(pf, flow_id); + + pthread_rwlock_wrlock(&poas.lock); + + pf->state = POA_FLOW_ALLOCATED; + + rcu_wrlock(&poa->guard); + + rcu_publish(rx_rb); + rcu_assign(pf->rx_rb, rx_rb); + + rcu_wrunlock(&poa->guard); + + pthread_rwlock_unlock(&poas.lock); +} + +void poa_flow_detach(struct poa_flow * pf) +{ + struct poa * poa = pf->poa; + + pthread_rwlock_wrlock(&poas.lock); + + pf->state = POA_FLOW_DEAD; + + rcu_wrlock(&poa->guard); + + rcu_assign(pf->rx_rb, NULL); + + rcu_wrunlock(&poa->guard); + + pthread_rwlock_unlock(&poas.lock); + + pf_destroy(pf); +} + +static size_t flows_updown(struct poa * poa, + bool up) +{ + struct list_head * p; + size_t n = 0; + + list_for_each(p, &poa->flows) { + struct poa_flow * pf; + struct flow * flow; + + pf = list_entry(p, struct poa_flow, pend); + if (pf->state != POA_FLOW_ALLOCATED || pf->flow_id < 0) + continue; + + flow = &proc.flows[proc.id_to_fd[pf->flow_id].fd]; + if (flow->info.id != pf->flow_id) + continue; + + if (((flow->oflags & FLOWFDOWN) != 0) != !up) + ++n; + + if (up) { + flow->oflags &= ~FLOWFDOWN; + + ssm_rbuff_clr_flags(flow->rx_rb, RB_FLOWDOWN); + } else { + flow->oflags |= FLOWFDOWN; + + ssm_rbuff_set_flags(flow->rx_rb, RB_FLOWDOWN); + } + + ssm_flow_set_notify(proc.fqset, pf->flow_id, + up ? FLOW_UP : FLOW_DOWN); + } + + return n; +} + +size_t poa_link_updown(int id, + bool up) +{ + struct list_head * p; + size_t n = 0; + + pthread_rwlock_wrlock(&proc.lock); + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->ops->poa_link_match == NULL) + continue; + + if (!poa->ops->poa_link_match(poa, id)) + continue; + + n += flows_updown(poa, up); + } + + pthread_rwlock_unlock(&poas.lock); + pthread_rwlock_unlock(&proc.lock); + + return n; +} + +static size_t poa_ewma(size_t avg, + size_t sz) +{ + if (avg == 0) + return sz; + + avg = avg + (sz >> POA_AVG_SHIFT) - (avg >> POA_AVG_SHIFT); + + return avg == 0 ? 1 : avg; +} + +static void poa_avg_len_update(struct poa * poa, + size_t sz) +{ + STORE_RELAXED(&poa->avg_len, poa_ewma(LOAD_RELAXED(&poa->avg_len), sz)); +} + +static size_t poa_qlen(struct poa * poa) +{ + struct timespec now; + uint64_t ns; + size_t qlen; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + ns = TS_TO_UINT64(now); + if (ns - LOAD_RELAXED(&poa->q_time) < POA_QLEN_GATE) + return LOAD_RELAXED(&poa->q_cache); + + qlen = poa->ops->poa_qlen(poa); + + STORE_RELAXED(&poa->q_cache, qlen); + STORE_RELAXED(&poa->q_time, ns); + + return qlen; +} + +static void poa_cost_sample(struct poa * poa, + size_t before, + size_t len) +{ + size_t after; + + after = poa->ops->poa_qlen(poa); + if (after <= before) + return; /* drained; nothing to learn */ + + after -= before; + if (after < len || after > (len << 2) + 1024) + return; + + STORE_RELAXED(&poa->avg_ovh, + poa_ewma(LOAD_RELAXED(&poa->avg_ovh), after - len)); +} + +int poa_flow_tx(struct poa_flow * pf, + struct ssm_pk_buff * spb, + bool block, + const struct timespec * abstime) +{ + struct poa * poa = pf->poa; + size_t len = ssm_pk_buff_len(spb); + size_t before = 0; + bool sample; + int ret; + + sample = (LOAD_RELAXED(&poa->n_tx) & POA_COST_MASK) == 0; + if (sample) + before = poa->ops->poa_qlen(poa); + + ret = poa->ops->poa_send(poa, &pf->r_addr, pf->r_eid, spb, block, + abstime); + if (ret < 0) { /* the caller releases the buffer */ + POA_STAT_BUMP(poa, snd_fail); + return ret; + } + + POA_STAT_BUMP(poa, tx_pkts); + POA_STAT_ADD(poa, tx_bytes, len); + + FETCH_ADD_RELAXED(&poa->n_tx, 1); + + poa_avg_len_update(poa, len); + + if (sample) + poa_cost_sample(poa, before, len); + + poa_spb_release(spb); + + return 0; +} + +size_t poa_flow_qlen(const struct poa_flow * pf) +{ + struct poa * poa = pf->poa; + uint64_t bytes; + size_t cost; + size_t pkts; + size_t byts; + + if (poa->ops->poa_qpkts != NULL && + poa->ops->poa_qpkts(poa, &pkts, &byts) == 0) + return byts; + + cost = LOAD_RELAXED(&poa->avg_len) + LOAD_RELAXED(&poa->avg_ovh); + if (LOAD_RELAXED(&poa->avg_ovh) == 0 || cost == 0) + return poa_qlen(poa); /* overstated beats false empty */ + + bytes = (uint64_t) poa_qlen(poa) * LOAD_RELAXED(&poa->avg_len); + + return (size_t) (bytes / cost); +} + +size_t poa_flow_qpkts(const struct poa_flow * pf) +{ + struct poa * poa = pf->poa; + size_t cost; + size_t pkts; + size_t byts; + + if (poa->ops->poa_qpkts != NULL && + poa->ops->poa_qpkts(poa, &pkts, &byts) == 0) + return pkts; + + cost = LOAD_RELAXED(&poa->avg_len) + LOAD_RELAXED(&poa->avg_ovh); + if (LOAD_RELAXED(&poa->avg_ovh) == 0 || cost == 0) + return 0; + + return poa_qlen(poa) / cost; +} + +size_t poa_flow_mean_len(const struct poa_flow * pf) +{ + return LOAD_RELAXED(&pf->poa->avg_len); +} + +struct cap_est * poa_flow_cap_est(struct poa_flow * pf) +{ + return &pf->poa->cap; +} + +void poa_flow_ready(struct poa_flow * pf) +{ + buffer_t data; + int answer; + + if (pf == NULL) + return; + + clrbuf(data); + + pthread_rwlock_wrlock(&poas.lock); + + if (!pf->answered || pf->answer_sent) { + pthread_rwlock_unlock(&poas.lock); + return; + } + + answer = pf->answer; + + if (pf->answer_data.len > 0) { + data.data = malloc(pf->answer_data.len); + if (data.data != NULL) { + memcpy(data.data, pf->answer_data.data, + pf->answer_data.len); + data.len = pf->answer_data.len; + } + } + + pf->answer_sent = true; + + pthread_rwlock_unlock(&poas.lock); + + mgmt_send(pf->poa, &pf->r_addr, POA_FLOW_REPLY, pf->eid, + pf->r_eid, qos_raw, answer, &data); + + freebuf(data); +} + +int poa_flow_alloc_resp(int flow_id, + int response, + const buffer_t * data) +{ + struct poa_deferred * d; + struct poa_flow * pf; + + if (flow_id < 0 || flow_id >= SYS_MAX_FLOWS) + return -EPERM; + + pthread_mutex_lock(&poas.mtx); + + pf = poas.id_to_pf[flow_id]; + if (pf != NULL) { + pthread_mutex_unlock(&poas.mtx); + return pf_answer(pf, response, data); + } + + if (poas.deferred.len >= POA_DEFER_MAX) { + pthread_mutex_unlock(&poas.mtx); + return -ENOMEM; + } + + d = malloc(sizeof(*d)); + if (d == NULL) { + pthread_mutex_unlock(&poas.mtx); + return -ENOMEM; + } + + memset(d, 0, sizeof(*d)); + + clock_gettime(PTHREAD_COND_CLOCK, &d->t0); + + d->flow_id = flow_id; + d->response = response; + + if (data != NULL && data->len > 0) { + d->data.data = malloc(data->len); + if (d->data.data == NULL) { + free(d); + pthread_mutex_unlock(&poas.mtx); + return -ENOMEM; + } + memcpy(d->data.data, data->data, data->len); + + d->data.len = data->len; + } + + llist_add_tail(&d->next, &poas.deferred); + + pthread_mutex_unlock(&poas.mtx); + + return 0; +} + +int poa_flow_update(int flow_id, + const buffer_t * data) +{ + struct poa_flow * pf; + + pf = pf_get(flow_id); + if (pf == NULL) + return -EPERM; + + return mgmt_send(pf->poa, &pf->r_addr, POA_FLOW_UPDATE, pf->eid, + pf->r_eid, qos_raw, 0, data); +} + +/* The PoA state is released when the flow itself is torn down. */ +int poa_flow_dealloc(int flow_id) +{ + (void) flow_id; + + return 0; +} + +/* PoA id 0 = management channel. */ +static struct poa * poa_create(enum poa_type type, + const struct poa_ops * ops, + size_t n_eids) +{ + struct poa * poa; + + poa = malloc(sizeof(*poa)); + if (poa == NULL) + goto fail_malloc; + + memset(poa, 0, sizeof(*poa)); + + poa->eid_to_pf = malloc(sizeof(*poa->eid_to_pf) * n_eids); + if (poa->eid_to_pf == NULL) + goto fail_map; + + memset(poa->eid_to_pf, 0, sizeof(*poa->eid_to_pf) * n_eids); + + poa->eids = bmp_create(n_eids - 1, 1); + if (poa->eids == NULL) + goto fail_bmp; + + if (rcu_guard_init(&poa->guard) != 0) + goto fail_guard; + + list_head_init(&poa->next); + list_head_init(&poa->flows); + + poa->type = type; + poa->ops = ops; + poa->mpl = ops->mpl; + poa->n_eids = n_eids; + + return poa; + + fail_guard: + bmp_destroy(poa->eids); + fail_bmp: + free(poa->eid_to_pf); + fail_map: + free(poa); + fail_malloc: + return NULL; +} + +static void poa_destroy(struct poa * poa) +{ + rcu_guard_fini(&poa->guard); + + bmp_destroy(poa->eids); + + free(poa->eid_to_pf); + free(poa); +} + +static void poa_teardown(struct poa * poa) +{ + if (poas.state >= POA_RUNNING) + poa->ops->poa_stop(poa); + + mgmt_frames_purge(poa); + + poa->ops->poa_detach(poa); + + poa_destroy(poa); +} + +static void poa_detach_all(void) +{ + pthread_rwlock_wrlock(&poas.lock); + + while (!list_is_empty(&poas.list)) { + struct poa * poa; + + poa = list_first_entry(&poas.list, struct poa, next); + + list_del(&poa->next); + + pthread_rwlock_unlock(&poas.lock); + + poa_teardown(poa); + + pthread_rwlock_wrlock(&poas.lock); + } + + pthread_rwlock_unlock(&poas.lock); +} + +static int poa_do_detach(const struct poa_detach_req * req) +{ + struct list_head * p; + struct poa * found = NULL; + + pthread_rwlock_wrlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (!poa_has_id(poa, &req->spec)) + continue; + + found = poa; + break; + } + + if (found == NULL) { + pthread_rwlock_unlock(&poas.lock); + return -ENOENT; + } + + if (!list_is_empty(&found->flows)) { + pthread_rwlock_unlock(&poas.lock); + return -EBUSY; + } + + list_del(&found->next); + + pthread_rwlock_unlock(&poas.lock); + + poa_teardown(found); + + return 0; +} + +static int poa_del(const struct poa_spec * spec) +{ + struct poa_detach_req req; + int cs; + int ret; + + memset(&req, 0, sizeof(req)); + + req.spec = *spec; + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs); + + if (poas.state < POA_RUNNING) { /* set before workers run */ + ret = poa_do_detach(&req); + goto out; + } + + pthread_mutex_lock(&poas.mgmt_mtx); + + if (poas.mgmt_stop) { /* stopped: poa_fini reaps these */ + pthread_mutex_unlock(&poas.mgmt_mtx); + + ret = -EBUSY; + goto out; + } + + list_add_tail(&req.next, &poas.detach); + + pthread_cond_signal(&poas.mgmt_cond); + + while (!req.done) + pthread_cond_wait(&poas.done_cond, &poas.mgmt_mtx); + + pthread_mutex_unlock(&poas.mgmt_mtx); + + ret = req.result; + out: + pthread_setcancelstate(cs, NULL); + + return ret; +} + +static __inline__ bool mgmt_idle(void) +{ + if (poas.mgmt_stop) + return false; + + if (!llist_is_empty(&poas.mgmt_frames)) + return false; + + return list_is_empty(&poas.detach); +} + +static void detach_run(void) +{ + while (!list_is_empty(&poas.detach)) { + struct poa_detach_req * req; + + req = list_first_entry(&poas.detach, struct poa_detach_req, + next); + list_del(&req->next); + + pthread_mutex_unlock(&poas.mgmt_mtx); + + req->result = poa_do_detach(req); + + pthread_mutex_lock(&poas.mgmt_mtx); + + req->done = true; + + pthread_cond_broadcast(&poas.done_cond); + } +} + +static void * mgmt_handler(void * o) +{ + struct timespec intv = TIMESPEC_INIT_MS(POA_SWEEP_TIMEO); + + (void) o; + + while (true) { + struct poa_mgmt_frame * frame; + struct timespec abstime; + + pthread_mutex_lock(&poas.mgmt_mtx); + + detach_run(); + + while (mgmt_idle()) { + clock_gettime(PTHREAD_COND_CLOCK, &abstime); + ts_add(&abstime, &intv, &abstime); + + if (pthread_cond_timedwait(&poas.mgmt_cond, + &poas.mgmt_mtx, + &abstime) == ETIMEDOUT) { + pthread_mutex_unlock(&poas.mgmt_mtx); + sweep_pending(); + pthread_mutex_lock(&poas.mgmt_mtx); + } + } + + if (poas.mgmt_stop) { + detach_run(); /* nobody else serves these */ + + pthread_mutex_unlock(&poas.mgmt_mtx); + break; + } + + if (llist_is_empty(&poas.mgmt_frames)) { + pthread_mutex_unlock(&poas.mgmt_mtx); + continue; + } + + frame = llist_first_entry(&poas.mgmt_frames, + struct poa_mgmt_frame, next); + llist_del(&frame->next, &poas.mgmt_frames); + + --frame->poa->n_mgmt; + + pthread_mutex_unlock(&poas.mgmt_mtx); + + mgmt_frame_handle(frame); + + free(frame); + } + + return (void *) 0; +} + +#ifndef HAVE_ETH +/* Only the Ethernet transport reports link events. */ +int poa_monitor_open(void) +{ + return -1; +} + +void poa_monitor_read(int fd) +{ + (void) fd; +} +#endif + +static void * poa_monitor(void * o) +{ + (void) o; + + while (true) + poa_monitor_read(poas.mon_fd); + + return (void *) 0; +} + +static int monitor_start(void) +{ + poas.mon_fd = poa_monitor_open(); + if (poas.mon_fd < 0) + return 0; + + if (pthread_create(&poas.monitor, NULL, poa_monitor, NULL) == 0) + return 0; + + close(poas.mon_fd); + + poas.mon_fd = -1; + + return -1; +} + +static void monitor_stop(void) +{ + if (poas.mon_fd < 0) + return; + + pthread_cancel(poas.monitor); + pthread_join(poas.monitor, NULL); + + close(poas.mon_fd); + + poas.mon_fd = -1; +} + +int poa_start(void) +{ + struct list_head * p; + struct list_head * h; + + if (poas.state == POA_NULL) + return 0; + + if (pthread_create(&poas.mgmt_handler, NULL, mgmt_handler, NULL) != 0) + goto fail_mgmt; + + if (monitor_start() < 0) + goto fail_monitor; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->ops->poa_start(poa) < 0) + goto fail_reader; + } + + poas.state = POA_RUNNING; + + pthread_rwlock_unlock(&poas.lock); + + return 0; + + fail_reader: + list_for_each(h, &poas.list) { + struct poa * poa = list_entry(h, struct poa, next); + + if (h == p) + break; + + poa->ops->poa_stop(poa); + } + + pthread_rwlock_unlock(&poas.lock); + + monitor_stop(); + fail_monitor: + pthread_mutex_lock(&poas.mgmt_mtx); + + poas.mgmt_stop = true; + + pthread_cond_broadcast(&poas.mgmt_cond); + pthread_mutex_unlock(&poas.mgmt_mtx); + + pthread_join(poas.mgmt_handler, NULL); + fail_mgmt: + return -1; +} + +void poa_stop(void) +{ + struct list_head * p; + + if (poas.state < POA_RUNNING) + return; + + pthread_mutex_lock(&poas.mgmt_mtx); + + poas.mgmt_stop = true; + + pthread_cond_broadcast(&poas.mgmt_cond); + pthread_mutex_unlock(&poas.mgmt_mtx); + + pthread_join(poas.mgmt_handler, NULL); + + monitor_stop(); + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + poa->ops->poa_stop(poa); + } + + pthread_rwlock_unlock(&poas.lock); + + poas.state = POA_INIT; +} + +void poa_fini(void) +{ + if (poas.state == POA_NULL) + return; + + poa_stop(); + + poas.state = POA_NULL; + + poa_detach_all(); + +#ifdef PROC_FLOW_STATS + rib_unreg(POA_RIB); +#endif + + pthread_rwlock_destroy(&poas.lock); + pthread_cond_destroy(&poas.done_cond); + pthread_cond_destroy(&poas.mgmt_cond); + pthread_mutex_destroy(&poas.mgmt_mtx); + pthread_cond_destroy(&poas.cond); + pthread_mutex_destroy(&poas.mtx); +} + +static size_t poa_count(void) +{ + struct list_head * p; + size_t n = 0; + + list_for_each(p, &poas.list) + n++; + + return n; +} + +static int poa_add(const struct poa_spec * spec, + const struct poa_ops * ops, + size_t n_eids) +{ + struct list_head * p; + struct poa * poa; + int err; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + poa = list_entry(p, struct poa, next); + if (poa_has_id(poa, spec)) { + pthread_rwlock_unlock(&poas.lock); + return -EPERM; + } + } + + pthread_rwlock_unlock(&poas.lock); + + poa = poa_create(spec->type, ops, n_eids); + if (poa == NULL) + return -ENOMEM; + + err = poa->ops->poa_attach(poa, spec); + if (err < 0) + goto fail_bind; + + err = poa_addr_name(&poa->local, poa->name, sizeof(poa->name)); + if (err < 0) + goto fail_start; + + err = -1; + + pthread_rwlock_wrlock(&poas.lock); + + if (poa_count() >= POA_MAX_POAS) { + pthread_rwlock_unlock(&poas.lock); + goto fail_start; + } + + if (poas.state >= POA_RUNNING && poa->ops->poa_start(poa) < 0) { + pthread_rwlock_unlock(&poas.lock); + goto fail_start; + } + + list_add_tail(&poa->next, &poas.list); + + pthread_rwlock_unlock(&poas.lock); + + return 0; + + fail_start: + poa->ops->poa_detach(poa); + fail_bind: + poa_destroy(poa); + return err; +} + +/* The single place a type is bound to its transport. */ +int poa_attach(const struct poa_spec * poa) +{ + if (poa == NULL) + return -EINVAL; + + switch (poa->type) { + case POA_UDP4: + /* FALLTHRU */ + case POA_UDP6: + return poa_add(poa, &udp_poa_ops, POA_MAX_EIDS); + case POA_ETH: +#ifdef HAVE_ETH + return poa_add(poa, ð_poa_ops, POA_MAX_EIDS); +#else + return -ENOTSUP; +#endif + default: + return -ENOTSUP; + } +} + +int poa_detach(const struct poa_spec * poa) +{ + if (poa == NULL) + return -EINVAL; + + return poa_del(poa); +} + +ssize_t poa_list(struct poa_spec * specs, + size_t max) +{ + struct list_head * p; + size_t n = 0; + + if (specs == NULL) + return -EINVAL; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (n++ >= max) + continue; + + memset(specs, 0, sizeof(*specs)); + + poa->ops->poa_spec(poa, specs); + + specs++; + } + + pthread_rwlock_unlock(&poas.lock); + + return (ssize_t) n; +} + +/* + * Complete peer for dst on the backend serving its type. The ops are + * borrowed under poas.lock and called outside it (see poa_query); if + * every PoA of the type detaches in between, the query's broadcast + * reaches nothing and reports -EPERM, as the lookup would. + */ +static int poa_peer_resolve(const char * dst, + struct poa_addr * peer) +{ + const struct poa_ops * ops = NULL; + struct list_head * p; + + pthread_rwlock_rdlock(&poas.lock); + + list_for_each(p, &poas.list) { + struct poa * poa = list_entry(p, struct poa, next); + + if (poa->type == peer->type) { + ops = poa->ops; + break; + } + } + + pthread_rwlock_unlock(&poas.lock); + + if (ops == NULL) /* nothing could carry the flow */ + return -EPERM; + + if (ops->poa_query == NULL) /* these addresses arrive complete */ + return 0; + + return ops->poa_query(dst, NULL, peer); +} + +/* + * Three steps: the IRMd creates the flow and prepares the key exchange, + * the PoA handshakes with the peer, the IRMd completes the + * exchange and hands us the key. + */ +int poa_flow_alloc(const char * dst, + const struct poa_addr * addr, + qosspec_t * qs, + const struct timespec * timeo) +{ + struct flow_info flow; + struct poa_flow * pf; + struct poa_addr peer; + struct crypt_sk crypt; + struct timespec t0; + struct timespec t1; + uint8_t key[SYMMKEYSZ]; + uint8_t buf[SOCK_BUF_SIZE]; + buffer_t msg = {SOCK_BUF_SIZE, buf}; + buffer_t req; + buffer_t resp; + uint32_t mtu = 0; + int err; + + if (addr == NULL) + return -EINVAL; + + if (qs != NULL && qs->service == SVC_STREAM && qs->loss != 0) + return -EINVAL; + + peer = *addr; + + err = poa_peer_resolve(dst, &peer); + if (err < 0) + return err; + + addr = &peer; + + err = poa_check(addr); + if (err < 0) + return err; + + memset(&flow, 0, sizeof(flow)); + + flow.n_pid = getpid(); + flow.n_1_pid = getpid(); + flow.qs = qs == NULL ? qos_raw : *qs; + if (poa_flow_alloc__irm_req_ser(&msg, &flow, dst) < 0) + return -ENOMEM; + + err = send_recv_msg(&msg); + if (err < 0) + return err; + + clrbuf(req); + clrbuf(resp); + + err = poa_flow__irm_result_des(&msg, &flow, &req); + if (err < 0) + return err; + + clock_gettime(PTHREAD_COND_CLOCK, &t0); + + err = poa_alloc(addr, flow.qs, &req, &resp, &pf, &mtu, timeo); + + freebuf(req); + + if (err < 0) + goto fail_alloc; + + clock_gettime(PTHREAD_COND_CLOCK, &t1); + + flow.mtu = mtu; + flow.mpl = pf->poa->mpl; + msg.len = SOCK_BUF_SIZE; + msg.data = buf; + if (poa_flow_alloc_r__irm_req_ser(&msg, &flow, &resp, 0) < 0) { + err = -ENOMEM; + goto fail_resp; + } + + freebuf(resp); + + err = send_recv_msg(&msg); + if (err < 0) + goto fail_msg; + + crypt.key = key; + crypt.epoch = 0; + crypt.role = CRYPT_ROLE_INIT; + + err = flow__irm_result_des(&msg, &flow, &crypt); + if (err < 0) + goto fail_msg; + + err = flow_init(&flow, &crypt, ts_diff_ns(&t1, &t0), pf); + + crypt_secure_clear(key, SYMMKEYSZ); + + if (err < 0) + goto fail_msg; + + if (qs != NULL) + *qs = flow.qs; + + return err; + + fail_resp: + freebuf(resp); + fail_msg: + poa_alloc_fail(pf); + return err; + fail_alloc: + msg.len = SOCK_BUF_SIZE; + msg.data = buf; + if (poa_flow_alloc_r__irm_req_ser(&msg, &flow, NULL, err) == 0) + send_recv_msg(&msg); + + return err; +} + diff --git a/src/lib/poa/poa.h b/src/lib/poa/poa.h new file mode 100644 index 00000000..9edb0335 --- /dev/null +++ b/src/lib/poa/poa.h @@ -0,0 +1,366 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Points of attachment (PoA) - internal API + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#ifndef OUROBOROS_LIB_POA_POA_H +#define OUROBOROS_LIB_POA_POA_H + +#include <ouroboros/atomics.h> +#include <ouroboros/ipcp-dev.h> +#include <ouroboros/list.h> +#include <ouroboros/qos.h> +#include <ouroboros/rcu.h> +#include <ouroboros/ssm_flow_set.h> +#include <ouroboros/ssm_pool.h> +#include <ouroboros/ssm_rbuff.h> +#include <ouroboros/time.h> +#include <ouroboros/utils.h> + +#include "../cap.h" + +#include <errno.h> +#include <limits.h> +#include <poll.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> + +#define POA_MGMT_EID 0 /* reserved for the mgmt channel */ + +#define POA_FLOW_REQ 1 +#define POA_FLOW_REPLY 2 +#define POA_FLOW_UPDATE 3 +#define POA_NAME_QUERY 4 +#define POA_NAME_REPLY 5 + +#define POA_QUERY_HLEN 32 /* SHA3-256, the query hash algorithm */ + +/* Fits "udp6.<ip6>.<port>", the longest display/RIB entry name. */ +#define POA_NAME_STRLEN 63 + +/* Management message; every transport frames it the same way. */ +struct poa_mgmt_msg { + uint8_t code; + uint8_t resv[3]; + uint32_t s_eid; + uint32_t d_eid; + int32_t response; + uint64_t bandwidth; + uint32_t delay; + uint32_t loss; + uint32_t ber; + uint32_t max_gap; + uint32_t timeout; + uint8_t availability; + uint8_t service; + uint16_t data_len; +} __attribute__((packed)); + +struct poa; +struct poa_flow; + +#ifdef PROC_FLOW_STATS +#define POA_STAT_BUMP(poa, field) FETCH_ADD_RELAXED(&(poa)->stat.field, 1) +#define POA_STAT_ADD(poa, field, v) FETCH_ADD_RELAXED(&(poa)->stat.field, (v)) +#define POA_STAT_SUB(poa, field, v) FETCH_SUB_RELAXED(&(poa)->stat.field, (v)) +#define POA_STAT_LOAD(poa, field) LOAD_RELAXED(&(poa)->stat.field) +#else +#define POA_STAT_BUMP(poa, field) ((void) (poa)) +#define POA_STAT_ADD(poa, field, v) ((void) (poa), (void) (v)) +#define POA_STAT_SUB(poa, field, v) ((void) (poa), (void) (v)) +#define POA_STAT_LOAD(poa, field) ((void) (poa), (size_t) 0) +#endif + +struct poa_stat { + size_t n_flows; /* gauge; the RIB reads it without poas.lock */ + size_t rx_pkts; /* packets delivered to a flow */ + size_t rx_bytes; /* payload bytes delivered */ + size_t tx_pkts; /* packets handed to the transport */ + size_t tx_bytes; /* payload bytes handed to the transport */ + size_t mgmt_rx; /* frames queued for the mgmt handler */ + size_t mgmt_tx; /* management frames sent */ + size_t bad_eid; /* no flow on the EID a peer sent */ + size_t dlv_fail; /* the rx ring above would not take it */ + size_t buf_fail; /* no buffer to receive into */ + size_t rcv_fail; /* transport read failed; the reader exits */ + size_t snd_fail; /* transport send failed */ + size_t qry_tx; /* name queries broadcast */ + size_t qry_rx; /* name queries received */ + size_t rep_tx; /* name replies sent, query matched */ + size_t rep_rx; /* name replies received */ +}; + +/* Spacing between transmit-depth samples; a depth costs a syscall. */ +#define POA_QLEN_GATE (100 * 1000) /* ns */ + + +/* Transport operations; public poa_X() dispatches to ops->poa_X. */ +struct poa_ops { + /* Parse own arm of the spec; validate; fill local and priv. */ + int (* poa_attach)(struct poa * poa, + const struct poa_spec * spec); + + void (* poa_detach)(struct poa * poa); + + int (* poa_start)(struct poa * poa); + + void (* poa_stop)(struct poa * poa); + + /* Full queue: -EAGAIN unless block; then wait, to abstime if set. */ + int (* poa_send)(struct poa * poa, + const struct poa_addr * dst, + uint32_t eid, + struct ssm_pk_buff * spb, + bool block, + const struct timespec * abstime); + + int (* poa_send_mgmt)(struct poa * poa, + const struct poa_addr * dst, + const uint8_t * buf, + size_t len); + + int (* poa_query)(const char * dst, + const struct timespec * timeo, + struct poa_addr * addr); + + uint32_t (* poa_mtu)(struct poa * poa, + const struct poa_addr * dst); + + /* Bytes queued in the transmit path of the PoA. */ + size_t (* poa_qlen)(struct poa * poa); + + /* Depth from the queue itself; NULL infers it from qlen. */ + int (* poa_qpkts)(struct poa * poa, + size_t * pkts, + size_t * byts); + + int (* poa_rib)(struct poa * poa, + char * buf, + size_t len); + + /* Identity as a spec, e.g. for poa_list. */ + void (* poa_spec)(const struct poa * poa, + struct poa_spec * spec); + + /* Same identity as spec? Caller matched poa->type already. */ + bool (* poa_has_id)(const struct poa * poa, + const struct poa_spec * spec); + + /* Carries dst? Caller matched poa->type already. */ + bool (* poa_match)(const struct poa * poa, + const struct poa_addr * dst); + + /* + * Flows ride the link this id names; NULL: no link events. + * Ids are meaningful only to the backend whose monitor + * produced them; a single backend owns the monitor. + */ + bool (* poa_link_match)(const struct poa * poa, + int id); + + /* Maximum packet lifetime in the transport, seconds. */ + time_t mpl; +}; +struct poa { + struct list_head next; + + enum poa_type type; + const struct poa_ops * ops; + void * priv; + + struct poa_addr local; /* what peers dial us on */ + + /* Display/RIB entry name for local, e.g. "udp4.<ip>.<port>". */ + char name[POA_NAME_STRLEN + 1]; + + time_t mpl; + + /* Mean sent packet size (bytes), EWMA over the send path. */ + size_t avg_len; + /* Cost of one packet in the queue, in the transport's terms. */ + size_t avg_ovh; + size_t n_tx; + + /* Last queue depth read, and when, in the transport's terms. */ + size_t q_cache; + uint64_t q_time; + + /* Capacity estimator of the queue the flows on this PoA share. */ + struct cap_est cap; + + /* Queued management frames, capped; poas.mgmt_mtx guards. */ + size_t n_mgmt; + +#ifdef PROC_FLOW_STATS + struct poa_stat stat; +#endif + + struct bmp * eids; + struct poa_flow ** eid_to_pf; + size_t n_eids; + + struct list_head flows; /* live flows, for repeats */ + + /* Keeps a flow and its ring alive under the receive path. */ + struct rcu_guard guard; +}; + +/* poa/poa.c is part of the dev.c translation unit. */ +int poa_init(const char * name); + +int poa_start(void); + +void poa_stop(void); + +void poa_fini(void); + +/* Also answer name queries for the layer once enrolled. */ +int poa_set_layer(const char * layer); + +/* Hash of a name this process answers queries for? */ +bool poa_has_name(const uint8_t * hash); + +int poa_flow_tx(struct poa_flow * pf, + struct ssm_pk_buff * spb, + bool block, + const struct timespec * abstime); + +size_t poa_flow_qlen(const struct poa_flow * pf); + +size_t poa_flow_qpkts(const struct poa_flow * pf); + +struct cap_est * poa_flow_cap_est(struct poa_flow * pf); + +size_t poa_flow_mean_len(const struct poa_flow * pf); + +void poa_flow_attach(struct poa_flow * pf, + int flow_id, + struct ssm_rbuff * rx_rb); + +void poa_flow_ready(struct poa_flow * pf); + +void poa_flow_detach(struct poa_flow * pf); + +struct poa_flow * poa_flow_take_pending(int flow_id); + +/* Addresses and management messages (poa/addr.c). */ +int poa_addr_cmp(const struct poa_addr * a, + const struct poa_addr * b); + +/* Display/RIB entry name, e.g. "udp4.<ip>.<port>". */ +int poa_addr_name(const struct poa_addr * a, + char * buf, + size_t len); + +void poa_mgmt_msg_ser(struct poa_mgmt_msg * msg, + uint8_t code, + uint32_t s_eid, + uint32_t d_eid, + qosspec_t qs, + int response, + size_t data_len); + +void poa_mgmt_msg_qos(const struct poa_mgmt_msg * msg, + qosspec_t * qs); + +/* Called by the transports. */ +void poa_rx_pkt(struct poa * poa, + uint32_t eid, + struct ssm_pk_buff * spb); + +void poa_rx_mgmt(struct poa * poa, + const struct poa_addr * src, + const uint8_t * buf, + size_t len); + +/* Reserve a buffer for a received packet, with transport headroom. */ +int poa_spb_reserve(struct ssm_pk_buff ** spb, + size_t len); + +void poa_spb_release(struct ssm_pk_buff * spb); + +/* + * All flows on PoAs whose poa_link_match reports this link id go up + * or down with it. Returns the number of flows whose state changed. + */ +size_t poa_link_updown(int id, + bool up); + +/* + * Link monitor: one socket for the whole subsystem, opened by + * poa_start(). Returns -1 where the transport has no monitor. + */ +int poa_monitor_open(void); + +/* Reads one batch of link events; cancellation point. */ +void poa_monitor_read(int fd); + +/* Broadcast a mgmt frame on every PoA matching dst; # sent. */ +int poa_bcast_mgmt(const struct poa_addr * dst, + const uint8_t * buf, + size_t len); + +/* Transport op tables. */ +extern const struct poa_ops udp_poa_ops; +extern const struct poa_ops eth_poa_ops; + +/* + * Waits for a descriptor to take another packet, up to abstime. + * A NULL deadline waits indefinitely. Transports call this when + * their send reports the transmit queue full. + */ +static __inline__ int poa_wait_out(int fd, + const struct timespec * abstime) +{ + struct pollfd pfd; + struct timespec now; + long ms = -1; + bool clamped = false; + int ret; + + if (abstime != NULL) { + clock_gettime(PTHREAD_COND_CLOCK, &now); + + if (ts_diff_ns(abstime, &now) <= 0) + return -ETIMEDOUT; + + ms = ts_diff_ms(abstime, &now) + 1; /* sub-ms must wait */ + if (ms > INT_MAX) { /* poll takes an int */ + ms = INT_MAX; + clamped = true; + } + } + + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + + ret = poll(&pfd, 1, (int) ms); + if (ret < 0) + return errno == EINTR ? 0 : -EIO; + + if (ret == 0) + return clamped ? 0 : -ETIMEDOUT; /* clamped: retry */ + + return 0; +} + +#endif /* OUROBOROS_LIB_POA_POA_H */ diff --git a/src/lib/poa/udp.c b/src/lib/poa/udp.c new file mode 100644 index 00000000..6753347a --- /dev/null +++ b/src/lib/poa/udp.c @@ -0,0 +1,633 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Points of attachment (PoA) - UDP transport + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#if defined(__APPLE__) +#define _BSD_SOURCE +#define _DARWIN_C_SOURCE +#elif defined(__FreeBSD__) +#define __BSD_VISIBLE 1 +#elif defined(__linux__) || defined(__CYGWIN__) +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif +#else +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + +#include "config.h" + +#define OUROBOROS_PREFIX "poa-udp" + +#include <ouroboros/endian.h> +#include <ouroboros/errno.h> +#include <ouroboros/logs.h> +#include <ouroboros/time.h> +#include <ouroboros/utils.h> + +#include "poa.h" + +#ifdef __linux__ +#include <linux/sockios.h> +#endif +#include <arpa/inet.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define UDP_HDR_LEN sizeof(uint32_t) /* PoA id */ +#define UDP_MAX_PAYLOAD (POA_UDP_RD_BUF - UDP_HDR_LEN) +/* The reader buffer must fit a full mgmt frame at any tuning. */ +#define UDP_MAX_PACKET MAX(POA_UDP_RD_BUF, POA_MGMT_FRAME_SIZE + UDP_HDR_LEN) +#define UDP_IP4_OVERH 28U /* IPv4 + UDP */ +#define UDP_IP6_OVERH 48U /* IPv6 + UDP */ +/* Wait for the link to come back before reading it again. */ +#define UDP_DOWN_TIMEO 100 /* ms */ + +union udp_saddr { + struct sockaddr sa; + struct sockaddr_in in; + struct sockaddr_in6 in6; +}; + +struct udp_priv { + int s_fd; + int af; + union udp_saddr s_saddr; + pthread_t reader; + bool running; +}; + +static socklen_t saddr_len(int af) +{ + if (af == AF_INET) + return sizeof(struct sockaddr_in); + + return sizeof(struct sockaddr_in6); +} + +static void addr_to_saddr(const struct poa_addr * addr, + union udp_saddr * saddr) +{ + memset(saddr, 0, sizeof(*saddr)); + + if (addr->type == POA_UDP4) { + saddr->in.sin_family = AF_INET; + saddr->in.sin_addr = addr->udp4.ip_addr; + saddr->in.sin_port = htons(addr->udp4.port); + } else { + saddr->in6.sin6_family = AF_INET6; + saddr->in6.sin6_addr = addr->udp6.ip_addr; + saddr->in6.sin6_port = htons(addr->udp6.port); + } +} + +static void saddr_to_addr(const union udp_saddr * saddr, + struct poa_addr * addr) +{ + memset(addr, 0, sizeof(*addr)); + + if (saddr->sa.sa_family == AF_INET) { + addr->type = POA_UDP4; + addr->udp4.ip_addr = saddr->in.sin_addr; + addr->udp4.port = ntohs(saddr->in.sin_port); + } else { + addr->type = POA_UDP6; + addr->udp6.ip_addr = saddr->in6.sin6_addr; + addr->udp6.port = ntohs(saddr->in6.sin6_port); + } +} + +/* A datagram longer than the buffer arrives truncated: drop it. */ +static void * udp_reader(void * o) +{ + struct poa * poa = (struct poa *) o; + struct udp_priv * priv = (struct udp_priv *) poa->priv; + struct timespec down = TIMESPEC_INIT_MS(UDP_DOWN_TIMEO); + uint8_t * buf; + + buf = malloc(UDP_MAX_PACKET); + if (buf == NULL) + return (void *) -1; + + pthread_cleanup_push(free, buf); + + while (true) { + struct ssm_pk_buff * spb; + union udp_saddr r_saddr; + struct poa_addr src; + struct msghdr mh; + struct iovec iov; + ssize_t n; + uint32_t eid; + size_t plen; + + iov.iov_base = buf; + iov.iov_len = UDP_MAX_PACKET; + + memset(&mh, 0, sizeof(mh)); + + mh.msg_name = &r_saddr; + mh.msg_namelen = sizeof(r_saddr); + mh.msg_iov = &iov; + mh.msg_iovlen = 1; + + n = recvmsg(priv->s_fd, &mh, 0); + if (n < 0) { + if (errno == EINTR) + continue; + + POA_STAT_BUMP(poa, rcv_fail); + + if (errno == ENETDOWN) { + nanosleep(&down, NULL); + continue; + } + + log_err("Reader stopped: %s.", strerror(errno)); + break; + } + + if ((mh.msg_flags & MSG_TRUNC) != 0) { + POA_STAT_BUMP(poa, buf_fail); + continue; + } + + if ((size_t) n < UDP_HDR_LEN) + continue; + + eid = ntoh32(*(uint32_t *) buf); + plen = (size_t) n - UDP_HDR_LEN; + + saddr_to_addr(&r_saddr, &src); + + if (eid == POA_MGMT_EID) { + poa_rx_mgmt(poa, &src, buf + UDP_HDR_LEN, plen); + continue; + } + + if (poa_spb_reserve(&spb, plen) < 0) { + POA_STAT_BUMP(poa, buf_fail); + continue; + } + + memcpy(ssm_pk_buff_head(spb), buf + UDP_HDR_LEN, plen); + + poa_rx_pkt(poa, eid, spb); + } + + pthread_cleanup_pop(true); + + return (void *) 0; +} + +/* Reads the bound address back: an ephemeral port is only known after. */ +static int udp_attach(struct poa * poa, + const struct poa_spec * spec) +{ + struct udp_priv * priv; + socklen_t len; + int af; + + af = spec->type == POA_UDP4 ? AF_INET : AF_INET6; + + priv = malloc(sizeof(*priv)); + if (priv == NULL) + return -ENOMEM; + + memset(priv, 0, sizeof(*priv)); + + priv->af = af; + priv->s_fd = socket(af, SOCK_DGRAM, IPPROTO_UDP); + if (priv->s_fd < 0) { + log_err("Failed to create socket: %s.", strerror(errno)); + goto fail_socket; + } + + if (af == AF_INET) { + priv->s_saddr.in.sin_family = AF_INET; + priv->s_saddr.in.sin_addr = spec->udp4.ip_addr; + priv->s_saddr.in.sin_port = htons(spec->udp4.port); + } else { + int on = 1; + + if (setsockopt(priv->s_fd, IPPROTO_IPV6, IPV6_V6ONLY, + &on, sizeof(on)) < 0) { + log_err("Failed to set IPV6_V6ONLY: %s.", + strerror(errno)); + goto fail_bind; + } + + priv->s_saddr.in6.sin6_family = AF_INET6; + priv->s_saddr.in6.sin6_addr = spec->udp6.ip_addr; + priv->s_saddr.in6.sin6_port = htons(spec->udp6.port); + } + + if (bind(priv->s_fd, &priv->s_saddr.sa, saddr_len(af)) < 0) { + log_err("Failed to bind: %s.", strerror(errno)); + goto fail_bind; + } + + poa->priv = priv; + len = saddr_len(af); + if (getsockname(priv->s_fd, &priv->s_saddr.sa, &len) < 0) + log_warn("Failed to read the bound address: %s.", + strerror(errno)); + + saddr_to_addr(&priv->s_saddr, &poa->local); + + return 0; + + fail_bind: + close(priv->s_fd); + fail_socket: + poa->priv = NULL; + + free(priv); + + return -EIO; +} + +static void udp_detach(struct poa * poa) +{ + struct udp_priv * priv = (struct udp_priv *) poa->priv; + + if (priv == NULL) + return; + + close(priv->s_fd); + + free(priv); + + poa->priv = NULL; +} + +/* One reader per socket, so a flow cannot be reordered on receive. */ +static int udp_start(struct poa * poa) +{ + struct udp_priv * priv = (struct udp_priv *) poa->priv; + + if (pthread_create(&priv->reader, NULL, udp_reader, poa) != 0) + return -1; + + priv->running = true; + + return 0; +} + +static void udp_stop(struct poa * poa) +{ + struct udp_priv * priv = (struct udp_priv *) poa->priv; + + if (!priv->running) + return; + + pthread_cancel(priv->reader); + pthread_join(priv->reader, NULL); + + priv->running = false; +} + +/* + * The PoA id is a separate iovec, so the buffer needs no headroom. + * MSG_DONTWAIT: the reader blocks on this socket. + */ +static int udp_sendv(struct udp_priv * priv, + const struct poa_addr * dst, + uint32_t eid, + const uint8_t * body, + size_t len, + bool block, + const struct timespec * abstime) +{ + union udp_saddr saddr; + struct msghdr msg; + struct iovec iov[2]; + uint32_t hdr; + int ret; + + if (len > UDP_MAX_PAYLOAD) + return -EMSGSIZE; + + addr_to_saddr(dst, &saddr); + + hdr = hton32(eid); + + iov[0].iov_base = &hdr; + iov[0].iov_len = sizeof(hdr); + iov[1].iov_base = (void *) body; + iov[1].iov_len = len; + + memset(&msg, 0, sizeof(msg)); + + msg.msg_name = &saddr; + msg.msg_namelen = saddr_len(priv->af); + msg.msg_iov = iov; + msg.msg_iovlen = len > 0 ? 2 : 1; + while (sendmsg(priv->s_fd, &msg, MSG_DONTWAIT) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + return -EIO; + + if (!block) + return -EAGAIN; + + ret = poa_wait_out(priv->s_fd, abstime); + if (ret < 0) + return ret; + } + + return 0; +} + +static int udp_send(struct poa * poa, + const struct poa_addr * dst, + uint32_t eid, + struct ssm_pk_buff * spb, + bool block, + const struct timespec * abstime) +{ + return udp_sendv((struct udp_priv *) poa->priv, dst, eid, + ssm_pk_buff_head(spb), ssm_pk_buff_len(spb), + block, abstime); +} + +static int udp_send_mgmt(struct poa * poa, + const struct poa_addr * dst, + const uint8_t * buf, + size_t len) +{ + struct timespec timeo = TIMESPEC_INIT_MS(POA_MGMT_SND_TIMEO); + struct timespec abstime; + + clock_gettime(PTHREAD_COND_CLOCK, &abstime); + ts_add(&abstime, &timeo, &abstime); + + return udp_sendv((struct udp_priv *) poa->priv, dst, POA_MGMT_EID, + buf, len, true, &abstime); +} + +/* The PoA id header eats into the usable MTU. */ +static uint32_t udp_mtu(struct poa * poa, + const struct poa_addr * dst) +{ + struct udp_priv * priv = (struct udp_priv *) poa->priv; + uint32_t fallback; + uint32_t overh; +#if defined(__linux__) && (defined(IP_MTU) || defined(IPV6_MTU)) + union udp_saddr saddr; + socklen_t len; + int sock; + int mtu = 0; +#endif + if (priv->af == AF_INET) { + fallback = POA_UDP4_MTU; + overh = UDP_IP4_OVERH; + } else { + fallback = POA_UDP6_MTU; + overh = UDP_IP6_OVERH; + } + + fallback -= UDP_HDR_LEN; + if (fallback > UDP_MAX_PAYLOAD) + fallback = UDP_MAX_PAYLOAD; + +#if defined(__linux__) && (defined(IP_MTU) || defined(IPV6_MTU)) + + addr_to_saddr(dst, &saddr); + + sock = socket(priv->af, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return fallback; + + if (connect(sock, &saddr.sa, saddr_len(priv->af)) < 0) + goto fallback; + + len = sizeof(mtu); + +#if defined(IP_MTU) + if (priv->af == AF_INET) { + if (getsockopt(sock, IPPROTO_IP, IP_MTU, &mtu, &len) < 0) + goto fallback; + } +#endif +#if defined(IPV6_MTU) + if (priv->af == AF_INET6) { + if (getsockopt(sock, IPPROTO_IPV6, IPV6_MTU, &mtu, &len) < 0) + goto fallback; + } +#endif + close(sock); + + if (mtu <= (int) (overh + UDP_HDR_LEN)) + return fallback; + + return MIN((uint32_t) mtu - overh - UDP_HDR_LEN, UDP_MAX_PAYLOAD); + + fallback: + close(sock); +#else + (void) dst; + (void) overh; +#endif + return fallback; +} + +/* All flows on the PoA share the socket, so this is aggregate. */ +static size_t udp_qlen(struct poa * poa) +{ +#if defined(__linux__) && defined(SIOCOUTQ) + struct udp_priv * priv = (struct udp_priv *) poa->priv; + int qlen; + + qlen = 0; + if (ioctl(priv->s_fd, SIOCOUTQ, &qlen) < 0) + return 0; + + return (size_t) qlen; +#else + (void) poa; + + return 0; +#endif +} + +/* The kernel keeps no per-socket drop count for UDP. */ +static int udp_rib(struct poa * poa, + char * buf, + size_t len) +{ + struct udp_priv * priv = (struct udp_priv *) poa->priv; + socklen_t optlen; + size_t sndbuf = 0; + size_t rcvbuf = 0; + int val; + int size; + + optlen = sizeof(val); + if (getsockopt(priv->s_fd, SOL_SOCKET, SO_SNDBUF, &val, &optlen) == 0) + sndbuf = (size_t) val; + + optlen = sizeof(val); + if (getsockopt(priv->s_fd, SOL_SOCKET, SO_RCVBUF, &val, &optlen) == 0) + rcvbuf = (size_t) val; + + size = snprintf(buf, len, + "Socket sndbuf (bytes): %zu\n" + "Socket rcvbuf (bytes): %zu\n", + sndbuf, rcvbuf); + if (size < 0 || (size_t) size >= len) + return -1; + + return size; +} + +/* + * Asks the kernel which address it would send from: connect() does the + * real route lookup and sends nothing, so this honours the default + * route, metrics and policy rules alike. + */ +static int udp_src_addr(const struct poa_addr * dst, + struct poa_addr * src) +{ + union udp_saddr saddr; + socklen_t len; + int af; + int fd; + + if (dst->type != POA_UDP4 && dst->type != POA_UDP6) + return -EINVAL; + + af = dst->type == POA_UDP4 ? AF_INET : AF_INET6; + + addr_to_saddr(dst, &saddr); + + fd = socket(af, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) + return -EIO; + + if (connect(fd, &saddr.sa, saddr_len(af)) < 0) + goto fail; + + len = saddr_len(af); + if (getsockname(fd, &saddr.sa, &len) < 0) + goto fail; + + close(fd); + + saddr_to_addr(&saddr, src); + + return 0; + + fail: + close(fd); + + return -EIO; +} + +static void udp_spec(const struct poa * poa, + struct poa_spec * spec) +{ + spec->type = poa->type; + + if (poa->type == POA_UDP4) + spec->udp4 = poa->local.udp4; + else + spec->udp6 = poa->local.udp6; +} + +static bool udp_has_id(const struct poa * poa, + const struct poa_spec * spec) +{ + if (poa->type == POA_UDP4) { + if (poa->local.udp4.port != spec->udp4.port) + return false; + + return memcmp(&poa->local.udp4.ip_addr, + &spec->udp4.ip_addr, + sizeof(spec->udp4.ip_addr)) == 0; + } + + if (poa->local.udp6.port != spec->udp6.port) + return false; + + return memcmp(&poa->local.udp6.ip_addr, &spec->udp6.ip_addr, + sizeof(spec->udp6.ip_addr)) == 0; +} + +static bool udp_addr_is_any(const struct poa_addr * addr) +{ + static const struct in6_addr any6 = IN6ADDR_ANY_INIT; + + if (addr->type == POA_UDP4) + return addr->udp4.ip_addr.s_addr == htonl(INADDR_ANY); + + return memcmp(&addr->udp6.ip_addr, &any6, sizeof(any6)) == 0; +} + +/* + * Our end of the flow: the IP the kernel would send to dst from. A + * PoA bound to the wildcard is the catch-all and a failed probe + * matches any. Ports are not compared: the probe's is ephemeral. + */ +static bool udp_match(const struct poa * poa, + const struct poa_addr * dst) +{ + struct poa_addr src; + + if (udp_addr_is_any(&poa->local)) + return true; + + if (udp_src_addr(dst, &src) < 0) + return true; + + if (poa->type == POA_UDP4) + return memcmp(&poa->local.udp4.ip_addr, + &src.udp4.ip_addr, + sizeof(src.udp4.ip_addr)) == 0; + + return memcmp(&poa->local.udp6.ip_addr, &src.udp6.ip_addr, + sizeof(src.udp6.ip_addr)) == 0; +} + +const struct poa_ops udp_poa_ops = { + .poa_attach = udp_attach, + .poa_detach = udp_detach, + .poa_start = udp_start, + .poa_stop = udp_stop, + .poa_send = udp_send, + .poa_send_mgmt = udp_send_mgmt, + .poa_mtu = udp_mtu, + .poa_qlen = udp_qlen, + .poa_rib = udp_rib, + .poa_spec = udp_spec, + .poa_has_id = udp_has_id, + .poa_match = udp_match, + .mpl = POA_UDP_MPL +}; diff --git a/src/lib/protobuf.c b/src/lib/protobuf.c index a824d357..6eec1159 100644 --- a/src/lib/protobuf.c +++ b/src/lib/protobuf.c @@ -22,6 +22,7 @@ #define _DEFAULT_SOURCE +#include <ouroboros/errno.h> #include <ouroboros/protobuf.h> #include <ouroboros/crypt.h> #include <ouroboros/proc.h> @@ -82,6 +83,7 @@ flow_info_msg_t * flow_info_s_to_msg(const struct flow_info * s) msg->state = s->state; msg->uid = s->uid; msg->mtu = s->mtu; + msg->max_rtt = s->max_rtt; msg->qos = qos_spec_s_to_msg(&s->qs); if (msg->qos == NULL) goto fail_msg; @@ -109,6 +111,7 @@ struct flow_info flow_info_msg_to_s(const flow_info_msg_t * msg) s.state = msg->state; s.uid = msg->uid; s.mtu = msg->mtu; + s.max_rtt = msg->max_rtt; s.qs = qos_spec_msg_to_s(msg->qos); return s; @@ -215,6 +218,166 @@ struct layer_info layer_info_msg_to_s(const layer_info_msg_t * msg) return s; } + +static bool mac_is_zero(const uint8_t * mac) +{ + static const uint8_t zero[POA_MAC_SIZE] = { 0 }; + + return memcmp(mac, zero, POA_MAC_SIZE) == 0; +} + +poa_addr_msg_t * poa_addr_s_to_msg(const struct poa_addr * s) +{ + poa_addr_msg_t * msg; + + assert(s != NULL); + + msg = malloc(sizeof(*msg)); + if (msg == NULL) + goto fail_malloc; + + poa_addr_msg__init(msg); + + msg->type = s->type; + + if (s->hostname[0] != '\0') { + msg->hostname = strdup(s->hostname); + if (msg->hostname == NULL) + goto fail_msg; + } + + switch (s->type) { + case POA_UDP4: + msg->has_ip4 = true; + msg->ip4 = s->udp4.ip_addr.s_addr; + msg->has_port = true; + msg->port = s->udp4.port; + break; + case POA_UDP6: + msg->has_ip6 = true; + msg->ip6.len = sizeof(s->udp6.ip_addr); + msg->ip6.data = malloc(msg->ip6.len); + if (msg->ip6.data == NULL) + goto fail_msg; + memcpy(msg->ip6.data, &s->udp6.ip_addr, msg->ip6.len); + + msg->has_port = true; + msg->port = s->udp6.port; + break; + case POA_ETH: + msg->has_ethertype = true; + msg->ethertype = s->eth.dst.ethertype; + msg->has_mac = true; + msg->mac.len = POA_MAC_SIZE; + msg->mac.data = malloc(POA_MAC_SIZE); + if (msg->mac.data == NULL) + goto fail_msg; + memcpy(msg->mac.data, s->eth.dst.mac, POA_MAC_SIZE); + + if (!mac_is_zero(s->eth.src.mac)) { + msg->has_src_mac = true; + msg->src_mac.len = POA_MAC_SIZE; + msg->src_mac.data = malloc(POA_MAC_SIZE); + if (msg->src_mac.data == NULL) + goto fail_msg; + memcpy(msg->src_mac.data, s->eth.src.mac, POA_MAC_SIZE); + } + + if (s->eth.src.dev[0] != '\0') { + msg->dev = strdup(s->eth.src.dev); + if (msg->dev == NULL) + goto fail_msg; + } + break; + case POA_UDP: + msg->has_port = true; + msg->port = s->udp4.port; + break; + default: + goto fail_msg; + } + + return msg; + + fail_msg: + poa_addr_msg__free_unpacked(msg, NULL); + fail_malloc: + return NULL; +} + +struct poa_addr poa_addr_msg_to_s(const poa_addr_msg_t * msg) +{ + struct poa_addr s; + + assert(msg != NULL); + + memset(&s, 0, sizeof(s)); + + s.type = POA_INVALID; + + if (msg->hostname != NULL) { + if (strlen(msg->hostname) > POA_HOST_STRLEN) + return s; + strcpy(s.hostname, msg->hostname); + } + + switch (msg->type) { + case POA_UDP4: + if (!msg->has_ip4 || !msg->has_port) + return s; + + s.udp4.ip_addr.s_addr = msg->ip4; + s.udp4.port = msg->port; + break; + case POA_UDP6: + if (!msg->has_ip6 || !msg->has_port) + return s; + + if (msg->ip6.len != sizeof(s.udp6.ip_addr) + || msg->ip6.data == NULL) + return s; + + memcpy(&s.udp6.ip_addr, msg->ip6.data, msg->ip6.len); + + s.udp6.port = msg->port; + break; + case POA_ETH: + if (!msg->has_ethertype) + return s; + + s.eth.src.ethertype = msg->ethertype; + s.eth.dst.ethertype = msg->ethertype; + + if (msg->mac.len != POA_MAC_SIZE || msg->mac.data == NULL) + return s; + + memcpy(s.eth.dst.mac, msg->mac.data, POA_MAC_SIZE); + + if (msg->src_mac.len == POA_MAC_SIZE) + memcpy(s.eth.src.mac, msg->src_mac.data, POA_MAC_SIZE); + + if (msg->dev != NULL) { + if (strlen(msg->dev) > DEV_NAME_SIZE) + return s; + strcpy(s.eth.src.dev, msg->dev); + } + break; + case POA_UDP: + if (msg->hostname == NULL || msg->hostname[0] == '\0' + || !msg->has_port) + return s; + + s.udp4.port = msg->port; + break; + default: + return s; + } + + s.type = msg->type; + + return s; +} + ipcp_info_msg_t * ipcp_info_s_to_msg(const struct ipcp_info * s) { ipcp_info_msg_t * msg; @@ -362,6 +525,8 @@ dt_config_msg_t * dt_config_s_to_msg(const struct dt_config * s) msg->addr_size = s->addr_size; msg->eid_size = s->eid_size; msg->max_ttl = s->max_ttl; + msg->has_max_rtt = true; + msg->max_rtt = s->max_rtt; msg->routing = routing_config_s_to_msg(&s->routing); if (msg->routing == NULL) goto fail_routing; @@ -381,27 +546,12 @@ struct dt_config dt_config_msg_to_s(const dt_config_msg_t * msg) s.addr_size = msg->addr_size; s.eid_size = msg->eid_size; s.max_ttl = msg->max_ttl; + s.max_rtt = msg->has_max_rtt ? msg->max_rtt : 200; /* ms */ s.routing = routing_config_msg_to_s(msg->routing); return s; } -struct dir_dht_config dir_dht_config_msg_to_s(const dir_dht_config_msg_t * msg) -{ - struct dir_dht_config s; - - assert(msg != NULL); - - s.params.alpha = msg->alpha; - s.params.k = msg->k; - s.params.t_expire = msg->t_expire; - s.params.t_refresh = msg->t_refresh; - s.params.t_replicate = msg->t_replicate; - s.peer = msg->peer; - - return s; -} - dir_dht_config_msg_t * dir_dht_config_s_to_msg(const struct dir_dht_config * s) { dir_dht_config_msg_t * msg; @@ -424,22 +574,18 @@ dir_dht_config_msg_t * dir_dht_config_s_to_msg(const struct dir_dht_config * s) return msg; } -struct dir_config dir_config_msg_to_s(const dir_config_msg_t * msg) +struct dir_dht_config dir_dht_config_msg_to_s(const dir_dht_config_msg_t * msg) { - struct dir_config s; + struct dir_dht_config s; assert(msg != NULL); - switch (msg->pol) { - case DIR_DHT: - s.dht = dir_dht_config_msg_to_s(msg->dht); - break; - default: - /* No checks here */ - break; - } - - s.pol = msg->pol; + s.params.alpha = msg->alpha; + s.params.k = msg->k; + s.params.t_expire = msg->t_expire; + s.params.t_refresh = msg->t_refresh; + s.params.t_replicate = msg->t_replicate; + s.peer = msg->peer; return s; } @@ -476,6 +622,25 @@ dir_config_msg_t * dir_config_s_to_msg(const struct dir_config * s) return NULL; } +struct dir_config dir_config_msg_to_s(const dir_config_msg_t * msg) +{ + struct dir_config s; + + assert(msg != NULL); + + switch (msg->pol) { + case DIR_DHT: + s.dht = dir_dht_config_msg_to_s(msg->dht); + break; + default: + /* No checks here */ + break; + } + + s.pol = msg->pol; + + return s; +} uni_config_msg_t * uni_config_s_to_msg(const struct uni_config * s) { @@ -522,9 +687,9 @@ struct uni_config uni_config_msg_to_s(const uni_config_msg_t * msg) return s; } -udp4_config_msg_t * udp4_config_s_to_msg(const struct udp4_config * s) +udp4_poa_msg_t * udp4_poa_s_to_msg(const struct udp4_poa * s) { - udp4_config_msg_t * msg; + udp4_poa_msg_t * msg; assert(s != NULL); @@ -532,32 +697,30 @@ udp4_config_msg_t * udp4_config_s_to_msg(const struct udp4_config * s) if (msg == NULL) return NULL; - udp4_config_msg__init(msg); + udp4_poa_msg__init(msg); - msg->ip_addr = s->ip_addr.s_addr; - msg->dns_addr = s->dns_addr.s_addr; - msg->port = s->port; + msg->ip_addr = s->ip_addr.s_addr; + msg->port = s->port; return msg; } -struct udp4_config udp4_config_msg_to_s(const udp4_config_msg_t * msg) +struct udp4_poa udp4_poa_msg_to_s(const udp4_poa_msg_t * msg) { - struct udp4_config s; + struct udp4_poa s; assert(msg != NULL); - s.ip_addr.s_addr = msg->ip_addr; - s.dns_addr.s_addr = msg->dns_addr; - s.port = msg->port; + s.ip_addr.s_addr = msg->ip_addr; + s.port = msg->port; return s; } #define IN6_LEN (size_t) sizeof(struct in6_addr) -udp6_config_msg_t * udp6_config_s_to_msg(const struct udp6_config * s) +udp6_poa_msg_t * udp6_poa_s_to_msg(const struct udp6_poa * s) { - udp6_config_msg_t * msg; + udp6_poa_msg_t * msg; assert(s != NULL); @@ -565,7 +728,7 @@ udp6_config_msg_t * udp6_config_s_to_msg(const struct udp6_config * s) if (msg == NULL) goto fail_malloc; - udp6_config_msg__init(msg); + udp6_poa_msg__init(msg); msg->ip_addr.data = malloc(IN6_LEN); if (msg->ip_addr.data == NULL) @@ -574,42 +737,33 @@ udp6_config_msg_t * udp6_config_s_to_msg(const struct udp6_config * s) msg->ip_addr.len = IN6_LEN; memcpy(msg->ip_addr.data, &s->ip_addr.s6_addr, IN6_LEN); - msg->dns_addr.data = malloc(IN6_LEN); - if (msg->dns_addr.data == NULL) - goto fail_msg; - - msg->dns_addr.len = IN6_LEN; - memcpy(msg->dns_addr.data, &s->dns_addr.s6_addr, IN6_LEN); - msg->port = s->port; return msg; fail_msg: - udp6_config_msg__free_unpacked(msg, NULL); + udp6_poa_msg__free_unpacked(msg, NULL); fail_malloc: return NULL; } -struct udp6_config udp6_config_msg_to_s(const udp6_config_msg_t * msg) +struct udp6_poa udp6_poa_msg_to_s(const udp6_poa_msg_t * msg) { - struct udp6_config s; + struct udp6_poa s; assert(msg != NULL); assert(msg->ip_addr.len == IN6_LEN); - assert(msg->dns_addr.len == IN6_LEN); memcpy(&s.ip_addr.s6_addr, msg->ip_addr.data, IN6_LEN); - memcpy(&s.dns_addr.s6_addr, msg->dns_addr.data, IN6_LEN); s.port = msg->port; return s; } -eth_config_msg_t * eth_config_s_to_msg(const struct eth_config * s) +eth_poa_msg_t * eth_poa_s_to_msg(const struct eth_poa * s) { - eth_config_msg_t * msg; + eth_poa_msg_t * msg; assert(s != NULL); @@ -617,7 +771,7 @@ eth_config_msg_t * eth_config_s_to_msg(const struct eth_config * s) if (msg == NULL) goto fail_malloc; - eth_config_msg__init(msg); + eth_poa_msg__init(msg); msg->dev = strdup(s->dev); if (msg->dev == NULL) @@ -625,28 +779,115 @@ eth_config_msg_t * eth_config_s_to_msg(const struct eth_config * s) msg->ethertype = s->ethertype; + msg->has_mac = true; + msg->mac.len = POA_MAC_SIZE; + msg->mac.data = malloc(POA_MAC_SIZE); + if (msg->mac.data == NULL) + goto fail_msg; + + memcpy(msg->mac.data, s->mac, POA_MAC_SIZE); + return msg; fail_msg: - eth_config_msg__free_unpacked(msg, NULL); + eth_poa_msg__free_unpacked(msg, NULL); fail_malloc: return NULL; } -struct eth_config eth_config_msg_to_s(const eth_config_msg_t * msg) +struct eth_poa eth_poa_msg_to_s(const eth_poa_msg_t * msg) { - struct eth_config s; + struct eth_poa s; assert(msg != NULL); assert(strlen(msg->dev) <= DEV_NAME_SIZE); + memset(&s, 0, sizeof(s)); + strcpy(s.dev, msg->dev); + s.ethertype = msg->ethertype; + if (msg->mac.len == POA_MAC_SIZE) + memcpy(s.mac, msg->mac.data, POA_MAC_SIZE); + return s; } +poa_spec_msg_t * poa_spec_s_to_msg(const struct poa_spec * s) +{ + poa_spec_msg_t * msg; + + assert(s != NULL); + + msg = malloc(sizeof(*msg)); + if (msg == NULL) + goto fail_malloc; + + poa_spec_msg__init(msg); + + switch (s->type) { + case POA_UDP4: + msg->udp4 = udp4_poa_s_to_msg(&s->udp4); + if (msg->udp4 == NULL) + goto fail_msg; + break; + case POA_UDP6: + msg->udp6 = udp6_poa_s_to_msg(&s->udp6); + if (msg->udp6 == NULL) + goto fail_msg; + break; + case POA_ETH: + msg->eth = eth_poa_s_to_msg(&s->eth); + if (msg->eth == NULL) + goto fail_msg; + break; + default: + goto fail_msg; + } + + return msg; + + fail_msg: + poa_spec_msg__free_unpacked(msg, NULL); + fail_malloc: + return NULL; +} + +struct poa_spec poa_spec_msg_to_s(const poa_spec_msg_t * msg) +{ + struct poa_spec s; + + memset(&s, 0, sizeof(s)); + + s.type = POA_INVALID; + + if (msg == NULL) + return s; + + if (msg->udp4 != NULL) { + s.type = POA_UDP4; + s.udp4 = udp4_poa_msg_to_s(msg->udp4); + } else if (msg->udp6 != NULL) { + if (msg->udp6->ip_addr.len != IN6_LEN + || msg->udp6->ip_addr.data == NULL) + return s; + + s.type = POA_UDP6; + s.udp6 = udp6_poa_msg_to_s(msg->udp6); + } else if (msg->eth != NULL) { + if (msg->eth->dev == NULL + || strlen(msg->eth->dev) > DEV_NAME_SIZE) + return s; + + s.type = POA_ETH; + s.eth = eth_poa_msg_to_s(msg->eth); + } + + return s; +} + ipcp_config_msg_t * ipcp_config_s_to_msg(const struct ipcp_config * s) { ipcp_config_msg_t * msg; @@ -669,23 +910,6 @@ ipcp_config_msg_t * ipcp_config_s_to_msg(const struct ipcp_config * s) break; case IPCP_BROADCAST: break; - case IPCP_ETH_LLC: - /* FALLTHRU */ - case IPCP_ETH_DIX: - msg->eth = eth_config_s_to_msg(&s->eth); - if (msg->eth == NULL) - goto fail_msg; - break; - case IPCP_UDP4: - msg->udp4 = udp4_config_s_to_msg(&s->udp4); - if (msg->udp4 == NULL) - goto fail_msg; - break; - case IPCP_UDP6: - msg->udp6 = udp6_config_s_to_msg(&s->udp6); - if (msg->udp6 == NULL) - goto fail_msg; - break; default: /* No checks here */ break; @@ -711,6 +935,8 @@ struct ipcp_config ipcp_config_msg_to_s(const ipcp_config_msg_t * msg) assert(msg != NULL); + memset(&s, 0, sizeof(s)); + s.type = msg->ipcp_type; s.layer_info = layer_info_msg_to_s(msg->layer_info); @@ -721,17 +947,6 @@ struct ipcp_config ipcp_config_msg_to_s(const ipcp_config_msg_t * msg) case IPCP_UNICAST: s.unicast = uni_config_msg_to_s(msg->unicast); break; - case IPCP_ETH_LLC: - /* FALLTHRU */ - case IPCP_ETH_DIX: - s.eth = eth_config_msg_to_s(msg->eth); - break; - case IPCP_UDP4: - s.udp4 = udp4_config_msg_to_s(msg->udp4); - break; - case IPCP_UDP6: - s.udp6 = udp6_config_msg_to_s(msg->udp6); - break; case IPCP_BROADCAST: break; default: diff --git a/src/lib/random.c b/src/lib/random.c index 2c9a6c0d..a132f470 100644 --- a/src/lib/random.c +++ b/src/lib/random.c @@ -28,6 +28,8 @@ #include <stdlib.h> #elif defined(HAVE_SYS_RANDOM) #include <sys/random.h> +#include <errno.h> +#include <stdint.h> #elif defined(HAVE_LIBGCRYPT) #include <gcrypt.h> #elif defined(HAVE_OPENSSL_RNG) @@ -42,7 +44,21 @@ int random_buffer(void * buf, arc4random_buf(buf, len); return 0; #elif defined(HAVE_SYS_RANDOM) - return getrandom(buf, len, GRND_NONBLOCK); + size_t off = 0; + ssize_t ret; + + while (off < len) { + ret = getrandom((uint8_t *) buf + off, len - off, + GRND_NONBLOCK); + if (ret < 0) { + if (errno == EINTR) + continue; + return -1; + } + off += (size_t) ret; + } + + return 0; #elif defined(HAVE_LIBGCRYPT) gcry_randomize(buf, len, GCRY_STRONG_RANDOM); return 0; diff --git a/src/lib/serdes-irm.c b/src/lib/serdes-irm.c index 65f2c02d..20b25a1b 100644 --- a/src/lib/serdes-irm.c +++ b/src/lib/serdes-irm.c @@ -174,6 +174,54 @@ int flow__irm_result_des(buffer_t * buf, else memset(sk->key, 0, SYMMKEYSZ); + sk->epoch = msg->has_generation ? (uint8_t) msg->generation : 0; + + if (msg->sym_key.len == SYMMKEYSZ) + crypt_secure_clear(msg->sym_key.data, msg->sym_key.len); + + irm_msg__free_unpacked(msg, NULL); + + return 0; + fail: + irm_msg__free_unpacked(msg, NULL); + fail_msg: + return err; +} + +int flow_rekey__irm_result_des(buffer_t * buf, + struct crypt_sk * sk, + bool * has_key, + bool * initiator) +{ + irm_msg_t * msg; + int err; + + msg = irm_msg__unpack(NULL, buf->len, buf->data); + if (msg == NULL) { + err = -EIRMD; + goto fail_msg; + } + + if (!msg->has_result) { + err = -EIRMD; + goto fail; + } + + if (msg->result < 0) { + err = msg->result; + goto fail; + } + + *has_key = msg->has_sym_key && msg->sym_key.len == SYMMKEYSZ; + if (*has_key) { + memcpy(sk->key, msg->sym_key.data, SYMMKEYSZ); + sk->nid = NID_undef; + sk->epoch = msg->has_generation ? + (uint8_t) msg->generation : 0; + *initiator = msg->has_rk_initiator && msg->rk_initiator; + crypt_secure_clear(msg->sym_key.data, msg->sym_key.len); + } + irm_msg__free_unpacked(msg, NULL); return 0; @@ -222,6 +270,44 @@ int flow_dealloc__irm_req_ser(buffer_t * buf, return -ENOMEM; } +int flow_update__irm_req_ser(buffer_t * buf, + const struct flow_info * flow, + bool rekey) +{ + irm_msg_t * msg; + size_t len; + + msg = malloc(sizeof(*msg)); + if (msg == NULL) + goto fail_malloc; + + irm_msg__init(msg); + + msg->code = IRM_MSG_CODE__IRM_FLOW_UPDATE; + msg->flow_info = flow_info_s_to_msg(flow); + if (msg->flow_info == NULL) + goto fail_msg; + + msg->has_rekey = true; + msg->rekey = rekey; + + len = irm_msg__get_packed_size(msg); + if (len == 0 || len > buf->len) + goto fail_msg; + + buf->len = len; + + irm_msg__pack(msg, buf->data); + irm_msg__free_unpacked(msg, NULL); + + return 0; + + fail_msg: + irm_msg__free_unpacked(msg, NULL); + fail_malloc: + return -ENOMEM; +} + int ipcp_flow_dealloc__irm_req_ser(buffer_t * buf, const struct flow_info * flow) { @@ -355,6 +441,7 @@ int proc_exit__irm_req_ser(buffer_t * buf) return -ENOMEM; } +/* data is borrowed from the caller; detach before free. */ int ipcp_flow_req_arr__irm_req_ser(buffer_t * buf, const buffer_t * dst, const struct flow_info * flow, @@ -398,6 +485,188 @@ int ipcp_flow_req_arr__irm_req_ser(buffer_t * buf, return 0; fail_msg: + /* hash/pk are borrowed from the caller; detach before free. */ + msg->hash.len = 0; + msg->hash.data = NULL; + msg->pk.len = 0; + msg->pk.data = NULL; + irm_msg__free_unpacked(msg, NULL); + fail_malloc: + return -ENOMEM; +} + +static int __ep_flow_ser(buffer_t * buf, + const struct flow_info * flow, + const buffer_t * data, + int response, + const char * dst, + int code) +{ + irm_msg_t * msg; + size_t len; + int err = -ENOMEM; + + msg = malloc(sizeof(*msg)); + if (msg == NULL) + goto fail_malloc; + + irm_msg__init(msg); + + msg->code = code; + msg->flow_info = flow_info_s_to_msg(flow); + if (msg->flow_info == NULL) + goto fail_msg; + + if (dst != NULL) { + msg->dst = strdup(dst); + if (msg->dst == NULL) + goto fail_msg; + } + + if (data != NULL) { + msg->has_pk = true; + msg->pk.len = data->len; + msg->pk.data = data->data; + } + + msg->has_response = true; + msg->response = response; + + len = irm_msg__get_packed_size(msg); + if (len == 0 || len > buf->len) + goto fail_msg; + + buf->len = len; + + irm_msg__pack(msg, buf->data); + + err = 0; + fail_msg: + msg->pk.len = 0; + msg->pk.data = NULL; + + irm_msg__free_unpacked(msg, NULL); + fail_malloc: + return err; +} + +int poa_flow_alloc__irm_req_ser(buffer_t * buf, + const struct flow_info * flow, + const char * dst) +{ + return __ep_flow_ser(buf, flow, NULL, 0, dst, + IRM_MSG_CODE__IRM_POA_FLOW_ALLOC); +} + +int poa_flow_alloc_r__irm_req_ser(buffer_t * buf, + const struct flow_info * flow, + const buffer_t * data, + int response) +{ + return __ep_flow_ser(buf, flow, data, response, NULL, + IRM_MSG_CODE__IRM_POA_FLOW_ALLOC_R); +} + +int ipcp_poa_flow_req_arr__irm_req_ser(buffer_t * buf, + const struct flow_info * flow, + const buffer_t * data) +{ + return __ep_flow_ser(buf, flow, data, 0, NULL, + IRM_MSG_CODE__IPCP_POA_FLOW_REQ_ARR); +} + +int poa_flow__irm_result_des(buffer_t * buf, + struct flow_info * flow, + buffer_t * data) +{ + irm_msg_t * msg; + int err; + + msg = irm_msg__unpack(NULL, buf->len, buf->data); + if (msg == NULL) { + err = -EIRMD; + goto fail_msg; + } + + if (!msg->has_result) { + err = -EIRMD; + goto fail; + } + + if (msg->result < 0) { + err = msg->result; + goto fail; + } + + if (msg->flow_info == NULL) { + err = -EBADF; + goto fail; + } + + *flow = flow_info_msg_to_s(msg->flow_info); + + clrbuf(*data); + + if (msg->has_pk && msg->pk.len > 0) { + data->data = malloc(msg->pk.len); + if (data->data == NULL) { + err = -ENOMEM; + goto fail; + } + memcpy(data->data, msg->pk.data, msg->pk.len); + + data->len = msg->pk.len; + } + + irm_msg__free_unpacked(msg, NULL); + + return 0; + fail: + irm_msg__free_unpacked(msg, NULL); + fail_msg: + return err; +} + +int ipcp_flow_update_arr__irm_req_ser(buffer_t * buf, + const struct flow_info * flow, + const buffer_t * data) +{ + irm_msg_t * msg; + size_t len; + + msg = malloc(sizeof(*msg)); + if (msg == NULL) + goto fail_malloc; + + irm_msg__init(msg); + + msg->code = IRM_MSG_CODE__IPCP_FLOW_UPDATE_ARR; + msg->flow_info = flow_info_s_to_msg(flow); + if (msg->flow_info == NULL) + goto fail_msg; + + msg->has_pk = true; + msg->pk.len = data->len; + msg->pk.data = data->data; + + len = irm_msg__get_packed_size(msg); + if (len == 0 || len > buf->len) + goto fail_msg; + + buf->len = len; + + irm_msg__pack(msg, buf->data); + + /* Don't free data! */ + msg->pk.len = 0; + msg->pk.data = NULL; + irm_msg__free_unpacked(msg, NULL); + + return 0; + fail_msg: + /* pk.data is borrowed from the caller; detach before free. */ + msg->pk.len = 0; + msg->pk.data = NULL; irm_msg__free_unpacked(msg, NULL); fail_malloc: return -ENOMEM; diff --git a/src/lib/ssm/flow_set.c b/src/lib/ssm/flow_set.c index cb38e6fd..2e33b408 100644 --- a/src/lib/ssm/flow_set.c +++ b/src/lib/ssm/flow_set.c @@ -299,26 +299,34 @@ void ssm_flow_set_notify(struct ssm_flow_set * set, int event) { struct flowevent * e; + ssize_t idx; assert(set); assert(!(flow_id < 0) && flow_id < SYS_MAX_FLOWS); pthread_mutex_lock(set->lock); - if (set->mtable[flow_id] == -1) { + idx = set->mtable[flow_id]; + if (idx == -1) { pthread_mutex_unlock(set->lock); return; } - e = fqueue_ptr(set, set->mtable[flow_id]) + - set->heads[set->mtable[flow_id]]; + /* Ring full: drop redundant FLOW_PKT, reserve a slot for ctrl. */ + if (set->heads[idx] >= SSM_RBUFF_SIZE + || (event == FLOW_PKT && set->heads[idx] >= SSM_RBUFF_SIZE - 1)) { + pthread_mutex_unlock(set->lock); + return; + } + + e = fqueue_ptr(set, idx) + set->heads[idx]; e->flow_id = flow_id; e->event = event; - ++set->heads[set->mtable[flow_id]]; + ++set->heads[idx]; - pthread_cond_signal(&set->conds[set->mtable[flow_id]]); + pthread_cond_signal(&set->conds[idx]); pthread_mutex_unlock(set->lock); } diff --git a/src/lib/ssm/pool.c b/src/lib/ssm/pool.c index 5607a360..705de147 100644 --- a/src/lib/ssm/pool.c +++ b/src/lib/ssm/pool.c @@ -38,10 +38,20 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> #include <unistd.h> #include <sys/mman.h> #include <sys/stat.h> +static __inline__ uint64_t pool_now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return (uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec; +} + /* Global Shared Packet Pool (GSPP) configuration */ static const struct ssm_size_class_cfg ssm_gspp_cfg[SSM_POOL_MAX_CLASSES] = { { (1 << 8), SSM_GSPP_256_BLOCKS }, @@ -236,6 +246,7 @@ static void init_size_classes(struct ssm_pool * pool) STORE(&blk->refcount, 0); blk->allocator_pid = 0; + blk->alloc_ts = 0; STORE(&blk->next_offset, 0); list_add_head(&sc->shards[0].free_list, blk, @@ -266,19 +277,31 @@ static size_t reclaim_pid_from_sc(struct _ssm_size_class * sc, size_t i; size_t recovered = 0; struct ssm_pk_buff * blk; + uint64_t now; + uint64_t min_age_ns; - region = (uint8_t *) pool_base + sc->pool_start; + region = (uint8_t *) pool_base + sc->pool_start; + now = pool_now_ns(); + min_age_ns = (uint64_t) SSM_POOL_RECLAIM_AGE_S * 1000000000ULL; for (i = 0; i < sc->object_count; ++i) { blk = (struct ssm_pk_buff *)(region + i * sc->object_size); - if (blk->allocator_pid == pid && LOAD(&blk->refcount) > 0) { - STORE(&blk->refcount, 0); - blk->allocator_pid = 0; - list_add_head(&shard->free_list, blk, pool_base); - FETCH_ADD(&shard->free_count, 1); - recovered++; - } + if (blk->allocator_pid != pid) + continue; + + if (LOAD(&blk->refcount) == 0) + continue; + + /* Recent: a live consumer may still hold the handoff. */ + if (now - blk->alloc_ts < min_age_ns) + continue; + + STORE(&blk->refcount, 0); + blk->allocator_pid = 0; + list_add_head(&shard->free_list, blk, pool_base); + FETCH_ADD(&shard->free_count, 1); + recovered++; } return recovered; @@ -339,6 +362,7 @@ static __inline__ ssize_t init_block(struct ssm_pool * pool, { STORE(&blk->refcount, 1); blk->allocator_pid = getpid(); + blk->alloc_ts = pool_now_ns(); blk->size = (uint32_t) (sc->object_size - sizeof(struct ssm_pk_buff)); blk->pk_head = SSM_PK_BUFF_HEADSPACE; diff --git a/src/lib/ssm/rbuff.c b/src/lib/ssm/rbuff.c index c149c306..0480bce1 100644 --- a/src/lib/ssm/rbuff.c +++ b/src/lib/ssm/rbuff.c @@ -27,6 +27,7 @@ #include <ouroboros/ssm_rbuff.h> #include <ouroboros/lockfile.h> +#include <ouroboros/atomics.h> #include <ouroboros/errno.h> #include <ouroboros/fccntl.h> #include <ouroboros/pthread.h> @@ -53,11 +54,6 @@ #define MODB(x) ((x) & (SSM_RBUFF_SIZE - 1)) -#define LOAD_RELAXED(ptr) (__atomic_load_n(ptr, __ATOMIC_RELAXED)) -#define LOAD_ACQUIRE(ptr) (__atomic_load_n(ptr, __ATOMIC_ACQUIRE)) -#define STORE_RELEASE(ptr, val) \ - (__atomic_store_n(ptr, val, __ATOMIC_RELEASE)) - #define HEAD(rb) (rb->shm_base[LOAD_RELAXED(rb->head)]) #define TAIL(rb) (rb->shm_base[LOAD_RELAXED(rb->tail)]) #define HEAD_IDX(rb) (LOAD_ACQUIRE(rb->head)) @@ -67,20 +63,45 @@ #define ADVANCE_TAIL(rb) \ (STORE_RELEASE(rb->tail, MODB(LOAD_RELAXED(rb->tail) + 1))) #define QUEUED(rb) (MODB(HEAD_IDX(rb) - TAIL_IDX(rb))) -#define IS_FULL(rb) (QUEUED(rb) == (SSM_RBUFF_SIZE - 1)) #define IS_EMPTY(rb) (HEAD_IDX(rb) == TAIL_IDX(rb)) + +/* Delay-bound the TX queue delay at rate * target. */ +#define TXQ_MIN_SLOTS 4 /* floor: jitter margin */ +#define TXQ_INIT_SLOTS 64 /* ceiling until measured */ +#define TXQ_EWMA_N 4 /* EWMA weight 1/4 */ +#define TXQ_SPW_SHIFT 3 /* aim: 8 samples per window */ +#define TXQ_PERIOD_INIT 16 /* writes between samples */ +#define TXQ_PERIOD_MIN 4 +#define TXQ_PERIOD_MAX 64 +#define TXQ_MIN_DT_NS 1000LL /* shorter windows are noise */ +#define TXQ_MAX_RATE BILLION /* keeps rate * target in s64 */ +#define TXQ_UNLIMITED (SSM_RBUFF_SIZE - 1) +#define TXQ_DATA_MAX (TXQ_UNLIMITED - SSM_RBUFF_TXQ_RESERVE) + struct ssm_rbuff { ssize_t * shm_base; /* start of shared memory */ size_t * head; /* start of ringbuffer */ size_t * tail; - size_t * acl; /* access control */ + size_t * flags; /* out-of-band flags (RB_*) */ pthread_mutex_t * mtx; /* lock for cond vars only */ pthread_cond_t * add; /* signal when new data */ pthread_cond_t * del; /* signal when data removed */ pid_t pid; /* pid of the owner */ int flow_id; /* flow_id of the flow */ size_t n_users; /* in-flight users */ + struct { + uint64_t target; /* target queue delay, ns */ + uint64_t rate; /* EWMA drain rate, slots/s */ + uint64_t ns; /* window start, 0 = unset */ + size_t limit; /* current occupancy limit */ + size_t wr; /* writes this window */ + size_t due; /* sample when wr hits this */ + size_t period; /* writes between samples */ + size_t q0; /* queued at window start */ + bool idle; /* ring ran empty this one */ + bool measured; /* rate holds a measurement */ + } txq; /* tx delay limiter state */ }; #define MM_FLAGS (PROT_READ | PROT_WRITE) @@ -114,18 +135,29 @@ static struct ssm_rbuff * rbuff_create(pid_t pid, rb->shm_base = shm_base; rb->head = (size_t *) (rb->shm_base + (SSM_RBUFF_SIZE)); rb->tail = (size_t *) (rb->head + 1); - rb->acl = (size_t *) (rb->tail + 1); - rb->mtx = (pthread_mutex_t *) (rb->acl + 1); + rb->flags = (size_t *) (rb->tail + 1); + rb->mtx = (pthread_mutex_t *) (rb->flags + 1); rb->add = (pthread_cond_t *) (rb->mtx + 1); rb->del = rb->add + 1; rb->pid = pid; rb->flow_id = flow_id; rb->n_users = 0; + rb->txq.target = 0; + rb->txq.rate = 0; + rb->txq.ns = 0; + rb->txq.limit = TXQ_INIT_SLOTS; + rb->txq.wr = 0; + rb->txq.due = TXQ_PERIOD_INIT; + rb->txq.period = TXQ_PERIOD_INIT; + rb->txq.q0 = 0; + rb->txq.idle = false; + rb->txq.measured = false; return rb; fail_truncate: close(fd); + if (flags & O_CREAT) shm_unlink(fn); fail_open: @@ -158,30 +190,30 @@ struct ssm_rbuff * ssm_rbuff_create(pid_t pid, if (rb == NULL) goto fail_rb; - if (pthread_mutexattr_init(&mattr)) + if (pthread_mutexattr_init(&mattr) != 0) goto fail_mattr; pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); #ifdef HAVE_ROBUST_MUTEX pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST); #endif - if (pthread_mutex_init(rb->mtx, &mattr)) + if (pthread_mutex_init(rb->mtx, &mattr) != 0) goto fail_mutex; - if (pthread_condattr_init(&cattr)) + if (pthread_condattr_init(&cattr) != 0) goto fail_cattr; pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED); #ifndef __APPLE__ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK); #endif - if (pthread_cond_init(rb->add, &cattr)) + if (pthread_cond_init(rb->add, &cattr) != 0) goto fail_add; - if (pthread_cond_init(rb->del, &cattr)) + if (pthread_cond_init(rb->del, &cattr) != 0) goto fail_del; - *rb->acl = ACL_RDWR; + *rb->flags = RB_RDWR; *rb->head = 0; *rb->tail = 0; @@ -230,44 +262,231 @@ void ssm_rbuff_close(struct ssm_rbuff * rb) { assert(rb); - /* - * Caller must set ACL_FLOWDOWN first; if a user becomes - * cancellable, push a cleanup that decrements n_users. - */ - while (__atomic_load_n(&rb->n_users, __ATOMIC_SEQ_CST) > 0) { - struct timespec tic = { 0, 100000 }; + while (LOAD(&rb->n_users) > 0) { + struct timespec tic = TIMESPEC_INIT_US(100); + nanosleep(&tic, NULL); } rbuff_destroy(rb); } -int ssm_rbuff_write(struct ssm_rbuff * rb, - size_t off) +/* Cancel cleanup for a blocked reader: unlock mtx AND drop the n_users ref. */ +static void __cleanup_rbuff_reader(void * o) +{ + struct ssm_rbuff * rb = (struct ssm_rbuff *) o; + + pthread_mutex_unlock(rb->mtx); + FETCH_SUB(&rb->n_users, 1); +} + +static bool txq_is_on(struct ssm_rbuff * rb) +{ + return LOAD_RELAXED(&rb->txq.target) != 0; +} + +/* Occupancy that holds the delay at the target; rate 0 gets the floor. */ +static size_t rbuff_txq_slots(uint64_t rate, + uint64_t target) +{ + uint64_t slots; + + slots = rate * target / BILLION; + if (slots < TXQ_MIN_SLOTS) + return TXQ_MIN_SLOTS; + + return slots > TXQ_UNLIMITED ? TXQ_UNLIMITED : (size_t) slots; +} + +/* Ceiling for one write (taking into account priority). */ +static size_t rbuff_txq_ceiling(struct ssm_rbuff * rb, + bool prio) +{ + size_t lim; + size_t max; + + if (!txq_is_on(rb)) + return TXQ_UNLIMITED; + + if (!rb->txq.measured) + lim = TXQ_INIT_SLOTS; + else + lim = LOAD_RELAXED(&rb->txq.limit); + + max = TXQ_DATA_MAX; + + if (prio) { + lim *= SSM_RBUFF_TXQ_PRIO_MUL; + max = TXQ_UNLIMITED; + } + + return lim > max ? max : lim; +} + +/* Opens a measurement window at now_ns. Caller holds rb->mtx. */ +static void rbuff_txq_anchor(struct ssm_rbuff * rb, + uint64_t now_ns, + size_t queued) +{ + rb->txq.ns = now_ns; + rb->txq.q0 = queued; + rb->txq.wr = 0; + rb->txq.due = rb->txq.period; + rb->txq.idle = false; +} + +/* Enough dequeues to resolve a rate? */ +static bool txq_is_blind(struct ssm_rbuff * rb, + int64_t drained, + int64_t dt_ns) +{ + int64_t target = (int64_t) LOAD_RELAXED(&rb->txq.target); + + if (drained * 2 >= (int64_t) rb->txq.period) + return false; + + return dt_ns < (target >> TXQ_SPW_SHIFT); +} + +/* Leaves the window open and retries a period later. */ +static void rbuff_txq_defer(struct ssm_rbuff * rb) +{ + rb->txq.due = rb->txq.wr + rb->txq.period; +} + +/* Aims the sample period at 1 << TXQ_SPW_SHIFT per target window. */ +static size_t rbuff_txq_retune(size_t period, + int64_t dt_ns, + int64_t target) +{ + if (dt_ns > (target >> TXQ_SPW_SHIFT)) { + period /= 2; + return period < TXQ_PERIOD_MIN ? TXQ_PERIOD_MIN : period; + } + + if (dt_ns < (target >> (TXQ_SPW_SHIFT + 1))) { + period *= 2; + return period > TXQ_PERIOD_MAX ? TXQ_PERIOD_MAX : period; + } + + return period; +} + +/* Only raise the estimate if the window ran empty. Call holding rb->mtx. */ +static void rbuff_txq_sample(struct ssm_rbuff * rb, + size_t queued) +{ + struct timespec now; + uint64_t now_ns; + int64_t dt_ns; + int64_t target; + int64_t drained; + int64_t sample; + int64_t rate; + size_t limit; + size_t was; + + clock_gettime(PTHREAD_COND_CLOCK, &now); + + now_ns = TS_TO_UINT64(now); + + dt_ns = (int64_t) (now_ns - rb->txq.ns); + if (rb->txq.ns == 0 || dt_ns < 0) { + rbuff_txq_anchor(rb, now_ns, queued); + return; + } + + if (dt_ns < TXQ_MIN_DT_NS) { + rbuff_txq_defer(rb); + return; + } + + target = (int64_t) LOAD_RELAXED(&rb->txq.target); + drained = (int64_t) rb->txq.wr + (int64_t) rb->txq.q0 + - (int64_t) queued; + assert(drained >= 0); + + sample = drained * BILLION / dt_ns; + rate = (int64_t) rb->txq.rate; + if (sample > rate && txq_is_blind(rb, drained, dt_ns)) { + rbuff_txq_defer(rb); + return; + } + + if (!rb->txq.measured) + rate = sample; + else if (rb->txq.idle && queued <= TXQ_MIN_SLOTS) + rate = sample > rate ? sample : rate; + else + rate = (rate * (TXQ_EWMA_N - 1) + sample) / TXQ_EWMA_N; + + if (rate > TXQ_MAX_RATE) + rate = TXQ_MAX_RATE; + + limit = rbuff_txq_slots((uint64_t) rate, (uint64_t) target); + was = rbuff_txq_ceiling(rb, false); + + rb->txq.rate = (uint64_t) rate; + rb->txq.period = rbuff_txq_retune(rb->txq.period, dt_ns, target); + + STORE_RELAXED(&rb->txq.limit, limit); + STORE_RELAXED(&rb->txq.measured, true); + + if (rbuff_txq_ceiling(rb, false) > was) + pthread_cond_broadcast(rb->del); + + rbuff_txq_anchor(rb, now_ns, queued); +} + +/* + * Counts one enqueue. A prio write triggers no sample: it is the only + * traffic left in a stall, and would shrink the ceiling it needs. + */ +static void rbuff_txq_touch(struct ssm_rbuff * rb, + bool was_empty, + bool prio) +{ + ++rb->txq.wr; + + if (was_empty) + rb->txq.idle = true; + + if (prio) + return; + + if (rb->txq.wr >= rb->txq.due) + rbuff_txq_sample(rb, QUEUED(rb)); +} + +/* prio outranks new data up to its own, higher, ceiling. */ +static int rbuff_write_nb(struct ssm_rbuff * rb, + size_t off, + bool prio) { - size_t acl; + size_t flags; bool was_empty; int ret = 0; assert(rb != NULL); - __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_ADD(&rb->n_users, 1); - acl = __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); - if (acl != ACL_RDWR) { - if (acl & ACL_FLOWDOWN) { + flags = LOAD(rb->flags); + if (flags != RB_RDWR) { + if (flags & RB_FLOWDOWN) { ret = -EFLOWDOWN; - goto fail_acl; + goto fail_flags; } - if (acl & ACL_RDONLY) { + + if (!(flags & RB_WR)) { ret = -ENOTALLOC; - goto fail_acl; + goto fail_flags; } } robust_mutex_lock(rb->mtx); - if (IS_FULL(rb)) { + if (QUEUED(rb) >= rbuff_txq_ceiling(rb, prio)) { ret = -EAGAIN; goto fail_mutex; } @@ -275,91 +494,126 @@ int ssm_rbuff_write(struct ssm_rbuff * rb, was_empty = IS_EMPTY(rb); HEAD(rb) = (ssize_t) off; + ADVANCE_HEAD(rb); if (was_empty) pthread_cond_broadcast(rb->add); + if (txq_is_on(rb)) + rbuff_txq_touch(rb, was_empty, prio); + pthread_mutex_unlock(rb->mtx); - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_SUB(&rb->n_users, 1); + return 0; fail_mutex: pthread_mutex_unlock(rb->mtx); - fail_acl: - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + fail_flags: + FETCH_SUB(&rb->n_users, 1); return ret; } +int ssm_rbuff_write(struct ssm_rbuff * rb, + size_t off) +{ + return rbuff_write_nb(rb, off, false); +} + +/* For a packet the peer is already waiting on; skips the limit. */ +int ssm_rbuff_write_prio(struct ssm_rbuff * rb, + size_t off) +{ + return rbuff_write_nb(rb, off, true); +} + int ssm_rbuff_write_b(struct ssm_rbuff * rb, size_t off, const struct timespec * abstime) { - size_t acl; + size_t flags; int ret = 0; + int err; bool was_empty; assert(rb != NULL); - __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_ADD(&rb->n_users, 1); - acl = __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); - if (acl != ACL_RDWR) { - if (acl & ACL_FLOWDOWN) { + flags = LOAD(rb->flags); + if (flags != RB_RDWR) { + if (flags & RB_FLOWDOWN) { ret = -EFLOWDOWN; - goto fail_acl; + goto fail_flags; } - if (acl & ACL_RDONLY) { + + if (!(flags & RB_WR)) { ret = -ENOTALLOC; - goto fail_acl; + goto fail_flags; } } robust_mutex_lock(rb->mtx); - pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx); + pthread_cleanup_push(__cleanup_rbuff_reader, rb); - while (IS_FULL(rb) && ret != -ETIMEDOUT) { - acl = __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); - if (acl & ACL_FLOWDOWN) { + while (QUEUED(rb) >= rbuff_txq_ceiling(rb, false)) { + flags = LOAD(rb->flags); + if (flags & RB_FLOWDOWN) { ret = -EFLOWDOWN; break; } - ret = -robust_wait(rb->del, rb->mtx, abstime); + + err = robust_wait(rb->del, rb->mtx, abstime); + if (err == EOWNERDEAD) + continue; + + if (err != 0) { + ret = -err; + break; + } } pthread_cleanup_pop(false); - if (ret != -ETIMEDOUT && ret != -EFLOWDOWN) { + if (ret == 0) { was_empty = IS_EMPTY(rb); HEAD(rb) = (ssize_t) off; + ADVANCE_HEAD(rb); + if (was_empty) pthread_cond_broadcast(rb->add); + + if (txq_is_on(rb)) + rbuff_txq_touch(rb, was_empty, false); } pthread_mutex_unlock(rb->mtx); - fail_acl: - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + fail_flags: + FETCH_SUB(&rb->n_users, 1); return ret; } -static int check_rb_acl(struct ssm_rbuff * rb) +static int check_rb_flags(struct ssm_rbuff * rb) { - size_t acl; + size_t flags; assert(rb != NULL); - acl = __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); - - if (acl & ACL_FLOWDOWN) + flags = LOAD(rb->flags); + if (flags & RB_FLOWDOWN) return -EFLOWDOWN; - if (acl & ACL_FLOWPEER) + if (flags & RB_FLOWPEER) return -EFLOWPEER; + if (!(flags & RB_RD)) + return -ENOTALLOC; + return -EAGAIN; } @@ -369,10 +623,10 @@ ssize_t ssm_rbuff_read(struct ssm_rbuff * rb) assert(rb != NULL); - __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_ADD(&rb->n_users, 1); if (IS_EMPTY(rb)) { - ret = check_rb_acl(rb); + ret = check_rb_flags(rb); goto out; } @@ -380,11 +634,13 @@ ssize_t ssm_rbuff_read(struct ssm_rbuff * rb) if (IS_EMPTY(rb)) { pthread_mutex_unlock(rb->mtx); - ret = check_rb_acl(rb); + + ret = check_rb_flags(rb); goto out; } ret = TAIL(rb); + ADVANCE_TAIL(rb); pthread_cond_broadcast(rb->del); @@ -392,7 +648,8 @@ ssize_t ssm_rbuff_read(struct ssm_rbuff * rb) pthread_mutex_unlock(rb->mtx); out: - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_SUB(&rb->n_users, 1); + return ret; } @@ -400,25 +657,29 @@ ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb, const struct timespec * abstime) { ssize_t idx = -1; - size_t acl; + size_t flags; assert(rb != NULL); - __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_ADD(&rb->n_users, 1); - acl = __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); - if (IS_EMPTY(rb) && (acl & ACL_FLOWDOWN)) { + flags = LOAD(rb->flags); + if (IS_EMPTY(rb) && (flags & RB_FLOWDOWN)) { idx = -EFLOWDOWN; goto out; } robust_mutex_lock(rb->mtx); - pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx); + pthread_cleanup_push(__cleanup_rbuff_reader, rb); + + while (IS_EMPTY(rb)) { + if (idx == -ETIMEDOUT) + break; + + if (check_rb_flags(rb) != -EAGAIN) + break; - while (IS_EMPTY(rb) && - idx != -ETIMEDOUT && - check_rb_acl(rb) == -EAGAIN) { idx = -robust_wait(rb->add, rb->mtx, abstime); } @@ -426,10 +687,11 @@ ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb, if (!IS_EMPTY(rb)) { idx = TAIL(rb); + ADVANCE_TAIL(rb); pthread_cond_broadcast(rb->del); } else if (idx != -ETIMEDOUT) { - idx = check_rb_acl(rb); + idx = check_rb_flags(rb); } pthread_mutex_unlock(rb->mtx); @@ -437,45 +699,114 @@ ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb, assert(idx != -EAGAIN); out: - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_SUB(&rb->n_users, 1); return idx; } -void ssm_rbuff_set_acl(struct ssm_rbuff * rb, - uint32_t flags) +void ssm_rbuff_set_flags(struct ssm_rbuff * rb, + uint32_t flags) { assert(rb != NULL); robust_mutex_lock(rb->mtx); - __atomic_store_n(rb->acl, (size_t) flags, __ATOMIC_SEQ_CST); + + FETCH_OR(rb->flags, (size_t) flags); pthread_cond_broadcast(rb->add); pthread_cond_broadcast(rb->del); + pthread_mutex_unlock(rb->mtx); } -uint32_t ssm_rbuff_get_acl(struct ssm_rbuff * rb) +void ssm_rbuff_clr_flags(struct ssm_rbuff * rb, + uint32_t flags) { assert(rb != NULL); - return (uint32_t) __atomic_load_n(rb->acl, __ATOMIC_SEQ_CST); + robust_mutex_lock(rb->mtx); + + FETCH_AND(rb->flags, ~(size_t) flags); + pthread_cond_broadcast(rb->add); + pthread_cond_broadcast(rb->del); + + pthread_mutex_unlock(rb->mtx); +} + +uint32_t ssm_rbuff_get_flags(struct ssm_rbuff * rb) +{ + assert(rb != NULL); + + return (uint32_t) LOAD(rb->flags); +} + +/* Current occupancy limit; SSM_RBUFF_SIZE - 1 when unlimited. */ +size_t ssm_rbuff_get_limit(struct ssm_rbuff * rb) +{ + assert(rb != NULL); + + return rbuff_txq_ceiling(rb, false); +} + +/* Wakes up writers because target may have changed. */ +void ssm_rbuff_set_txq_target(struct ssm_rbuff * rb, + const struct timespec * ts) +{ + uint64_t target; + size_t limit; + + assert(rb != NULL); + assert(ts != NULL); + assert(ts->tv_sec >= 0); + assert(ts->tv_nsec >= 0); + assert(ts->tv_nsec < BILLION); + + target = TS_TO_UINT64(*ts); + + assert(target <= SSM_RBUFF_TXQ_MAX_DELAY); + + robust_mutex_lock(rb->mtx); + + limit = rbuff_txq_slots(rb->txq.rate, target); + + rb->txq.period = TXQ_PERIOD_INIT; + + rbuff_txq_anchor(rb, 0, QUEUED(rb)); + + STORE_RELAXED(&rb->txq.limit, limit); + STORE_RELAXED(&rb->txq.target, target); + + pthread_cond_broadcast(rb->del); + + pthread_mutex_unlock(rb->mtx); +} + +/* Current target queueing delay for the tx occupancy limiter. */ +void ssm_rbuff_get_txq_target(struct ssm_rbuff * rb, + struct timespec * ts) +{ + assert(rb != NULL); + assert(ts != NULL); + + UINT64_TO_TS(LOAD_RELAXED(&rb->txq.target), ts); } void ssm_rbuff_fini(struct ssm_rbuff * rb) { assert(rb != NULL); - __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_ADD(&rb->n_users, 1); robust_mutex_lock(rb->mtx); - pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx); + pthread_cleanup_push(__cleanup_rbuff_reader, rb); while (!IS_EMPTY(rb)) robust_wait(rb->del, rb->mtx, NULL); - pthread_cleanup_pop(true); + pthread_cleanup_pop(false); + + pthread_mutex_unlock(rb->mtx); - __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST); + FETCH_SUB(&rb->n_users, 1); } size_t ssm_rbuff_queued(struct ssm_rbuff * rb) diff --git a/src/lib/ssm/ssm.h.in b/src/lib/ssm/ssm.h.in index b86327a1..a17c8edd 100644 --- a/src/lib/ssm/ssm.h.in +++ b/src/lib/ssm/ssm.h.in @@ -39,6 +39,8 @@ #define SSM_FLOW_SET_PREFIX "@SSM_FLOW_SET_PREFIX@" #define SSM_POOL_NAME "@SSM_POOL_NAME@" #define SSM_RBUFF_SIZE @SSM_RBUFF_SIZE@ +#define SSM_RBUFF_TXQ_PRIO_MUL @SSM_RBUFF_TXQ_PRIO_MUL@ +#define SSM_RBUFF_TXQ_RESERVE @SSM_RBUFF_TXQ_RESERVE@ /* Packet buffer space reservation */ #define SSM_PK_BUFF_HEADSPACE @SSM_PK_BUFF_HEADSPACE@ @@ -83,6 +85,7 @@ /* Size class configuration */ #define SSM_POOL_MAX_CLASSES 9 #define SSM_POOL_SHARDS @SSM_POOL_SHARDS@ +#define SSM_POOL_RECLAIM_AGE_S @SSM_POOL_RECLAIM_AGE_S@ /* Internal structures - exposed for testing */ #ifdef __cplusplus @@ -125,6 +128,7 @@ struct ssm_pk_buff { uint32_t pk_head; /* Head offset into data */ uint32_t pk_tail; /* Tail offset into data */ uint32_t off; /* Block offset in pool */ + uint64_t alloc_ts; /* CLOCK_MONOTONIC ns at alloc */ uint8_t data[]; /* Packet data */ }; diff --git a/src/lib/ssm/tests/pool_test.c b/src/lib/ssm/tests/pool_test.c index 0f9db24d..f86fbd9e 100644 --- a/src/lib/ssm/tests/pool_test.c +++ b/src/lib/ssm/tests/pool_test.c @@ -956,6 +956,8 @@ static int test_ssm_pool_reclaim_orphans(void) ssize_t ret3; pid_t my_pid; pid_t fake_pid = 99999; + struct timespec now; + uint64_t old_ts; TEST_START(); @@ -976,9 +978,15 @@ static int test_ssm_pool_reclaim_orphans(void) goto fail_alloc; } - /* Simulate blocks from another process by changing allocator_pid */ + /* Simulate blocks leaked by a dead process: foreign pid, aged out. */ + clock_gettime(CLOCK_MONOTONIC, &now); + old_ts = ((uint64_t) now.tv_sec - (SSM_POOL_RECLAIM_AGE_S + 1)) + * 1000000000ULL + (uint64_t) now.tv_nsec; + spb1->allocator_pid = fake_pid; spb2->allocator_pid = fake_pid; + spb1->alloc_ts = old_ts; + spb2->alloc_ts = old_ts; /* Keep spb3 with our pid */ /* Reclaim orphans from fake_pid */ diff --git a/src/lib/ssm/tests/rbuff_test.c b/src/lib/ssm/tests/rbuff_test.c index 58cb39c3..b7ef3dfb 100644 --- a/src/lib/ssm/tests/rbuff_test.c +++ b/src/lib/ssm/tests/rbuff_test.c @@ -34,6 +34,10 @@ #include <ouroboros/errno.h> #include <ouroboros/time.h> +/* Mirrors TXQ_MIN_SLOTS in ssm/rbuff.c; keep in sync. */ +#define FLOOR_SLOTS 4 +#define CEIL_SLOTS (SSM_RBUFF_SIZE - 1 - SSM_RBUFF_TXQ_RESERVE) + #include <errno.h> #include <stdio.h> #include <unistd.h> @@ -54,6 +58,7 @@ static int test_ssm_rbuff_create_destroy(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail: @@ -100,6 +105,7 @@ static int test_ssm_rbuff_write_read(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: @@ -131,6 +137,7 @@ static int test_ssm_rbuff_read_empty(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: @@ -160,6 +167,7 @@ static int test_ssm_rbuff_fill_drain(void) i, ssm_rbuff_queued(rb)); goto fail_rb; } + if (ssm_rbuff_write(rb, i) < 0) { printf("Failed to write at index %zu.\n", i); goto fail_rb; @@ -195,21 +203,23 @@ static int test_ssm_rbuff_fill_drain(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: while (ssm_rbuff_read(rb) >= 0) ; + ssm_rbuff_destroy(rb); fail: TEST_FAIL(); return TEST_RC_FAIL; } -static int test_ssm_rbuff_acl(void) +static int test_ssm_rbuff_flags(void) { struct ssm_rbuff * rb; - uint32_t acl; + uint32_t flags; TEST_START(); @@ -219,16 +229,17 @@ static int test_ssm_rbuff_acl(void) goto fail; } - acl = ssm_rbuff_get_acl(rb); - if (acl != ACL_RDWR) { - printf("Expected ACL_RDWR, got %u.\n", acl); + flags = ssm_rbuff_get_flags(rb); + if (flags != RB_RDWR) { + printf("Expected RB_RDWR, got %u.\n", flags); goto fail_rb; } - ssm_rbuff_set_acl(rb, ACL_RDONLY); - acl = ssm_rbuff_get_acl(rb); - if (acl != ACL_RDONLY) { - printf("Expected ACL_RDONLY, got %u.\n", acl); + ssm_rbuff_clr_flags(rb, RB_WR); + + flags = ssm_rbuff_get_flags(rb); + if (flags != RB_RD) { + printf("Expected RB_RD, got %u.\n", flags); goto fail_rb; } @@ -237,7 +248,8 @@ static int test_ssm_rbuff_acl(void) goto fail_rb; } - ssm_rbuff_set_acl(rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(rb, RB_FLOWDOWN); + if (ssm_rbuff_write(rb, 1) != -EFLOWDOWN) { printf("Expected -EFLOWDOWN on FLOWDOWN.\n"); goto fail_rb; @@ -251,6 +263,7 @@ static int test_ssm_rbuff_acl(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: @@ -302,6 +315,7 @@ static int test_ssm_rbuff_open_close(void) ssm_rbuff_destroy(rb1); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb2: @@ -348,8 +362,10 @@ static void * reader_thread(void * arg) val = ssm_rbuff_read(args->rb); while (val < 0) { nanosleep(&delay, NULL); + val = ssm_rbuff_read(args->rb); } + if (val != i) { printf("Expected %d, got %zd.\n", i, val); return (void *) -1; @@ -359,7 +375,7 @@ static void * reader_thread(void * arg) return NULL; } -static void * blocking_writer_thread(void * arg) +static void * blocking_wr_thread(void * arg) { struct thread_args * args = (struct thread_args *) arg; int i; @@ -372,7 +388,7 @@ static void * blocking_writer_thread(void * arg) return NULL; } -static void * blocking_reader_thread(void * arg) +static void * blocking_rd_thread(void * arg) { struct thread_args * args = (struct thread_args *) arg; int i; @@ -391,13 +407,13 @@ static void * blocking_reader_thread(void * arg) static int test_ssm_rbuff_blocking(void) { - struct ssm_rbuff * rb; - pthread_t wthread; - pthread_t rthread; - struct thread_args args; - struct timespec delay = {0, 10 * MILLION}; - void * ret_w; - void * ret_r; + struct ssm_rbuff * rb; + pthread_t wthread; + pthread_t rthread; + struct thread_args args; + struct timespec delay = {0, 10 * MILLION}; + void * ret_w; + void * ret_r; TEST_START(); @@ -410,15 +426,14 @@ static int test_ssm_rbuff_blocking(void) args.rb = rb; args.iterations = 50; args.delay_us = 0; - - if (pthread_create(&rthread, NULL, blocking_reader_thread, &args)) { + if (pthread_create(&rthread, NULL, blocking_rd_thread, &args) != 0) { printf("Failed to create reader thread.\n"); goto fail_rthread; } nanosleep(&delay, NULL); - if (pthread_create(&wthread, NULL, blocking_writer_thread, &args)) { + if (pthread_create(&wthread, NULL, blocking_wr_thread, &args) != 0) { printf("Failed to create writer thread.\n"); pthread_cancel(rthread); goto fail_wthread; @@ -435,6 +450,7 @@ static int test_ssm_rbuff_blocking(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_ret: @@ -482,8 +498,7 @@ static int test_ssm_rbuff_blocking_timeout(void) (end.tv_nsec - start.tv_nsec) / 1000000L; if (elapsed_ms < 90 || elapsed_ms > 200) { - printf("Timeout took %ld ms, expected ~100 ms.\n", - elapsed_ms); + printf("Timeout took %ld ms, expected ~100 ms.\n", elapsed_ms); goto fail_rb; } @@ -502,8 +517,7 @@ static int test_ssm_rbuff_blocking_timeout(void) clock_gettime(PTHREAD_COND_CLOCK, &end); if (ret != -ETIMEDOUT) { - printf("Expected -ETIMEDOUT on full buffer, got %zd.\n", - ret); + printf("Expected -ETIMEDOUT on full buffer, got %zd.\n", ret); goto fail_rb; } @@ -522,11 +536,13 @@ static int test_ssm_rbuff_blocking_timeout(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: while (ssm_rbuff_read(rb) >= 0) ; + ssm_rbuff_destroy(rb); fail: TEST_FAIL(); @@ -553,7 +569,7 @@ static int test_ssm_rbuff_blocking_flowdown(void) clock_gettime(PTHREAD_COND_CLOCK, &now); ts_add(&now, &interval, &abs_timeout); - ssm_rbuff_set_acl(rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(rb, RB_FLOWDOWN); ret = ssm_rbuff_read_b(rb, &abs_timeout); if (ret != -EFLOWDOWN) { @@ -561,7 +577,7 @@ static int test_ssm_rbuff_blocking_flowdown(void) goto fail_rb; } - ssm_rbuff_set_acl(rb, ACL_RDWR); + ssm_rbuff_clr_flags(rb, RB_FLOWDOWN); for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) { if (ssm_rbuff_write(rb, i) < 0) { @@ -573,7 +589,7 @@ static int test_ssm_rbuff_blocking_flowdown(void) clock_gettime(PTHREAD_COND_CLOCK, &now); ts_add(&now, &interval, &abs_timeout); - ssm_rbuff_set_acl(rb, ACL_FLOWDOWN); + ssm_rbuff_set_flags(rb, RB_FLOWDOWN); ret = ssm_rbuff_write_b(rb, 999, &abs_timeout); if (ret != -EFLOWDOWN) { @@ -581,18 +597,21 @@ static int test_ssm_rbuff_blocking_flowdown(void) goto fail_rb; } - ssm_rbuff_set_acl(rb, ACL_RDWR); + ssm_rbuff_clr_flags(rb, RB_FLOWDOWN); + while (ssm_rbuff_read(rb) >= 0) ; ssm_rbuff_destroy(rb); TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: while (ssm_rbuff_read(rb) >= 0) ; + ssm_rbuff_destroy(rb); fail: TEST_FAIL(); @@ -601,12 +620,12 @@ static int test_ssm_rbuff_blocking_flowdown(void) static int test_ssm_rbuff_threaded(void) { - struct ssm_rbuff * rb; - pthread_t wthread; - pthread_t rthread; - struct thread_args args; - void * ret_w; - void * ret_r; + struct ssm_rbuff * rb; + pthread_t wthread; + pthread_t rthread; + struct thread_args args; + void * ret_w; + void * ret_r; TEST_START(); @@ -619,13 +638,12 @@ static int test_ssm_rbuff_threaded(void) args.rb = rb; args.iterations = 100; args.delay_us = 100; - - if (pthread_create(&wthread, NULL, writer_thread, &args)) { + if (pthread_create(&wthread, NULL, writer_thread, &args) != 0) { printf("Failed to create writer thread.\n"); goto fail_rb; } - if (pthread_create(&rthread, NULL, reader_thread, &args)) { + if (pthread_create(&rthread, NULL, reader_thread, &args) != 0) { printf("Failed to create reader thread.\n"); pthread_cancel(wthread); pthread_join(wthread, NULL); @@ -643,9 +661,393 @@ static int test_ssm_rbuff_threaded(void) ssm_rbuff_destroy(rb); TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ssm_rbuff_limit_off(void) +{ + struct ssm_rbuff * rb; + size_t i; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 11); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + if (ssm_rbuff_get_limit(rb) != SSM_RBUFF_SIZE - 1) { + printf("Expected default limit %d, got %zu.\n", + SSM_RBUFF_SIZE - 1, ssm_rbuff_get_limit(rb)); + goto fail_rb; + } + + for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) { + if (ssm_rbuff_write(rb, i) < 0) { + printf("Failed to write at index %zu.\n", i); + goto fail_rb; + } + } + + if (ssm_rbuff_write(rb, 999) != -EAGAIN) { + printf("Expected -EAGAIN on physically full buffer.\n"); + goto fail_rb; + } + + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ssm_rbuff_limit_slow(void) +{ + struct ssm_rbuff * rb; + struct timespec dfl = TIMESPEC_INIT_MS(SSM_RBUFF_TXQ_DELAY); + struct timespec delay = {0, 10 * MILLION}; + size_t limit; + size_t i; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 12); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + + for (i = 0; i < 32; ++i) { + if (ssm_rbuff_write_b(rb, i, NULL) < 0) { + printf("Failed to write at index %zu.\n", i); + goto fail_rb; + } + nanosleep(&delay, NULL); + + if (ssm_rbuff_read(rb) < 0) { + printf("Failed to read at index %zu.\n", i); + goto fail_rb; + } + } + + limit = ssm_rbuff_get_limit(rb); + if (limit > FLOOR_SLOTS) { + printf("Expected limit near the floor, got %zu.\n", limit); + goto fail_rb; + } + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ssm_rbuff_limit_fast(void) +{ + struct ssm_rbuff * rb; + struct timespec dfl = TIMESPEC_INIT_MS(SSM_RBUFF_TXQ_DELAY); + size_t limit; + size_t i; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 13); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + + for (i = 0; i < 200; ++i) { + if (ssm_rbuff_write_b(rb, i, NULL) < 0) { + printf("Failed to write at index %zu.\n", i); + goto fail_rb; + } + + if (ssm_rbuff_read(rb) < 0) { + printf("Failed to read at index %zu.\n", i); + goto fail_rb; + } + } + + limit = ssm_rbuff_get_limit(rb); + if (limit != CEIL_SLOTS) { + printf("Expected limit %d, got %zu.\n", CEIL_SLOTS, limit); + goto fail_rb; + } + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ssm_rbuff_limit_floor(void) +{ + struct ssm_rbuff * rb; + struct timespec dfl = TIMESPEC_INIT_MS(SSM_RBUFF_TXQ_DELAY); + struct timespec interval = {0, 50 * MILLION}; + struct timespec now; + struct timespec abs_timeout; + size_t limit; + int ret = 0; + size_t i; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 14); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + + clock_gettime(PTHREAD_COND_CLOCK, &now); + ts_add(&now, &interval, &abs_timeout); + + for (i = 0; i < SSM_RBUFF_SIZE; ++i) { + ret = ssm_rbuff_write_b(rb, i, &abs_timeout); + if (ret == -ETIMEDOUT) + break; + + if (ret < 0) { + printf("Write failed at index %zu: %d.\n", i, ret); + goto fail_rb; + } + } + + if (ret != -ETIMEDOUT) { + printf("Expected the limiter to block the ring.\n"); + goto fail_rb; + } + + limit = ssm_rbuff_get_limit(rb); + if (limit > FLOOR_SLOTS) { + printf("Expected floor limit, got %zu.\n", limit); + goto fail_rb; + } + + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A fresh ring is unlimited; rx rings must not inherit a bound. */ +static int test_ssm_rbuff_txq_target(void) +{ + struct ssm_rbuff * rb; + struct timespec dfl = TIMESPEC_INIT_MS(SSM_RBUFF_TXQ_DELAY); + struct timespec delay = {0, 5 * MILLION}; + struct timespec small = {0, 2 * MILLION}; + struct timespec big = {0, 200 * MILLION}; + struct timespec def; + struct timespec got; + size_t limit_small; + size_t limit_big; + size_t i; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 15); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + ssm_rbuff_get_txq_target(rb, &got); + + if (got.tv_sec != 0 || got.tv_nsec != 0) { + printf("A new ring is not unlimited.\n"); + goto fail_rb; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + ssm_rbuff_get_txq_target(rb, &def); + + ssm_rbuff_set_txq_target(rb, &small); + + for (i = 0; i < 64; ++i) { + if (ssm_rbuff_write_b(rb, i, NULL) < 0) { + printf("Failed to write at index %zu.\n", i); + goto fail_rb; + } + nanosleep(&delay, NULL); + + if (ssm_rbuff_read(rb) < 0) { + printf("Failed to read at index %zu.\n", i); + goto fail_rb; + } + } + + limit_small = ssm_rbuff_get_limit(rb); + + ssm_rbuff_set_txq_target(rb, &big); + + for (i = 0; i < 64; ++i) { + if (ssm_rbuff_write_b(rb, i, NULL) < 0) { + printf("Failed to write at index %zu.\n", i); + goto fail_rb; + } + nanosleep(&delay, NULL); + + if (ssm_rbuff_read(rb) < 0) { + printf("Failed to read at index %zu.\n", i); + goto fail_rb; + } + } + + limit_big = ssm_rbuff_get_limit(rb); + if (limit_big <= limit_small) { + printf("Expected a larger target to grow the limit: " + "%zu -> %zu.\n", limit_small, limit_big); + goto fail_rb; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + ssm_rbuff_get_txq_target(rb, &got); + + if (got.tv_sec != def.tv_sec || got.tv_nsec != def.tv_nsec) { + printf("NULL did not restore the default target.\n"); + goto fail_rb; + } + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Ages the seed sample past the estimator's dt floor at write 16. */ +static int test_ssm_rbuff_write_over_limit(void) +{ + struct ssm_rbuff * rb; + struct timespec dfl = TIMESPEC_INIT_MS(SSM_RBUFF_TXQ_DELAY); + struct timespec age = {0, 20 * 1000}; + size_t count; + int ret = 0; + + TEST_START(); + + rb = ssm_rbuff_create(getpid(), 16); + if (rb == NULL) { + printf("Failed to create rbuff.\n"); + goto fail; + } + + ssm_rbuff_set_txq_target(rb, &dfl); + + for (count = 0; count < SSM_RBUFF_SIZE; ++count) { + ret = ssm_rbuff_write(rb, count); + if (ret == -EAGAIN) + break; + + if (ret < 0) { + printf("Write failed at index %zu: %d.\n", count, ret); + goto fail_rb; + } + + if (count == 16) + nanosleep(&age, NULL); + } + + if (ret != -EAGAIN) { + printf("Expected the limiter to reject a write.\n"); + goto fail_rb; + } + + if (count >= SSM_RBUFF_SIZE / 2) { + printf("Expected -EAGAIN well before a full ring, " + "got %zu writes.\n", count); + goto fail_rb; + } + + if (ssm_rbuff_queued(rb) != count) { + printf("Queued %zu does not match write count %zu.\n", + ssm_rbuff_queued(rb), count); + goto fail_rb; + } + + while (ssm_rbuff_read(rb) >= 0) + ; + + ssm_rbuff_destroy(rb); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_rb: + while (ssm_rbuff_read(rb) >= 0) + ; + ssm_rbuff_destroy(rb); fail: TEST_FAIL(); @@ -664,12 +1066,18 @@ int rbuff_test(int argc, ret |= test_ssm_rbuff_write_read(); ret |= test_ssm_rbuff_read_empty(); ret |= test_ssm_rbuff_fill_drain(); - ret |= test_ssm_rbuff_acl(); + ret |= test_ssm_rbuff_flags(); ret |= test_ssm_rbuff_open_close(); ret |= test_ssm_rbuff_threaded(); ret |= test_ssm_rbuff_blocking(); ret |= test_ssm_rbuff_blocking_timeout(); ret |= test_ssm_rbuff_blocking_flowdown(); + ret |= test_ssm_rbuff_limit_off(); + ret |= test_ssm_rbuff_limit_slow(); + ret |= test_ssm_rbuff_limit_fast(); + ret |= test_ssm_rbuff_limit_floor(); + ret |= test_ssm_rbuff_txq_target(); + ret |= test_ssm_rbuff_write_over_limit(); return ret; } diff --git a/src/lib/tests/CMakeLists.txt b/src/lib/tests/CMakeLists.txt index 32836589..d470d539 100644 --- a/src/lib/tests/CMakeLists.txt +++ b/src/lib/tests/CMakeLists.txt @@ -10,10 +10,13 @@ create_test_sourcelist(${PARENT_DIR}_tests test_suite.c auth_test_slh_dsa.c bitmap_test.c btree_test.c + cap_test.c crypt_test.c + poa_test.c hash_test.c kex_test.c kex_test_ml_kem.c + keyrot_test.c md5_test.c sha3_test.c sockets_test.c @@ -24,6 +27,15 @@ create_test_sourcelist(${PARENT_DIR}_tests test_suite.c add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests}) +if(HAVE_LIBURCU) + # poa_test.c pulls in poa.h, whose urcu guard needs C99. + set_source_files_properties(poa_test.c PROPERTIES + COMPILE_OPTIONS "-std=gnu99") +endif() + +target_include_directories(${PARENT_DIR}_test PRIVATE + ${CMAKE_SOURCE_DIR}/src/lib) + disable_test_logging_for_target(${PARENT_DIR}_test) target_link_libraries(${PARENT_DIR}_test ouroboros-common) diff --git a/src/lib/tests/auth_test.c b/src/lib/tests/auth_test.c index 0f3ef715..61f97683 100644 --- a/src/lib/tests/auth_test.c +++ b/src/lib/tests/auth_test.c @@ -24,11 +24,14 @@ #include <test/test.h> #include <ouroboros/crypt.h> +#include <ouroboros/name.h> #include <ouroboros/random.h> #include <ouroboros/utils.h> #include <test/certs/ecdsa.h> +#include <string.h> + #define TEST_MSG_SIZE 1500 static int test_auth_create_destroy_ctx(void) @@ -138,6 +141,47 @@ static int test_check_crt_name(void) return TEST_RC_FAIL; } +static int test_crt_name_confusion(void) +{ + char name[NAME_SIZE + 1]; + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(confused_crt_ec, &crt) < 0) { + printf("Failed to load name-confusion certificate.\n"); + goto fail_load; + } + + /* Must extract the real CN, not the "CN=" decoy in the O field. */ + if (crypt_get_crt_name(crt, name) < 0) { + printf("Failed to extract name from certificate.\n"); + goto fail_check; + } + + if (strcmp(name, "attacker.unittest.o7s") != 0) { + printf("Extracted '%s', expected real CN.\n", name); + goto fail_check; + } + + /* The decoy name in the O field must never authenticate. */ + if (crypt_check_crt_name(crt, "victim.unittest.o7s") == 0) { + printf("Accepted spoofed name from O field.\n"); + goto fail_check; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_check: + crypt_free_crt(crt); + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + static int test_load_free_privkey(void) { void * key; @@ -260,7 +304,7 @@ static int test_store_add(void) fail_add: crypt_free_crt(_root_ca_crt); fail_load: - crypt_free_crt(_root_ca_crt); + auth_destroy_ctx(ctx); fail_create: TEST_FAIL(); return TEST_RC_FAIL; @@ -400,6 +444,98 @@ static int test_verify_crt_missing_root_ca(void) return TEST_RC_FAIL; } +/* auth_verify_crt_pin: pin must lie in the verified chain (NULL: any) */ +static int test_verify_crt_pin(void) +{ + struct auth_ctx * auth; + void * _root_ca_crt; + void * _im_ca_crt; + void * _signed_server_crt; + void * _other_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(root_ca_crt_ec, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load_root_ca; + } + + if (crypt_load_crt_str(im_ca_crt_ec, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed; + } + + if (crypt_load_crt_str(other_ca_crt_ec, &_other_ca_crt) < 0) { + printf("Failed to load out-of-chain crt from string.\n"); + goto fail_load_other; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _im_ca_crt) < 0) { + printf("Failed to accept pin on intermediate CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _root_ca_crt) < 0) { + printf("Failed to accept pin on root CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _other_ca_crt) == 0) { + printf("Failed to reject out-of-chain pin.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, NULL) < 0) { + printf("Failed to accept NULL (any) pin.\n"); + goto fail_verify; + } + + crypt_free_crt(_other_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_other_ca_crt); + fail_load_other: + crypt_free_crt(_signed_server_crt); + fail_load_signed: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int test_auth_sign(void) { uint8_t buf[TEST_MSG_SIZE]; @@ -573,6 +709,7 @@ int auth_test(int argc, #ifdef HAVE_OPENSSL ret |= test_load_free_crt(); ret |= test_check_crt_name(); + ret |= test_crt_name_confusion(); ret |= test_crypt_get_pubkey_crt(); ret |= test_load_free_privkey(); ret |= test_load_free_pubkey(); @@ -580,12 +717,14 @@ int auth_test(int argc, ret |= test_store_add(); ret |= test_verify_crt(); ret |= test_verify_crt_missing_root_ca(); + ret |= test_verify_crt_pin(); ret |= test_auth_sign(); ret |= test_auth_bad_signature(); ret |= test_crt_str(); #else (void) test_load_free_crt; (void) test_check_crt_name; + (void) test_crt_name_confusion; (void) test_crypt_get_pubkey_crt; (void) test_load_free_privkey; (void) test_load_free_pubkey; @@ -593,11 +732,13 @@ int auth_test(int argc, (void) test_store_add; (void) test_verify_crt; (void) test_verify_crt_missing_root_ca; + (void) test_verify_crt_pin; (void) test_auth_sign; (void) test_auth_bad_signature; (void) test_crt_str; - ret = TEST_RC_SKIP; + if (ret == 0) + ret = TEST_RC_SKIP; #endif return ret; } diff --git a/src/lib/tests/auth_test_ml_dsa.c b/src/lib/tests/auth_test_ml_dsa.c index cc72e61b..e324c32d 100644 --- a/src/lib/tests/auth_test_ml_dsa.c +++ b/src/lib/tests/auth_test_ml_dsa.c @@ -333,7 +333,7 @@ int auth_test_ml_dsa(int argc, (void) argc; (void) argv; -#ifdef HAVE_OPENSSL_ML_DSA +#ifdef HAVE_ML ret |= test_auth_create_destroy_ctx(); ret |= test_load_free_crt(); ret |= test_load_free_privkey(); diff --git a/src/lib/tests/auth_test_slh_dsa.c b/src/lib/tests/auth_test_slh_dsa.c index 511d20fe..e9af8da8 100644 --- a/src/lib/tests/auth_test_slh_dsa.c +++ b/src/lib/tests/auth_test_slh_dsa.c @@ -344,7 +344,7 @@ int auth_test_slh_dsa(int argc, (void) argc; (void) argv; -#ifdef HAVE_OPENSSL_SLH_DSA +#ifdef HAVE_SLH ret |= test_auth_create_destroy_ctx(); ret |= test_load_free_crt(); ret |= test_load_free_privkey(); diff --git a/src/lib/tests/cap_test.c b/src/lib/tests/cap_test.c new file mode 100644 index 00000000..ea0e1fef --- /dev/null +++ b/src/lib/tests/cap_test.c @@ -0,0 +1,427 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Unit tests for link capacity estimation + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#include "../cap.c" + +#include <test/test.h> + +#include <inttypes.h> +#include <stdbool.h> + +#define TICK (50 * 1000ULL) /* 50 us between packets */ +#define LEN 1000ULL /* default packet size (B) */ +#define QLEN (8 * LEN) /* steady backlog (bytes) */ +#define RATE (LEN * BILLION / TICK) /* LEN per TICK = 20 MB/s */ + +#define SHP_LEN 1250ULL /* shaped-link packet (B) */ +#define SHP_STEP 20 /* packets per shaped window */ +#define SHP_RATE (SHP_LEN * BILLION / (SHP_STEP * TICK)) + +/* Draining CAP_N_MIN of these outlasts CAP_T_MAX without a gap. */ +#define LOW_STEP (250 * TICK) /* 12.5 ms between packets */ +#define LOW_RATE (LEN * BILLION / LOW_STEP) + +/* Within the quarter-log2 band the wire code publishes. */ +static bool rate_is_near(uint64_t got, + uint64_t exp) +{ + return got >= exp - exp / 8 && got <= exp + exp / 8; +} + +static int test_cap_est_clear(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + if (cap_rate(&e) != 0) { + printf("Fresh estimator not unknown.\n"); + goto fail; + } + + for (i = 1; i <= 40; i++) + cap_update_at(&e, QLEN, LEN, i * TICK); + + if (cap_rate(&e) == 0) { + printf("No estimate to clear.\n"); + goto fail; + } + + cap_clear(&e); + + if (cap_rate(&e) != 0) { + printf("Clear did not drop the estimate.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* 1000 B every 50 us, ring steady at 8: drain = 20 MB/s. */ +static int test_cap_est_busy_window(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 40; i++) + cap_update_at(&e, QLEN, LEN, i * TICK); + + if (!rate_is_near(cap_rate(&e), RATE)) { + printf("Estimated rate: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_est_idle_tolerated(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 40; i++) + cap_update_at(&e, i == 21 ? 0 : QLEN, LEN, i * TICK); + + if (!rate_is_near(cap_rate(&e), RATE)) { + printf("Grazed window: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_est_mostly_idle_rejects(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 100; i++) + cap_update_at(&e, 0, LEN, i * TICK); + + if (cap_rate(&e) != 0) { + printf("Idle ring estimated %" PRIu64 ".\n", cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* 1000 B every 100 us: 10 slots/ms closes on a 2 ms window. */ +static int test_cap_est_slow_link_extends(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 30; i++) + cap_update_at(&e, QLEN, LEN, i * 2 * TICK); + + if (!rate_is_near(cap_rate(&e), RATE / 2)) { + printf("Slow link: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) (RATE / 2), cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* 1250 B every ms; one empty observation per 20 packets. */ +static int test_cap_est_shaped_link(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 100; i++) + cap_update_at(&e, i % SHP_STEP == 0 ? 0 : 6 * SHP_LEN, + SHP_LEN, i * SHP_STEP * TICK); + + if (!rate_is_near(cap_rate(&e), SHP_RATE)) { + printf("Shaped link: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) SHP_RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Open a window, trickle 4 slots, then ~200 ms of silence. */ +static int test_cap_est_stale_discard(void) +{ + struct cap_est e; + uint64_t t; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 5; i++) + cap_update_at(&e, QLEN, LEN, i * CAP_T_MIN); + + t = 205 * CAP_T_MIN; + + cap_update_at(&e, QLEN, LEN, t); + + if (cap_rate(&e) != 0) { + printf("Gap window estimated %" PRIu64 ".\n", cap_rate(&e)); + goto fail; + } + + for (i = 1; i <= 40; i++) + cap_update_at(&e, QLEN, LEN, t + i * TICK); + + if (!rate_is_near(cap_rate(&e), RATE)) { + printf("Post-gap: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_cap_est_empty_start_no_raise(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + cap_update_at(&e, 0, LEN, CAP_T_MIN); + + for (i = 1; i <= 40; i++) + cap_update_at(&e, QLEN, LEN, CAP_T_MIN + i * TICK); + + if (cap_rate(&e) != 0) { + printf("Empty-start window raised to %" PRIu64 ".\n", + cap_rate(&e)); + goto fail; + } + + for (i = 41; i <= 60; i++) + cap_update_at(&e, QLEN, LEN, CAP_T_MIN + i * TICK); + + if (!rate_is_near(cap_rate(&e), RATE)) { + printf("Backlogged window: exp %" PRIu64 ", got %" PRIu64 + ".\n", (uint64_t) RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Max filter: fast attack on a high sample, slow release on the + * lower samples from a halved packet size (10 MB/s). + */ +static int test_cap_est_max_filter(void) +{ + struct cap_est e; + uint64_t high; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 40; i++) + cap_update_at(&e, QLEN, LEN, i * TICK); + + high = cap_rate(&e); + if (!rate_is_near(high, RATE)) { + printf("Attack missed: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) RATE, high); + goto fail; + } + + for (i = 41; i <= 80; i++) + cap_update_at(&e, QLEN, LEN / 2, i * TICK); + + if (cap_rate(&e) >= high) { + printf("Release did not decay: %" PRIu64 ".\n", cap_rate(&e)); + goto fail; + } + + if (cap_rate(&e) <= RATE / 2) { + printf("Release collapsed to %" PRIu64 ".\n", cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* No window close within CAP_T_MIN of the last one. */ +static int test_cap_est_gate(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + cap_update_at(&e, QLEN, LEN, CAP_T_MIN); + + for (i = 0; i < 5; i++) + cap_update_at(&e, QLEN, LEN, CAP_T_MIN + CAP_T_MIN / 2); + + if (e.t_gate != CAP_T_MIN) { + printf("Window closed inside the gate.\n"); + goto fail; + } + + if (LOAD_RELAXED(&e.c_pkt) != 6) { + printf("Gated packets not counted.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * A link slow enough that CAP_N_MIN packets take longer than + * CAP_T_MAX to drain still publishes, as long as the sender keeps + * offering: only silence voids a window. + */ +static int test_cap_est_low_rate_publishes(void) +{ + struct cap_est e; + size_t i; + + TEST_START(); + + cap_clear(&e); + + for (i = 1; i <= 20; i++) + cap_update_at(&e, QLEN, LEN, i * LOW_STEP); + + if (!rate_is_near(cap_rate(&e), LOW_RATE)) { + printf("Low rate: exp %" PRIu64 ", got %" PRIu64 ".\n", + (uint64_t) LOW_RATE, cap_rate(&e)); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int cap_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_cap_est_clear(); + ret |= test_cap_est_busy_window(); + ret |= test_cap_est_idle_tolerated(); + ret |= test_cap_est_mostly_idle_rejects(); + ret |= test_cap_est_slow_link_extends(); + ret |= test_cap_est_shaped_link(); + ret |= test_cap_est_stale_discard(); + ret |= test_cap_est_empty_start_no_raise(); + ret |= test_cap_est_max_filter(); + ret |= test_cap_est_gate(); + ret |= test_cap_est_low_rate_publishes(); + + return ret; +} diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c index 028c4eb5..88c9634a 100644 --- a/src/lib/tests/crypt_test.c +++ b/src/lib/tests/crypt_test.c @@ -30,6 +30,7 @@ #include <stdio.h> #define TEST_PACKET_SIZE 1500 +#define TEST_N_PACKETS 1000 extern const uint16_t crypt_supported_nids[]; extern const uint16_t md_supported_nids[]; @@ -39,9 +40,10 @@ static int test_crypt_create_destroy(void) struct crypt_ctx * ctx; uint8_t key[SYMMKEYSZ]; struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = KEY_ROTATION_BIT + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT }; TEST_START(); @@ -49,12 +51,20 @@ static int test_crypt_create_destroy(void) memset(key, 0, sizeof(key)); ctx = crypt_create_ctx(&sk); +#ifdef HAVE_OPENSSL if (ctx == NULL) { printf("Failed to initialize cryptography.\n"); goto fail; } crypt_destroy_ctx(ctx); +#else + if (ctx != NULL) { + printf("Created cipher context without a backend.\n"); + crypt_destroy_ctx(ctx); + goto fail; + } +#endif TEST_SUCCESS(); @@ -67,18 +77,27 @@ static int test_crypt_create_destroy(void) static int test_crypt_encrypt_decrypt(int nid) { uint8_t pkt[TEST_PACKET_SIZE]; - struct crypt_ctx * ctx; + struct crypt_ctx * tx; + struct crypt_ctx * rx; uint8_t key[SYMMKEYSZ]; - struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = KEY_ROTATION_BIT + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP }; buffer_t in; buffer_t out; buffer_t out2; const char * cipher; + sk_tx.nid = nid; + sk_rx.nid = nid; + cipher = crypt_nid_to_str(nid); TEST_START("(%s)", cipher); @@ -92,53 +111,63 @@ static int test_crypt_encrypt_decrypt(int nid) goto fail_init; } - ctx = crypt_create_ctx(&sk); - if (ctx == NULL) { - printf("Failed to initialize cryptography.\n"); + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to initialize TX cryptography.\n"); goto fail_init; } + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to initialize RX cryptography.\n"); + goto fail_tx; + } + in.len = sizeof(pkt); in.data = pkt; - if (crypt_encrypt(ctx, in, &out) < 0) { + if (crypt_encrypt(tx, in, &out) < 0) { printf("Encryption failed.\n"); goto fail_encrypt; } if (out.len < in.len) { printf("Encryption returned too little data.\n"); - goto fail_encrypt; + goto fail_chk; } - if (crypt_decrypt(ctx, out, &out2) < 0) { + if (crypt_decrypt(rx, out, &out2) < 0) { printf("Decryption failed.\n"); goto fail_decrypt; } if (out2.len != in.len) { printf("Decrypted data length does not match original.\n"); - goto fail_chk; + goto fail_chk2; } if (memcmp(in.data, out2.data, in.len) != 0) { printf("Decrypted data does not match original.\n"); - goto fail_chk; + goto fail_chk2; } - crypt_destroy_ctx(ctx); freebuf(out2); freebuf(out); + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); TEST_SUCCESS("(%s)", cipher); return TEST_RC_SUCCESS; - fail_chk: + fail_chk2: freebuf(out2); fail_decrypt: + fail_chk: freebuf(out); fail_encrypt: - crypt_destroy_ctx(ctx); + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); fail_init: TEST_FAIL("(%s)", cipher); return TEST_RC_FAIL; @@ -155,6 +184,230 @@ static int test_encrypt_decrypt_all(void) return ret; } +static int test_crypt_multi_packet(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + int i; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + in.len = sizeof(pkt); + in.data = pkt; + + for (i = 0; i < TEST_N_PACKETS; i++) { + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed at packet %d.\n", i); + goto fail_rx; + } + + if (crypt_decrypt(rx, enc, &dec) < 0) { + printf("Decryption failed at packet %d.\n", i); + freebuf(enc); + goto fail_rx; + } + + if (dec.len != in.len || + memcmp(in.data, dec.data, in.len) != 0) { + printf("Data mismatch at packet %d.\n", i); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(dec); + freebuf(enc); + } + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_multi_packet_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_multi_packet(crypt_supported_nids[i]); + + return ret; +} + +static int test_crypt_aad_tamper(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + /* Only AEAD ciphers bind the selector as AAD. */ + if (crypt_get_tagsz(tx) == 0) { + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + } + + in.len = sizeof(pkt); + in.data = pkt; + + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed.\n"); + goto fail_rx; + } + + /* Flip a seq byte: epoch/node stay valid so the AEAD tag rejects. */ + enc.data[5] ^= 0x01; + + if (crypt_decrypt(rx, enc, &dec) == 0) { + printf("Decryption accepted a tampered selector.\n"); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + enc.data[5] ^= 0x01; + + if (crypt_decrypt(rx, enc, &dec) < 0) { + printf("Decryption failed after a rejected packet.\n"); + freebuf(enc); + goto fail_rx; + } + + if (dec.len != in.len || memcmp(dec.data, in.data, in.len) != 0) { + printf("Decrypted data mismatch after rejection.\n"); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(dec); + freebuf(enc); + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_aad_tamper_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_aad_tamper(crypt_supported_nids[i]); + + return ret; +} + #ifdef HAVE_OPENSSL #include <openssl/evp.h> #include <openssl/obj_mac.h> @@ -256,22 +509,17 @@ static int test_md_nid_values(void) } #endif -static int test_key_rotation(void) +static int test_crypt_headsz(void) { - uint8_t pkt[TEST_PACKET_SIZE]; - struct crypt_ctx * tx_ctx; - struct crypt_ctx * rx_ctx; - uint8_t key[SYMMKEYSZ]; - struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = 7 + struct crypt_ctx * ctx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk = { + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT }; - buffer_t in; - buffer_t enc; - buffer_t dec; - uint32_t i; - uint32_t threshold; + int headsz; TEST_START(); @@ -280,155 +528,66 @@ static int test_key_rotation(void) goto fail; } - if (random_buffer(pkt, sizeof(pkt)) < 0) { - printf("Failed to generate random data.\n"); - goto fail; - } - - tx_ctx = crypt_create_ctx(&sk); - if (tx_ctx == NULL) { - printf("Failed to create TX context.\n"); + ctx = crypt_create_ctx(&sk); + if (ctx == NULL) { + printf("Failed to initialize cryptography.\n"); goto fail; } - rx_ctx = crypt_create_ctx(&sk); - if (rx_ctx == NULL) { - printf("Failed to create RX context.\n"); - goto fail_tx; - } - - in.len = sizeof(pkt); - in.data = pkt; - - threshold = (1U << sk.rot_bit); - - /* Encrypt and decrypt across multiple rotations */ - for (i = 0; i < threshold * 3; i++) { - if (crypt_encrypt(tx_ctx, in, &enc) < 0) { - printf("Encryption failed at packet %u.\n", i); - goto fail_rx; - } - - if (crypt_decrypt(rx_ctx, enc, &dec) < 0) { - printf("Decryption failed at packet %u.\n", i); - freebuf(enc); - goto fail_rx; - } - - if (dec.len != in.len || - memcmp(in.data, dec.data, in.len) != 0) { - printf("Data mismatch at packet %u.\n", i); - freebuf(dec); - freebuf(enc); - goto fail_rx; - } - - freebuf(dec); - freebuf(enc); + headsz = crypt_get_headsz(ctx); + if (headsz != 6) { + printf("Unexpected header size: %d (expected 6).\n", headsz); + goto fail_ctx; } - crypt_destroy_ctx(rx_ctx); - crypt_destroy_ctx(tx_ctx); + crypt_destroy_ctx(ctx); TEST_SUCCESS(); return TEST_RC_SUCCESS; - fail_rx: - crypt_destroy_ctx(rx_ctx); - fail_tx: - crypt_destroy_ctx(tx_ctx); + fail_ctx: + crypt_destroy_ctx(ctx); fail: TEST_FAIL(); return TEST_RC_FAIL; } -static int test_key_phase_bit(void) +static int test_crypt_ct_cmp(void) { - uint8_t pkt[TEST_PACKET_SIZE]; - struct crypt_ctx * ctx; - uint8_t key[SYMMKEYSZ]; - struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = 7 - }; - buffer_t in; - buffer_t out; - uint32_t count; - uint32_t threshold; - uint8_t phase_before; - uint8_t phase_after; - int ivsz; + uint8_t a[64]; + uint8_t b[64]; + size_t i; TEST_START(); - if (random_buffer(key, sizeof(key)) < 0) { - printf("Failed to generate random key.\n"); - goto fail; - } + for (i = 0; i < sizeof(a); i++) + a[i] = (uint8_t) i; - if (random_buffer(pkt, sizeof(pkt)) < 0) { - printf("Failed to generate random data.\n"); - goto fail; - } + memcpy(b, a, sizeof(a)); - ctx = crypt_create_ctx(&sk); - if (ctx == NULL) { - printf("Failed to initialize cryptography.\n"); + if (crypt_ct_cmp(a, b, sizeof(a)) != 0) { + printf("Equal buffers should compare equal.\n"); goto fail; } - ivsz = crypt_get_ivsz(ctx); - if (ivsz <= 0) { - printf("Invalid IV size.\n"); - goto fail_ctx; + if (crypt_ct_cmp(a, b, 0) != 0) { + printf("Zero length should compare equal.\n"); + goto fail; } - in.len = sizeof(pkt); - in.data = pkt; - - /* Encrypt packets up to just before rotation threshold */ - threshold = (1U << sk.rot_bit); - - /* Encrypt threshold - 1 packets (indices 0 to threshold-2) */ - for (count = 0; count < threshold - 1; count++) { - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed at count %u.\n", count); - goto fail_ctx; + for (i = 0; i < sizeof(a); i++) { + b[i] ^= 0x01; + if (crypt_ct_cmp(a, b, sizeof(a)) == 0) { + printf("Difference at byte %zu not detected.\n", i); + goto fail; } - freebuf(out); - } - - /* Packet at index threshold-1: phase should still be initial */ - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed before rotation.\n"); - goto fail_ctx; - } - phase_before = (out.data[0] & 0x80) ? 1 : 0; - freebuf(out); - /* Packet at index threshold: phase should have toggled */ - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed at rotation threshold.\n"); - goto fail_ctx; + b[i] ^= 0x01; } - phase_after = (out.data[0] & 0x80) ? 1 : 0; - freebuf(out); - - /* Phase bit should have toggled */ - if (phase_before == phase_after) { - printf("Phase bit did not toggle: before=%u, after=%u.\n", - phase_before, phase_after); - goto fail_ctx; - } - - crypt_destroy_ctx(ctx); TEST_SUCCESS(); return TEST_RC_SUCCESS; - fail_ctx: - crypt_destroy_ctx(ctx); fail: TEST_FAIL(); return TEST_RC_FAIL; @@ -444,16 +603,20 @@ int crypt_test(int argc, ret |= test_crypt_create_destroy(); ret |= test_encrypt_decrypt_all(); + ret |= test_crypt_ct_cmp(); #ifdef HAVE_OPENSSL ret |= test_cipher_nid_values(); ret |= test_md_nid_values(); - ret |= test_key_rotation(); - ret |= test_key_phase_bit(); + ret |= test_multi_packet_all(); + ret |= test_aad_tamper_all(); + ret |= test_crypt_headsz(); #else - (void) test_key_rotation; - (void) test_key_phase_bit; + (void) test_multi_packet_all; + (void) test_aad_tamper_all; + (void) test_crypt_headsz; - return TEST_RC_SKIP; + if (ret == 0) + ret = TEST_RC_SKIP; #endif return ret; } diff --git a/src/lib/tests/hash_test.c b/src/lib/tests/hash_test.c index 451d3c25..a2ba62cc 100644 --- a/src/lib/tests/hash_test.c +++ b/src/lib/tests/hash_test.c @@ -39,6 +39,11 @@ struct vec_entry { char * out; }; +struct mix_entry { + uint64_t in; + uint64_t out; +}; + static int test_crc8(void) { int ret = 0; @@ -288,6 +293,36 @@ static int test_sha3(void) return ret; } +static int test_mix64(void) +{ + int ret = 0; + + struct mix_entry vec [] = { + { 0x0000000000000000ULL, 0x0000000000000000ULL }, + { 0x123456789abcdefeULL, 0xb1943cfea4f78f08ULL } + }; + + size_t n = sizeof(vec) / sizeof(vec[0]); + size_t i; + + TEST_START(); + + for (i = 0; i < n; i++) { + uint64_t res = hash_mix64(vec[i].in); + + if (res != vec[i].out) { + printf("Mix failed %016llx != %016llx.\n", + (unsigned long long) res, + (unsigned long long) vec[i].out); + ret |= -1; + } + } + + TEST_END(ret); + + return ret; +} + int hash_test(int argc, char ** argv) { @@ -308,5 +343,7 @@ int hash_test(int argc, ret |= test_sha3(); + ret |= test_mix64(); + return ret; } diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c index 6a4f802e..d7629f95 100644 --- a/src/lib/tests/kex_test.c +++ b/src/lib/tests/kex_test.c @@ -44,6 +44,9 @@ #define KEX_CONFIG_NONE \ "none\n" +#define KEX_CONFIG_NO_ENC \ + "encryption=none\n" + #define KEX_CONFIG_WHITESPACE \ "# Comment line\n" \ "kex = X448" \ @@ -58,6 +61,31 @@ "kex=X25519\n" \ "digest=sha384\n" +#define KEX_CONFIG_AUTH \ + "auth=required\n" + +#define KEX_CONFIG_AUTH_INVALID \ + "auth=mandatory\n" + +#define KEX_CONFIG_AUTH_OPTIONAL \ + "auth=optional\n" + +#define KEX_CONFIG_AUTH_THEN_NO_ENC \ + "auth=required\n" \ + "digest=sha512\n" \ + "encryption=none\n" + +#define KEX_CONFIG_NO_ENC_THEN_AUTH \ + "encryption=none\n" \ + "auth=required\n" \ + "digest=sha512\n" + +#define KEX_CONFIG_CACERT \ + "cacert=/etc/ouroboros/security/cacert/ca.crt\n" + +#define KEX_CONFIG_UNKNOWN_KEY \ + "autth=required\n" + /* Test key material for key loading tests */ #define X25519_PRIVKEY_PEM \ "-----BEGIN PRIVATE KEY-----\n" \ @@ -77,6 +105,9 @@ extern const uint16_t kex_supported_nids[]; int parse_sec_config(struct sec_config * cfg, FILE * fp); +int crypt_load_sec_config(struct sec_config * cfg, + FILE * fp); + static int test_kex_create_destroy(void) { struct sec_config cfg; @@ -151,17 +182,9 @@ static int test_kex_get_algo_from_pk(const char * algo) pk.len = (size_t) len; pk.data = buf; - /* Use raw decode for hybrid KEMs, DER for others */ - if (IS_HYBRID_KEM(algo)) { - if (kex_get_algo_from_pk_raw(pk, extracted_algo) < 0) { - printf("Failed to extract algo from pk.\n"); - goto fail_pkp; - } - } else { - if (kex_get_algo_from_pk_der(pk, extracted_algo) < 0) { - printf("Failed to extract algo from pk.\n"); - goto fail_pkp; - } + if (kex_get_algo_from_pk_der(pk, extracted_algo) < 0) { + printf("Failed to extract algo from pk.\n"); + goto fail_pkp; } /* All algorithms should now return the specific group name */ @@ -190,6 +213,11 @@ static int test_kex_get_algo_from_pk_all(void) for (i = 0; kex_supported_nids[i] != NID_undef; i++) { const char * algo = kex_nid_to_str(kex_supported_nids[i]); + + /* Raw hybrid PKs are opaque, OAP carries the NID */ + if (IS_HYBRID_KEM(algo)) + continue; + ret |= test_kex_get_algo_from_pk(algo); } @@ -213,6 +241,7 @@ static int test_kex_dhe_derive(const char * algo) memset(&kex, 0, sizeof(kex)); SET_KEX_ALGO(&kex, algo); + SET_KEX_KDF_NID(&kex, NID_sha256); len = kex_pkp_create(&kex, &pkp1, buf1); if (len < 0) { @@ -276,7 +305,7 @@ static int test_kex_validate_algo(void) goto fail; } -#ifdef HAVE_OPENSSL_ML_KEM +#ifdef HAVE_ML if (kex_validate_algo("ML-KEM-768") != 0) { printf("ML-KEM-768 should be valid.\n"); goto fail; @@ -324,6 +353,7 @@ static int test_kex_dhe_corrupted_pubkey(const char * algo) memset(&kex, 0, sizeof(kex)); SET_KEX_ALGO(&kex, algo); + SET_KEX_KDF_NID(&kex, NID_sha256); len = kex_pkp_create(&kex, &pkp, buf); if (len < 0) { @@ -375,6 +405,8 @@ static int test_kex_dhe_wrong_algo(void) memset(&kex2, 0, sizeof(kex2)); SET_KEX_ALGO(&kex1, algo1); SET_KEX_ALGO(&kex2, algo2); + SET_KEX_KDF_NID(&kex1, NID_sha256); + SET_KEX_KDF_NID(&kex2, NID_sha256); if (kex_pkp_create(&kex1, &pkp1, buf1) < 0) { printf("Failed to create first key pair.\n"); @@ -411,6 +443,57 @@ static int test_kex_dhe_wrong_algo(void) return TEST_RC_FAIL; } +static int test_kex_dhe_no_kdf(void) +{ + struct sec_config kex; + void * pkp1; + void * pkp2; + buffer_t pk2; + ssize_t len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s[SYMMKEYSZ]; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, "X25519"); + + if (kex_pkp_create(&kex, &pkp1, buf1) < 0) { + printf("Failed to create first key pair.\n"); + goto fail; + } + + len = kex_pkp_create(&kex, &pkp2, buf2); + if (len < 0) { + printf("Failed to create second key pair.\n"); + goto fail_pkp1; + } + + pk2.len = (size_t) len; + pk2.data = buf2; + + /* No KDF configured: derive must fail, not fall back. */ + if (kex_dhe_derive(&kex, pkp1, pk2, s) == 0) { + printf("Derive succeeded without a KDF.\n"); + goto fail_pkp2; + } + + kex_pkp_destroy(pkp2); + kex_pkp_destroy(pkp1); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_pkp2: + kex_pkp_destroy(pkp2); + fail_pkp1: + kex_pkp_destroy(pkp1); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + static int test_kex_load_dhe_privkey(void) { void * key; @@ -639,7 +722,8 @@ static int test_kex_parse_config_custom(void) return TEST_RC_FAIL; } -static int test_kex_parse_config_none(void) +/* The old bare 'none' keyword must be rejected loudly */ +static int test_kex_parse_config_none_rejected(void) { struct sec_config kex; FILE * fp; @@ -654,14 +738,51 @@ static int test_kex_parse_config_none(void) goto fail; } + if (parse_sec_config(&kex, fp) == 0) { + printf("Bare 'none' keyword should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_no_enc(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_NO_ENC); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + if (parse_sec_config(&kex, fp) < 0) { - printf("Failed to parse 'none' config.\n"); + printf("Failed to parse encryption=none config.\n"); fclose(fp); goto fail; } - if (kex.x.nid != NID_undef) { - printf("'none' keyword should disable encryption.\n"); + if (kex.x.nid != NID_undef || kex.c.nid != NID_undef) { + printf("encryption=none should disable encryption.\n"); + fclose(fp); + goto fail; + } + + if (kex.d.nid != NID_sha256) { + printf("encryption=none should keep the digest.\n"); fclose(fp); goto fail; } @@ -799,6 +920,309 @@ static int test_kex_parse_config_digest(void) return TEST_RC_FAIL; } +static int test_kex_parse_config_auth(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth config.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("auth=required not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_auth_invalid(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH_INVALID); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Invalid auth value should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A caller-seeded req_auth survives parsing when no auth= line is set */ +static int test_kex_parse_config_auth_seed(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + kex.a.req = true; + + fp = FMEMOPEN_STR(KEX_CONFIG_NO_ENC); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse config.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("Seeded req_auth should survive parsing.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* An explicit auth=optional clears a caller-seeded req_auth */ +static int test_kex_parse_config_auth_optional(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + kex.a.req = true; + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH_OPTIONAL); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth=optional config.\n"); + fclose(fp); + goto fail; + } + + if (kex.a.req) { + printf("auth=optional should clear req_auth.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* encryption=none must not drop auth=required or the digest */ +static int test_kex_parse_config_auth_no_enc(const char * config) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(config); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth + encryption=none.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("encryption=none should not drop required auth.\n"); + fclose(fp); + goto fail; + } + + if (kex.x.nid != NID_undef) { + printf("encryption=none should disable encryption.\n"); + fclose(fp); + goto fail; + } + + if (kex.d.nid != NID_sha512) { + printf("encryption=none should keep the digest.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_cacert(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_CACERT); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse cacert config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.a.cacert, + "/etc/ouroboros/security/cacert/ca.crt") != 0) { + printf("cacert not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + if (kex.a.req) { + printf("cacert must not imply req_auth.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_unknown_key(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_UNKNOWN_KEY); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Unknown key should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +#ifndef HAVE_OPENSSL +/* A present security config must be refused without a backend. */ +static int test_kex_load_config_enotsup(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + fp = FMEMOPEN_STR(KEX_CONFIG_CUSTOM); + if (fp == NULL) { + printf("Failed to open config stream.\n"); + goto fail; + } + + if (crypt_load_sec_config(&kex, fp) != -ENOTSUP) { + printf("Loaded a config without a crypto backend.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif + int kex_test(int argc, char ** argv) { @@ -808,9 +1232,18 @@ int kex_test(int argc, (void) argv; ret |= test_kex_create_destroy(); - ret |= test_kex_parse_config_empty(); - ret |= test_kex_parse_config_none(); #ifdef HAVE_OPENSSL + ret |= test_kex_parse_config_empty(); + ret |= test_kex_parse_config_none_rejected(); + ret |= test_kex_parse_config_no_enc(); + ret |= test_kex_parse_config_auth(); + ret |= test_kex_parse_config_auth_invalid(); + ret |= test_kex_parse_config_auth_seed(); + ret |= test_kex_parse_config_auth_optional(); + ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_AUTH_THEN_NO_ENC); + ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_NO_ENC_THEN_AUTH); + ret |= test_kex_parse_config_cacert(); + ret |= test_kex_parse_config_unknown_key(); ret |= test_kex_parse_config_custom(); ret |= test_kex_parse_config_whitespace(); ret |= test_kex_parse_config_cipher(); @@ -821,10 +1254,23 @@ int kex_test(int argc, ret |= test_kex_validate_algo(); ret |= test_kex_get_algo_from_pk_all(); ret |= test_kex_dhe_wrong_algo(); + ret |= test_kex_dhe_no_kdf(); ret |= test_kex_dhe_corrupted_pubkey_all(); ret |= test_kex_load_dhe_privkey(); ret |= test_kex_load_dhe_pubkey(); #else + ret |= test_kex_load_config_enotsup(); + + (void) test_kex_parse_config_empty; + (void) test_kex_parse_config_none_rejected; + (void) test_kex_parse_config_no_enc; + (void) test_kex_parse_config_auth; + (void) test_kex_parse_config_auth_invalid; + (void) test_kex_parse_config_auth_seed; + (void) test_kex_parse_config_auth_optional; + (void) test_kex_parse_config_auth_no_enc; + (void) test_kex_parse_config_cacert; + (void) test_kex_parse_config_unknown_key; (void) test_kex_parse_config_custom; (void) test_kex_parse_config_whitespace; (void) test_kex_parse_config_cipher; @@ -833,12 +1279,11 @@ int kex_test(int argc, (void) test_kex_all; (void) test_kex_validate_algo; (void) test_kex_get_algo_from_pk_all; - (void) test_kex_dhe_wrong_algo(); + (void) test_kex_dhe_wrong_algo; + (void) test_kex_dhe_no_kdf; (void) test_kex_dhe_corrupted_pubkey_all; (void) test_kex_load_dhe_privkey; (void) test_kex_load_dhe_pubkey; - - ret = TEST_RC_SKIP; #endif return ret; } diff --git a/src/lib/tests/kex_test_ml_kem.c b/src/lib/tests/kex_test_ml_kem.c index 7761c3dc..3059946c 100644 --- a/src/lib/tests/kex_test_ml_kem.c +++ b/src/lib/tests/kex_test_ml_kem.c @@ -220,7 +220,7 @@ static int test_kex_kem(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, kdf, s1); else ct_len = kex_kem_encap(pk, buf2, kdf, s1); @@ -267,6 +267,7 @@ static int test_kex_kem_corrupted_ciphertext(const char * algo) uint8_t s1[SYMMKEYSZ]; uint8_t s2[SYMMKEYSZ]; int kdf; + int ret; TEST_START("(%s)", algo); @@ -285,7 +286,7 @@ static int test_kex_kem_corrupted_ciphertext(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, kdf, s1); else ct_len = kex_kem_encap(pk, buf2, kdf, s1); @@ -301,14 +302,15 @@ static int test_kex_kem_corrupted_ciphertext(const char * algo) buf2[0] ^= 0xFF; buf2[ct_len - 1] ^= 0xFF; - /* ML-KEM uses implicit rejection */ - if (kex_kem_decap(pkp, ct, kdf, s2) < 0) { + /* EC hybrids may reject the corrupted point explicitly */ + ret = kex_kem_decap(pkp, ct, kdf, s2); + if (strstr(algo, "SecP") == NULL && ret < 0) { printf("Decapsulation failed unexpectedly.\n"); goto fail_pkp; } - /* The shared secrets should NOT match with corrupted CT */ - if (memcmp(s1, s2, SYMMKEYSZ) == 0) { + /* Corrupted CT must never yield the original secret */ + if (ret == 0 && memcmp(s1, s2, SYMMKEYSZ) == 0) { printf("Corrupted ciphertext produced same secret.\n"); goto fail_pkp; } @@ -360,7 +362,7 @@ static int test_kex_kem_wrong_keypair(const char * algo) } if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk1, buf3, NID_sha256, s1); + ct_len = kex_kem_encap_raw(algo, pk1, buf3, NID_sha256, s1); else ct_len = kex_kem_encap(pk1, buf3, NID_sha256, s1); @@ -422,7 +424,7 @@ static int test_kex_kem_truncated_ciphertext(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, NID_sha256, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, NID_sha256, s1); else ct_len = kex_kem_encap(pk, buf2, NID_sha256, s1); @@ -528,7 +530,7 @@ int kex_test_ml_kem(int argc, (void) argc; (void) argv; -#ifdef HAVE_OPENSSL_ML_KEM +#ifdef HAVE_ML ret |= test_kex_load_kem_privkey(); ret |= test_kex_load_kem_pubkey(); ret |= test_kex_kem_all(); diff --git a/src/lib/tests/keyrot_test.c b/src/lib/tests/keyrot_test.c new file mode 100644 index 00000000..efdc718e --- /dev/null +++ b/src/lib/tests/keyrot_test.c @@ -0,0 +1,1234 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the key-rotation schedule + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "config.h" + +#include <test/test.h> + +#ifdef HAVE_OPENSSL +#include <ouroboros/crypt.h> +#include <ouroboros/errno.h> +#include <ouroboros/pthread.h> + +#include "crypt/keyrot.h" + +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +static const uint8_t SEED_A[SYMMKEYSZ] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 +}; + +static const uint8_t SEED_B[SYMMKEYSZ] = { + 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, + 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, + 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, + 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0 +}; + +static int test_create_destroy(void) +{ + struct keyrot * kr; + + TEST_START(); + + kr = keyrot_create(SEED_A, 0, 0); + if (kr == NULL) + goto fail; + + keyrot_destroy(kr); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_epoch_range(void) +{ + struct keyrot * a; + + TEST_START(); + + /* epoch is a 4-bit wire field; 16 and up must be refused. */ + if (keyrot_create(SEED_A, 16, 0) != NULL) + goto fail; + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + if (keyrot_rekey(a, SEED_A, 16) == 0) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Epochs of the live batches (cur, prev) must stay unique. */ +static int test_rekey_dup_epoch(void) +{ + struct keyrot * a; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + if (keyrot_rekey(a, SEED_B, 0) != -1) { + printf("New key to the current epoch not a conflict.\n"); + goto fail_a; + } + + if (keyrot_rekey(a, SEED_B, 1) != 0) { + printf("Re-key to a fresh epoch refused.\n"); + goto fail_a; + } + + if (keyrot_rekey(a, SEED_B, 1) != -EREPLAY) { + printf("Same key to the current epoch not a replay.\n"); + goto fail_a; + } + + if (keyrot_rekey(a, SEED_A, 1) != -1) { + printf("New key to the current epoch not a conflict.\n"); + goto fail_a; + } + + if (keyrot_rekey(a, SEED_A, 0) != -EREPLAY) { + printf("Same key to the previous epoch not a replay.\n"); + goto fail_a; + } + + if (keyrot_rekey(a, SEED_B, 0) != -1) { + printf("New key to the previous epoch not a conflict.\n"); + goto fail_a; + } + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The 4-bit wire epoch legitimately wraps 15 -> 0. */ +static int test_rekey_epoch_wrap(void) +{ + struct keyrot * a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + + TEST_START(); + + a = keyrot_create(SEED_A, 14, 0); + if (a == NULL) + goto fail; + + if (keyrot_rekey(a, SEED_B, 15) != 0) + goto fail_a; + + if (keyrot_rekey(a, SEED_A, 0) != 0) { + printf("Epoch wrap 15 -> 0 refused.\n"); + goto fail_a; + } + + keyrot_tx_promote(a); + + if (keyrot_tx_next(a, sel, &k, n) != 0) { + printf("TX failed after epoch wrap.\n"); + goto fail_a; + } + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tx_deterministic(void) +{ + struct keyrot * a; + struct keyrot * b; + uint8_t sela[KR_SELECTOR_LEN]; + uint8_t selb[KR_SELECTOR_LEN]; + uint8_t na[KR_NONCE_LEN]; + uint8_t nb[KR_NONCE_LEN]; + uint8_t ka[SYMMKEYSZ]; + const uint8_t * pa; + const uint8_t * pb; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 0); + if (b == NULL) + goto fail_a; + + if (keyrot_tx_next(a, sela, &pa, na) != 0) + goto fail_b; + + /* Copy out: pa points into the tcache, pb may reuse the slot. */ + memcpy(ka, pa, SYMMKEYSZ); + if (keyrot_tx_next(b, selb, &pb, nb) != 0) + goto fail_b; + + if (memcmp(sela, selb, KR_SELECTOR_LEN) != 0) + goto fail_b; + + if (memcmp(ka, pb, SYMMKEYSZ) != 0) + goto fail_b; + + if (memcmp(na, nb, KR_NONCE_LEN) != 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_selector_layout(void) +{ + struct keyrot * a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + + TEST_START(); + + a = keyrot_create(SEED_A, 3, 0); + if (a == NULL) + goto fail; + + /* First packet: epoch 3, node 0, seq 0 */ + if (keyrot_tx_next(a, sel, &k, nonce) != 0) + goto fail_a; + + if ((sel[0] >> 4) != 3) /* epoch */ + goto fail_a; + + if ((((sel[0] & 0x0F) << 8) | sel[1]) != 0) /* node */ + goto fail_a; + + if (sel[2] != 0 || sel[3] != 0 || sel[4] != 0 || sel[5] != 0) + goto fail_a; + + /* Second packet: seq advances to 1 */ + if (keyrot_tx_next(a, sel, &k, nonce) != 0) + goto fail_a; + + if (sel[5] != 1) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_nodes_left_initial(void) +{ + struct keyrot * a; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + if (keyrot_tx_nodes_left(a) != KEY_NODE_COUNT) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_roundtrip(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t ntx[KR_NONCE_LEN]; + uint8_t nrx[KR_NONCE_LEN]; + uint8_t ktx[SYMMKEYSZ]; + const uint8_t * ptx; + const uint8_t * prx; + struct kr_rx rx; + int i; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + for (i = 0; i < 256; i++) { + if (keyrot_tx_next(a, sel, &ptx, ntx) != 0) + goto fail_b; + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0) + goto fail_b; + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + if (memcmp(ntx, nrx, KR_NONCE_LEN) != 0) + goto fail_b; + } + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_direction_separation(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sela[KR_SELECTOR_LEN]; + uint8_t selb[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + uint8_t ka[SYMMKEYSZ]; + const uint8_t * pa; + const uint8_t * pb; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + if (keyrot_tx_next(a, sela, &pa, n) != 0) + goto fail_b; + + memcpy(ka, pa, SYMMKEYSZ); + if (keyrot_tx_next(b, selb, &pb, n) != 0) + goto fail_b; + + /* Same position, different role -> different leaf key */ + if (memcmp(ka, pb, SYMMKEYSZ) == 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Build a selector by hand (test knows the wire format). */ +static void mk_sel(uint8_t epoch, + uint16_t node, + uint32_t seq, + uint8_t sel[KR_SELECTOR_LEN]) +{ + sel[0] = (uint8_t) ((epoch << 4) | ((node >> 8) & 0x0F)); + sel[1] = (uint8_t) (node & 0xFF); + sel[2] = (uint8_t) (seq >> 24); + sel[3] = (uint8_t) (seq >> 16); + sel[4] = (uint8_t) (seq >> 8); + sel[5] = (uint8_t) (seq); +} + +static int test_random_access(void) +{ + struct keyrot * b; + uint8_t s0[KR_SELECTOR_LEN]; + uint8_t s5[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + uint8_t k_first[SYMMKEYSZ]; + uint8_t k_node5[SYMMKEYSZ]; + const uint8_t * p; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 0, s0); + mk_sel(0, 5, 12345, s5); /* a far-ahead node, mid-span */ + + /* Jump straight to node 0 */ + if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0) + goto fail_b; + + memcpy(k_first, p, SYMMKEYSZ); + + /* Jump forward to node 5 (simulates a burst skip) */ + if (keyrot_rx_lookup(b, s5, &p, n, &rx) != 0) + goto fail_b; + + memcpy(k_node5, p, SYMMKEYSZ); + + /* Different nodes must yield different keys */ + if (memcmp(k_first, k_node5, SYMMKEYSZ) == 0) + goto fail_b; + + /* Jump back to node 0: still works, identical (no wedge) */ + if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0) + goto fail_b; + + if (memcmp(k_first, p, SYMMKEYSZ) != 0) + goto fail_b; + + /* Out-of-range node must be rejected */ + mk_sel(0, KEY_NODE_COUNT, 0, s0); + if (keyrot_rx_lookup(b, s0, &p, n, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Look up and commit one within-node counter on epoch 0. Returns 0 on + * accept, 1 on a rejected commit (replay or too old), and -1 if the + * lookup itself failed - kept distinct so a reject assertion can never + * pass on an unrelated lookup miss. + */ +static int commit_ctr(struct keyrot * kr, + uint32_t ctr) +{ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + mk_sel(0, 0, ctr, sel); + + if (keyrot_rx_lookup(kr, sel, &k, n, &rx) != 0) + return -1; + + return keyrot_rx_commit(kr, &rx) == 0 ? 0 : 1; +} + +static int test_replay_window(void) +{ + struct keyrot * b; + struct keyrot * c; + uint32_t base; + uint32_t jump; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* Fresh counters accepted; an immediate replay is rejected. */ + if (commit_ctr(b, 100) != 0) + goto fail_b; + + if (commit_ctr(b, 100) != 1) + goto fail_b; + + /* In-window reorder: accepted once, rejected on replay. */ + if (commit_ctr(b, 105) != 0) + goto fail_b; + + if (commit_ctr(b, 102) != 0) + goto fail_b; + + if (commit_ctr(b, 102) != 1) + goto fail_b; + + /* Too-old boundary: the window edge is rejected, just inside is not. */ + base = 4 * KEY_REPLAY_WINDOW; + if (commit_ctr(b, base) != 0) + goto fail_b; + + if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64)) != 1) + goto fail_b; + + if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64) + 1) != 0) + goto fail_b; + + /* + * RFC 6479 slack-word regression: two low counters, then a + * forward jump of a full bitmap that aliases their slot, then a + * replay of a low counter. Without the reserved slack word this + * replay is wrongly accepted. + */ + c = keyrot_create(SEED_A, 0, 1); + if (c == NULL) + goto fail_b; + + if (commit_ctr(c, 70) != 0) + goto fail_c; + + if (commit_ctr(c, 74) != 0) + goto fail_c; + + jump = KEY_REPLAY_WINDOW + 63; + if (commit_ctr(c, jump) != 0) + goto fail_c; + + if (commit_ctr(c, 74) != 1) + goto fail_c; + + keyrot_destroy(c); + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_c: + keyrot_destroy(c); + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_lookup_no_commit(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + int i; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 100, sel); + + /* Repeated lookups are pre-AEAD and must not consume the slot. */ + for (i = 0; i < 4; i++) { + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + } + + /* The slot is still fresh, so the first commit accepts ... */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + /* ... and only the commit advanced it, so the next is a replay. */ + if (keyrot_rx_commit(b, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_commit_prev_batch(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* Capture a packet under cur (epoch 0). */ + mk_sel(0, 0, 7, sel); + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + /* Re-key: the captured batch becomes prev and the flag clears. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* The straggler commits under prev without claiming a switch. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + /* prev still holds a replay window: its replay is rejected. */ + if (keyrot_rx_commit(b, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_replay_forward_clear(void) +{ + struct keyrot * d; + uint32_t low; + uint32_t alias; + uint32_t jump; + + TEST_START(); + + d = keyrot_create(SEED_A, 0, 1); + if (d == NULL) + goto fail; + + /* alias shares low's slot a window away; the jump must clear it. */ + low = 10; + alias = low + KEY_REPLAY_WINDOW; + jump = alias + KEY_REPLAY_WINDOW / 2; + + if (commit_ctr(d, low) != 0) + goto fail_d; + + if (commit_ctr(d, jump) != 0) + goto fail_d; + + if (commit_ctr(d, alias) != 0) + goto fail_d; + + if (commit_ctr(d, alias) != 1) + goto fail_d; + + keyrot_destroy(d); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_d: + keyrot_destroy(d); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_rekey_overlap(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t old_sel[KR_SELECTOR_LEN]; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t ntx[KR_NONCE_LEN]; + uint8_t nrx[KR_NONCE_LEN]; + uint8_t ktx[SYMMKEYSZ]; + const uint8_t * ptx; + const uint8_t * prx; + struct kr_rx rx; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + /* Send one gen-0 packet; keep its selector for the overlap. */ + if (keyrot_tx_next(a, old_sel, &ptx, ntx) != 0) + goto fail_b; + + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0) + goto fail_b; + + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + + /* Both ends re-key to epoch 1 with a fresh seed. */ + if (keyrot_rekey(a, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* TX is gated until promotion; promote a to emit the new epoch. */ + keyrot_tx_promote(a); + + /* New gen-1 traffic works. */ + if (keyrot_tx_next(a, sel, &ptx, ntx) != 0) + goto fail_b; + + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0) + goto fail_b; + + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + + /* A straggling gen-0 packet still decrypts (overlap window). */ + if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0) + goto fail_b; + + /* An unknown epoch is rejected. */ + mk_sel(7, 0, 0, sel); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tx_gate(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * p; + struct kr_rx rx; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + /* Both re-key to epoch 1; TX must stay on epoch 0 until promoted. */ + if (keyrot_rekey(a, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* a's TX still stamps the old epoch (0). */ + if (keyrot_tx_next(a, sel, &p, n) != 0) + goto fail_b; + + if ((sel[0] >> 4) != 0) + goto fail_b; + + /* b decrypts the old-epoch packet via its prev batch. */ + if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0) + goto fail_b; + + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + /* b has not yet seen the new epoch from a. */ + if (keyrot_peer_switched(b)) + goto fail_b; + + /* a promotes; its TX now stamps the new epoch (1). */ + keyrot_tx_promote(a); + if (keyrot_tx_next(a, sel, &p, n) != 0) + goto fail_b; + + if ((sel[0] >> 4) != 1) + goto fail_b; + + /* b sees the new epoch and reports the peer switched. */ + if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0) + goto fail_b; + + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (!keyrot_peer_switched(b)) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_peer_switched_commit_only(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* A re-key clears the flag until a packet is seen on cur. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + mk_sel(1, 0, 0, sel); + + /* Lookup is pre-AEAD: selecting a key must not flip the flag. */ + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + /* Commit runs post-AEAD and is what records the peer switched. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (!keyrot_peer_switched(b)) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_commit_evicted(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 3, sel); + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + /* Two re-keys drop the captured batch from both cur and prev. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_A, 2) != 0) + goto fail_b; + + /* Commit on an evicted batch is a silent no-op, not a fault. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* TX fails closed when the tx_epoch batch is evicted, until promote. */ +static int test_tx_fail_closed(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 0); + if (b == NULL) + goto fail; + + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_tx_next(b, sel, &k, n) != 0) { + printf("TX should keep the old epoch after one re-key.\n"); + goto fail_b; + } + + /* Second re-key without promote evicts the TX epoch-0 batch. */ + if (keyrot_rekey(b, SEED_A, 2) != 0) + goto fail_b; + + if (keyrot_tx_next(b, sel, &k, n) == 0) { + printf("TX should fail closed with tx_epoch evicted.\n"); + goto fail_b; + } + + keyrot_tx_promote(b); + + if (keyrot_tx_next(b, sel, &k, n) != 0) { + printf("TX should resync after promote.\n"); + goto fail_b; + } + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Concurrency: many TX threads + RX + re-key share one keyrot. The + * (epoch, counter) the TX side stamps must be globally unique (no AEAD + * nonce reuse). Capped below 16 re-keys so epoch maps 1:1 to a batch and + * the wire epoch never wraps (a wrapped epoch under a fresh key is not + * reuse but would false-trip the uniqueness check). Run under TSan to + * catch data races the static reviews can't. + */ +#define CT_THREADS 4 +#define CT_PKTS 2000 +#define CT_REKEYS 8 + +struct ct_rec { + uint8_t epoch; + uint64_t ctr; +}; + +struct ct_arg { + struct keyrot * kr; + struct ct_rec * recs; + size_t n; +}; + +static void * ct_tx_thread(void * a) +{ + struct ct_arg * arg = a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + uint64_t ctr; + size_t i; + size_t j; + + for (i = 0; i < CT_PKTS; i++) { + if (keyrot_tx_next(arg->kr, sel, &k, nonce) != 0) + continue; + + ctr = 0; + for (j = 0; j < 8; j++) + ctr = (ctr << 8) | nonce[j]; + + arg->recs[arg->n].epoch = (uint8_t) (sel[0] >> 4); + arg->recs[arg->n].ctr = ctr; + arg->n++; + } + + return NULL; +} + +static void * ct_rx_thread(void * a) +{ + struct keyrot * kr = a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + size_t i; + + /* Exercise rx_lookup against re-key reclaim; results ignored. */ + for (i = 0; i < CT_PKTS; i++) { + mk_sel((uint8_t) (i % 16), 0, (uint32_t) i, sel); + if (keyrot_rx_lookup(kr, sel, &k, nonce, &rx) == 0) + (void) keyrot_rx_commit(kr, &rx); + } + + return NULL; +} + +static void * ct_rekey_thread(void * a) +{ + struct keyrot * kr = a; + struct timespec t; + int e; + + t.tv_sec = 0; + t.tv_nsec = 2 * 1000 * 1000; /* 2 ms */ + + for (e = 1; e <= CT_REKEYS; e++) { + nanosleep(&t, NULL); + if (keyrot_rekey(kr, (e & 1) ? SEED_B : SEED_A, + (uint8_t) e) != 0) + break; + keyrot_tx_promote(kr); + } + + return NULL; +} + +static int ct_cmp(const void * x, + const void * y) +{ + const struct ct_rec * a = x; + const struct ct_rec * b = y; + + if (a->epoch != b->epoch) + return a->epoch < b->epoch ? -1 : 1; + + if (a->ctr != b->ctr) + return a->ctr < b->ctr ? -1 : 1; + + return 0; +} + +static int test_concurrent_nonce_unique(void) +{ + struct keyrot * kr; + struct ct_arg arg[CT_THREADS]; + pthread_t tx[CT_THREADS]; + pthread_t rx; + pthread_t rk; + struct ct_rec * all; + size_t total; + size_t i; + bool reuse = false; + + TEST_START(); + + kr = keyrot_create(SEED_A, 0, 0); + if (kr == NULL) + goto fail; + + all = malloc(sizeof(*all) * CT_THREADS * CT_PKTS); + if (all == NULL) + goto fail_kr; + + for (i = 0; i < CT_THREADS; i++) { + arg[i].kr = kr; + arg[i].n = 0; + arg[i].recs = all + i * CT_PKTS; + } + + for (i = 0; i < CT_THREADS; i++) + pthread_create(&tx[i], NULL, ct_tx_thread, &arg[i]); + + pthread_create(&rx, NULL, ct_rx_thread, kr); + pthread_create(&rk, NULL, ct_rekey_thread, kr); + + for (i = 0; i < CT_THREADS; i++) + pthread_join(tx[i], NULL); + + pthread_join(rx, NULL); + pthread_join(rk, NULL); + + total = 0; + for (i = 0; i < CT_THREADS; i++) { + memmove(all + total, all + i * CT_PKTS, + arg[i].n * sizeof(*all)); + total += arg[i].n; + } + + qsort(all, total, sizeof(*all), ct_cmp); + + for (i = 1; i < total; i++) + if (ct_cmp(&all[i - 1], &all[i]) == 0) { + printf("(epoch %u, ctr %llu) reused\n", + all[i].epoch, + (unsigned long long) all[i].ctr); + reuse = true; + break; + } + + free(all); + + if (reuse) + goto fail_kr; + + keyrot_destroy(kr); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_kr: + keyrot_destroy(kr); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif /* HAVE_OPENSSL */ + +int keyrot_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL + ret |= test_create_destroy(); + ret |= test_epoch_range(); + ret |= test_rekey_dup_epoch(); + ret |= test_rekey_epoch_wrap(); + ret |= test_tx_deterministic(); + ret |= test_selector_layout(); + ret |= test_nodes_left_initial(); + ret |= test_roundtrip(); + ret |= test_direction_separation(); + ret |= test_random_access(); + ret |= test_peer_switched_commit_only(); + ret |= test_commit_evicted(); + ret |= test_tx_fail_closed(); + ret |= test_replay_window(); + ret |= test_lookup_no_commit(); + ret |= test_commit_prev_batch(); + ret |= test_replay_forward_clear(); + ret |= test_rekey_overlap(); + ret |= test_tx_gate(); + ret |= test_concurrent_nonce_unique(); +#endif + return ret; +} diff --git a/src/lib/tests/poa_test.c b/src/lib/tests/poa_test.c new file mode 100644 index 00000000..99886769 --- /dev/null +++ b/src/lib/tests/poa_test.c @@ -0,0 +1,307 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Flow PoA tests + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 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., http://www.fsf.org/about/contact/. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#define _DEFAULT_SOURCE +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include <test/test.h> + +#include "poa/addr.c" +#ifdef HAVE_RAW_SOCKETS +#include "poa/eth.c" +#endif + +#include <arpa/inet.h> +#include <stdio.h> + +#ifdef HAVE_RAW_SOCKETS +static const uint8_t eth_our_mac[POA_MAC_SIZE] = + { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; +static const uint8_t eth_far_mac[POA_MAC_SIZE] = + { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; +static const uint8_t eth_bc_mac[POA_MAC_SIZE] = + { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; +#endif + +/* PoA core stubs: the reader threads never run in this test. */ +void poa_rx_pkt(struct poa * poa, + uint32_t eid, + struct ssm_pk_buff * spb) +{ + (void) poa; + (void) eid; + (void) spb; +} + +void poa_rx_mgmt(struct poa * poa, + const struct poa_addr * src, + const uint8_t * buf, + size_t len) +{ + (void) poa; + (void) src; + (void) buf; + (void) len; +} + +int poa_spb_reserve(struct ssm_pk_buff ** spb, + size_t len) +{ + (void) spb; + (void) len; + + return -1; +} + +size_t poa_link_updown(int ifindex, + bool up) +{ + (void) ifindex; + (void) up; + + return 0; +} + +bool poa_has_name(const uint8_t * hash) +{ + (void) hash; + + return false; +} + +int poa_bcast_mgmt(const struct poa_addr * dst, + const uint8_t * buf, + size_t len) +{ + (void) dst; + (void) buf; + (void) len; + + return 0; +} + +static void udp4_addr(struct poa_addr * addr, + const char * ip, + uint16_t port) +{ + memset(addr, 0, sizeof(*addr)); + + addr->type = POA_UDP4; + addr->udp4.port = port; + + inet_pton(AF_INET, ip, &addr->udp4.ip_addr); +} + +static int test_poa_addr_cmp(void) +{ + struct poa_addr a; + struct poa_addr b; + + TEST_START(); + + udp4_addr(&a, "10.0.0.10", 3435); + udp4_addr(&b, "10.0.0.10", 3435); + + if (poa_addr_cmp(&a, &b) != 0) { + printf("Identical addresses did not match.\n"); + goto fail; + } + + udp4_addr(&b, "10.0.0.10", 3436); + + if (poa_addr_cmp(&a, &b) == 0) { + printf("Addresses with a different port matched.\n"); + goto fail; + } + + udp4_addr(&b, "10.0.0.11", 3435); + + if (poa_addr_cmp(&a, &b) == 0) { + printf("Addresses with a different ip matched.\n"); + goto fail; + } + + b.type = POA_UDP6; + if (poa_addr_cmp(&a, &b) == 0) { + printf("Addresses of a different type matched.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_poa_mgmt_msg_qos(void) +{ + struct poa_mgmt_msg msg; + qosspec_t qs; + + TEST_START(); + + poa_mgmt_msg_ser(&msg, POA_FLOW_REQ, 1, 2, qos_stream, 0, 0); + + if (msg.code != POA_FLOW_REQ) { + printf("Wrong code in management message.\n"); + goto fail; + } + + if (ntoh32(msg.s_eid) != 1 || ntoh32(msg.d_eid) != 2) { + printf("Wrong PoA ids in management message.\n"); + goto fail; + } + + memset(&qs, 0, sizeof(qs)); + + poa_mgmt_msg_qos(&msg, &qs); + + if (memcmp(&qs, &qos_stream, sizeof(qs)) != 0) { + printf("QoS did not survive the management message.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +#ifdef HAVE_RAW_SOCKETS + +static void eth_fake_priv(struct eth_priv * priv, + struct poa * e) +{ + memset(priv, 0, sizeof(*priv)); + memset(e, 0, sizeof(*e)); + + e->type = POA_ETH; + + strcpy(e->local.eth.src.dev, "test0"); + + priv->poa = e; + priv->ethertype = htons(0xA000); + priv->mtu = 1500; + + memcpy(priv->hw_addr, eth_our_mac, POA_MAC_SIZE); +} + +static int test_eth_frame(void) +{ + struct eth_priv priv; + struct poa e; + uint8_t buf[64]; + struct eth_hdr * hdr = (struct eth_hdr *) buf; + const char * body = "hello"; + size_t n; + size_t plen; + uint32_t eid; + + TEST_START(); + + eth_fake_priv(&priv, &e); + + eth_hdr_ser(&priv, hdr, eth_our_mac, 7, strlen(body)); + + memcpy(buf + ETH_HDR_TOT_SIZE, body, strlen(body)); + + n = ETH_HDR_TOT_SIZE + strlen(body); + if (frame_parse(&priv, buf, n, &eid, &plen) < 0) { + printf("Failed to parse a valid frame.\n"); + goto fail; + } + + if (eid != 7 || plen != strlen(body)) { + printf("Expected eid 7 len %zu, got %u len %zu.\n", + strlen(body), eid, plen); + goto fail; + } + + hdr->poa.hcs ^= 0xFF; + + if (frame_parse(&priv, buf, n, &eid, &plen) == 0) { + printf("Accepted a corrupt header checksum.\n"); + goto fail; + } + + hdr->poa.hcs ^= 0xFF; + hdr->ethertype ^= 0xFF; + + if (frame_parse(&priv, buf, n, &eid, &plen) == 0) { + printf("Accepted a foreign Ethertype.\n"); + goto fail; + } + + hdr->ethertype ^= 0xFF; + + if (frame_parse(&priv, buf, ETH_HDR_SIZE + 2, &eid, &plen) == 0) { + printf("Accepted a truncated frame.\n"); + goto fail; + } + + eth_hdr_ser(&priv, hdr, eth_far_mac, 7, strlen(body)); + + if (frame_parse(&priv, buf, n, &eid, &plen) == 0) { + printf("Accepted a frame for another host.\n"); + goto fail; + } + + eth_hdr_ser(&priv, hdr, eth_bc_mac, 7, strlen(body)); + + if (frame_parse(&priv, buf, n, &eid, &plen) < 0) { + printf("Rejected a broadcast frame.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +#endif /* HAVE_RAW_SOCKETS */ + +int poa_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_poa_addr_cmp(); + ret |= test_poa_mgmt_msg_qos(); +#ifdef HAVE_RAW_SOCKETS + ret |= test_eth_frame(); +#endif + + return ret; +} diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 6b418838..bb601733 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -15,6 +15,10 @@ set(IRM_SOURCES irm/irm_ipcp_destroy.c irm/irm_ipcp_bootstrap.c irm/irm_ipcp_enroll.c + irm/irm_ipcp_poa.c + irm/irm_ipcp_poa_attach.c + irm/irm_ipcp_poa_detach.c + irm/irm_ipcp_poa_list.c irm/irm_ipcp_list.c irm/irm_ipcp_connect.c irm/irm_ipcp_disconnect.c diff --git a/src/tools/irm/irm_bind.c b/src/tools/irm/irm_bind.c index 3107837a..2d3fc554 100644 --- a/src/tools/irm/irm_bind.c +++ b/src/tools/irm/irm_bind.c @@ -78,8 +78,8 @@ static int do_cmd(const char * argv0, const struct cmd * c; for (c = cmds; c->cmd; ++c) { - if (!matches(argv0, c->cmd)) - return c->func(argc, argv); + if (matches(argv0, c->cmd) == 0) + return c->func(argc - 1, argv + 1); } fprintf(stderr, "\"%s\" is unknown, try \"irm bind help\".\n", argv0); diff --git a/src/tools/irm/irm_bind_ipcp.c b/src/tools/irm/irm_bind_ipcp.c index 4c183534..b6223074 100644 --- a/src/tools/irm/irm_bind_ipcp.c +++ b/src/tools/irm/irm_bind_ipcp.c @@ -61,15 +61,20 @@ int do_bind_ipcp(int argc, ssize_t len; ssize_t i; + if (argc < 1) { + usage(); + return -1; + } + + ipcp = *argv; + ++argv; + --argc; + while (argc > 0) { if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "ipcp") == 0) { - ipcp = *(argv + 1); - ++argv; - --argc; } else { printf("\"%s\" is unknown, try \"irm " "bind ipcp\".\n", *argv); diff --git a/src/tools/irm/irm_bind_process.c b/src/tools/irm/irm_bind_process.c index fee0c46b..c401df14 100644 --- a/src/tools/irm/irm_bind_process.c +++ b/src/tools/irm/irm_bind_process.c @@ -61,20 +61,24 @@ int do_bind_process(int argc, char * name = NULL; char * t; + if (argc < 1) { + usage(); + return -1; + } + + pid = strtol(*argv, &t, 10); + if (*argv == t || *t != '\0' || kill(pid, 0)) { + printf("\"%s\" is not a valid process id.\n", *argv); + return -1; + } + ++argv; + --argc; + while (argc > 1) { if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "process") == 0) { - pid = strtol(*(argv + 1), &t, 10); - if (*(argv + 1) == t || *t != '\0' || kill(pid, 0)) { - printf("\"%s\" is not a valid process id.\n", - *(argv + 1)); - return -1; - } - ++argv; - --argc; } else { printf("\"%s\" is unknown, try \"irm " "bind process\".\n", *argv); diff --git a/src/tools/irm/irm_bind_program.c b/src/tools/irm/irm_bind_program.c index 14d09db7..592c2ec3 100644 --- a/src/tools/irm/irm_bind_program.c +++ b/src/tools/irm/irm_bind_program.c @@ -67,18 +67,23 @@ int do_bind_program(int argc, int ret = 0; char * temp = NULL; + if (argc < 1) { + usage(); + return -1; + } + + temp = realpath(*argv, NULL); + if (temp != NULL) + *argv = temp; + prog = *argv; + ++argv; + --argc; + while (argc > 0) { if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "program") == 0) { - ++argv; - temp = realpath(*argv, NULL); - if (temp != NULL) - *argv = temp; - prog = *argv; - --argc; } else if (strcmp(*argv, "auto") == 0) { flags |= BIND_AUTO; } else if (strcmp(*argv, "--") == 0) { diff --git a/src/tools/irm/irm_ipcp.c b/src/tools/irm/irm_ipcp.c index 34458a20..5c0db9cf 100644 --- a/src/tools/irm/irm_ipcp.c +++ b/src/tools/irm/irm_ipcp.c @@ -46,6 +46,7 @@ static void usage(void) printf("Usage: irm ipcp [OPERATION]\n\n" "where OPERATION in {create destroy\n" " bootstrap enroll\n" + " poa\n" " connect disconnect\n" " list\n" " help}\n"); @@ -68,6 +69,7 @@ static const struct cmd { { "destroy", do_destroy_ipcp }, { "bootstrap", do_bootstrap_ipcp }, { "enroll", do_enroll_ipcp }, + { "poa", poa_cmd }, { "connect", do_connect_ipcp }, { "disconnect", do_disconnect_ipcp }, { "list", do_list_ipcp}, diff --git a/src/tools/irm/irm_ipcp_bootstrap.c b/src/tools/irm/irm_ipcp_bootstrap.c index de73b076..bbcf2312 100644 --- a/src/tools/irm/irm_ipcp_bootstrap.c +++ b/src/tools/irm/irm_ipcp_bootstrap.c @@ -53,10 +53,6 @@ #define UNICAST "unicast" #define BROADCAST "broadcast" -#define IP_UDP4 "udp4" -#define IP_UDP6 "udp6" -#define ETH_LLC "eth-llc" -#define ETH_DIX "eth-dix" #define LOCAL "local" #define MD5 "MD5" @@ -76,18 +72,13 @@ #define DT(x) default_dt_config.x #define DHT(x) default_dht_config.params.x #define UNI(x) default_uni_config.x -#define DIX(x) eth_dix_default_conf.eth.x -#define LLC(x) eth_llc_default_conf.eth.x -#define UD4(x) udp4_default_conf.udp4.x -#define UD6(x) udp6_default_conf.udp6.x static char * usage_str = \ "Usage: irm ipcp bootstrap\n" " name <ipcp name>\n" " layer <layer name>\n" " [type [TYPE]]\n" - "where TYPE in {" UNICAST " " BROADCAST " " LOCAL " " - IP_UDP4 " " IP_UDP6 " " ETH_LLC " " ETH_DIX "},\n\n" + "where TYPE in {" UNICAST " " BROADCAST " " LOCAL "},\n\n" "if TYPE == " UNICAST "\n" " [addr_auth <ADDRESS_POLICY> (default: %s)]\n" " [directory <DIRECTORY_POLICY> (default: %s)]\n" @@ -105,7 +96,8 @@ static char * usage_str = \ " [Data Transfer Constants]\n" " [addr <address size> (default: %d)]\n" " [eid <eid size> (default: %d)]\n" - " [ttl <max time-to-live>, default: %d)]\n\n" + " [ttl <max time-to-live>, default: %d)]\n" + " [rtt <max layer RTT in ms> (default: %d)]\n\n" "if DIRECTORY_POLICY == " DHT_DIR "\n" " [dht_alpha <search factor> (default: %u)]\n" " [dht_k <replication factor> (default: %u)]\n" @@ -116,28 +108,6 @@ static char * usage_str = \ " [ls_t_recalc <pff recalc interval (s)> (default: %ld)]\n" " [ls_t_update <LSA update interval (s)> (default: %ld)]\n" " [ls_t_timeo <link timeout (s)> (default: %ld)]\n\n" - "if TYPE == " IP_UDP4 "\n" - " ip <IP address in dotted notation>\n" - " [port <UDP port> (default: %d)]\n" - " [dns <DDNS IPv4 address in dotted notation>" - " (default: none)]\n\n" - "if TYPE == " IP_UDP6 "\n" - " ip <IPv6 address>\n" - " [port <UDP port> (default: %d)]\n" - " [dns <DDNS IPv6 address>" - " (default: none)]\n\n" - - "if TYPE == " ETH_LLC "\n" - " dev <interface name>\n" - " [hash [ALGORITHM] (default: %s)]\n" - "where ALGORITHM in {" SHA3_224 " " SHA3_256 " " - SHA3_384 " " SHA3_512 "}\n\n" - "if TYPE == " ETH_DIX "\n" - " dev <interface name>\n" - " [ethertype <ethertype> (default: 0x%4X)]\n" - " [hash [ALGORITHM] (default: %s)]\n" - "where ALGORITHM in {" SHA3_224 " " SHA3_256 " " - SHA3_384 " " SHA3_512 "}\n\n" "if TYPE == " LOCAL "\n" " [hash [ALGORITHM] (default: %s)]\n" "where ALGORITHM in {" SHA3_224 " " SHA3_256 " " @@ -152,22 +122,13 @@ static void usage(void) /* unicast */ FLAT_RANDOM, DHT_DIR, SHA3_256, LINK_STATE, MB_ECN_CA, /* dt */ - DT(addr_size), DT(eid_size), DT(max_ttl), + DT(addr_size), DT(eid_size), DT(max_ttl), DT(max_rtt), /* dht */ DHT(alpha), DHT(k), DHT(t_expire), DHT(t_refresh), DHT(t_replicate), /* ls */ default_ls_config.t_recalc, default_ls_config.t_update, default_ls_config.t_timeo, - /* udp4 */ - UD4(port), - /* udp6 */ - UD6(port), - /* eth_llc */ - SHA3_256, - /* eth_dix */ - DIX(ethertype), - SHA3_256, /* local */ SHA3_256, /* broadcast */ @@ -184,23 +145,14 @@ int do_bootstrap_ipcp(int argc, uint8_t addr_size = DT(addr_size); uint8_t eid_size = DT(eid_size); uint8_t max_ttl = DT(max_ttl); + uint16_t max_rtt = DT(max_rtt); struct routing_config routing = default_routing_config; enum pol_addr_auth addr_auth_type = UNI(addr_auth_type); enum pol_cong_avoid cong_avoid = UNI(cong_avoid); enum pol_dir_hash hash_algo = DIR_HASH_SHA3_256; - char * ipstr = NULL; - char * dnsstr = NULL; - struct in_addr ip4_addr = {.s_addr = INADDR_ANY}; - struct in_addr dns4_addr = UD4(dns_addr); - int port4 = UD4(port); - struct in6_addr ip6_addr = IN6ADDR_ANY_INIT; - struct in6_addr dns6_addr = UD6(dns_addr); - int port6 = UD6(port); char * ipcp_type = NULL; enum ipcp_type type = IPCP_INVALID; char * layer = NULL; - char * dev = NULL; - uint16_t ethertype = DIX(ethertype); struct ipcp_list_info * ipcps; ssize_t len = 0; int i = 0; @@ -209,6 +161,14 @@ int do_bootstrap_ipcp(int argc, while (argc > 0) { cargs = 2; + if (matches(*argv, "autobind") == 0) + cargs = 1; + + if (argc < cargs) { + usage(); + return -1; + } + if (matches(*argv, "type") == 0) { ipcp_type = *(argv + 1); } else if (matches(*argv, "layer") == 0) { @@ -226,33 +186,14 @@ int do_bootstrap_ipcp(int argc, hash_algo = DIR_HASH_SHA3_512; else goto unknown_param; - } else if (matches(*argv, "ip") == 0) { - ipstr = *(argv + 1); - } else if (matches(*argv, "dns") == 0) { - dnsstr = *(argv + 1); - } else if (matches(*argv, "device") == 0) { - dev = *(argv + 1); - } else if (matches(*argv, "ethertype") == 0) { - /* NOTE: We might do some more checks on strtol. */ - if (matches(*(argv + 1), "0x") == 0) - ethertype = strtol(*(argv + 1), NULL, 0); - else - ethertype = strtol(*(argv + 1), NULL, 16); - if (ethertype < 0x0600 || ethertype >= 0xFFFF) { - printf("Invalid Ethertype: \"%s\".\n" - "Recommended range: 0xA000-0xEFFF.\n", - *(argv + 1)); - return -1; - } } else if (matches(*argv, "addr") == 0) { addr_size = atoi(*(argv + 1)); } else if (matches(*argv, "eid") == 0) { eid_size = atoi(*(argv + 1)); } else if (matches(*argv, "ttl") == 0) { max_ttl = atoi(*(argv + 1)); - } else if (matches(*argv, "port") == 0) { - port4 = atoi(*(argv + 1)); - port6 = port4; + } else if (matches(*argv, "rtt") == 0) { + max_rtt = atoi(*(argv + 1)); } else if (matches(*argv, "autobind") == 0) { autobind = true; cargs = 1; @@ -330,55 +271,11 @@ int do_bootstrap_ipcp(int argc, type = IPCP_UNICAST; else if (matches(ipcp_type, BROADCAST) == 0) type = IPCP_BROADCAST; - else if (matches(ipcp_type, IP_UDP4) == 0) - type = IPCP_UDP4; - else if (matches(ipcp_type, IP_UDP6) == 0) - type = IPCP_UDP6; - else if (matches(ipcp_type, ETH_DIX) == 0) - type = IPCP_ETH_DIX; - else if (matches(ipcp_type, ETH_LLC) == 0) - type = IPCP_ETH_LLC; else if (matches(ipcp_type, LOCAL) == 0) type = IPCP_LOCAL; else goto fail_usage; } - if (type == IPCP_UDP4) { - if (inet_pton (AF_INET, ipstr, &ip4_addr) != 1) { - printf("Invalid IPv4 address: \"%s\".\n", ipstr); - goto fail_usage; - } - - if (ip4_addr.s_addr == INADDR_ANY) { - printf("Cannot use IPv4 address: \"%s\".\n", ipstr); - goto fail_usage; - } - - if (dnsstr != NULL && - inet_pton(AF_INET, dnsstr, &dns4_addr) != 1) { - printf("Invalid DNS IPv4 address: \"%s\".\n", dnsstr); - goto fail_usage; - } - } - - if (type == IPCP_UDP6) { - if (inet_pton(AF_INET6, ipstr, &ip6_addr) != 1) { - printf("Invalid IPv6 address: \"%s\".\n", ipstr); - goto fail_usage; - } - - if (IN6_IS_ADDR_UNSPECIFIED(&ip6_addr)) { - printf("Cannot use IPv6 address: \"%s\".\n", ipstr); - goto fail_usage; - } - - if (dnsstr != NULL && - inet_pton(AF_INET6, dnsstr, &dns6_addr) != 1) { - printf("Invalid DNS IPv6 address: \"%s\".\n", dnsstr); - goto fail_usage; - } - } - if (pid == -1) { if (ipcp_type == NULL) { printf("No IPCPs matching %s found.\n\n", ipcp); @@ -422,34 +319,12 @@ int do_bootstrap_ipcp(int argc, conf.unicast.dt.addr_size = addr_size; conf.unicast.dt.eid_size = eid_size; conf.unicast.dt.max_ttl = max_ttl; + conf.unicast.dt.max_rtt = max_rtt; conf.unicast.dt.routing = routing; conf.unicast.addr_auth_type = addr_auth_type; conf.unicast.cong_avoid = cong_avoid; conf.unicast.dir = dir_config; break; - case IPCP_UDP4: - conf.udp4.ip_addr = ip4_addr; - conf.udp4.dns_addr = dns4_addr; - conf.udp4.port = port4; - break; - case IPCP_UDP6: - conf.udp6.ip_addr = ip6_addr; - conf.udp6.dns_addr = dns6_addr; - conf.udp6.port = port6; - break; - case IPCP_ETH_DIX: - conf.eth.ethertype = ethertype; - /* FALLTHRU */ - case IPCP_ETH_LLC: - if (dev == NULL) - goto fail_usage; - if (strlen(dev) > DEV_NAME_SIZE) { - printf("Device name too long.\n\n"); - goto fail_usage; - } - - strcpy(conf.eth.dev, dev); - break; case IPCP_BROADCAST: /* FALLTHRU */ case IPCP_LOCAL: diff --git a/src/tools/irm/irm_ipcp_connect.c b/src/tools/irm/irm_ipcp_connect.c index fb21faec..66646e7d 100644 --- a/src/tools/irm/irm_ipcp_connect.c +++ b/src/tools/irm/irm_ipcp_connect.c @@ -44,6 +44,7 @@ #include <stdio.h> #include <stdlib.h> +#include <arpa/inet.h> #include <string.h> #define DT "dt" @@ -55,10 +56,14 @@ static void usage(void) " name <ipcp name>\n" " dst <name of destination IPCP>\n" " [component [COMPONENT]]\n" - "where COMPONENT in {" DT " " MGMT "}\n\n" + " [udp [UDP_PEER]]\n" + " [eth [dev <device>]" + " [ethertype <ethertype>]]\n" + "where COMPONENT in {" DT " " MGMT "}\n" + "and UDP_PEER is <IP address or host>[:<port>]\n\n" "if COMPONENT == " DT "\n" - " [qos [QOS]\n" - "where QOS in {raw, best, voice, video, data}\n"); + " [qos [QOS]]\n" + "where QOS in {raw, safe, rt, rt-safe, msg}\n"); } int do_connect_ipcp(int argc, @@ -69,13 +74,29 @@ int do_connect_ipcp(int argc, char * comp = "*"; char * component = NULL; char * qos = NULL; + char * udpstr = NULL; + char * devstr = NULL; + uint16_t ethertype = POA_ETHERTYPE; + bool eth = false; + struct poa_addr addr; + struct poa_addr * pa = NULL; struct ipcp_list_info * ipcps; ssize_t len = 0; pid_t pid = -1; ssize_t i; + int cargs; qosspec_t qs = qos_raw; while (argc > 0) { + cargs = 2; + if (strcmp(*argv, "eth") == 0) + cargs = 1; + + if (argc < cargs) { + usage(); + return -1; + } + if (matches(*argv, "name") == 0) { ipcp = *(argv + 1); } else if (matches(*argv, "dst") == 0) { @@ -84,21 +105,66 @@ int do_connect_ipcp(int argc, comp = *(argv + 1); } else if (matches(*argv, "qos") == 0) { qos = *(argv + 1); + } else if (strcmp(*argv, "udp") == 0) { + udpstr = *(argv + 1); + } else if (strcmp(*argv, "dev") == 0) { + devstr = *(argv + 1); + } else if (strcmp(*argv, "ethertype") == 0) { + if (parse_ethertype(*(argv + 1), ðertype) < 0) { + printf("Invalid ethertype: \"%s\".\n", + *(argv + 1)); + return -1; + } + } else if (strcmp(*argv, "eth") == 0) { + eth = true; + cargs = 1; } else { printf("\"%s\" is unknown, try \"irm " "ipcp connect\".\n", *argv); return -1; } - argc -= 2; - argv += 2; + argc -= cargs; + argv += cargs; } - if (ipcp == NULL || dst == NULL || comp == NULL) { + if (ipcp == NULL || comp == NULL) { usage(); return -1; } + memset(&addr, 0, sizeof(addr)); + + if (udpstr != NULL) { + if (poa_addr_set_udp(&addr, udpstr) < 0) + return -1; + pa = &addr; + } + + if (eth) { + if (udpstr != NULL) { + printf("Connect via udp or eth, not both.\n"); + return -1; + } + + if (dst == NULL) { + usage(); + return -1; + } + + if (poa_addr_set_eth(&addr, devstr, ethertype) < 0) + return -1; + pa = &addr; + } + + if (dst == NULL && pa == NULL) { + usage(); + return -1; + } + + if (dst == NULL) + dst = ""; + if (qos != NULL) { if (strcmp(qos, "raw") == 0) qs = qos_raw; @@ -110,9 +176,11 @@ int do_connect_ipcp(int argc, qs = qos_rt_safe; else if (strcmp(qos, "msg") == 0) qs = qos_msg; - else if (strcmp(qos, "stream") == 0) - qs = qos_stream; - else + else if (strcmp(qos, "stream") == 0) { + printf("Stream QoS is not allowed on " + "IPCP component flows.\n"); + return -1; + } else printf("Unknown QoS cube, defaulting to raw.\n"); } @@ -129,13 +197,13 @@ int do_connect_ipcp(int argc, if (wildcard_match(comp, MGMT) == 0) { component = MGMT_COMP; /* FIXME: move to qos_msg when stable */ - if (irm_connect_ipcp(pid, dst, component, qos_raw)) + if (irm_connect_ipcp(pid, dst, component, qos_raw, pa)) return -1; } if (wildcard_match(comp, DT) == 0) { component = DT_COMP; - if (irm_connect_ipcp(pid, dst, component, qs)) + if (irm_connect_ipcp(pid, dst, component, qs, pa)) return -1; } diff --git a/src/tools/irm/irm_ipcp_create.c b/src/tools/irm/irm_ipcp_create.c index c6b2074b..15cfedde 100644 --- a/src/tools/irm/irm_ipcp_create.c +++ b/src/tools/irm/irm_ipcp_create.c @@ -46,10 +46,6 @@ #define UNICAST "unicast" #define BROADCAST "broadcast" -#define UDP4 "udp4" -#define UDP6 "udp6" -#define ETH_LLC "eth-llc" -#define ETH_DIX "eth-dix" #define LOCAL "local" static void usage(void) @@ -57,8 +53,7 @@ static void usage(void) printf("Usage: irm ipcp create\n" " name <ipcp name>\n" " type [TYPE]\n\n" - "where TYPE in {" UNICAST " " BROADCAST " " LOCAL " " - UDP4 " " UDP6 " " ETH_LLC " " ETH_DIX "}\n"); + "where TYPE in {" UNICAST " " BROADCAST " " LOCAL "}\n"); } int do_create_ipcp(int argc, @@ -93,16 +88,8 @@ int do_create_ipcp(int argc, type = IPCP_UNICAST; else if (strcmp(ipcp_type, BROADCAST) == 0) type = IPCP_BROADCAST; - else if (strcmp(ipcp_type, UDP4) == 0) - type = IPCP_UDP4; - else if (strcmp(ipcp_type, UDP6) == 0) - type = IPCP_UDP6; else if (strcmp(ipcp_type, LOCAL) == 0) type = IPCP_LOCAL; - else if (strcmp(ipcp_type, ETH_LLC) == 0) - type = IPCP_ETH_LLC; - else if (strcmp(ipcp_type, ETH_DIX) == 0) - type = IPCP_ETH_DIX; else { printf("IPCP type \"%s\" is unknown.\n", ipcp_type); usage(); diff --git a/src/tools/irm/irm_ipcp_enroll.c b/src/tools/irm/irm_ipcp_enroll.c index 350b536e..54fed022 100644 --- a/src/tools/irm/irm_ipcp_enroll.c +++ b/src/tools/irm/irm_ipcp_enroll.c @@ -44,6 +44,7 @@ #include "irm_ops.h" #include "irm_utils.h" +#include <arpa/inet.h> #include <string.h> #define UNICAST "unicast" @@ -57,7 +58,10 @@ static void usage(void) " [dst <destination to enroll with>]\n" " [type [TYPE], default = " UNICAST "]\n" " [autobind]\n" - "where TYPE in {" UNICAST " " BROADCAST "}\n"); + " [udp [UDP_PEER]]\n" + " [eth [dev <device>] [ethertype <ethertype>]]\n" + "where TYPE in {" UNICAST " " BROADCAST "}\n" + "and UDP_PEER is <IP address or host>[:<port>]\n"); } static int get_layer_name(const char * ipcp, @@ -86,6 +90,12 @@ int do_enroll_ipcp(int argc, char * ipcp = NULL; char * layer = NULL; char * dst = NULL; + char * udpstr = NULL; + char * devstr = NULL; + uint16_t ethertype = POA_ETHERTYPE; + bool eth = false; + struct poa_addr addr; + struct poa_addr * pa = NULL; struct ipcp_list_info * ipcps; pid_t pid = -1; ssize_t len = 0; @@ -97,14 +107,37 @@ int do_enroll_ipcp(int argc, while (argc > 0) { cargs = 2; + if (strcmp(*argv, "eth") == 0) + cargs = 1; + else if (matches(*argv, "autobind") == 0) + cargs = 1; + + if (argc < cargs) { + usage(); + return -1; + } + if (matches(*argv, "name") == 0) { ipcp = *(argv + 1); } else if (matches(*argv, "type") == 0) { ipcp_type = *(argv + 1); } else if (matches(*argv, "layer") == 0) { layer = *(argv + 1); - } else if (matches(*argv, "dst") == 0) { + } else if (strcmp(*argv, "dst") == 0) { dst = *(argv + 1); + } else if (strcmp(*argv, "udp") == 0) { + udpstr = *(argv + 1); + } else if (strcmp(*argv, "dev") == 0) { + devstr = *(argv + 1); + } else if (strcmp(*argv, "ethertype") == 0) { + if (parse_ethertype(*(argv + 1), ðertype) < 0) { + printf("Invalid ethertype: \"%s\".\n", + *(argv + 1)); + return -1; + } + } else if (strcmp(*argv, "eth") == 0) { + eth = true; + cargs = 1; } else if (matches(*argv, "autobind") == 0) { autobind = true; cargs = 1; @@ -126,6 +159,25 @@ int do_enroll_ipcp(int argc, if (dst == NULL) dst = layer; + memset(&addr, 0, sizeof(addr)); + + if (udpstr != NULL) { + if (poa_addr_set_udp(&addr, udpstr) < 0) + return -1; + pa = &addr; + } + + if (eth) { + if (udpstr != NULL) { + printf("Enroll via udp or eth, not both.\n"); + return -1; + } + + if (poa_addr_set_eth(&addr, devstr, ethertype) < 0) + return -1; + pa = &addr; + } + if (strcmp(ipcp_type, UNICAST) == 0) type = IPCP_UNICAST; else if (strcmp(ipcp_type, BROADCAST) == 0) @@ -155,7 +207,7 @@ int do_enroll_ipcp(int argc, pid = ipcps[i].pid; - if (irm_enroll_ipcp(pid, dst)) { + if (irm_enroll_ipcp(pid, dst, pa) < 0) { printf("Failed to enroll IPCP.\n"); goto fail; } diff --git a/src/tools/irm/irm_ipcp_list.c b/src/tools/irm/irm_ipcp_list.c index a211a02b..60154efb 100644 --- a/src/tools/irm/irm_ipcp_list.c +++ b/src/tools/irm/irm_ipcp_list.c @@ -48,10 +48,6 @@ #define UNICAST "unicast" #define BROADCAST "broadcast" -#define UDP4 "udp4" -#define UDP6 "udp6" -#define ETH_LLC "eth-llc" -#define ETH_DIX "eth-dix" #define LOCAL "local" static void usage(void) @@ -60,8 +56,7 @@ static void usage(void) " [name <ipcp name>]\n" " [layer <layer_name>]\n\n" " [type [TYPE]]\n\n" - "where TYPE = {" UNICAST " " LOCAL " " - UDP4 " " UDP6 " " ETH_LLC " " ETH_DIX "}\n"); + "where TYPE = {" UNICAST " " BROADCAST " " LOCAL "}\n"); } static char * str_type(enum ipcp_type type) @@ -71,14 +66,6 @@ static char * str_type(enum ipcp_type type) return UNICAST; case IPCP_BROADCAST: return BROADCAST; - case IPCP_ETH_LLC: - return ETH_LLC; - case IPCP_ETH_DIX: - return ETH_DIX; - case IPCP_UDP4: - return UDP4; - case IPCP_UDP6: - return UDP6; case IPCP_LOCAL: return LOCAL; default: @@ -116,16 +103,8 @@ int do_list_ipcp(int argc, type = IPCP_UNICAST; else if (strcmp(ipcp_type, BROADCAST) == 0) type = IPCP_BROADCAST; - else if (strcmp(ipcp_type, UDP4) == 0) - type = IPCP_UDP4; - else if (strcmp(ipcp_type, UDP6) == 0) - type = IPCP_UDP6; else if (strcmp(ipcp_type, LOCAL) == 0) type = IPCP_LOCAL; - else if (strcmp(ipcp_type, ETH_LLC) == 0) - type = IPCP_ETH_LLC; - else if (strcmp(ipcp_type, ETH_DIX) == 0) - type = IPCP_ETH_DIX; else { usage(); return -1; diff --git a/src/tools/irm/irm_ipcp_poa.c b/src/tools/irm/irm_ipcp_poa.c new file mode 100644 index 00000000..cd939020 --- /dev/null +++ b/src/tools/irm/irm_ipcp_poa.c @@ -0,0 +1,98 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Connect components of unicast or broadcast IPC processes + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "irm_ops.h" +#include "irm_utils.h" + +#include <stdio.h> +#include <string.h> + +static void usage(void) +{ + printf("Usage: irm ipcp poa [OPERATION]\n\n" + "where OPERATION in {attach detach list help}\n"); +} + +static int do_help(int argc, + char ** argv) +{ + (void) argc; + (void) argv; + + usage(); + + return 0; +} + +static const struct cmd { + const char * cmd; + int (* func)(int argc, char ** argv); +} cmds[] = { + { "attach", do_attach_ipcp }, + { "detach", do_detach_ipcp }, + { "list", do_list_poas }, + { "help", do_help }, + { NULL, NULL } +}; + +static int do_cmd(const char * argv0, + int argc, + char ** argv) +{ + const struct cmd * c; + + for (c = cmds; c->cmd != NULL; ++c) + if (matches(argv0, c->cmd) == 0) + return c->func(argc - 1, argv + 1); + + fprintf(stderr, "\"%s\" is unknown, try \"irm ipcp poa help\".\n", + argv0); + + return -1; +} + +int poa_cmd(int argc, + char ** argv) +{ + if (argc < 1) { + usage(); + return -1; + } + + return do_cmd(argv[0], argc, argv); +} diff --git a/src/tools/irm/irm_ipcp_poa_attach.c b/src/tools/irm/irm_ipcp_poa_attach.c new file mode 100644 index 00000000..74a83344 --- /dev/null +++ b/src/tools/irm/irm_ipcp_poa_attach.c @@ -0,0 +1,153 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Connect components of unicast or broadcast IPC processes + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <ouroboros/irm.h> + +#include "irm_ops.h" +#include "irm_utils.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void usage(void) +{ + printf("Usage: irm ipcp poa attach\n" + " name <ipcp name>\n" + " [udp [UDP_POA]]\n" + " [eth [ETH_POA]]\n" + "where exactly one of udp or eth is given\n" + "and UDP_POA is <local IP address>[:<port>]\n" + "and ETH_POA is dev <device> [ethertype <ethertype>]\n"); +} + +int do_attach_ipcp(int argc, + char ** argv) +{ + char * ipcp = NULL; + char * udpstr = NULL; + char * devstr = NULL; + uint16_t ethertype = POA_ETHERTYPE; + bool eth = false; + struct poa_spec poa; + char str[POA_STRLEN + 1]; + struct ipcp_list_info * ipcps; + ssize_t len = 0; + pid_t pid = -1; + ssize_t i; + int cargs; + + while (argc > 0) { + cargs = 2; + + if (strcmp(*argv, "eth") == 0) + cargs = 1; + + if (argc < cargs) { + usage(); + return -1; + } + + if (matches(*argv, "name") == 0) { + ipcp = *(argv + 1); + } else if (strcmp(*argv, "udp") == 0) { + udpstr = *(argv + 1); + } else if (strcmp(*argv, "dev") == 0) { + devstr = *(argv + 1); + } else if (strcmp(*argv, "ethertype") == 0) { + if (parse_ethertype(*(argv + 1), ðertype) < 0) { + printf("Invalid ethertype: \"%s\".\n", + *(argv + 1)); + return -1; + } + } else if (strcmp(*argv, "eth") == 0) { + eth = true; + cargs = 1; + } else { + printf("\"%s\" is unknown, try \"irm ipcp " + "poa attach\".\n", *argv); + return -1; + } + + argc -= cargs; + argv += cargs; + } + + if (ipcp == NULL) { + usage(); + return -1; + } + + if (eth && udpstr != NULL) { + printf("A PoA is udp or eth, not both.\n"); + return -1; + } + + if (eth && devstr == NULL) { + printf("An eth PoA needs a device.\n"); + return -1; + } + + if (!eth && udpstr == NULL) { + usage(); + return -1; + } + + if (poa_spec_set(&poa, udpstr, devstr, ethertype) < 0) + return -1; + + len = irm_list_ipcps(&ipcps); + for (i = 0; i < len; i++) + if (strcmp(ipcps[i].name, ipcp) == 0) + pid = ipcps[i].pid; + + free(ipcps); + + if (pid == -1) { + printf("No such IPCP: \"%s\".\n", ipcp); + return -1; + } + + if (irm_attach_ipcp(pid, &poa) < 0) { + poa_spec_str(&poa, str, sizeof(str)); + printf("Failed to attach PoA %s on IPCP %s.\n", str, ipcp); + return -1; + } + + return 0; +} diff --git a/src/tools/irm/irm_ipcp_poa_detach.c b/src/tools/irm/irm_ipcp_poa_detach.c new file mode 100644 index 00000000..ce6bef13 --- /dev/null +++ b/src/tools/irm/irm_ipcp_poa_detach.c @@ -0,0 +1,153 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Connect components of unicast or broadcast IPC processes + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <ouroboros/irm.h> + +#include "irm_ops.h" +#include "irm_utils.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void usage(void) +{ + printf("Usage: irm ipcp poa detach\n" + " name <ipcp name>\n" + " [udp [UDP_POA]]\n" + " [eth [ETH_POA]]\n" + "where exactly one of udp or eth is given\n" + "and UDP_POA is <local IP address>[:<port>]\n" + "and ETH_POA is dev <device> [ethertype <ethertype>]\n"); +} + +int do_detach_ipcp(int argc, + char ** argv) +{ + char * ipcp = NULL; + char * udpstr = NULL; + char * devstr = NULL; + uint16_t ethertype = POA_ETHERTYPE; + bool eth = false; + struct poa_spec poa; + char str[POA_STRLEN + 1]; + struct ipcp_list_info * ipcps; + ssize_t len = 0; + pid_t pid = -1; + ssize_t i; + int cargs; + + while (argc > 0) { + cargs = 2; + + if (strcmp(*argv, "eth") == 0) + cargs = 1; + + if (argc < cargs) { + usage(); + return -1; + } + + if (matches(*argv, "name") == 0) { + ipcp = *(argv + 1); + } else if (strcmp(*argv, "udp") == 0) { + udpstr = *(argv + 1); + } else if (strcmp(*argv, "dev") == 0) { + devstr = *(argv + 1); + } else if (strcmp(*argv, "ethertype") == 0) { + if (parse_ethertype(*(argv + 1), ðertype) < 0) { + printf("Invalid ethertype: \"%s\".\n", + *(argv + 1)); + return -1; + } + } else if (strcmp(*argv, "eth") == 0) { + eth = true; + cargs = 1; + } else { + printf("\"%s\" is unknown, try \"irm ipcp " + "poa detach\".\n", *argv); + return -1; + } + + argc -= cargs; + argv += cargs; + } + + if (ipcp == NULL) { + usage(); + return -1; + } + + if (eth && udpstr != NULL) { + printf("A PoA is udp or eth, not both.\n"); + return -1; + } + + if (eth && devstr == NULL) { + printf("An eth PoA needs a device.\n"); + return -1; + } + + if (!eth && udpstr == NULL) { + usage(); + return -1; + } + + if (poa_spec_set(&poa, udpstr, devstr, ethertype) < 0) + return -1; + + len = irm_list_ipcps(&ipcps); + for (i = 0; i < len; i++) + if (strcmp(ipcps[i].name, ipcp) == 0) + pid = ipcps[i].pid; + + free(ipcps); + + if (pid == -1) { + printf("No such IPCP: \"%s\".\n", ipcp); + return -1; + } + + if (irm_detach_ipcp(pid, &poa) < 0) { + poa_spec_str(&poa, str, sizeof(str)); + printf("Failed to detach PoA %s on IPCP %s.\n", str, ipcp); + return -1; + } + + return 0; +} diff --git a/src/tools/irm/irm_ipcp_poa_list.c b/src/tools/irm/irm_ipcp_poa_list.c new file mode 100644 index 00000000..8797c3c1 --- /dev/null +++ b/src/tools/irm/irm_ipcp_poa_list.c @@ -0,0 +1,136 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * List the points of attachment of an IPC process + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <ouroboros/irm.h> + +#include "irm_ops.h" +#include "irm_utils.h" + +#include <arpa/inet.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void usage(void) +{ + printf("Usage: irm ipcp poa list\n" + " name <ipcp name>\n"); +} + +static void print_poa(const struct poa_spec * poa) +{ + char buf[INET6_ADDRSTRLEN]; + + switch (poa->type) { + case POA_UDP4: + if (inet_ntop(AF_INET, &poa->udp4.ip_addr, buf, + sizeof(buf)) == NULL) + return; + printf("%-6s %s:%u\n", "udp4", buf, poa->udp4.port); + break; + case POA_UDP6: + if (inet_ntop(AF_INET6, &poa->udp6.ip_addr, buf, + sizeof(buf)) == NULL) + return; + printf("%-6s [%s]:%u\n", "udp6", buf, poa->udp6.port); + break; + case POA_ETH: + printf("%-6s %s 0x%04X %02x:%02x:%02x:%02x:%02x:%02x\n", + "eth", poa->eth.dev, poa->eth.ethertype, + poa->eth.mac[0], poa->eth.mac[1], poa->eth.mac[2], + poa->eth.mac[3], poa->eth.mac[4], poa->eth.mac[5]); + break; + default: + break; + } +} + +int do_list_poas(int argc, + char ** argv) +{ + char * ipcp = NULL; + struct poa_spec * poas = NULL; + struct ipcp_list_info * ipcps; + ssize_t len = 0; + pid_t pid = -1; + ssize_t n; + ssize_t i; + + while (argc > 1) { + if (matches(*argv, "name") == 0) { + ipcp = *(argv + 1); + } else { + printf("\"%s\" is unknown, try \"irm ipcp " + "poa list\".\n", *argv); + return -1; + } + + argc -= 2; + argv += 2; + } + + if (ipcp == NULL) { + usage(); + return -1; + } + + len = irm_list_ipcps(&ipcps); + for (i = 0; i < len; i++) + if (strcmp(ipcps[i].name, ipcp) == 0) + pid = ipcps[i].pid; + + free(ipcps); + + if (pid == -1) { + printf("No such IPCP: \"%s\".\n", ipcp); + return -1; + } + + n = irm_list_poas(pid, &poas); + if (n < 0) { + printf("Failed to list PoAs of %s.\n", ipcp); + return -1; + } + + for (i = 0; i < n; i++) + print_poa(&poas[i]); + + free(poas); + + return 0; +} diff --git a/src/tools/irm/irm_name_create.c b/src/tools/irm/irm_name_create.c index 1055700c..034b4c99 100644 --- a/src/tools/irm/irm_name_create.c +++ b/src/tools/irm/irm_name_create.c @@ -51,10 +51,10 @@ #define RR "round-robin" #define SPILL "spillover" -#define SENC "<security_dir>/server/<name>/enc.conf" +#define SSEC "<security_dir>/server/<name>/sec.conf" #define SCRT "<security_dir>/server/<name>/crt.pem" #define SKEY "<security_dir>/server/<name>/key.pem" -#define CENC "<security_dir>/client/<name>/enc.conf" +#define CSEC "<security_dir>/client/<name>/sec.conf" #define CCRT "<security_dir>/client/<name>/crt.pem" #define CKEY "<security_dir>/client/<name>/key.pem" @@ -63,10 +63,10 @@ static void usage(void) printf("Usage: irm name create\n" " <name>. max %d chars.\n" " [lb LB_POLICY], default: %s\n" - " [sencpath <path>, default: " SENC "]\n" + " [ssecpath <path>, default: " SSEC "]\n" " [scrtpath <path>, default: " SCRT "]\n" " [skeypath <path>, default: " SKEY "]\n" - " [cencpath <path>, default: " CENC "]\n" + " [csecpath <path>, default: " CSEC "]\n" " [ccrtpath <path>, default: " CCRT "]\n" " [ckeypath <path>, default: " CKEY "]\n" "\n" @@ -105,10 +105,10 @@ int do_create_name(int argc, { struct name_info info = {}; char * name = NULL; - char * sencpath = NULL; + char * ssecpath = NULL; char * scrtpath = NULL; char * skeypath = NULL; - char * cencpath = NULL; + char * csecpath = NULL; char * ccrtpath = NULL; char * ckeypath = NULL; char * lb_pol = RR; @@ -117,16 +117,21 @@ int do_create_name(int argc, --argc; while (argc > 0) { + if (argc < 2) { + usage(); + return -1; + } + if (matches(*argv, "lb") == 0) { lb_pol = *(argv + 1); - } else if (matches(*argv, "sencpath") == 0) { - sencpath = *(argv + 1); + } else if (matches(*argv, "ssecpath") == 0) { + ssecpath = *(argv + 1); } else if (matches(*argv, "scrtpath") == 0) { scrtpath = *(argv + 1); } else if (matches(*argv, "skeypath") == 0) { skeypath = *(argv + 1); - } else if (matches(*argv, "cencpath") == 0) { - cencpath = *(argv + 1); + } else if (matches(*argv, "csecpath") == 0) { + csecpath = *(argv + 1); } else if (matches(*argv, "ccrtpath") == 0) { ccrtpath = *(argv + 1); } else if (matches(*argv, "ckeypath") == 0) { @@ -151,7 +156,7 @@ int do_create_name(int argc, strcpy(info.name, name); - if (sencpath != NULL && cp_chk_path(info.s.enc, sencpath) < 0) + if (ssecpath != NULL && cp_chk_path(info.s.sec, ssecpath) < 0) goto fail; if (scrtpath != NULL && cp_chk_path(info.s.crt, scrtpath) < 0) @@ -160,7 +165,7 @@ int do_create_name(int argc, if (skeypath != NULL && cp_chk_path(info.s.key, skeypath) < 0) goto fail; - if (cencpath != NULL && cp_chk_path(info.c.enc, cencpath) < 0) + if (csecpath != NULL && cp_chk_path(info.c.sec, csecpath) < 0) goto fail; if (ccrtpath != NULL && cp_chk_path(info.c.crt, ccrtpath) < 0) diff --git a/src/tools/irm/irm_name_list.c b/src/tools/irm/irm_name_list.c index 37e1f023..a5a33073 100644 --- a/src/tools/irm/irm_name_list.c +++ b/src/tools/irm/irm_name_list.c @@ -49,6 +49,12 @@ #define RR "round-robin" #define SPILL "spillover" +static void usage(void) +{ + printf("Usage: irm name list\n" + " [name <name>]\n"); +} + static char * str_pol(enum pol_balance p) { switch(p) { @@ -70,7 +76,12 @@ int do_list_name(int argc, ssize_t i; while (argc > 0) { - if (matches(*argv, "list") == 0) { + if (argc < 2) { + usage(); + return -1; + } + + if (matches(*argv, "name") == 0) { name = *(argv + 1); } else { printf("\"%s\" is unknown, try \"irm " diff --git a/src/tools/irm/irm_name_reg.c b/src/tools/irm/irm_name_reg.c index 860f4a70..25e9fbb8 100644 --- a/src/tools/irm/irm_name_reg.c +++ b/src/tools/irm/irm_name_reg.c @@ -80,6 +80,11 @@ int do_reg_name(int argc, --argc; while (argc > 0) { + if (argc < 2) { + usage(); + return -1; + } + if (matches(*argv, "layer") == 0) { layers[layers_len++] = *(argv + 1); if (layers_len > MAX_LAYERS) { diff --git a/src/tools/irm/irm_name_unreg.c b/src/tools/irm/irm_name_unreg.c index abf08548..1b2cf29c 100644 --- a/src/tools/irm/irm_name_unreg.c +++ b/src/tools/irm/irm_name_unreg.c @@ -76,6 +76,11 @@ int do_unreg_name(int argc, --argc; while (argc > 0) { + if (argc < 2) { + usage(); + return -1; + } + if (matches(*argv, "layer") == 0) { layers[layers_len++] = *(argv + 1); if (layers_len > MAX_LAYERS) { diff --git a/src/tools/irm/irm_ops.h b/src/tools/irm/irm_ops.h index 195c5cbc..7f6c65cd 100644 --- a/src/tools/irm/irm_ops.h +++ b/src/tools/irm/irm_ops.h @@ -51,6 +51,18 @@ int do_bootstrap_ipcp(int argc, int do_enroll_ipcp(int argc, char ** argv); +int poa_cmd(int argc, + char ** argv); + +int do_attach_ipcp(int argc, + char ** argv); + +int do_detach_ipcp(int argc, + char ** argv); + +int do_list_poas(int argc, + char ** argv); + int do_connect_ipcp(int argc, char ** argv); diff --git a/src/tools/irm/irm_unbind.c b/src/tools/irm/irm_unbind.c index 4e5914a9..f98d0931 100644 --- a/src/tools/irm/irm_unbind.c +++ b/src/tools/irm/irm_unbind.c @@ -78,8 +78,8 @@ static int do_cmd(const char * argv0, const struct cmd * c; for (c = cmds; c->cmd; ++c) - if (!matches(argv0, c->cmd)) - return c->func(argc, argv); + if (matches(argv0, c->cmd) == 0) + return c->func(argc - 1, argv + 1); fprintf(stderr, "\"%s\" is unknown, try \"irm unbind help\".\n", argv0); diff --git a/src/tools/irm/irm_unbind_ipcp.c b/src/tools/irm/irm_unbind_ipcp.c index 23e25057..5d78603d 100644 --- a/src/tools/irm/irm_unbind_ipcp.c +++ b/src/tools/irm/irm_unbind_ipcp.c @@ -63,15 +63,20 @@ int do_unbind_ipcp(int argc, ssize_t len; ssize_t i; + if (argc < 1) { + usage(); + return -1; + } + + ipcp = *argv; + ++argv; + --argc; + while (argc > 0) { if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "ipcp") == 0) { - ipcp = *(argv + 1); - ++argv; - --argc; } else { printf("\"%s\" is unknown, try \"irm " "unbind ipcp\".\n", *argv); diff --git a/src/tools/irm/irm_unbind_process.c b/src/tools/irm/irm_unbind_process.c index bc7e545c..dfa91f4b 100644 --- a/src/tools/irm/irm_unbind_process.c +++ b/src/tools/irm/irm_unbind_process.c @@ -58,15 +58,20 @@ int do_unbind_process(int argc, pid_t pid = -1; char * name = NULL; + if (argc < 1) { + usage(); + return -1; + } + + pid = strtol(*argv, NULL, 10); + ++argv; + --argc; + while (argc > 1) { if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "process") == 0) { - pid = strtol(*(argv + 1), NULL, 10); - ++argv; - --argc; } else { printf("\"%s\" is unknown, try \"irm " "unbind process\".\n", *argv); diff --git a/src/tools/irm/irm_unbind_program.c b/src/tools/irm/irm_unbind_program.c index 031b9909..4ee8ce24 100644 --- a/src/tools/irm/irm_unbind_program.c +++ b/src/tools/irm/irm_unbind_program.c @@ -57,15 +57,25 @@ int do_unbind_program(int argc, char * name = NULL; char * prog = NULL; + if (argc < 1) { + usage(); + return -1; + } + + prog = *argv; + ++argv; + --argc; + while (argc > 0) { + if (argc < 2) { + usage(); + return -1; + } + if (matches(*argv, "name") == 0) { name = *(argv + 1); ++argv; --argc; - } else if (matches(*argv, "program") == 0) { - prog = *(argv + 1); - ++argv; - --argc; } else { printf("\"%s\" is unknown, try \"irm " "unbind program\".\n", *argv); diff --git a/src/tools/irm/irm_utils.c b/src/tools/irm/irm_utils.c index 69873097..c43accec 100644 --- a/src/tools/irm/irm_utils.c +++ b/src/tools/irm/irm_utils.c @@ -77,10 +77,21 @@ */ -#include <string.h> +#if defined(__linux__) || defined(__CYGWIN__) +#define _DEFAULT_SOURCE +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include <ouroboros/ipcp.h> #include "irm_utils.h" +#include <arpa/inet.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + int matches(const char * cmd, const char * pattern) { @@ -123,3 +134,223 @@ int wildcard_match(const char * pattern, } } } + +/* Splits "<addr>[:<port>]"; bare IPv6 needs no brackets. */ +static int parse_udp_str(const char * str, + char * host, + int * port) +{ + struct in6_addr v6; + char buf[POA_HOST_STRLEN + 1]; + char * p; + char * end; + long n; + + *port = POA_UDP_PORT; + + if (strlen(str) > POA_HOST_STRLEN) + goto fail; + + strcpy(buf, str); + + if (buf[0] == '[') { + p = strchr(buf, ']'); + if (p == NULL) + goto fail; + + *p++ = '\0'; + + strcpy(host, buf + 1); + + if (*p == '\0') + return 0; + + if (*p != ':') + goto fail; + + ++p; + } else if (inet_pton(AF_INET6, buf, &v6) == 1) { + strcpy(host, buf); + return 0; + } else { + p = strrchr(buf, ':'); + if (p == NULL) { + strcpy(host, buf); + return 0; + } + + *p++ = '\0'; + + strcpy(host, buf); + } + + n = strtol(p, &end, 10); + if (*p == '\0' || *end != '\0' || n < 1 || n > 65535) + goto fail; + + *port = (int) n; + + return 0; + fail: + printf("Invalid UDP address: \"%s\".\n", str); + return -1; +} + +/* An unresolved name is left for the IRMd, which picks the family. */ +int poa_addr_set_udp(struct poa_addr * addr, + const char * str) +{ + char host[POA_HOST_STRLEN + 1]; + int port; + + if (parse_udp_str(str, host, &port) < 0) + return -1; + + if (inet_pton(AF_INET, host, &addr->udp4.ip_addr) == 1) { + addr->type = POA_UDP4; + addr->udp4.port = port; + return 0; + } + + if (inet_pton(AF_INET6, host, &addr->udp6.ip_addr) == 1) { + addr->type = POA_UDP6; + addr->udp6.port = port; + return 0; + } + + addr->type = POA_UDP; + addr->udp4.port = port; + + strcpy(addr->hostname, host); + + return 0; +} + +/* + * Parses a hex ethertype; rejects garbage and out-of-range values. + * Overflow clamps to LONG_MAX and lands in the range check. + */ +int parse_ethertype(const char * str, + uint16_t * ethertype) +{ + char * end; + long val; + + val = strtol(str, &end, 16); + + if (end == str || *end != '\0') + return -1; + + if (val < 0 || val > 0xFFFF) + return -1; + + *ethertype = (uint16_t) val; + + return 0; +} + +int poa_addr_set_eth(struct poa_addr * addr, + const char * devstr, + uint16_t ethertype) +{ + addr->type = POA_ETH; + + addr->eth.src.ethertype = ethertype; + addr->eth.dst.ethertype = ethertype; + + if (devstr != NULL) { + if (strlen(devstr) > DEV_NAME_SIZE) { + printf("Invalid device name: \"%s\".\n", devstr); + return -1; + } + + strcpy(addr->eth.src.dev, devstr); + } + + return 0; +} + +/* Matches src/ipcpd/ipcp.c; keep in sync. */ +void poa_spec_str(const struct poa_spec * poa, + char * buf, + size_t len) +{ + char addr[INET6_ADDRSTRLEN]; + + switch (poa->type) { + case POA_UDP4: + if (inet_ntop(AF_INET, &poa->udp4.ip_addr, + addr, sizeof(addr)) == NULL) + break; + + snprintf(buf, len, "udp4 %s:%u", addr, poa->udp4.port); + return; + case POA_UDP6: + if (inet_ntop(AF_INET6, &poa->udp6.ip_addr, + addr, sizeof(addr)) == NULL) + break; + + snprintf(buf, len, "udp6 [%s]:%u", addr, poa->udp6.port); + return; + case POA_ETH: + snprintf(buf, len, "eth %s 0x%04X", poa->eth.dev, + poa->eth.ethertype); + return; + default: + break; + } + + snprintf(buf, len, "(unknown)"); +} + +int poa_spec_set(struct poa_spec * poa, + const char * udpstr, + const char * devstr, + uint16_t ethertype) +{ + char host[POA_HOST_STRLEN + 1]; + int port; + + memset(poa, 0, sizeof(*poa)); + + if ((udpstr != NULL) + (devstr != NULL) > 1) { + printf("A PoA is an address or a device.\n"); + return -1; + } + + if (udpstr != NULL) { + if (parse_udp_str(udpstr, host, &port) < 0) + return -1; + + if (inet_pton(AF_INET, host, &poa->udp4.ip_addr) == 1) { + poa->type = POA_UDP4; + poa->udp4.port = port; + return 0; + } + + if (inet_pton(AF_INET6, host, &poa->udp6.ip_addr) == 1) { + poa->type = POA_UDP6; + poa->udp6.port = port; + return 0; + } + + printf("Invalid IP address: \"%s\".\n", udpstr); + return -1; + } + + if (devstr != NULL) { + if (strlen(devstr) > DEV_NAME_SIZE) { + printf("Invalid device name: \"%s\".\n", devstr); + return -1; + } + + poa->type = POA_ETH; + poa->eth.ethertype = ethertype; + + strcpy(poa->eth.dev, devstr); + + return 0; + } + + return -1; +} diff --git a/src/tools/irm/irm_utils.h b/src/tools/irm/irm_utils.h index c6d4bf18..3760b4e4 100644 --- a/src/tools/irm/irm_utils.h +++ b/src/tools/irm/irm_utils.h @@ -79,10 +79,43 @@ #ifndef OUROBOROS_TOOLS_IRM_UTILS_H #define OUROBOROS_TOOLS_IRM_UTILS_H -int matches(const char * cmd, - const char * pattern); +#include <ouroboros/ipcp.h> -int wildcard_match(const char * pattern, - const char * string); +#include <arpa/inet.h> + +#include <stdint.h> + +int matches(const char * cmd, + const char * pattern); + +int wildcard_match(const char * pattern, + const char * string); + +/* Fill one PoA to attach or detach: an address or a device. */ +int poa_spec_set(struct poa_spec * poa, + const char * udpstr, + const char * devstr, + uint16_t ethertype); + +/* Fill a peer PoA address; a host name is resolved by the IRMd. */ +int poa_addr_set_udp(struct poa_addr * addr, + const char * str); + +/* Fits "eth <dev> 0x<type>", the longest PoA rendering. */ +/* Matches src/ipcpd/ipcp.c; keep in sync. */ +#define POA_STRLEN (DEV_NAME_SIZE + 11) + +/* Names a PoA the way the IPCP logs it. */ +void poa_spec_str(const struct poa_spec * poa, + char * buf, + size_t len); + +int poa_addr_set_eth(struct poa_addr * addr, + const char * devstr, + uint16_t ethertype); + +/* Parses a hex ethertype; rejects garbage and out-of-range values. */ +int parse_ethertype(const char * str, + uint16_t * ethertype); #endif /* OUROBOROS_TOOLS_IRM_UTILS_H */ diff --git a/src/tools/ocbr/ocbr_client.c b/src/tools/ocbr/ocbr_client.c index 36c07d43..60d4a0fc 100644 --- a/src/tools/ocbr/ocbr_client.c +++ b/src/tools/ocbr/ocbr_client.c @@ -88,10 +88,12 @@ int client_main(char * server, struct timespec start; struct timespec end; struct timespec intv = {(gap / BILLION), gap % BILLION}; - int ms; - const char * qenv; - qosspec_t qs; - qosspec_t * qsp; + int ms; + ssize_t rc; + int werr = 0; + const char * qenv; + qosspec_t qs; + qosspec_t * qsp; qsp = NULL; @@ -147,7 +149,9 @@ int client_main(char * server, ts_add(&end, &intv, &end); memcpy(buf, &seqnr, sizeof(seqnr)); - if (flow_write(fd, buf, size) < 0) { + rc = flow_write(fd, buf, size); + if (rc < 0) { + werr = (int) rc; stop = true; continue; } @@ -165,7 +169,10 @@ int client_main(char * server, } else { /* flood */ while (!stop) { clock_gettime(CLOCK_REALTIME, &end); - if (flow_write(fd, buf, size) < 0) { + + rc = flow_write(fd, buf, size); + if (rc < 0) { + werr = (int) rc; stop = true; continue; } @@ -183,6 +190,10 @@ int client_main(char * server, ms = ts_diff_ms(&end, &start); + /* Codes at or above 1000 are ouroboros errno.h values. */ + if (werr != 0) + printf("Send ended on write error %d.\n", werr); + printf("sent statistics: " "%9ld packets, %12ld bytes in %9d ms, %4.4f Mb/s\n", seqnr, seqnr * size, ms, (seqnr / (ms * 1000.0)) * size * 8.0); diff --git a/src/tools/ocbr/ocbr_server.c b/src/tools/ocbr/ocbr_server.c index c98b33e9..f9f7e3c8 100644 --- a/src/tools/ocbr/ocbr_server.c +++ b/src/tools/ocbr/ocbr_server.c @@ -50,7 +50,7 @@ #include <stdlib.h> #include <pthread.h> -#define THREADS_SIZE 10 +#define THREADS_SIZE 64 pthread_t listen_thread; pthread_t threads[THREADS_SIZE]; diff --git a/src/tools/oftp/oftp.c b/src/tools/oftp/oftp.c index 1ae99403..b66ef650 100644 --- a/src/tools/oftp/oftp.c +++ b/src/tools/oftp/oftp.c @@ -35,7 +35,12 @@ * OF THE POSSIBILITY OF SUCH DAMAGE. */ +#if defined(__APPLE__) +/* macOS hides O_NOFOLLOW behind the BSD feature set. */ +#define _DARWIN_C_SOURCE +#else #define _POSIX_C_SOURCE 200809L +#endif #include <ouroboros/crc64.h> #include <ouroboros/dev.h> @@ -65,9 +70,11 @@ static void apply_rto_min_env(int fd) env = getenv("OFTP_FRCT_RTO_MIN"); if (env == NULL) return; + v = strtol(env, NULL, 10); if (v <= 0) return; + if (fccntl(fd, FRCTSRTOMIN, (time_t) v) < 0) fprintf(stderr, "oftp: failed to set RTO_MIN=%ld ns\n", v); @@ -81,9 +88,11 @@ static void apply_stream_ring_sz_env(int fd) env = getenv("OFTP_FRCT_STREAM_RING_SZ"); if (env == NULL) return; + v = strtol(env, NULL, 10); if (v <= 0) return; + if (fccntl(fd, FRCTSRRINGSZ, (size_t) v) < 0) fprintf(stderr, "oftp: failed to set STREAM_RING_SZ=%ld\n", v); @@ -301,7 +310,7 @@ static int server_main(const char * outpath) fd = flow_accept(&qs, NULL); if (fd < 0) { fprintf(stderr, "flow_accept failed: %d\n", fd); - if (out != stdout) + if (outpath != NULL) fclose(out); return 1; } @@ -311,7 +320,7 @@ static int server_main(const char * outpath) "oftp: rejecting non-stream flow (service=%u)\n", qs.service); flow_dealloc(fd); - if (out != stdout) { + if (outpath != NULL) { fclose(out); unlink(outpath); } @@ -325,7 +334,7 @@ static int server_main(const char * outpath) flow_dealloc(fd); - if (out != stdout) { + if (outpath != NULL) { fclose(out); /* Drop the half-written file on abort/interrupt. */ if (rc != 0) @@ -358,7 +367,7 @@ static int client_main(const char * name, fd = flow_alloc(name, &qs, NULL); if (fd < 0) { fprintf(stderr, "flow_alloc failed: %d\n", fd); - if (in != stdin) + if (inpath != NULL) fclose(in); return 2; } @@ -370,7 +379,7 @@ static int client_main(const char * name, flow_dealloc(fd); - if (in != stdin) + if (inpath != NULL) fclose(in); return rc; diff --git a/src/tools/oping/oping_client.c b/src/tools/oping/oping_client.c index 4b01315d..b60c73cb 100644 --- a/src/tools/oping/oping_client.c +++ b/src/tools/oping/oping_client.c @@ -193,8 +193,6 @@ void * writer(void * o) pthread_cleanup_push(free, buf); while (!stop && client.sent < client.count) { - nanosleep(&wait, NULL); - clock_gettime(CLOCK_MONOTONIC, &now); msg->type = htonl(ECHO_REQUEST); @@ -206,6 +204,8 @@ void * writer(void * o) printf("Failed to send packet.\n"); stop = true; } + + nanosleep(&wait, NULL); } pthread_cleanup_pop(true); |
