summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt9
-rw-r--r--cmake/config/global.cmake12
-rw-r--r--cmake/config/ipcp/common.cmake11
-rw-r--r--cmake/config/ipcp/eth.cmake22
-rw-r--r--cmake/config/ipcp/udp.cmake16
-rw-r--r--cmake/config/ipcp/unicast.cmake19
-rw-r--r--cmake/config/irmd.cmake8
-rw-r--r--cmake/config/lib.cmake103
-rw-r--r--cmake/config/lib/common.cmake58
-rw-r--r--cmake/config/lib/crypt.cmake41
-rw-r--r--cmake/config/lib/frct.cmake43
-rw-r--r--cmake/config/lib/poa.cmake37
-rw-r--r--cmake/config/lib/ssm.cmake (renamed from cmake/config/ssm.cmake)82
-rw-r--r--cmake/config/tests.cmake6
-rw-r--r--cmake/dependencies.cmake13
-rw-r--r--cmake/dependencies/crypt/openssl.cmake58
-rw-r--r--cmake/dependencies/eth/netmap.cmake24
-rw-r--r--cmake/dependencies/system/liburcu.cmake45
-rw-r--r--cmake/dependencies/udp/ddns.cmake31
-rw-r--r--doc/man/fccntl.318
-rw-r--r--doc/man/ouroboros-tutorial.743
-rw-r--r--doc/man/ouroboros.8132
-rw-r--r--include/ouroboros/atomics.h2
-rw-r--r--include/ouroboros/crypt.h113
-rw-r--r--include/ouroboros/fccntl.h2
-rw-r--r--include/ouroboros/flow.h2
-rw-r--r--include/ouroboros/fqueue.h3
-rw-r--r--include/ouroboros/hash.h3
-rw-r--r--include/ouroboros/ipcp-dev.h126
-rw-r--r--include/ouroboros/ipcp.h105
-rw-r--r--include/ouroboros/irm.h25
-rw-r--r--include/ouroboros/logs.h7
-rw-r--r--include/ouroboros/name.h6
-rw-r--r--include/ouroboros/np1_flow.h4
-rw-r--r--include/ouroboros/protobuf.h30
-rw-r--r--include/ouroboros/pthread.h6
-rw-r--r--include/ouroboros/qos.h6
-rw-r--r--include/ouroboros/rcu.h110
-rw-r--r--include/ouroboros/serdes-irm.h31
-rw-r--r--include/ouroboros/ssm_rbuff.h32
-rw-r--r--include/test/certs/ecdsa.h37
-rw-r--r--irmd.conf.in100
-rw-r--r--sec.conf.in (renamed from enc.conf.in)61
-rw-r--r--src/ipcpd/CMakeLists.txt5
-rw-r--r--src/ipcpd/broadcast/main.c13
-rw-r--r--src/ipcpd/common/connmgr.c98
-rw-r--r--src/ipcpd/common/connmgr.h17
-rw-r--r--src/ipcpd/config.h.in27
-rw-r--r--src/ipcpd/eth/CMakeLists.txt21
-rw-r--r--src/ipcpd/eth/eth.c2359
-rw-r--r--src/ipcpd/ipcp.c327
-rw-r--r--src/ipcpd/ipcp.h16
-rw-r--r--src/ipcpd/local/CMakeLists.txt1
-rw-r--r--src/ipcpd/local/main.c68
-rw-r--r--src/ipcpd/local/reg.c217
-rw-r--r--src/ipcpd/local/reg.h (renamed from src/ipcpd/eth/llc.c)27
-rw-r--r--src/ipcpd/shim-data.c582
-rw-r--r--src/ipcpd/shim-data.h109
-rw-r--r--src/ipcpd/udp/CMakeLists.txt14
-rw-r--r--src/ipcpd/udp/udp.c1254
-rw-r--r--src/ipcpd/udp/udp4.c42
-rw-r--r--src/ipcpd/udp/udp6.c42
-rw-r--r--src/ipcpd/unicast/CMakeLists.txt3
-rw-r--r--src/ipcpd/unicast/ca.c203
-rw-r--r--src/ipcpd/unicast/ca.h43
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.c1107
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.h27
-rw-r--r--src/ipcpd/unicast/ca/nop.c41
-rw-r--r--src/ipcpd/unicast/ca/nop.h20
-rw-r--r--src/ipcpd/unicast/ca/ops.h30
-rw-r--r--src/ipcpd/unicast/ca/tests/CMakeLists.txt78
-rw-r--r--src/ipcpd/unicast/ca/tests/ca_test.c392
-rw-r--r--src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c1294
-rw-r--r--src/ipcpd/unicast/ca/tests/mb_ecn_test.c3156
-rw-r--r--src/ipcpd/unicast/cap.c99
-rw-r--r--src/ipcpd/unicast/cap.h (renamed from src/ipcpd/eth/dix.c)22
-rw-r--r--src/ipcpd/unicast/dir/dht.c72
-rw-r--r--src/ipcpd/unicast/dir/tests/dht_test.c63
-rw-r--r--src/ipcpd/unicast/dt.c342
-rw-r--r--src/ipcpd/unicast/dt.h3
-rw-r--r--src/ipcpd/unicast/fa.c457
-rw-r--r--src/ipcpd/unicast/fa.h6
-rw-r--r--src/ipcpd/unicast/main.c16
-rw-r--r--src/ipcpd/unicast/pff/alternate.c2
-rw-r--r--src/ipcpd/unicast/pff/multipath.c22
-rw-r--r--src/ipcpd/unicast/pff/multipath.h4
-rw-r--r--src/ipcpd/unicast/pff/pft.c48
-rw-r--r--src/ipcpd/unicast/pff/pft.h7
-rw-r--r--src/ipcpd/unicast/pff/simple.c22
-rw-r--r--src/ipcpd/unicast/pff/simple.h4
-rw-r--r--src/ipcpd/unicast/pff/tests/pft_test.c302
-rw-r--r--src/ipcpd/unicast/psched.c222
-rw-r--r--src/ipcpd/unicast/psched.h6
-rw-r--r--src/ipcpd/unicast/routing/link-state.c6
-rw-r--r--src/ipcpd/unicast/tests/CMakeLists.txt34
-rw-r--r--src/ipcpd/unicast/tests/cap_test.c177
-rw-r--r--src/irmd/CMakeLists.txt6
-rw-r--r--src/irmd/config.h.in12
-rw-r--r--src/irmd/configfile.c572
-rw-r--r--src/irmd/ipcp.c161
-rw-r--r--src/irmd/ipcp.h70
-rw-r--r--src/irmd/irmd.h53
-rw-r--r--src/irmd/main.c1633
-rw-r--r--src/irmd/oap.h42
-rw-r--r--src/irmd/oap/auth.c441
-rw-r--r--src/irmd/oap/auth.h41
-rw-r--r--src/irmd/oap/cli.c319
-rw-r--r--src/irmd/oap/hdr.c526
-rw-r--r--src/irmd/oap/hdr.h104
-rw-r--r--src/irmd/oap/internal.h25
-rw-r--r--src/irmd/oap/io.c36
-rw-r--r--src/irmd/oap/io.h2
-rw-r--r--src/irmd/oap/srv.c293
-rw-r--r--src/irmd/oap/tests/common.c295
-rw-r--r--src/irmd/oap/tests/common.h34
-rw-r--r--src/irmd/oap/tests/oap_test.c1115
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c446
-rw-r--r--src/irmd/reg/flow.c17
-rw-r--r--src/irmd/reg/flow.h18
-rw-r--r--src/irmd/reg/name.c3
-rw-r--r--src/irmd/reg/proc.c3
-rw-r--r--src/irmd/reg/prog.c3
-rw-r--r--src/irmd/reg/reg.c593
-rw-r--r--src/irmd/reg/reg.h240
-rw-r--r--src/irmd/reg/tests/reg_test.c167
-rw-r--r--src/lib/CMakeLists.txt19
-rw-r--r--src/lib/cap.c187
-rw-r--r--src/lib/cap.h63
-rw-r--r--src/lib/config.h.in37
-rw-r--r--src/lib/crypt.c579
-rw-r--r--src/lib/crypt/keyrot.c775
-rw-r--r--src/lib/crypt/keyrot.h74
-rw-r--r--src/lib/crypt/openssl.c1088
-rw-r--r--src/lib/crypt/openssl.h232
-rw-r--r--src/lib/dev.c867
-rw-r--r--src/lib/frct.c646
-rw-r--r--src/lib/hash.c15
-rw-r--r--src/lib/irm.c127
-rw-r--r--src/lib/pb/ipcp.proto8
-rw-r--r--src/lib/pb/ipcp_config.proto28
-rw-r--r--src/lib/pb/irm.proto14
-rw-r--r--src/lib/pb/model.proto14
-rw-r--r--src/lib/poa/addr.c142
-rw-r--r--src/lib/poa/eth.c2011
-rw-r--r--src/lib/poa/poa.c2498
-rw-r--r--src/lib/poa/poa.h366
-rw-r--r--src/lib/poa/udp.c633
-rw-r--r--src/lib/protobuf.c391
-rw-r--r--src/lib/random.c18
-rw-r--r--src/lib/serdes-irm.c269
-rw-r--r--src/lib/ssm/flow_set.c18
-rw-r--r--src/lib/ssm/pool.c40
-rw-r--r--src/lib/ssm/rbuff.c491
-rw-r--r--src/lib/ssm/ssm.h.in4
-rw-r--r--src/lib/ssm/tests/pool_test.c10
-rw-r--r--src/lib/ssm/tests/rbuff_test.c488
-rw-r--r--src/lib/tests/CMakeLists.txt12
-rw-r--r--src/lib/tests/auth_test.c145
-rw-r--r--src/lib/tests/auth_test_ml_dsa.c2
-rw-r--r--src/lib/tests/auth_test_slh_dsa.c2
-rw-r--r--src/lib/tests/cap_test.c427
-rw-r--r--src/lib/tests/crypt_test.c473
-rw-r--r--src/lib/tests/hash_test.c37
-rw-r--r--src/lib/tests/kex_test.c487
-rw-r--r--src/lib/tests/kex_test_ml_kem.c20
-rw-r--r--src/lib/tests/keyrot_test.c1234
-rw-r--r--src/lib/tests/poa_test.c307
-rw-r--r--src/tools/CMakeLists.txt4
-rw-r--r--src/tools/irm/irm_bind.c4
-rw-r--r--src/tools/irm/irm_bind_ipcp.c13
-rw-r--r--src/tools/irm/irm_bind_process.c22
-rw-r--r--src/tools/irm/irm_bind_program.c19
-rw-r--r--src/tools/irm/irm_ipcp.c2
-rw-r--r--src/tools/irm/irm_ipcp_bootstrap.c157
-rw-r--r--src/tools/irm/irm_ipcp_connect.c90
-rw-r--r--src/tools/irm/irm_ipcp_create.c15
-rw-r--r--src/tools/irm/irm_ipcp_enroll.c58
-rw-r--r--src/tools/irm/irm_ipcp_list.c23
-rw-r--r--src/tools/irm/irm_ipcp_poa.c98
-rw-r--r--src/tools/irm/irm_ipcp_poa_attach.c153
-rw-r--r--src/tools/irm/irm_ipcp_poa_detach.c153
-rw-r--r--src/tools/irm/irm_ipcp_poa_list.c136
-rw-r--r--src/tools/irm/irm_name_create.c29
-rw-r--r--src/tools/irm/irm_name_list.c13
-rw-r--r--src/tools/irm/irm_name_reg.c5
-rw-r--r--src/tools/irm/irm_name_unreg.c5
-rw-r--r--src/tools/irm/irm_ops.h12
-rw-r--r--src/tools/irm/irm_unbind.c4
-rw-r--r--src/tools/irm/irm_unbind_ipcp.c13
-rw-r--r--src/tools/irm/irm_unbind_process.c13
-rw-r--r--src/tools/irm/irm_unbind_program.c18
-rw-r--r--src/tools/irm/irm_utils.c233
-rw-r--r--src/tools/irm/irm_utils.h41
-rw-r--r--src/tools/ocbr/ocbr_client.c23
-rw-r--r--src/tools/ocbr/ocbr_server.c2
-rw-r--r--src/tools/oftp/oftp.c19
-rw-r--r--src/tools/oping/oping_client.c4
197 files changed, 30867 insertions, 8293 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bfabd711..e624c22c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -51,15 +51,16 @@ include(config/global)
include(dependencies)
-include(config/lib)
-include(config/ssm)
+include(config/lib/common)
+include(config/lib/crypt)
+include(config/lib/poa)
+include(config/lib/frct)
+include(config/lib/ssm)
include(config/irmd)
include(config/ipcp/common)
include(config/ipcp/unicast)
include(config/ipcp/broadcast)
include(config/ipcp/local)
-include(config/ipcp/eth)
-include(config/ipcp/udp)
include(tests)
include(include)
diff --git a/cmake/config/global.cmake b/cmake/config/global.cmake
index 0ac256bb..1e172724 100644
--- a/cmake/config/global.cmake
+++ b/cmake/config/global.cmake
@@ -25,8 +25,16 @@ set(SHM_LOCKFILE_NAME "/${SHM_PREFIX}.lockfile" CACHE INTERNAL
# Secure memory configuration
set(IRMD_SECMEM_MAX 1048576 CACHE STRING "IRMd secure heap size")
-set(PROC_SECMEM_MAX 1048576 CACHE STRING "Process secure heap size")
-set(SECMEM_GUARD 32 CACHE STRING "Secure heap min size")
+# ~8 KiB secure heap per encrypted flow (cur+prev node slabs); the total
+# is rounded up to a power of two for the OpenSSL secure-heap allocator.
+set(PROC_SECMEM_FLOWS 512 CACHE STRING
+ "Max concurrent encrypted flows the per-process secure heap is sized for")
+math(EXPR PROC_SECMEM_NEED "${PROC_SECMEM_FLOWS} * 8192")
+set(PROC_SECMEM_MAX 4096)
+while(PROC_SECMEM_MAX LESS PROC_SECMEM_NEED)
+ math(EXPR PROC_SECMEM_MAX "${PROC_SECMEM_MAX} * 2")
+endwhile()
+set(SECMEM_MINSIZE 32 CACHE STRING "Secure heap min alloc size")
# Container/deployment options
set(BUILD_CONTAINER FALSE CACHE BOOL
diff --git a/cmake/config/ipcp/common.cmake b/cmake/config/ipcp/common.cmake
index 0c873b76..79c822a3 100644
--- a/cmake/config/ipcp/common.cmake
+++ b/cmake/config/ipcp/common.cmake
@@ -42,12 +42,11 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
"Slack value for high resolution timers on Linux systems.")
endif()
-# ipcpd-eth flow statistics (requires FUSE - eth.c relies on
-# IPCP_ETH_FLOW_STATS implying HAVE_FUSE for rib_reg/rib_unreg)
+# Per-flow statistics exposed via the RIB (requires FUSE).
if(HAVE_FUSE)
- set(IPCP_ETH_FLOW_STATS FALSE CACHE BOOL
- "Enable ipcpd-eth flow statistics via RIB")
- if(IPCP_ETH_FLOW_STATS)
- message(STATUS "ipcpd-eth flow statistics enabled")
+ set(IPCP_FLOW_STATS TRUE CACHE BOOL
+ "Enable per-flow statistics via the RIB")
+ if(IPCP_FLOW_STATS)
+ message(STATUS "IPCP flow statistics enabled")
endif()
endif()
diff --git a/cmake/config/ipcp/eth.cmake b/cmake/config/ipcp/eth.cmake
deleted file mode 100644
index d336d647..00000000
--- a/cmake/config/ipcp/eth.cmake
+++ /dev/null
@@ -1,22 +0,0 @@
-# Ethernet IPCP configuration options for Ouroboros
-# Options for eth-llc and eth-dix IPCPs
-
-set(IPCP_ETH_LLC_TARGET ipcpd-eth-llc)
-set(IPCP_ETH_DIX_TARGET ipcpd-eth-dix)
-
-set(IPCP_ETH_RD_THR 1 CACHE STRING
- "Number of reader threads in Ethernet IPCP")
-set(IPCP_ETH_WR_THR 1 CACHE STRING
- "Number of writer threads in Ethernet IPCP")
-set(IPCP_ETH_QDISC_BYPASS false CACHE BOOL
- "Bypass the Qdisc in the kernel when using raw sockets")
-set(IPCP_ETH_SNDBUF 0 CACHE STRING
- "Raw socket SO_SNDBUF in bytes; 0 = leave kernel default (wmem_default)")
-set(IPCP_ETH_RCVBUF 0 CACHE STRING
- "Raw socket SO_RCVBUF in bytes; 0 = leave kernel default (rmem_default)")
-set(IPCP_ETH_LO_MTU 9000 CACHE STRING
- "Restrict Ethernet MTU over loopback interfaces")
-set(IPCP_ETH_MGMT_FRAME_SIZE 9000 CACHE STRING
- "Management frame buffer size for Ethernet IPCPs")
-set(IPCP_ETH_MPL 100 CACHE STRING
- "Default maximum packet lifetime for the Ethernet IPCPs, in ms")
diff --git a/cmake/config/ipcp/udp.cmake b/cmake/config/ipcp/udp.cmake
deleted file mode 100644
index af84a844..00000000
--- a/cmake/config/ipcp/udp.cmake
+++ /dev/null
@@ -1,16 +0,0 @@
-# UDP IPCP configuration options for Ouroboros
-# Options for udp4 and udp6 IPCPs
-
-set(IPCP_UDP4_TARGET ipcpd-udp4)
-set(IPCP_UDP6_TARGET ipcpd-udp6)
-
-set(IPCP_UDP_RD_THR 3 CACHE STRING
- "Number of reader threads in UDP IPCPs")
-set(IPCP_UDP_WR_THR 3 CACHE STRING
- "Number of writer threads in UDP IPCPs")
-set(IPCP_UDP_MPL 5000 CACHE STRING
- "Default maximum packet lifetime for the UDP IPCPs, in ms")
-set(IPCP_UDP4_MTU 1472 CACHE STRING
- "Fallback UDP4 layer MTU when getsockopt(IP_MTU) is unavailable, in bytes")
-set(IPCP_UDP6_MTU 1452 CACHE STRING
- "Fallback UDP6 layer MTU when getsockopt(IPV6_MTU) is unavailable, in bytes")
diff --git a/cmake/config/ipcp/unicast.cmake b/cmake/config/ipcp/unicast.cmake
index b8d4d516..92eeae65 100644
--- a/cmake/config/ipcp/unicast.cmake
+++ b/cmake/config/ipcp/unicast.cmake
@@ -9,6 +9,25 @@ set(IPCP_UNICAST_MTU 1400 CACHE STRING
set(PFT_SIZE 256 CACHE STRING
"Prefix forwarding table size for the Unicast IPCP")
+# Aggregate congestion-avoidance context interning. One ctx is shared
+# per (peer, qos cube); this is the hash table bucket count. Must be a
+# power of two (the bucket index masks with CA_BUCKETS - 1).
+set(IPCP_CA_BUCKETS 64 CACHE STRING
+ "Hash buckets for aggregate CA context interning (power of two)")
+math(EXPR IPCP_CA_BUCKETS_POW2 "${IPCP_CA_BUCKETS} & (${IPCP_CA_BUCKETS} - 1)")
+if((IPCP_CA_BUCKETS LESS 1) OR (NOT IPCP_CA_BUCKETS_POW2 EQUAL 0))
+ message(FATAL_ERROR "IPCP_CA_BUCKETS must be a positive power of two")
+endif()
+
+# Per-flow (non-aggregated) congestion avoidance. Aggregate CA per
+# (peer, qos cube) is the production behaviour; enable this only to
+# build the legacy per-flow reference for A/B testing and bisection.
+set(IPCP_CA_PER_FLOW FALSE CACHE BOOL
+ "Use per-flow congestion avoidance (testing only)")
+if(IPCP_CA_PER_FLOW)
+ message(STATUS "IPCP per-flow congestion avoidance (testing build)")
+endif()
+
# Protocol debugging
set(DEBUG_PROTO_DHT FALSE CACHE BOOL
"Add DHT protocol debug logging")
diff --git a/cmake/config/irmd.cmake b/cmake/config/irmd.cmake
index 45d9e73d..bbb73bec 100644
--- a/cmake/config/irmd.cmake
+++ b/cmake/config/irmd.cmake
@@ -11,7 +11,7 @@ set(ENROLL_TIMEOUT 20000 CACHE STRING
set(REG_TIMEOUT 20000 CACHE STRING
"Timeout for registering a name (ms)")
set(QUERY_TIMEOUT 2000 CACHE STRING
- "Timeout to query a name with an IPCP (ms); must exceed shim retry budget")
+ "Timeout to query a name with an IPCP (ms); must exceed PoA retries")
set(CONNECT_TIMEOUT 20000 CACHE STRING
"Timeout to connect an IPCP to another IPCP (ms)")
set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
@@ -20,6 +20,12 @@ set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
# OAP (Ouroboros Authentication Protocol)
set(OAP_REPLAY_TIMER 20 CACHE STRING
"OAP replay protection window (s)")
+set(OAP_REPLAY_MAX 4096 CACHE STRING
+ "Maximum entries in the OAP replay cache (bounds memory/CPU under flood)")
+set(OAP_REKEY_TIMER 120 CACHE STRING
+ "Tier-2 re-key interval (s); bounds key age / PCS healing, 0 disables")
+set(OAP_CLIENT_AUTH_DEFAULT TRUE CACHE BOOL
+ "Client requires the server to authenticate by default")
set(DEBUG_PROTO_OAP FALSE CACHE BOOL
"Add Flow allocation protocol message output to IRMd debug logging")
diff --git a/cmake/config/lib.cmake b/cmake/config/lib.cmake
deleted file mode 100644
index 25130519..00000000
--- a/cmake/config/lib.cmake
+++ /dev/null
@@ -1,103 +0,0 @@
-# Library configuration options for Ouroboros
-# Options affecting libouroboros-common, libouroboros-dev, libouroboros-irm
-
-# Flow limits
-set(SYS_MAX_FLOWS 10240 CACHE STRING
- "Maximum number of total flows for this system")
-set(PROC_MAX_FLOWS 4096 CACHE STRING
- "Maximum number of flows in an application")
-set(PROC_RES_FDS 64 CACHE STRING
- "Number of reserved flow descriptors per application")
-set(PROC_MAX_FQUEUES 32 CACHE STRING
- "Maximum number of flow sets per application")
-
-# Threading
-if(NOT APPLE)
- set(PTHREAD_COND_CLOCK "CLOCK_MONOTONIC" CACHE STRING
- "Clock to use for condition variable timing")
-else()
- set(PTHREAD_COND_CLOCK "CLOCK_REALTIME" CACHE INTERNAL
- "Clock to use for condition variable timing")
-endif()
-
-# Timeouts
-set(SOCKET_TIMEOUT 500 CACHE STRING
- "Default timeout for responses from IPCPs (ms)")
-
-# QoS settings
-set(QOS_DISABLE_CRC TRUE CACHE BOOL
- "Ignores ber setting on all QoS cubes")
-
-include(utils/CPUUtils)
-detect_pclmul()
-detect_pmull()
-if(HAVE_PCLMUL)
- message(STATUS "CRC-64/NVMe backend: PCLMUL (x86 SSE4.1+PCLMUL)")
-elseif(HAVE_PMULL)
- message(STATUS "CRC-64/NVMe backend: PMULL (aarch64 crypto)")
-else()
- message(STATUS "CRC-64/NVMe backend: byte table (no acceleration)")
-endif()
-
-# Delta-t protocol timers (Watson bound: 3*MPL + A + R).
-# MPL is reported per IPCP (IPCP_*_MPL); A and R are FRCT-wide.
-set(DELTA_T_ACK 1000 CACHE STRING
- "Maximum time to acknowledge a packet (ms)")
-set(DELTA_T_RTX 30000 CACHE STRING
- "Maximum time to retransmit a packet (ms)")
-
-# FRCT configuration
-set(FRCT_REORDER_QUEUE_SIZE 128 CACHE STRING
- "Size of the reordering queue, must be a power of 2")
-set(FRCT_START_WINDOW 128 CACHE STRING
- "Start window, must be a power of 2")
-set(FRCT_LINUX_RTT_ESTIMATOR TRUE CACHE BOOL
- "Use Linux RTT estimator formula instead of the TCP RFC formula")
-set(FRCT_RTO_MDEV_MULTIPLIER 2 CACHE STRING
- "Multiplier for deviation term in the RTO: RTO = sRTT + (mdev << X)")
-set(FRCT_RTO_INC_FACTOR 0 CACHE STRING
- "Divisor for RTO increase after timeout: RTO += RTX >> X, 0: Karn/Partridge")
-set(FRCT_RTO_MIN 250 CACHE STRING
- "Hard floor for Retransmission Timeout (RTO) for FRCT (us)")
-set(FRCT_TICK_TIME 5000 CACHE STRING
- "Tick time for FRCT activity (retransmission, acknowledgments) (us)")
-set(FRCT_DEBUG_STDOUT FALSE CACHE BOOL
- "Print FRCT final counters to stdout at flow teardown")
-
-# Retransmission (RXM) configuration
-set(RXM_MIN_RESOLUTION 20 CACHE STRING
- "Minimum retransmission delay (ns), as a power to 2")
-set(RXM_WHEEL_MULTIPLIER 4 CACHE STRING
- "Factor for retransmission wheel levels as a power to 2")
-set(RXM_WHEEL_LEVELS 3 CACHE STRING
- "Number of levels in the retransmission wheel")
-set(RXM_WHEEL_SLOTS_PER_LEVEL 256 CACHE STRING
- "Number of slots per level in the retransmission wheel, must be a power of 2")
-
-# Acknowledgment wheel configuration
-set(ACK_WHEEL_SLOTS 256 CACHE STRING
- "Number of slots in the acknowledgment wheel, must be a power of 2")
-set(ACK_WHEEL_RESOLUTION 18 CACHE STRING
- "Minimum acknowledgment delay (ns), as a power to 2")
-
-# Thread pool manager (TPM) debugging
-set(TPM_DEBUG_REPORT_INTERVAL 0 CACHE STRING
- "Interval at wich the TPM will report long running threads (s), 0 disables")
-set(TPM_DEBUG_ABORT_TIMEOUT 0 CACHE STRING
- "TPM abort process after a thread reaches this timeout (s), 0 disables")
-
-# Encryption
-set(KEY_ROTATION_BIT 20 CACHE STRING
- "Bit position in packet counter that triggers key rotation (default 20 = every 2^20 packets)")
-
-# Flow statistics (requires FUSE)
-if(HAVE_FUSE)
- set(PROC_FLOW_STATS TRUE CACHE BOOL
- "Enable flow statistics tracking for application flows")
- if(PROC_FLOW_STATS)
- message(STATUS "Application flow statistics enabled")
- else()
- message(STATUS "Application flow statistics disabled")
- endif()
-endif()
-
diff --git a/cmake/config/lib/common.cmake b/cmake/config/lib/common.cmake
new file mode 100644
index 00000000..ebdc3196
--- /dev/null
+++ b/cmake/config/lib/common.cmake
@@ -0,0 +1,58 @@
+# Library configuration options for Ouroboros Options affecting libouroboros-common,
+# libouroboros-dev and libouroboros-irm as a whole; per-subsystem options sit beside this file
+
+# Flow limits
+set(SYS_MAX_FLOWS 10240 CACHE STRING
+ "Maximum number of total flows for this system")
+set(PROC_MAX_FLOWS 4096 CACHE STRING
+ "Maximum number of flows in an application")
+set(PROC_RES_FDS 64 CACHE STRING
+ "Number of reserved flow descriptors per application")
+set(PROC_MAX_FQUEUES 32 CACHE STRING
+ "Maximum number of flow sets per application")
+
+# Threading
+if(NOT APPLE)
+ set(PTHREAD_COND_CLOCK "CLOCK_MONOTONIC" CACHE STRING
+ "Clock to use for condition variable timing")
+else()
+ set(PTHREAD_COND_CLOCK "CLOCK_REALTIME" CACHE INTERNAL
+ "Clock to use for condition variable timing")
+endif()
+
+# Timeouts
+set(SOCKET_TIMEOUT 500 CACHE STRING
+ "Default timeout for responses from IPCPs (ms)")
+
+# QoS settings
+set(QOS_DISABLE_CRC TRUE CACHE BOOL
+ "Ignores ber setting on all QoS cubes")
+
+include(utils/CPUUtils)
+detect_pclmul()
+detect_pmull()
+if(HAVE_PCLMUL)
+ message(STATUS "CRC-64/NVMe backend: PCLMUL (x86 SSE4.1+PCLMUL)")
+elseif(HAVE_PMULL)
+ message(STATUS "CRC-64/NVMe backend: PMULL (aarch64 crypto)")
+else()
+ message(STATUS "CRC-64/NVMe backend: byte table (no acceleration)")
+endif()
+
+# Thread pool manager (TPM) debugging
+set(TPM_DEBUG_REPORT_INTERVAL 0 CACHE STRING
+ "Interval at wich the TPM will report long running threads (s), 0 disables")
+set(TPM_DEBUG_ABORT_TIMEOUT 0 CACHE STRING
+ "TPM abort process after a thread reaches this timeout (s), 0 disables")
+
+# Flow statistics (requires FUSE)
+if(HAVE_FUSE)
+ set(PROC_FLOW_STATS TRUE CACHE BOOL
+ "Enable flow statistics tracking for application flows")
+ if(PROC_FLOW_STATS)
+ message(STATUS "Application flow statistics enabled")
+ else()
+ message(STATUS "Application flow statistics disabled")
+ endif()
+endif()
+
diff --git a/cmake/config/lib/crypt.cmake b/cmake/config/lib/crypt.cmake
new file mode 100644
index 00000000..45fdaaed
--- /dev/null
+++ b/cmake/config/lib/crypt.cmake
@@ -0,0 +1,41 @@
+# Encryption configuration for Ouroboros Options for the key schedule and packet protection in
+# src/lib/crypt/
+
+# Encryption
+set(KEY_LEAF_BITS 20 CACHE STRING
+ "Packets per leaf key as a power of two (2^20 = AEAD-safe default)")
+set(KEY_NODE_BITS 6 CACHE STRING
+ "Leaf keys per node key, power of two (2^6 = 64; leak compartment)")
+set(KEY_NODE_COUNT 128 CACHE STRING
+ "Node keys per batch (N); <= 4096, the 12-bit on-wire node index")
+set(KEY_REKEY_WATERMARK 4 CACHE STRING
+ "Re-key when this many node keys remain; 0 disables the count trigger")
+set(KEY_REPLAY_WINDOW 2048 CACHE STRING
+ "RX replay window in packets; power of two, >= 128")
+set(KEY_REKEY_WM_CHECK_BITS 16 CACHE STRING
+ "Re-key watermark is consulted once per 2^n flow writes")
+if(NOT KEY_REPLAY_WINDOW MATCHES "^[0-9]+$")
+ message(FATAL_ERROR "KEY_REPLAY_WINDOW must be a positive integer")
+endif()
+math(EXPR _krw_p2 "${KEY_REPLAY_WINDOW} & (${KEY_REPLAY_WINDOW} - 1)")
+if(KEY_REPLAY_WINDOW LESS 128 OR NOT _krw_p2 EQUAL 0)
+ message(FATAL_ERROR "KEY_REPLAY_WINDOW must be a power of two >= 128")
+endif()
+
+# Re-key must finish within its lead window - KEY_REKEY_WATERMARK node keys worth of packets -
+# before the batch exhausts and TX fails closed. dev.c only evaluates the watermark once per
+# FLOW_WM_CHECK writes, so a lead below ~2x that leaves a high-rate flow no room to complete the
+# exchange. Production defaults are vast; this guards under-sized (test) geometries.
+if(KEY_REKEY_WATERMARK GREATER 0)
+ math(EXPR _rk_wm_check "1 << ${KEY_REKEY_WM_CHECK_BITS}")
+ math(EXPR _rk_lead
+ "${KEY_REKEY_WATERMARK} << (${KEY_LEAF_BITS} + ${KEY_NODE_BITS})")
+ math(EXPR _rk_min "2 * ${_rk_wm_check}")
+ if(_rk_lead LESS _rk_min)
+ message(WARNING
+ "Re-key lead is ${_rk_lead} packets vs the watermark check interval "
+ "${_rk_wm_check}; a high-rate flow may exhaust its key batch before the "
+ "re-key completes (TX fails closed until it does). Raise KEY_LEAF_BITS, "
+ "KEY_NODE_BITS, or KEY_REKEY_WATERMARK.")
+ endif()
+endif()
diff --git a/cmake/config/lib/frct.cmake b/cmake/config/lib/frct.cmake
new file mode 100644
index 00000000..1bd60aa0
--- /dev/null
+++ b/cmake/config/lib/frct.cmake
@@ -0,0 +1,43 @@
+# FRCT configuration for Ouroboros Options for the flow and retransmission control protocol in
+# src/lib/frct.c
+
+# Delta-t protocol timers (Watson bound: 3*MPL + A + R).
+# MPL is reported per IPCP (IPCP_*_MPL); A and R are FRCT-wide.
+set(DELTA_T_ACK 1000 CACHE STRING
+ "Maximum time to acknowledge a packet (ms)")
+set(DELTA_T_RTX 32000 CACHE STRING
+ "Maximum time to retransmit a packet (ms)")
+
+# FRCT configuration
+set(FRCT_REORDER_QUEUE_SIZE 128 CACHE STRING
+ "Size of the reordering queue, must be a power of 2")
+set(FRCT_START_WINDOW 128 CACHE STRING
+ "Start window, must be a power of 2")
+set(FRCT_LINUX_RTT_ESTIMATOR TRUE CACHE BOOL
+ "Use Linux RTT estimator formula instead of the TCP RFC formula")
+set(FRCT_RTO_MDEV_MULTIPLIER 2 CACHE STRING
+ "Multiplier for deviation term in the RTO: RTO = sRTT + (mdev << X)")
+set(FRCT_RTO_INC_FACTOR 0 CACHE STRING
+ "Divisor for RTO increase after timeout: RTO += RTX >> X, 0: Karn/Partridge")
+set(FRCT_RTO_MIN 250 CACHE STRING
+ "Hard floor for Retransmission Timeout (RTO) for FRCT (us)")
+set(FRCT_TICK_TIME 5000 CACHE STRING
+ "Tick time for FRCT activity (retransmission, acknowledgments) (us)")
+set(FRCT_DEBUG_STDOUT FALSE CACHE BOOL
+ "Print FRCT final counters to stdout at flow teardown")
+
+# Retransmission (RXM) configuration
+set(RXM_MIN_RESOLUTION 20 CACHE STRING
+ "Minimum retransmission delay (ns), as a power to 2")
+set(RXM_WHEEL_MULTIPLIER 4 CACHE STRING
+ "Factor for retransmission wheel levels as a power to 2")
+set(RXM_WHEEL_LEVELS 3 CACHE STRING
+ "Number of levels in the retransmission wheel")
+set(RXM_WHEEL_SLOTS_PER_LEVEL 256 CACHE STRING
+ "Number of slots per level in the retransmission wheel, must be a power of 2")
+
+# Acknowledgment wheel configuration
+set(ACK_WHEEL_SLOTS 256 CACHE STRING
+ "Number of slots in the acknowledgment wheel, must be a power of 2")
+set(ACK_WHEEL_RESOLUTION 18 CACHE STRING
+ "Minimum acknowledgment delay (ns), as a power to 2")
diff --git a/cmake/config/lib/poa.cmake b/cmake/config/lib/poa.cmake
new file mode 100644
index 00000000..c1ca10a8
--- /dev/null
+++ b/cmake/config/lib/poa.cmake
@@ -0,0 +1,37 @@
+# Flow Point of attachment (poa) configuration for Ouroboros
+
+set(POA_MGMT_FRAME_SIZE 16384 CACHE STRING
+ "Maximum size of a flow endpoint management frame, in bytes")
+
+# Management frames are retried or repeated, so a send never waits long.
+set(POA_MGMT_SND_TIMEO 100 CACHE STRING
+ "Deadline for sending a management frame, in ms")
+
+# Flows on one PoA share its transmit queue, so congestion avoidance keys its link estimator on the
+# PoA and needs a bound on their number.
+set(POA_MAX_POAS 16 CACHE STRING
+ "Maximum number of points of attachment per IPCP")
+
+# UDP endpoints
+set(POA_UDP_MPL 5000 CACHE STRING
+ "Default maximum packet lifetime for UDP flow endpoints, in ms")
+set(POA_UDP4_MTU 1472 CACHE STRING
+ "Fallback UDP4 endpoint MTU when getsockopt(IP_MTU) is unavailable")
+set(POA_UDP6_MTU 1452 CACHE STRING
+ "Fallback UDP6 endpoint MTU when getsockopt(IPV6_MTU) is unavailable")
+set(POA_UDP_RD_BUF 65535 CACHE STRING
+ "UDP endpoint receive buffer in bytes. Bounds the advertised MTU")
+
+# Ethernet endpoints
+set(POA_ETH_MPL 100 CACHE STRING
+ "Default maximum packet lifetime for Ethernet flow endpoints, in ms")
+set(POA_ETH_QDISC_BYPASS false CACHE BOOL
+ "Bypass the Qdisc in the kernel when using raw sockets")
+set(POA_ETH_SNDBUF 0 CACHE STRING
+ "Raw socket SO_SNDBUF in bytes (floored to one frame). 0 = kernel default")
+set(POA_ETH_RCVBUF 0 CACHE STRING
+ "Raw socket SO_RCVBUF in bytes. 0 = kernel default")
+set(POA_ETH_LO_MTU 9000 CACHE STRING
+ "Restrict Ethernet flow endpoint MTU over loopback interfaces")
+set(POA_ETH_RD_BUF 16384 CACHE STRING
+ "Cap on the Ethernet endpoint receive buffer and MTU, in bytes")
diff --git a/cmake/config/ssm.cmake b/cmake/config/lib/ssm.cmake
index 913396ec..aa9c4f75 100644
--- a/cmake/config/ssm.cmake
+++ b/cmake/config/lib/ssm.cmake
@@ -1,6 +1,5 @@
-# Secure Shared Memory (SSM) pool configuration for Ouroboros
-# This file defines the allocation parameters for the secure shared memory
-# pool allocator
+# Secure Shared Memory (SSM) pool configuration for Ouroboros This file defines the allocation
+# parameters for the secure shared memory pool allocator
# Shared memory pool naming configuration
set(SSM_PREFIX "ouroboros" CACHE STRING
@@ -19,18 +18,14 @@ set(SSM_PK_BUFF_HEADSPACE 256 CACHE STRING
"Bytes of headspace to reserve for future headers")
set(SSM_PK_BUFF_TAILSPACE 32 CACHE STRING
"Bytes of tailspace to reserve for future tails")
-# Sized to absorb burst arrivals from fragmented SDUs without
-# overflowing at the eth->FRCT boundary. Must hold at least one
-# full FRCT reorder window plus margin for transient app-thread
-# unavailability; 4x FRCT_REORDER_QUEUE_SIZE leaves comfortable
-# burst headroom. Floor at 1024 for small RQ configs.
-math(EXPR _SSM_RBUFF_DEFAULT "${FRCT_REORDER_QUEUE_SIZE} * 4")
-if(_SSM_RBUFF_DEFAULT LESS 1024)
- set(_SSM_RBUFF_DEFAULT 1024)
-endif()
-set(SSM_RBUFF_SIZE ${_SSM_RBUFF_DEFAULT} CACHE STRING
- "Number of blocks in rbuff buffer, must be a power of 2")
-unset(_SSM_RBUFF_DEFAULT)
+set(SSM_RBUFF_SIZE 1024 CACHE STRING
+ "Number of slots in a flow's rbuff ring; must be a power of 2")
+set(SSM_RBUFF_TXQ_DELAY 10 CACHE STRING
+ "Queueing delay a flow's tx ring may hold (ms); 0 is unlimited")
+set(SSM_RBUFF_TXQ_PRIO_MUL 2 CACHE STRING
+ "Retransmissions get this multiple of the tx occupancy limit")
+set(SSM_RBUFF_TXQ_PRIO_DIV 16 CACHE STRING
+ "Ring fraction (1/N) new data leaves to retransmissions")
set(SSM_RBUFF_PREFIX "/${SHM_PREFIX}.rbuff." CACHE INTERNAL
"Prefix for rbuff POSIX shared memory filenames")
set(SSM_FLOW_SET_PREFIX "/${SHM_PREFIX}.set." CACHE INTERNAL
@@ -39,9 +34,11 @@ set(SSM_FLOW_SET_PREFIX "/${SHM_PREFIX}.set." CACHE INTERNAL
# Number of shards per size class for reducing contention
set(SSM_POOL_SHARDS 4 CACHE STRING
"Number of allocator shards per size class")
+set(SSM_POOL_RECLAIM_AGE_S 60 CACHE STRING
+ "Minimum age in seconds before a block is presumed stale and reclaimed")
-# Global Shared Packet Pool (GSPP) - for privileged processes
-# Shared by all processes in 'ouroboros' group (~60 MB total)
+# Global Shared Packet Pool (GSPP) - for privileged processes Shared by all processes in 'ouroboros'
+# group (~60 MB total)
set(SSM_GSPP_256_BLOCKS 1024 CACHE STRING
"GSPP: Number of 256B blocks")
set(SSM_GSPP_512_BLOCKS 2048 CACHE STRING
@@ -61,8 +58,8 @@ set(SSM_GSPP_256K_BLOCKS 32 CACHE STRING
set(SSM_GSPP_1M_BLOCKS 16 CACHE STRING
"GSPP: Number of 1MB blocks")
-# Per-User Pool (PUP) - for unprivileged applications
-# Each unprivileged app gets its own smaller pool (~7.5 MB total)
+# Per-User Pool (PUP) - for unprivileged applications Each unprivileged app gets its own smaller
+# pool (~7.5 MB total)
set(SSM_PUP_256_BLOCKS 512 CACHE STRING
"PUP: Number of 256B blocks")
set(SSM_PUP_512_BLOCKS 512 CACHE STRING
@@ -155,10 +152,49 @@ message(STATUS " Blocks: ${SSM_PUP_256_BLOCKS}, ${SSM_PUP_512_BLOCKS}, "
"${SSM_PUP_16K_BLOCKS}, ${SSM_PUP_64K_BLOCKS}, ${SSM_PUP_256K_BLOCKS}, "
"${SSM_PUP_1M_BLOCKS}")
-# FRCT reorder queue must fit in every enabled size class. If RQ_SIZE
-# >= any backing pool, the receiver advertises a window the pool
-# cannot back; np1_flow_write fails under load and a single dropped
-# fragment wedges the flow. Auto-zeroed classes are skipped.
+math(EXPR SSM_RBUFF_TXQ_RESERVE
+ "${SSM_RBUFF_SIZE} / ${SSM_RBUFF_TXQ_PRIO_DIV}")
+
+set(SSM_RBUFF_TXQ_RESERVE ${SSM_RBUFF_TXQ_RESERVE} CACHE INTERNAL
+ "Top-of-ring slots new data may not use")
+
+math(EXPR SSM_RBUFF_TXQ_DATA_MAX
+ "${SSM_RBUFF_SIZE} - 1 - ${SSM_RBUFF_TXQ_RESERVE}")
+
+# The limiter asserts its target fits SSM_RBUFF_TXQ_MAX_DELAY (1 s).
+if(SSM_RBUFF_TXQ_DELAY LESS 0 OR SSM_RBUFF_TXQ_DELAY GREATER 1000)
+ message(FATAL_ERROR
+ "SSM_RBUFF_TXQ_DELAY (${SSM_RBUFF_TXQ_DELAY}) must be in "
+ "[0, 1000] ms; a longer target trips the limiter's own bound.")
+endif()
+
+# What the reserve leaves new data must still clear the 4-slot floor.
+if(SSM_RBUFF_TXQ_DATA_MAX LESS 4)
+ message(FATAL_ERROR
+ "SSM_RBUFF_TXQ_PRIO_DIV (${SSM_RBUFF_TXQ_PRIO_DIV}) leaves new "
+ "data ${SSM_RBUFF_TXQ_DATA_MAX} of ${SSM_RBUFF_SIZE} slots, "
+ "below the 4-slot floor: every write would be refused.")
+endif()
+
+# Both bounds keep the retransmission headroom above zero: the divisor
+# reserves it at saturation, the multiplier below it.
+if(SSM_RBUFF_TXQ_PRIO_DIV LESS 2 OR SSM_RBUFF_TXQ_RESERVE LESS 1)
+ message(FATAL_ERROR
+ "SSM_RBUFF_TXQ_PRIO_DIV (${SSM_RBUFF_TXQ_PRIO_DIV}) must be in "
+ "[2, SSM_RBUFF_SIZE (${SSM_RBUFF_SIZE})]: it reserves "
+ "${SSM_RBUFF_TXQ_RESERVE} of ${SSM_RBUFF_SIZE} slots for "
+ "retransmissions.")
+endif()
+
+if(SSM_RBUFF_TXQ_PRIO_MUL LESS 2)
+ message(FATAL_ERROR
+ "SSM_RBUFF_TXQ_PRIO_MUL (${SSM_RBUFF_TXQ_PRIO_MUL}) must be >= 2: "
+ "at 1 a retransmission gets the ceiling new data already has.")
+endif()
+
+# FRCT reorder queue must fit in every enabled size class. If RQ_SIZE >= any backing pool, the
+# receiver advertises a window the pool cannot back; np1_flow_write fails under load and a single
+# dropped fragment wedges the flow. Auto-zeroed classes are skipped.
foreach(_class 256 512 1K 2K)
if(SSM_PUP_${_class}_BLOCKS GREATER 0
AND NOT FRCT_REORDER_QUEUE_SIZE LESS SSM_PUP_${_class}_BLOCKS)
diff --git a/cmake/config/tests.cmake b/cmake/config/tests.cmake
index 37c06ca9..c0d0ed84 100644
--- a/cmake/config/tests.cmake
+++ b/cmake/config/tests.cmake
@@ -15,3 +15,9 @@ if(DISABLE_TESTS_CORE_DUMPS)
else()
message(STATUS "Core dumps in tests enabled")
endif()
+
+set(MB_ECN_LAB_FULL FALSE CACHE BOOL
+ "Run the full mb-ecn lab rate sweep, up to 10 Gb/s")
+if(MB_ECN_LAB_FULL)
+ message(STATUS "Full mb-ecn lab rate sweep enabled")
+endif()
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
index 109fe1d6..93c9c0de 100644
--- a/cmake/dependencies.cmake
+++ b/cmake/dependencies.cmake
@@ -7,6 +7,7 @@ include(dependencies/system/libraries)
include(dependencies/system/explicit_bzero)
include(dependencies/system/robustmutex)
include(dependencies/system/fuse)
+include(dependencies/system/liburcu)
include(dependencies/system/sysrandom)
# Cryptography
@@ -16,19 +17,19 @@ include(dependencies/crypt/libgcrypt)
# IRMd
include(dependencies/irmd/libtoml)
-# Ethernet IPCP backends
+# Ethernet PoA backends. netmap is last: it overrides the others.
include(dependencies/eth/rawsockets)
include(dependencies/eth/bpf)
include(dependencies/eth/netmap)
+
+# The Ethernet PoA needs a backend to send and receive frames.
if(HAVE_RAW_SOCKETS OR HAVE_BPF OR HAVE_NETMAP)
- set(HAVE_ETH TRUE CACHE INTERNAL "Ethernet IPCP support available")
+ set(HAVE_ETH TRUE)
else()
- unset(HAVE_ETH CACHE)
+ message(STATUS "No Ethernet backend, Ethernet PoAs disabled")
+ unset(HAVE_ETH)
endif()
-# UDP IPCP
-include(dependencies/udp/ddns)
-
# Coverage tools
include(dependencies/coverage/gcov)
include(dependencies/coverage/lcov)
diff --git a/cmake/dependencies/crypt/openssl.cmake b/cmake/dependencies/crypt/openssl.cmake
index 38eb826f..04451357 100644
--- a/cmake/dependencies/crypt/openssl.cmake
+++ b/cmake/dependencies/crypt/openssl.cmake
@@ -25,49 +25,39 @@ message(STATUS "OpenSSL support enabled, found version ${OPENSSL_VERSION}")
set(HAVE_OPENSSL TRUE CACHE INTERNAL
"OpenSSL cryptography support available")
-if(OPENSSL_VERSION VERSION_GREATER_EQUAL "3.4.0")
- set(DISABLE_ML_KEM FALSE CACHE BOOL
- "Disable ML-KEM support")
- set(DISABLE_ML_DSA FALSE CACHE BOOL
- "Disable ML-DSA support")
- if(NOT DISABLE_ML_KEM)
- set(HAVE_OPENSSL_ML_KEM TRUE CACHE INTERNAL
- "OpenSSL ML-KEM available")
- message(STATUS "OpenSSL ML-KEM support enabled")
- else()
- message(STATUS "OpenSSL ML-KEM support disabled")
- unset(HAVE_OPENSSL_ML_KEM CACHE)
- endif()
- if(NOT DISABLE_ML_DSA)
- set(HAVE_OPENSSL_ML_DSA TRUE CACHE INTERNAL
- "OpenSSL ML-DSA available")
- message(STATUS "OpenSSL ML-DSA support enabled")
- else()
- message(STATUS "OpenSSL ML-DSA support disabled")
- unset(HAVE_OPENSSL_ML_DSA CACHE)
- endif()
-else()
- message(STATUS
- "Install OpenSSL >= 3.4.0 for ML-KEM/ML-DSA")
- unset(HAVE_OPENSSL_ML_KEM CACHE)
- unset(HAVE_OPENSSL_ML_DSA CACHE)
-endif()
-
if(OPENSSL_VERSION VERSION_GREATER_EQUAL "3.5.0")
- set(DISABLE_SLH_DSA FALSE CACHE BOOL
+ set(DISABLE_ML FALSE CACHE BOOL
+ "Disable ML-KEM/ML-DSA support")
+ set(DISABLE_SLH FALSE CACHE BOOL
"Disable SLH-DSA support")
- if(NOT DISABLE_SLH_DSA)
- set(HAVE_OPENSSL_SLH_DSA TRUE CACHE INTERNAL
+ if(NOT DISABLE_ML)
+ set(HAVE_ML TRUE CACHE INTERNAL
+ "OpenSSL ML-KEM/ML-DSA available")
+ message(STATUS "OpenSSL ML-KEM/ML-DSA support enabled")
+ else()
+ message(STATUS "OpenSSL ML-KEM/ML-DSA support disabled")
+ unset(HAVE_ML CACHE)
+ endif()
+ if(NOT DISABLE_SLH)
+ set(HAVE_SLH TRUE CACHE INTERNAL
"OpenSSL SLH-DSA available")
message(STATUS "OpenSSL SLH-DSA support enabled")
else()
message(STATUS "OpenSSL SLH-DSA support disabled")
- unset(HAVE_OPENSSL_SLH_DSA CACHE)
+ unset(HAVE_SLH CACHE)
endif()
else()
message(STATUS
- "Install OpenSSL >= 3.5.0 for SLH-DSA")
- unset(HAVE_OPENSSL_SLH_DSA CACHE)
+ "Install OpenSSL >= 3.5.0 for PQC (ML-KEM/ML-DSA/SLH-DSA)")
+ unset(HAVE_ML CACHE)
+ unset(HAVE_SLH CACHE)
+endif()
+
+if(OPENSSL_VERSION VERSION_GREATER_EQUAL "4.1.0")
+ set(HAVE_OPENSSL_4_1 TRUE CACHE INTERNAL
+ "OpenSSL 4.1 or later")
+else()
+ unset(HAVE_OPENSSL_4_1 CACHE)
endif()
# Secure memory options are in cmake/config/global.cmake
diff --git a/cmake/dependencies/eth/netmap.cmake b/cmake/dependencies/eth/netmap.cmake
index 94ecd634..b31c4d03 100644
--- a/cmake/dependencies/eth/netmap.cmake
+++ b/cmake/dependencies/eth/netmap.cmake
@@ -1,18 +1,22 @@
-# netmap support (optional acceleration)
+# netmap support (kernel bypass). Explicit opt-in.
find_path(NETMAP_C_INCLUDE_DIR
net/netmap_user.h
HINTS /usr/include /usr/local/include)
mark_as_advanced(NETMAP_C_INCLUDE_DIR)
-if(NOT HAVE_RAW_SOCKETS AND NOT HAVE_BPF AND NETMAP_C_INCLUDE_DIR)
- set(DISABLE_NETMAP FALSE CACHE BOOL
- "Disable netmap support for ETH IPCPs")
- if(NOT DISABLE_NETMAP)
- message(STATUS "Netmap support for Ethernet IPCPs enabled")
- set(HAVE_NETMAP TRUE)
- else()
- message(STATUS "Netmap support for Ethernet IPCPs disabled by user")
- unset(HAVE_NETMAP)
+set(ENABLE_NETMAP FALSE CACHE BOOL
+ "Use netmap for Ethernet PoAs, overriding raw sockets or BPF")
+
+if(ENABLE_NETMAP)
+ if(NOT NETMAP_C_INCLUDE_DIR)
+ message(FATAL_ERROR "ENABLE_NETMAP is set, but netmap was not found.")
endif()
+
+ message(STATUS "Netmap support for Ethernet PoAs enabled")
+
+ set(HAVE_NETMAP TRUE)
+
+ unset(HAVE_RAW_SOCKETS)
+ unset(HAVE_BPF)
endif()
diff --git a/cmake/dependencies/system/liburcu.cmake b/cmake/dependencies/system/liburcu.cmake
new file mode 100644
index 00000000..89a7ab12
--- /dev/null
+++ b/cmake/dependencies/system/liburcu.cmake
@@ -0,0 +1,45 @@
+# Userspace RCU (liburcu) - optional. Enables lock-free data-plane key
+# rotation; absent => per-flow rwlock fallback. The "bulletproof" flavour
+# (urcu-bp) auto-registers reader threads, so application threads need no
+# RCU lifecycle plumbing.
+if(PkgConfig_FOUND)
+ pkg_check_modules(URCU_PKG QUIET IMPORTED_TARGET liburcu-bp)
+ if(URCU_PKG_FOUND AND NOT TARGET Urcu::Urcu)
+ add_library(Urcu::Urcu ALIAS PkgConfig::URCU_PKG)
+ endif()
+endif()
+
+if(NOT URCU_PKG_FOUND)
+ find_library(URCU_BP_LIBRARY urcu-bp QUIET)
+ find_library(URCU_COMMON_LIBRARY urcu-common QUIET)
+ find_path(URCU_INCLUDE_DIR urcu-bp.h QUIET)
+ if(URCU_BP_LIBRARY AND URCU_COMMON_LIBRARY AND URCU_INCLUDE_DIR)
+ set(URCU_PKG_FOUND TRUE)
+ if(NOT TARGET Urcu::Urcu)
+ add_library(Urcu::Urcu INTERFACE IMPORTED)
+ set_target_properties(Urcu::Urcu PROPERTIES
+ INTERFACE_LINK_LIBRARIES "${URCU_BP_LIBRARY};${URCU_COMMON_LIBRARY}"
+ INTERFACE_INCLUDE_DIRECTORIES "${URCU_INCLUDE_DIR}")
+ endif()
+ endif()
+endif()
+
+if(URCU_PKG_FOUND)
+ set(DISABLE_LIBURCU FALSE CACHE BOOL "Disable liburcu (RCU) support")
+ if(NOT DISABLE_LIBURCU)
+ if(URCU_PKG_VERSION)
+ message(STATUS "liburcu (RCU) support enabled (version ${URCU_PKG_VERSION})")
+ else()
+ message(STATUS "liburcu (RCU) support enabled")
+ endif()
+ set(HAVE_LIBURCU TRUE CACHE INTERNAL "Userspace RCU (liburcu) available")
+ else()
+ message(STATUS "liburcu (RCU) support disabled by user")
+ unset(HAVE_LIBURCU CACHE)
+ endif()
+else()
+ message(STATUS "Install liburcu (urcu-bp) for lock-free data-plane re-keying")
+ unset(HAVE_LIBURCU CACHE)
+endif()
+
+mark_as_advanced(URCU_BP_LIBRARY URCU_COMMON_LIBRARY URCU_INCLUDE_DIR)
diff --git a/cmake/dependencies/udp/ddns.cmake b/cmake/dependencies/udp/ddns.cmake
deleted file mode 100644
index e8208e47..00000000
--- a/cmake/dependencies/udp/ddns.cmake
+++ /dev/null
@@ -1,31 +0,0 @@
-# DDNS (Dynamic DNS) support detection
-# Requires nsupdate and nslookup tools
-
-find_program(NSUPDATE_EXECUTABLE
- NAMES nsupdate
- DOC "The nsupdate tool that enables DDNS")
-
-find_program(NSLOOKUP_EXECUTABLE
- NAMES nslookup
- DOC "The nslookup tool that resolves DNS names")
-
-mark_as_advanced(NSLOOKUP_EXECUTABLE NSUPDATE_EXECUTABLE)
-
-if(NSLOOKUP_EXECUTABLE AND NSUPDATE_EXECUTABLE)
- set(DISABLE_DDNS FALSE CACHE BOOL "Disable DDNS support")
- if(NOT DISABLE_DDNS)
- message(STATUS "DDNS support enabled")
- set(HAVE_DDNS TRUE CACHE INTERNAL "Dynamic DNS support available")
- else()
- message(STATUS "DDNS support disabled by user")
- unset(HAVE_DDNS CACHE)
- endif()
-else()
- if(NSLOOKUP_EXECUTABLE)
- message(STATUS "Install nsupdate to enable DDNS support")
- elseif(NSUPDATE_EXECUTABLE)
- message(STATUS "Install nslookup to enable DDNS support")
- else()
- message(STATUS "Install nslookup and nsupdate to enable DDNS support")
- endif()
-endif()
diff --git a/doc/man/fccntl.3 b/doc/man/fccntl.3
index 767342b9..3dcfc2d9 100644
--- a/doc/man/fccntl.3
+++ b/doc/man/fccntl.3
@@ -76,6 +76,17 @@ buffer. Takes a \fBsize_t \fIqlen\fR * as third argument.
\fBFLOWGTXQLEN\fR - get the current number of packets in the transmit
buffer. Takes a \fBsize_t \fIqlen\fR * as third argument.
+\fBFLOWSTXQDLY\fR - set the target queueing delay for the transmit
+occupancy limiter. Takes a \fBstruct timespec * \fItimeo\fR as third
+argument. A zero delay leaves the ring unlimited, bounded only by its
+slot count. The build-time default is applied when the flow is
+created. The delay must be a normalised, non-negative timespec no
+longer than the maximum the limiter accepts.
+
+\fBFLOWGTXQDLY\fR - retrieve the current target queueing delay for
+the transmit occupancy limiter. Takes a \fBstruct timespec * \fItimeo\fR
+as third argument.
+
\fBFRCTSFLAGS\fR - set the current flow flags. Takes an \fBuint16_t
\fIflags\fR as third argument. Supported flags are:
@@ -101,12 +112,15 @@ On success, \fBfccntl\fR() returns 0.
\fBfccntl\fR() can return
.B -EINVAL
-An invalid argument was passed.
+An invalid argument was passed. This is also returned when the delay
+passed to FLOWSTXQDLY is negative, is not normalised, or exceeds the
+maximum the transmit occupancy limiter accepts.
.B -EPERM
Operation not permitted. This is returned when requesting the value of
a timeout (FLOWGSNDTIMEO or FLOWGRCVTIMEO) when no such timeout was
-set.
+set, or when FLOWSTXQDLY/FLOWGTXQDLY is used on a flow with no
+transmit occupancy ring.
.B -EBADF
Invalid flow descriptor passed.
diff --git a/doc/man/ouroboros-tutorial.7 b/doc/man/ouroboros-tutorial.7
index 1fc02a02..54abba5b 100644
--- a/doc/man/ouroboros-tutorial.7
+++ b/doc/man/ouroboros-tutorial.7
@@ -47,37 +47,56 @@ The output should be
.SH PINGING A SERVER APPLICATION OVER THE LOOPBACK ADAPTER
With a running irmd, let's create an IPC process. For this tutorial,
-we will create and bootstrap an eth-llc IPCP over the loopback
-interface. To observe what's going on, open another terminal
-window. Note that "ipcp bootstrap" will create an IPCP if an IPCP by
-that name does not yet exist (See \fBouroboros\fR(8)).
+we will create a unicast IPCP, attach it to the loopback interface and
+bootstrap it. To observe what's going on, open another terminal
+window.
.RS 4
-$ irm ipcp bootstrap type eth-llc name llc layer llc if lo
+$ irm ipcp create name lan type unicast
.RE
.RS 4
==23918== irmd(II): Created IPCP 23932.
-.br
-==23932== ipcpd/eth-llc(II): Using raw socket device.
-.br
-==23918== irmd(II): Bootstrapped IPCP 23932 in layer llc.
+.RE
+
+An IPCP needs a \fIpoint of attachment\fR: the transmission technology
+it sends and receives on. Attach it to the loopback interface (See
+\fBouroboros\fR(8)).
+
+.RS 4
+$ irm ipcp poa attach name lan eth dev lo
+.RE
+
+.RS 4
+==23918== irmd(II): Attached IPCP 23932.
+.RE
+
+.RS 4
+$ irm ipcp bootstrap name lan layer lan
+.RE
+
+.RS 4
+==23918== irmd(II): Bootstrapped IPCP 23932.
.RE
Now that we have the IPCP bootstrapped, it can act as a local network
layer that can provide full connectivity between all processes in the
system. Let's test it using the oping application. First, let's choose
-a name for the server ("my.oping.server") and register in the llc
+a name for the server ("my.oping.server") and register it in the lan
layer.
.RS 4
-$ irm reg name my.oping.server layer llc
+$ irm name create my.oping.server
+.br
+$ irm name register my.oping.server layer lan
.RE
The IRMd should respond with
.RS 4
-==23918== irmd(II): Registered my.oping.server in llc as 716016b1.
+==23918== irmd(II): Created new name: my.oping.server.
+.br
+==23918== irmd(II): Registered my.oping.server with IPCP 23932 as 716016b1.
.RE
Now start a server of oping in the background (or in a different
diff --git a/doc/man/ouroboros.8 b/doc/man/ouroboros.8
index 759b1433..4c83e5a9 100644
--- a/doc/man/ouroboros.8
+++ b/doc/man/ouroboros.8
@@ -98,10 +98,13 @@ accessed by other processes.
In order to enroll an IPC process in a layer, some other member will
have to be reachable over a lower layer. IPCPs that wrap a legacy
transmission technology are all bootstrapped and thus need not enroll
-as they work directly over a physical connection. Ouroboros currently
-supports IPCPs over shared memory (local), L2 (eth-llc and eth-dix)
-and L3 (udp). The unicast and broadcast layers require connections to
-be established between IPCP components for its operation.
+as they work directly over a physical connection. A unicast or
+broadcast IPCP attaches to a transmission technology itself, as a
+\fIpoint of attachment\fR (PoA); Ouroboros currently supports PoAs on
+L2 (Ethernet) and L3 (UDP/IPv4 and UDP/IPv6), next to IPCPs over
+shared memory (local). The unicast and broadcast layers require
+connections to be established between IPCP components for its
+operation.
\fBConnecting the management components\fR using \fImanagement
flows\fR allows management information to be sent between IPCPs so
@@ -123,14 +126,6 @@ creates an IPCP process of type \fItype\fR in the system with name
.PP
\fBlocal\fR - create a loopback IPCP.
.PP
-\fBeth-llc\fR - create an IPCP that attaches to Ethernet using LLC frames.
-.PP
-\fBeth-dix\fR - create an IPCP that attaches to Ethernet using DIX frames.
-.PP
-\fBudp4\fR - create an IPCP that attaches to a UDP/IPv4 socket.
-.PP
-\fBudp6\fR - create an IPCP that attaches to a UDP/IPv6 socket.
-.PP
\fBunicast\fR - create a unicast IPCP that uses lower level layers.
.PP
\fBbroadcast\fR - create a broadcast IPCP that uses lower level layers.
@@ -161,69 +156,9 @@ Values for [\fIparam\fR] are dependent on \fItype\fR:
default: SHA3_256.
.RE
-.PP
-\fBeth-llc\fR
-.RS 4
-.PP
-dev \fIinterface\fR specifies the interface to bind the IPCP to.
-.PP
-[hash \fIpolicy\fR] specifies the hash function used for the directory,
-.br
-\fIpolicy\fR: SHA3_224, SHA3_256, SHA3_384, SHA3_512.
-.br
-default: SHA3_256.
-.RE
-.PP
-\fBeth-dix\fR
-.RS 4
-.PP
-dev \fIinterface\fR specifies the interface to bind the IPCP to.
-.PP
-[ethertype \fIethertype\fR] specifies the ethertype used for the layer.
-.br
-default: 0xA000.
-.PP
-[hash \fIpolicy\fR] specifies the hash function used for the directory,
-.br
-\fIpolicy\fR: SHA3_224, SHA3_256, SHA3_384, SHA3_512.
-.br
-default: SHA3_256.
-.RE
-.PP
-\fBudp4\fR
-.RS 4
-.PP
-ip \fIip\fR specifies the local IPv4 address to bind to
-.PP
-[dns \fIdns\fR] specifies an optional DDNS server that will be used for
-the directory.
-.PP
-[port \fIport\fR] specifies a UDP port that is used for sending and
-receiving ouroboros traffic. This must be the same for the entire UDP4
-layer. Parallel UDP4 layers should use different ports. This UDP port
-needs to be forwarded if the server is behind a NAT and wants to
-receive incoming requests.
-.br
-default: 3435
-.RE
-.PP
-\fBudp6\fR
-.RS 4
-.PP
-ip \fIip\fR specifies the local IPv6 address to bind to
-.PP
-[dns \fIdns\fR] specifies an optional DDNS server that will be used for
-the directory.
-.PP
-[port \fIport\fR] specifies a UDP port that is used for sending and
-receiving ouroboros traffic. This must be the same for the entire UDP6
-layer. Parallel UDP6 layers should use different ports.
-.br
-default: 3435
-.RE
.PP
\fBunicast\fR
@@ -293,6 +228,38 @@ default: SHA3_256.
.RE
.PP
+\fBirm ipcp poa attach\fR name \fIname\fR \fIpoa\fR
+.RS 4
+attaches the IPCP with name \fIname\fR to a point of attachment.
+Repeat to attach more than one. Exactly one \fIpoa\fR is given:
+.PP
+udp \fIip\fR[:\fIport\fR] attaches to a local IPv4 or IPv6 address.
+IPv6 addresses need brackets when a port is given: [\fIip\fR]:\fIport\fR.
+.br
+default port: 3435.
+.PP
+eth dev \fIdevice\fR [ethertype \fIethertype\fR] attaches to an Ethernet
+device.
+.br
+default: 0xA000.
+.RE
+
+.PP
+\fBirm ipcp poa detach\fR name \fIname\fR \fIpoa\fR
+.RS 4
+releases a point of attachment that the IPCP with name \fIname\fR is
+attached to. Flows over it are brought down first. \fIpoa\fR takes the
+same values as for \fBattach\fR.
+.RE
+
+.PP
+\fBirm ipcp poa list\fR name \fIname\fR
+.RS 4
+lists the points of attachment that the IPCP with name \fIname\fR is
+attached to.
+.RE
+
+.PP
\fBirm ipcp enroll\fR name \fIname\fR [type \fItype\fR] [dst \fIdst\fR] \
[layer \fIlayer\fR] [\fIautobind\fR]
.RS 4
@@ -308,6 +275,15 @@ the layer name is a shorthand for the destination name being the same
as the layer name.
.PP
[autobind] will automatically bind this IPCP to its name and the layer name.
+.PP
+[udp \fIip\fR[:\fIport\fR]] enrols over a point of attachment this
+IPCP is attached to, at a peer reachable at this IPv4 or IPv6 address
+or host name.
+.PP
+[eth [dev \fIdevice\fR] [ethertype \fIethertype\fR]] enrols over a
+point of attachment this IPCP is attached to, resolving the peer by a
+name query over Ethernet. [dev \fIdevice\fR] restricts the query to
+one attached device.
.RE
\fBirm ipcp connect\fR name \fIname\fR component \fIcomponent\fR dst
@@ -316,6 +292,18 @@ as the layer name.
connects a \fIcomponent\fR (\fBdt\fR or \fBmgmt\fR) of a unicast or
broadcast IPCP with name \fIname\fR to that component of the
destination IPCP within the same layer.
+.PP
+[qos \fIqos\fR] specifies the QoS cube for a \fBdt\fR flow: raw, safe,
+rt, rt-safe or msg.
+.PP
+[udp \fIip\fR[:\fIport\fR]] connects over a point of attachment
+this IPCP is attached to, to a peer reachable at this IPv4 or IPv6
+address or host name.
+.PP
+[eth [dev \fIdevice\fR] [ethertype \fIethertype\fR]] connects over a
+point of attachment this IPCP is attached to, resolving the peer by a
+name query over Ethernet. [dev \fIdevice\fR] restricts the query to
+one attached device.
.RE
\fBirm ipcp disconnect\fR name \fIname\fR component \fIcomponent\fR dst
diff --git a/include/ouroboros/atomics.h b/include/ouroboros/atomics.h
index 8e667522..9783fc78 100644
--- a/include/ouroboros/atomics.h
+++ b/include/ouroboros/atomics.h
@@ -35,5 +35,7 @@
#define FETCH_SUB_RELAXED(p, v) (__atomic_fetch_sub(p, v, __ATOMIC_RELAXED))
#define FETCH_ADD(p, v) (__atomic_fetch_add(p, v, __ATOMIC_SEQ_CST))
#define FETCH_SUB(p, v) (__atomic_fetch_sub(p, v, __ATOMIC_SEQ_CST))
+#define FETCH_OR(p, v) (__atomic_fetch_or(p, v, __ATOMIC_SEQ_CST))
+#define FETCH_AND(p, v) (__atomic_fetch_and(p, v, __ATOMIC_SEQ_CST))
#endif /* OUROBOROS_LIB_ATOMICS_H */
diff --git a/include/ouroboros/crypt.h b/include/ouroboros/crypt.h
index 5e082bb9..b567bf8d 100644
--- a/include/ouroboros/crypt.h
+++ b/include/ouroboros/crypt.h
@@ -28,18 +28,19 @@
#include <assert.h>
-#define IVSZ 16
+#define NONCESZ 16
#define SYMMKEYSZ 32
#define MAX_HASH_SIZE 64 /* SHA-512/BLAKE2b max */
#define KEX_ALGO_BUFSZ 32
#define KEX_CIPHER_BUFSZ 32
+#define CACERT_PATH_BUFSZ 256
/*
* On OSX the OpenSSL NIDs are automatically loaded with evp.h.
* Some have a different spelling. This header avoids the double definitions.
*/
- #define NID_undef 0
+#define NID_undef 0
/* Cipher NIDs (match OpenSSL values) */
#define NID_aes_128_gcm 895
@@ -50,7 +51,7 @@
#define NID_aes_256_ctr 906
#define NID_chacha20_poly1305 1018
- #if !defined (__APPLE__) || !defined ( HAVE_OPENSSL )
+#if !defined (__APPLE__) || !defined ( HAVE_OPENSSL )
/* KEX algorithm NIDs (match OpenSSL values) */
#define NID_X9_62_prime256v1 415
#define NID_secp384r1 715
@@ -64,8 +65,14 @@
#define NID_MLKEM512 1454
#define NID_MLKEM768 1455
#define NID_MLKEM1024 1456
-#define NID_X25519MLKEM768 2053 /* !! not in OpenSSL */
-#define NID_X448MLKEM1024 2054 /* !! not in OpenSSL */
+
+/* Hybrid KEM NIDs: project-assigned range, OpenSSL defines none */
+#define NID_X25519MLKEM768 2053
+#define NID_X448MLKEM1024 2054
+#define NID_SecP256r1MLKEM768 2055
+#define NID_SecP384r1MLKEM1024 2056
+#define NID_HYBRID_KEM_MIN NID_X25519MLKEM768
+#define NID_HYBRID_KEM_MAX NID_SecP384r1MLKEM1024
/* KDF NIDs (match OpenSSL values) */
#define NID_hkdf 1036
@@ -84,15 +91,8 @@
#define IS_KEM_ALGORITHM(algo) \
(strstr(algo, "ML-KEM") != NULL || strstr(algo, "MLKEM") != NULL)
-#define IS_HYBRID_KEM(algo) \
- ((strstr(algo, "X25519") != NULL || strstr(algo, "X448") != NULL) && \
- strstr(algo, "MLKEM") != NULL)
-
-#define X25519MLKEM768_PKSZ 1216 /* 32 + 1184 */
-#define X25519MLKEM768_CTSZ 1120 /* 32 + 1088 */
-#define X25519MLKEM768_SKSZ 2432 /* 32 + 2400 */
-#define X448MLKEM1024_PKSZ 1624 /* 56 + 1568 */
-#define X448MLKEM1024_SKSZ 3224 /* 56 + 3168 */
+#define IS_HYBRID_KEM_NID(nid) kex_nid_is_hybrid(nid)
+#define IS_HYBRID_KEM(algo) kex_nid_is_hybrid(kex_str_to_nid(algo))
#define CRYPT_KEY_BUFSZ 4096 /* Safe buffer for key material */
@@ -101,11 +101,15 @@
#define IS_KEX_ALGO_SET(cfg) ((cfg)->x.nid != NID_undef)
#define IS_KEX_CIPHER_SET(cfg) ((cfg)->c.nid != NID_undef)
+/* Flow role: forks the per-direction keys so each end's TX = peer's RX. */
+#define CRYPT_ROLE_INIT 0 /* flow allocator / OAP client */
+#define CRYPT_ROLE_RESP 1 /* flow acceptor / OAP server */
struct crypt_sk {
int nid;
uint8_t * key;
- uint8_t rot_bit; /* Rotation bit to control epoch */
+ uint8_t epoch; /* installed batch epoch */
+ uint8_t role; /* CRYPT_ROLE_INIT / _RESP */
};
struct sec_config {
@@ -114,18 +118,26 @@ struct sec_config {
int nid;
int mode;
} x; /* key exchange */
+
struct {
const char * str;
int nid;
} k; /* kdf */
+
struct {
const char * str;
int nid;
} c; /* cipher */
+
struct {
const char * str;
int nid;
} d; /* digest */
+
+ struct {
+ bool req; /* require peer auth */
+ char cacert[CACERT_PATH_BUFSZ]; /* pinned CA, "" = any */
+ } a; /* authentication */
};
/* Helper macros to set sec_config fields consistently */
@@ -211,9 +223,21 @@ void auth_destroy_ctx(struct auth_ctx * ctx);
int auth_add_crt_to_store(struct auth_ctx * ctx,
void * crt);
+/* Untrusted intermediates: used to build a path, never as trust anchors */
+int auth_add_crt_to_chain(struct auth_ctx * ctx,
+ void * crt);
+
int auth_verify_crt(struct auth_ctx * ctx,
void * crt);
+/* As auth_verify_crt, pin must be in the verified chain (NULL: any) */
+int auth_verify_crt_pin(struct auth_ctx * ctx,
+ void * crt,
+ void * pin);
+
+/* False for PQC keys: their signature digest is intrinsic */
+bool crypt_pk_requires_md(const void * pk);
+
int auth_sign(void * pkp,
int md_nid,
buffer_t msg,
@@ -243,10 +267,11 @@ ssize_t kex_kem_encap(buffer_t pk,
int kdf_nid,
uint8_t * s);
-ssize_t kex_kem_encap_raw(buffer_t pk,
- uint8_t * ct,
- int kdf_nid,
- uint8_t * s);
+ssize_t kex_kem_encap_raw(const char * algo,
+ buffer_t pk,
+ uint8_t * ct,
+ int kdf_nid,
+ uint8_t * s);
int kex_kem_decap(void * pkp,
buffer_t ct,
@@ -256,13 +281,12 @@ int kex_kem_decap(void * pkp,
int kex_get_algo_from_pk_der(buffer_t pk,
char * algo);
-int kex_get_algo_from_pk_raw(buffer_t pk,
- char * algo);
-
int kex_validate_algo(const char * algo);
int kex_validate_nid(int nid);
+bool kex_nid_is_hybrid(uint16_t nid);
+
const char * kex_nid_to_str(uint16_t nid);
uint16_t kex_str_to_nid(const char * algo);
@@ -289,12 +313,16 @@ const char * md_nid_to_str(uint16_t nid);
uint16_t md_str_to_nid(const char * kdf);
-ssize_t md_digest(int md_nid,
- buffer_t in,
- uint8_t * out);
+ssize_t md_digest(int md_nid,
+ buffer_t in,
+ uint8_t * out);
ssize_t md_len(int md_nid);
+int crypt_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out);
+
int crypt_encrypt(struct crypt_ctx * ctx,
buffer_t in,
buffer_t * out);
@@ -303,10 +331,37 @@ int crypt_decrypt(struct crypt_ctx * ctx,
buffer_t in,
buffer_t * out);
-int crypt_get_ivsz(struct crypt_ctx * ctx);
+/* One-shot AEAD over an explicit key/nonce. out = ciphertext ‖ tag. */
+int crypt_oneshot_seal(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ 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);
+
+int crypt_get_headsz(struct crypt_ctx * ctx);
int crypt_get_tagsz(struct crypt_ctx * ctx);
+int crypt_rekey(struct crypt_ctx * ctx,
+ struct crypt_sk * sk);
+
+/* Nodes remaining in the TX batch (re-key watermark). */
+int crypt_nodes_left(struct crypt_ctx * ctx);
+
+/* 1 once the peer has been observed on the current generation. */
+int crypt_peer_synced(struct crypt_ctx * ctx);
+
+/* Switch TX to the installed (new) batch (after peer synced/grace). */
+void crypt_tx_promote(struct crypt_ctx * ctx);
+
int crypt_load_crt_file(const char * path,
void ** crt);
@@ -337,11 +392,17 @@ int crypt_load_pubkey_file_to_der(const char * path,
buffer_t * buf);
int crypt_load_pubkey_raw_file(const char * path,
+ const char * algo,
buffer_t * buf);
int crypt_load_privkey_raw_file(const char * path,
+ const char * algo,
void ** key);
+int crypt_ct_cmp(const void * a,
+ const void * b,
+ size_t len);
+
int crypt_cmp_key(const void * key1,
const void * key2);
diff --git a/include/ouroboros/fccntl.h b/include/ouroboros/fccntl.h
index e91e91dd..9a048095 100644
--- a/include/ouroboros/fccntl.h
+++ b/include/ouroboros/fccntl.h
@@ -67,6 +67,8 @@
#define FLOWGRXQLEN 00000010 /* Get queue length on rx */
#define FLOWGTXQLEN 00000011 /* Get queue length on tx */
#define FLOWGMTU 00000012 /* Get per-packet MTU */
+#define FLOWSTXQDLY 00000013 /* Set tx queueing delay */
+#define FLOWGTXQDLY 00000014 /* Get tx queueing delay */
/* FRCT operations */
#define FRCTSFLAGS 00001000 /* Set flags for FRCT */
diff --git a/include/ouroboros/flow.h b/include/ouroboros/flow.h
index 8b096410..3d3c74ec 100644
--- a/include/ouroboros/flow.h
+++ b/include/ouroboros/flow.h
@@ -53,6 +53,8 @@ struct flow_info {
uint32_t mtu; /* n-1 layer MTU in bytes, 0 = unknown */
+ uint32_t max_rtt; /* declared max path RTT (ms), 0 = unknown */
+
struct qos_spec qs;
enum flow_state state;
diff --git a/include/ouroboros/fqueue.h b/include/ouroboros/fqueue.h
index 2546c79d..322da3ea 100644
--- a/include/ouroboros/fqueue.h
+++ b/include/ouroboros/fqueue.h
@@ -34,7 +34,8 @@ enum fqtype {
FLOW_UP = (1 << 2),
FLOW_ALLOC = (1 << 3),
FLOW_DEALLOC = (1 << 4),
- FLOW_PEER = (1 << 5)
+ FLOW_PEER = (1 << 5),
+ FLOW_UPD = (1 << 6)
};
struct flow_set;
diff --git a/include/ouroboros/hash.h b/include/ouroboros/hash.h
index 17ab98ac..c6609ffc 100644
--- a/include/ouroboros/hash.h
+++ b/include/ouroboros/hash.h
@@ -89,4 +89,7 @@ void str_hash(enum hash_algo algo,
void * dst,
const char * str);
+/* Non-cryptographic finalizer for hashing an integer key to a table index. */
+uint64_t hash_mix64(uint64_t key);
+
#endif /* OUROBOROS_LIB_HASH_H */
diff --git a/include/ouroboros/ipcp-dev.h b/include/ouroboros/ipcp-dev.h
index d23f757e..c0899c2b 100644
--- a/include/ouroboros/ipcp-dev.h
+++ b/include/ouroboros/ipcp-dev.h
@@ -24,52 +24,120 @@
#define OUROBOROS_LIB_IPCP_DEV_H
#include <ouroboros/ipcp.h>
+#include <ouroboros/qos.h>
#include <ouroboros/qoscube.h>
#include <ouroboros/ssm_pool.h>
#include <ouroboros/utils.h>
+#include <stdbool.h>
#include <stdint.h>
+#include <time.h>
-int ipcp_create_r(const struct ipcp_info * info);
+int ipcp_create_r(const struct ipcp_info * info);
-int ipcp_flow_req_arr(const buffer_t * dst,
- qosspec_t qs,
- time_t mpl,
- uint32_t mtu,
- const buffer_t * data);
+void ipcp_flow_set_max_rtt(uint32_t max_rtt);
-int ipcp_flow_alloc_reply(int fd,
- int response,
- time_t mpl,
- uint32_t mtu,
- const buffer_t * data);
+int ipcp_flow_req_arr(const buffer_t * dst,
+ qosspec_t qs,
+ time_t mpl,
+ uint32_t mtu,
+ const buffer_t * data);
+
+int ipcp_flow_update_arr(int flow_id,
+ const buffer_t * data);
+
+int ipcp_flow_alloc_reply(int fd,
+ int response,
+ time_t mpl,
+ uint32_t mtu,
+ const buffer_t * data);
+
+int ipcp_flow_read(int fd,
+ struct ssm_pk_buff ** spb);
+
+int ipcp_flow_write(int fd,
+ struct ssm_pk_buff * spb);
-int ipcp_flow_read(int fd,
- struct ssm_pk_buff ** spb);
+int np1_flow_read(int fd,
+ struct ssm_pk_buff ** spb,
+ struct ssm_pool * pool);
-int ipcp_flow_write(int fd,
- struct ssm_pk_buff * spb);
+int np1_flow_write(int fd,
+ struct ssm_pk_buff * spb,
+ struct ssm_pool * pool);
-int np1_flow_read(int fd,
- struct ssm_pk_buff ** spb,
- struct ssm_pool * pool);
+int ipcp_flow_dealloc(int fd);
+
+int ipcp_flow_fini(int fd);
+
+int ipcp_flow_get_qoscube(int fd,
+ qoscube_t * cube);
+
+/* Bytes queued in the transmit path of the flow. */
+size_t ipcp_flow_queued(int fd);
+
+/* Mean size of a packet written to the flow, bytes. */
+size_t ipcp_flow_mean_len(int fd);
+
+/*
+ * Capacity estimator of the transmit queue behind the flow.
+ * Flows attached to one point of attachment share a queue and
+ * share the estimator. Armed by the IPCP; unarmed flows read 0.
+ */
+int ipcp_flow_cap_arm(int fd);
-int np1_flow_write(int fd,
- struct ssm_pk_buff * spb,
- struct ssm_pool * pool);
+void ipcp_flow_cap_update(int fd,
+ size_t qlen,
+ size_t len);
-int ipcp_flow_dealloc(int fd);
+/* Estimated capacity of the transmit queue (bytes/s), 0 = unknown. */
+uint64_t ipcp_flow_cap(int fd);
-int ipcp_flow_fini(int fd);
+int ipcp_spb_reserve(struct ssm_pk_buff ** spb,
+ size_t len);
-int ipcp_flow_get_qoscube(int fd,
- qoscube_t * cube);
+void ipcp_spb_release(struct ssm_pk_buff * spb);
-size_t ipcp_flow_queued(int fd);
+/* PoA lifecycle; an IPCP owns its PoAs, applications have none. */
+int poa_init(const char * name);
-int ipcp_spb_reserve(struct ssm_pk_buff ** spb,
- size_t len);
+int poa_start(void);
+
+void poa_stop(void);
+
+void poa_fini(void);
+
+/* Also answer name queries for the layer; set at bootstrap/enroll. */
+int poa_set_layer(const char * layer);
+
+/* Attach or release a PoA; the type in the spec picks the transport. */
+int poa_attach(const struct poa_spec * poa);
+
+int poa_detach(const struct poa_spec * poa);
+
+/* The PoAs this process has attached. */
+ssize_t poa_list(struct poa_spec * specs,
+ size_t max);
+
+/* Allocate a flow to dst over the PoA that carries addr. */
+int poa_flow_alloc(const char * dst,
+ const struct poa_addr * addr,
+ qosspec_t * qs,
+ const struct timespec * timeo);
+
+/* Resolve dst on the attached PoAs; no flow is created. */
+int poa_query(const char * dst,
+ const struct timespec * timeo,
+ struct poa_addr * addr);
+
+/* PoA counterparts of the ipcp_flow_* operations */
+int poa_flow_alloc_resp(int flow_id,
+ int response,
+ const buffer_t * data);
+
+int poa_flow_update(int flow_id,
+ const buffer_t * data);
-void ipcp_spb_release(struct ssm_pk_buff * spb);
+int poa_flow_dealloc(int flow_id);
#endif /* OUROBOROS_LIB_IPCP_DEV_H */
diff --git a/include/ouroboros/ipcp.h b/include/ouroboros/ipcp.h
index e29b080a..4da3b81d 100644
--- a/include/ouroboros/ipcp.h
+++ b/include/ouroboros/ipcp.h
@@ -48,13 +48,19 @@ enum ipcp_type { /* IRMd uses order to select an IPCP for flow allocation. */
IPCP_LOCAL = 0,
IPCP_UNICAST,
IPCP_BROADCAST,
- IPCP_ETH_LLC,
- IPCP_ETH_DIX,
- IPCP_UDP4,
- IPCP_UDP6,
IPCP_INVALID
};
+/* The medium a PoA sits on. */
+enum poa_type {
+ POA_UDP4 = 0,
+ POA_UDP6,
+ /* A name to resolve; the family is DNS's pick. Port in udp4. */
+ POA_UDP,
+ POA_ETH,
+ POA_INVALID
+};
+
struct ipcp_info {
enum ipcp_type type;
pid_t pid;
@@ -124,9 +130,10 @@ enum pol_cong_avoid {
struct dt_config {
struct {
- uint8_t addr_size;
- uint8_t eid_size;
- uint8_t max_ttl;
+ uint8_t addr_size;
+ uint8_t eid_size;
+ uint8_t max_ttl;
+ uint16_t max_rtt; /* Declared max layer RTT (ms) */
};
struct routing_config routing; /* Routing policy */
};
@@ -135,6 +142,7 @@ static const struct dt_config default_dt_config = {
.addr_size = 4,
.eid_size = 8,
.max_ttl = 60,
+ .max_rtt = 200,
.routing = {
.pol = ROUTING_LINK_STATE,
.ls = {
@@ -254,23 +262,52 @@ static const struct uni_config default_uni_config = {
.cong_avoid = CA_MB_ECN
};
-struct eth_config {
+#define POA_UDP_PORT 3435 /* default UDP PoA port */
+#define POA_ETHERTYPE 0xA000 /* default Ethertype */
+#define POA_MAC_SIZE 6
+#define POA_HOST_STRLEN 255
+
+struct eth_poa {
char dev[DEV_NAME_SIZE + 1];
- uint16_t ethertype; /* DIX only*/
+ uint16_t ethertype;
+ uint8_t mac[POA_MAC_SIZE];
};
-struct udp4_config {
+struct udp4_poa {
struct in_addr ip_addr;
- struct in_addr dns_addr;
uint16_t port;
};
-struct udp6_config {
+struct udp6_poa {
struct in6_addr ip_addr;
- struct in6_addr dns_addr;
uint16_t port;
};
+/* PoA a unicast or broadcast IPCP attaches to. */
+struct poa_spec {
+ enum poa_type type;
+ union {
+ struct udp4_poa udp4;
+ struct udp6_poa udp6;
+ struct eth_poa eth;
+ };
+};
+
+
+/* Peer PoA to connect to. */
+struct poa_addr {
+ enum poa_type type;
+ char hostname[POA_HOST_STRLEN + 1]; /* resolved by IRMd */
+ union {
+ struct udp4_poa udp4;
+ struct udp6_poa udp6;
+ struct {
+ struct eth_poa src;
+ struct eth_poa dst;
+ } eth;
+ };
+};
+
/* Layers */
struct layer_info {
char name[LAYER_NAME_SIZE + 1];
@@ -283,58 +320,22 @@ struct ipcp_config {
struct layer_info layer_info;
enum ipcp_type type;
- union {
- struct uni_config unicast;
- struct udp4_config udp4;
- struct udp6_config udp6;
- struct eth_config eth;
- };
+ struct uni_config unicast;
};
/* default configurations */
static const struct ipcp_config local_default_conf = {
- .type = IPCP_LOCAL,
- .layer_info = {
- .dir_hash_algo = DIR_HASH_SHA3_256
- }
-};
-
-static const struct ipcp_config eth_dix_default_conf = {
- .type = IPCP_ETH_DIX,
.layer_info = {
.dir_hash_algo = DIR_HASH_SHA3_256
},
- .eth = {
- .ethertype=0xA000,
- }
-};
-
-static const struct ipcp_config eth_llc_default_conf = {
- .type = IPCP_ETH_LLC,
- .layer_info = {
- .dir_hash_algo = DIR_HASH_SHA3_256
- }
-};
-
-static const struct ipcp_config udp4_default_conf = {
- .type = IPCP_UDP4,
- .udp4 = {
- .port = 3435
- }
-};
-
-static const struct ipcp_config udp6_default_conf = {
- .type = IPCP_UDP6,
- .udp6 = {
- .port = 3435
- }
+ .type = IPCP_LOCAL
};
static const struct ipcp_config uni_default_conf = {
- .type = IPCP_UNICAST,
.layer_info = {
.dir_hash_algo = DIR_HASH_SHA3_256
},
+ .type = IPCP_UNICAST,
.unicast = {
.dt = {
.addr_size = 4,
diff --git a/include/ouroboros/irm.h b/include/ouroboros/irm.h
index 7cb71c21..3e8a24af 100644
--- a/include/ouroboros/irm.h
+++ b/include/ouroboros/irm.h
@@ -46,16 +46,29 @@ int irm_destroy_ipcp(pid_t pid);
ssize_t irm_list_ipcps(struct ipcp_list_info ** ipcps);
-int irm_enroll_ipcp(pid_t pid,
- const char * dst);
+int irm_enroll_ipcp(pid_t pid,
+ const char * dst,
+ const struct poa_addr * addr);
int irm_bootstrap_ipcp(pid_t pid,
const struct ipcp_config * conf);
-int irm_connect_ipcp(pid_t pid,
- const char * dst,
- const char * component,
- qosspec_t qs);
+/* Attach an IPCP to a medium; repeat for more than one PoA. */
+int irm_attach_ipcp(pid_t pid,
+ const struct poa_spec * poa);
+
+int irm_detach_ipcp(pid_t pid,
+ const struct poa_spec * poa);
+
+/* Caller frees *poas. */
+ssize_t irm_list_poas(pid_t pid,
+ struct poa_spec ** poas);
+
+int irm_connect_ipcp(pid_t pid,
+ const char * dst,
+ const char * component,
+ qosspec_t qs,
+ const struct poa_addr * addr);
int irm_disconnect_ipcp(pid_t pid,
const char * dst,
diff --git a/include/ouroboros/logs.h b/include/ouroboros/logs.h
index 58494531..1ae77673 100644
--- a/include/ouroboros/logs.h
+++ b/include/ouroboros/logs.h
@@ -29,6 +29,7 @@
#include <ouroboros/hash.h>
+#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdbool.h>
@@ -55,6 +56,8 @@ void log_fini(void);
#define __olog(CLR, LVL, SYSLVL, ...) \
do { \
+ int __cs; \
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &__cs); \
if (log_syslog) { \
syslog(SYSLVL, __VA_ARGS__); \
} else { \
@@ -64,10 +67,13 @@ void log_fini(void);
printf(CLR_RESET "\n"); \
fflush(stdout); \
} \
+ pthread_setcancelstate(__cs, NULL); \
} while (0)
#define __olog_id(CLR, LVL, SYSLVL, id, fmt, ...) \
do { \
+ int __cs; \
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &__cs); \
if (log_syslog) { \
syslog(SYSLVL, "[" HASH_FMT64 "] " fmt, \
HASH_VAL64(id), ## __VA_ARGS__); \
@@ -79,6 +85,7 @@ void log_fini(void);
printf(CLR_RESET "\n"); \
fflush(stdout); \
} \
+ pthread_setcancelstate(__cs, NULL); \
} while (0)
#ifndef OUROBOROS_DISABLE_LOGGING
diff --git a/include/ouroboros/name.h b/include/ouroboros/name.h
index a9393820..a3aac8c4 100644
--- a/include/ouroboros/name.h
+++ b/include/ouroboros/name.h
@@ -34,9 +34,9 @@ enum pol_balance {
};
struct name_sec_paths {
- char enc[NAME_PATH_SIZE + 1]; /* path to crypt for this name */
- char key[NAME_PATH_SIZE + 1]; /* path to key for this name */
- char crt[NAME_PATH_SIZE + 1]; /* path to crt for this name */
+ char sec[NAME_PATH_SIZE + 1]; /* path to sec.conf for this name */
+ char key[NAME_PATH_SIZE + 1]; /* path to key for this name */
+ char crt[NAME_PATH_SIZE + 1]; /* path to crt for this name */
};
struct name_info {
diff --git a/include/ouroboros/np1_flow.h b/include/ouroboros/np1_flow.h
index 309d01c2..758b6db8 100644
--- a/include/ouroboros/np1_flow.h
+++ b/include/ouroboros/np1_flow.h
@@ -36,6 +36,10 @@ int np1_flow_resp(int flow_id,
int np1_flow_dealloc(int flow_id,
time_t timeo);
+int np1_flow_fd(int flow_id);
+
+int np1_flow_id(int fd);
+
static const qosspec_t qos_np1 = {
.service = SVC_RAW,
.delay = UINT32_MAX,
diff --git a/include/ouroboros/protobuf.h b/include/ouroboros/protobuf.h
index 951e8fd6..7937f86b 100644
--- a/include/ouroboros/protobuf.h
+++ b/include/ouroboros/protobuf.h
@@ -24,9 +24,9 @@
#define OUROBOROS_LIB_PROTOBUF_H
#include <ouroboros/flow.h>
-#include <ouroboros/qos.h>
#include <ouroboros/ipcp.h>
#include <ouroboros/irm.h>
+#include <ouroboros/qos.h>
#include <ouroboros/serdes-irm.h>
#include <ouroboros/serdes-oep.h>
@@ -37,9 +37,10 @@ typedef RoutingConfigMsg routing_config_msg_t;
typedef DtConfigMsg dt_config_msg_t;
typedef DirConfigMsg dir_config_msg_t;
typedef DirDhtConfigMsg dir_dht_config_msg_t;
-typedef EthConfigMsg eth_config_msg_t;
-typedef Udp4ConfigMsg udp4_config_msg_t;
-typedef Udp6ConfigMsg udp6_config_msg_t;
+typedef EthPoaMsg eth_poa_msg_t;
+typedef Udp4PoaMsg udp4_poa_msg_t;
+typedef PoaSpecMsg poa_spec_msg_t;
+typedef Udp6PoaMsg udp6_poa_msg_t;
typedef UniConfigMsg uni_config_msg_t;
#include "ipcp.pb-c.h"
@@ -56,6 +57,7 @@ typedef FlowInfoMsg flow_info_msg_t;
typedef NameInfoMsg name_info_msg_t;
typedef LayerInfoMsg layer_info_msg_t;
typedef QosspecMsg qosspec_msg_t;
+typedef PoaAddrMsg poa_addr_msg_t;
#include "enroll.pb-c.h"
typedef EnrollReqMsg enroll_req_msg_t;
@@ -79,6 +81,10 @@ layer_info_msg_t * layer_info_s_to_msg(const struct layer_info * s);
struct layer_info layer_info_msg_to_s(const layer_info_msg_t * msg);
+poa_addr_msg_t * poa_addr_s_to_msg(const struct poa_addr * s);
+
+struct poa_addr poa_addr_msg_to_s(const poa_addr_msg_t * msg);
+
ipcp_info_msg_t * ipcp_info_s_to_msg(const struct ipcp_info * s);
struct ipcp_info ipcp_info_msg_to_s(const ipcp_info_msg_t * msg);
@@ -91,20 +97,24 @@ uni_config_msg_t * uni_config_s_to_msg(const struct uni_config * s);
struct uni_config uni_config_msg_to_s(const uni_config_msg_t * msg);
-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);
-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);
-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);
-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);
-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);
-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);
ipcp_config_msg_t * ipcp_config_s_to_msg(const struct ipcp_config * s);
+poa_spec_msg_t * poa_spec_s_to_msg(const struct poa_spec * s);
+
+struct poa_spec poa_spec_msg_to_s(const poa_spec_msg_t * msg);
+
struct ipcp_config ipcp_config_msg_to_s(const ipcp_config_msg_t * msg);
/* QoS */
diff --git a/include/ouroboros/pthread.h b/include/ouroboros/pthread.h
index cd500795..3ca79d10 100644
--- a/include/ouroboros/pthread.h
+++ b/include/ouroboros/pthread.h
@@ -24,6 +24,7 @@
#define OUROBOROS_LIB_PTHREAD_H
#include <pthread.h>
+#include <stdio.h>
static int __attribute__((unused)) __timedwait(pthread_cond_t * cond,
pthread_mutex_t * mtx,
@@ -48,4 +49,9 @@ static void __attribute__((unused)) __cleanup_mutex_unlock(void * mutex)
pthread_mutex_unlock((pthread_mutex_t *) mutex);
}
+static void __attribute__((unused)) __cleanup_fclose(void * fp)
+{
+ fclose((FILE *) fp);
+}
+
#endif /* OUROBOROS_LIB_PTHREAD_H */
diff --git a/include/ouroboros/qos.h b/include/ouroboros/qos.h
index 7980ad00..26fc2637 100644
--- a/include/ouroboros/qos.h
+++ b/include/ouroboros/qos.h
@@ -30,9 +30,9 @@
/* qos_spec.service: framing / reliability class. */
enum qos_service {
- SVC_RAW = 0, /* No FRCT; best-effort raw messages */
- SVC_MESSAGE = 1, /* FRCT, reliable ordered messages */
- SVC_STREAM = 2, /* FRCT, reliable ordered byte stream */
+ SVC_RAW = 0, /* No FRCT; best-effort raw messages */
+ SVC_MESSAGE = 1, /* FRCT, ordered messages, optional reliability */
+ SVC_STREAM = 2, /* FRCT, reliable ordered byte stream */
};
typedef struct qos_spec {
diff --git a/include/ouroboros/rcu.h b/include/ouroboros/rcu.h
new file mode 100644
index 00000000..b4e7d27c
--- /dev/null
+++ b/include/ouroboros/rcu.h
@@ -0,0 +1,110 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Read-mostly pointer publication (RCU, with a locked fallback)
+ *
+ * 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_RCU_H
+#define OUROBOROS_LIB_RCU_H
+
+/*
+ * Lock-free reads of published pointers via liburcu (urcu-bp) when
+ * available; a per-object rwlock fallback otherwise.
+ * Include config.h before this header so HAVE_LIBURCU is defined.
+ *
+ * Embed a struct rcu_guard in the object. A reader brackets its access
+ * with rcu_rdlock/rcu_rdunlock and reads published pointers via rcu_deref.
+ * A writer serialises with rcu_wrlock/rcu_wrunlock and publishes via
+ * rcu_assign; after unlock it reclaims a now-unreachable object with
+ * rcu_reclaim (waits out live readers) before freeing it. rcu_drain waits
+ * out all readers at teardown.
+ */
+
+#include <ouroboros/pthread.h>
+
+#ifdef HAVE_LIBURCU
+
+#include <urcu-bp.h>
+
+struct rcu_guard {
+ pthread_mutex_t w; /* serialises writers; readers use RCU */
+};
+
+#define rcu_guard_init(g) pthread_mutex_init(&(g)->w, NULL)
+#define rcu_guard_fini(g) pthread_mutex_destroy(&(g)->w)
+#define rcu_rdlock(g) ((void) (g), rcu_read_lock())
+#define rcu_rdunlock(g) ((void) (g), rcu_read_unlock())
+#define rcu_wrlock(g) pthread_mutex_lock(&(g)->w)
+#define rcu_wrunlock(g) pthread_mutex_unlock(&(g)->w)
+#define rcu_deref(p) rcu_dereference(p)
+#define rcu_assign(p, v) rcu_assign_pointer(p, v)
+#define rcu_reclaim(g) ((void) (g), synchronize_rcu())
+#define rcu_drain(g) ((void) (g), synchronize_rcu())
+
+/* TSan can miss the publish/consume barrier under urcu. */
+#if defined(__SANITIZE_THREAD__)
+#define RCU_TSAN_ANNOTATE
+#endif
+#if defined(__has_feature)
+#if __has_feature(thread_sanitizer)
+#define RCU_TSAN_ANNOTATE
+#endif
+#endif
+
+/*
+ * Publish/consume annotations re-expose liburcu's rcu_assign/rcu_deref edge to
+ * TSan, which cannot see liburcu's barriers. Call rcu_publish(p) before
+ * publishing p with rcu_assign, and rcu_consume(p) after reading it with
+ * rcu_deref. No-op without liburcu (the rwlock fallback already gives TSan the
+ * edge) or without TSan.
+ */
+#ifdef RCU_TSAN_ANNOTATE
+#include <sanitizer/tsan_interface.h>
+#define rcu_publish(p) __tsan_release(p)
+#define rcu_consume(p) __tsan_acquire(p)
+#else
+#define rcu_publish(p) ((void) (p))
+#define rcu_consume(p) ((void) (p))
+#endif
+
+#else /* !HAVE_LIBURCU : per-object rwlock fallback */
+
+struct rcu_guard {
+ pthread_rwlock_t rw; /* readers rd, writers wr */
+};
+
+#define rcu_guard_init(g) pthread_rwlock_init(&(g)->rw, NULL)
+#define rcu_guard_fini(g) pthread_rwlock_destroy(&(g)->rw)
+#define rcu_rdlock(g) pthread_rwlock_rdlock(&(g)->rw)
+#define rcu_rdunlock(g) pthread_rwlock_unlock(&(g)->rw)
+#define rcu_wrlock(g) pthread_rwlock_wrlock(&(g)->rw)
+#define rcu_wrunlock(g) pthread_rwlock_unlock(&(g)->rw)
+#define rcu_deref(p) (p)
+#define rcu_assign(p, v) ((p) = (v))
+#define rcu_reclaim(g) ((void) (g)) /* wrlock already excluded readers */
+#define rcu_drain(g) (pthread_rwlock_wrlock(&(g)->rw), \
+ pthread_rwlock_unlock(&(g)->rw))
+
+/* rwlock already gives TSan the publish/consume edge; no annotation. */
+#define rcu_publish(p) ((void) (p))
+#define rcu_consume(p) ((void) (p))
+
+#endif /* HAVE_LIBURCU */
+
+#endif /* OUROBOROS_LIB_RCU_H */
diff --git a/include/ouroboros/serdes-irm.h b/include/ouroboros/serdes-irm.h
index 1dfff4d9..bc3c46d2 100644
--- a/include/ouroboros/serdes-irm.h
+++ b/include/ouroboros/serdes-irm.h
@@ -31,6 +31,7 @@
#include <ouroboros/utils.h>
#include <inttypes.h>
+#include <stdbool.h>
int flow_alloc__irm_req_ser(buffer_t * buf,
const struct flow_info * flow,
@@ -51,6 +52,27 @@ int ipcp_flow_req_arr__irm_req_ser(buffer_t * buf,
const struct flow_info * flow,
const buffer_t * data);
+int poa_flow_alloc__irm_req_ser(buffer_t * buf,
+ const struct flow_info * flow,
+ const char * dst);
+
+int poa_flow_alloc_r__irm_req_ser(buffer_t * buf,
+ const struct flow_info * flow,
+ const buffer_t * data,
+ int response);
+
+int ipcp_poa_flow_req_arr__irm_req_ser(buffer_t * buf,
+ const struct flow_info * flow,
+ const buffer_t * data);
+
+int poa_flow__irm_result_des(buffer_t * buf,
+ struct flow_info * flow,
+ buffer_t * data);
+
+int ipcp_flow_update_arr__irm_req_ser(buffer_t * buf,
+ const struct flow_info * flow,
+ const buffer_t * data);
+
int ipcp_flow_alloc_reply__irm_msg_ser(buffer_t * buf,
const struct flow_info * flow,
int response,
@@ -64,6 +86,15 @@ int flow_dealloc__irm_req_ser(buffer_t * buf,
const struct flow_info * flow,
const struct timespec * timeo);
+int flow_update__irm_req_ser(buffer_t * buf,
+ const struct flow_info * flow,
+ bool rekey);
+
+int flow_rekey__irm_result_des(buffer_t * buf,
+ struct crypt_sk * sk,
+ bool * has_key,
+ bool * initiator);
+
int ipcp_flow_dealloc__irm_req_ser(buffer_t * buf,
const struct flow_info * info);
diff --git a/include/ouroboros/ssm_rbuff.h b/include/ouroboros/ssm_rbuff.h
index 2443b63d..297aad9f 100644
--- a/include/ouroboros/ssm_rbuff.h
+++ b/include/ouroboros/ssm_rbuff.h
@@ -28,10 +28,14 @@
#include <stdint.h>
-#define ACL_RDWR 0000
-#define ACL_RDONLY 0001
-#define ACL_FLOWDOWN 0002
-#define ACL_FLOWPEER 0004
+#define RB_RD 0001 /* read permitted (0 = no access) */
+#define RB_WR 0002 /* write permitted (0 = no access) */
+#define RB_RDWR (RB_RD | RB_WR)
+#define RB_FLOWDOWN 0004
+#define RB_FLOWPEER 0010
+#define RB_REKEY 0020 /* re-key seed parked (out-of-band signal) */
+
+#define SSM_RBUFF_TXQ_MAX_DELAY 1000000000LL
struct ssm_rbuff;
@@ -45,10 +49,21 @@ struct ssm_rbuff * ssm_rbuff_open(pid_t pid,
void ssm_rbuff_close(struct ssm_rbuff * rb);
-void ssm_rbuff_set_acl(struct ssm_rbuff * rb,
- uint32_t flags);
+void ssm_rbuff_set_flags(struct ssm_rbuff * rb,
+ uint32_t flags);
+
+void ssm_rbuff_clr_flags(struct ssm_rbuff * rb,
+ uint32_t flags);
+
+uint32_t ssm_rbuff_get_flags(struct ssm_rbuff * rb);
-uint32_t ssm_rbuff_get_acl(struct ssm_rbuff * rb);
+size_t ssm_rbuff_get_limit(struct ssm_rbuff * rb);
+
+void ssm_rbuff_set_txq_target(struct ssm_rbuff * rb,
+ const struct timespec * ts);
+
+void ssm_rbuff_get_txq_target(struct ssm_rbuff * rb,
+ struct timespec * ts);
void ssm_rbuff_fini(struct ssm_rbuff * rb);
@@ -57,6 +72,9 @@ int ssm_rbuff_mlock(struct ssm_rbuff * rb);
int ssm_rbuff_write(struct ssm_rbuff * rb,
size_t off);
+int ssm_rbuff_write_prio(struct ssm_rbuff * rb,
+ size_t off);
+
int ssm_rbuff_write_b(struct ssm_rbuff * rb,
size_t off,
const struct timespec * abstime);
diff --git a/include/test/certs/ecdsa.h b/include/test/certs/ecdsa.h
index 1d61a3f8..cbc4ed06 100644
--- a/include/test/certs/ecdsa.h
+++ b/include/test/certs/ecdsa.h
@@ -107,6 +107,23 @@ static const char * signed_server_crt_ec = \
"ktkxoHAFbjQEPQIhAMInHI7lvRmS0IMw1wBF/WlUZWKvhyU/TeMIZfk/JGCS\n"
"-----END CERTIFICATE-----\n";
+/* Valid CA outside the test chain, for cacert= pin mismatch */
+static __attribute__((unused)) const char * other_ca_crt_ec = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIICNjCCAdugAwIBAgIUTZcZ9hKXyCT/VgTw8TD1TB2mzrgwCgYIKoZIzj0EAwIw\n"
+"cDELMAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAK\n"
+"BgNVBAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMR4wHAYDVQQDDBVvdGhl\n"
+"ci1jYS51bml0dGVzdC5vN3MwHhcNMjYwNjEyMTU1MjAzWhcNNDYwNjA3MTU1MjAz\n"
+"WjBwMQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYDVQQHDAVHaGVudDEM\n"
+"MAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3MxHjAcBgNVBAMMFW90\n"
+"aGVyLWNhLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNtu\n"
+"FghMww2kQ6a+Coe6VPzfBRUZlm7y6/RfbRFPvErowOqKLQP+wCs8Rq46VmHCYTbB\n"
+"OlRwzJKcNoSeJ4MNWUqjUzBRMB0GA1UdDgQWBBTmEP8W6fgViKIjw8CpTuQwyuOi\n"
+"kTAfBgNVHSMEGDAWgBTmEP8W6fgViKIjw8CpTuQwyuOikTAPBgNVHRMBAf8EBTAD\n"
+"AQH/MAoGCCqGSM49BAMCA0kAMEYCIQDQOCfFcOJm49R975RBPfVMy0pXGx/YeQcy\n"
+"6WKAeLuTowIhAISdVZ6KxsgkwuswMtDWAkCBujep0XSBGXtXmi4959DH\n"
+"-----END CERTIFICATE-----\n";
+
/* Self-signed by server test-1.unittest.o7s using its key */
static __attribute__((unused)) const char * server_crt_ec = \
"-----BEGIN CERTIFICATE-----\n"
@@ -121,5 +138,25 @@ static __attribute__((unused)) const char * server_crt_ec = \
"gRo=\n"
"-----END CERTIFICATE-----\n";
+/*
+ * Name-confusion fixture: real CN is "attacker.unittest.o7s", but the
+ * O field value is "CN=victim.unittest.o7s" so the oneline subject is
+ * "/O=CN=victim.unittest.o7s/CN=attacker.unittest.o7s". A strstr("CN=")
+ * scan latches onto the decoy. The real CN must win.
+ */
+static __attribute__((unused)) const char * confused_crt_ec = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIIB1jCCAX2gAwIBAgIUCfXJzDQ3Sx5qcyVB9Rb4/FdZ+QowCgYIKoZIzj0EAwIw\n"
+"QTEfMB0GA1UECgwWQ049dmljdGltLnVuaXR0ZXN0Lm83czEeMBwGA1UEAwwVYXR0\n"
+"YWNrZXIudW5pdHRlc3QubzdzMB4XDTI2MDYxNDE5MDcwMVoXDTQ2MDYwOTE5MDcw\n"
+"MVowQTEfMB0GA1UECgwWQ049dmljdGltLnVuaXR0ZXN0Lm83czEeMBwGA1UEAwwV\n"
+"YXR0YWNrZXIudW5pdHRlc3QubzdzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n"
+"oLwrbLs3diGcjyY2ErvO/U6CoyyKfl/8e1nxBKXHSOkO5xVmFu+EobEQVFvabxE/\n"
+"x4RttKcGJqUe8vlyQexQq6NTMFEwHQYDVR0OBBYEFGBaOBzTsCakjBN61x0ZnHSk\n"
+"04T3MB8GA1UdIwQYMBaAFGBaOBzTsCakjBN61x0ZnHSk04T3MA8GA1UdEwEB/wQF\n"
+"MAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgFtBeVxlRuI7y9Bo/Dh97ajTbHJXYMkc6\n"
+"ZqflSN3Q/uACIHWoCVn6u6+JjF+Kj9zubFJ49RIQJthSeP8xj7yTeV17\n"
+"-----END CERTIFICATE-----\n";
+
#endif /* TEST_CERTS_H */
diff --git a/irmd.conf.in b/irmd.conf.in
index dee88392..c3cfe5e5 100644
--- a/irmd.conf.in
+++ b/irmd.conf.in
@@ -7,10 +7,6 @@
#
# [name.<name of service>] add a new name to the system.
# [local.<name for IPCP>] add a new local IPCP to the system.
-# [eth-llc.<name for IPCP>] add a new IPCP over Ethernet (LLC) to the system.
-# [eth-dix.<name for IPCP>] add a new IPCP over Ethernet (DIX) to the system.
-# [udp4.<name for IPCP>] add a new IPCP over UDP/IPv4 to the system.
-# [udp6.<name for IPCP>] add a new IPCP over UDP/IPv6 to the system.
# [broadcast.<name of IPCP>] add a new broadcast IPCP to the system.
# [unicast.<name of IPCP>] add a new unicast IPCP to the system.
#
@@ -32,6 +28,49 @@
# enrol=<dst>: Enrol with this neigbor (broadcast and unicast only).
# conn=[<dst>]: Create mgmt and data flows with these neighbors.
# reg=[<names>]: Register these names with the IPCP.
+#
+# enrol, and each element of conn, also accept a table instead of a bare
+# name, to reach that peer over one specific point of attachment:
+#
+# {dst=<name>, eth={}} Resolve dst by query over every
+# attached eth PoA.
+# {dst=<name>, eth={dev=<device>}} Same, pinned to one device. eth
+# accepts an optional
+# ethertype=<ethertype>, default
+# 0xA000.
+# {dst=<name>, udp=<addr>[:<port>]} Literal peer; IPv4, bracketed IPv6
+# ("[::1]:3435"), bare IPv6, or a
+# host name the IRMd resolves. Port
+# defaults to 3435.
+#
+# A table names at most one of eth or udp; dst is required.
+#
+# A unicast or broadcast IPCP binds its own points of attachment. udp
+# and eth are each an array of strings or tables, one udp for both
+# address families:
+#
+# udp = [<addr>, ...] <addr> is a literal IPv4 or
+# IPv6 address, with an optional
+# port ("<addr>:<port>",
+# bracketed for IPv6:
+# "[::1]:3435"). Port defaults
+# to 3435. A table,
+# {addr=<addr>[:<port>]}, is
+# also accepted.
+# eth = [<device>, ...] <device> is a bare device
+# name, or a table
+# {dev=<device>[, ethertype=
+# <ethertype>]}. Ethertype
+# defaults to 0xA000.
+#
+# Because a table element is accepted, the array-of-tables spelling
+# still works too, e.g. [[unicast.lan1.udp]] with addr="<addr>" as a
+# separate stanza; repeat the stanza, or the array, to attach more
+# than one PoA.
+#
+# Unlike conn/enrol above, a PoA binds locally: only literal addresses
+# are accepted here, not host names.
+#
# Bootstrap options can be set as in the examples below.
# For more details on the configuration options for each of the IPCP types,
# please refer to the Ouroboros man page.
@@ -56,12 +95,12 @@ prog=["@INSTALL_DIR@/ovpn"] # Defaults to [].
prog=["@INSTALL_DIR@/oping"] # Defaults to [].
args=["--listen"] # Defaults to disabled. Autostart server with these args.
lb="round-robin" # Defaults to spill (load-balancing options: spill, round-robin).
-# server_enc_file=/path/to/enc.conf Default: @OUROBOROS_SRV_CRT_DIR@/<name>/enc.conf
-# server_crt_file=/path/to/crt.pem Default: @OUROBOROS_SRV_CRT_DIR@/<name>/crt.pem
-# server_key_file=/path/to/key.pem Default: @OUROBOROS_SRV_CRT_DIR@/<name>/key.pem
-# client_enc_file=/path/to/enc.conf Default: @OUROBOROS_CLI_CRT_DIR@/<name>/enc.conf
-# client_crt_file=/path/to/crt.pem Default: @OUROBOROS_CLI_CRT_DIR@/<name>/crt.pem
-# client_key_file=/path/to/key.pem Default: @OUROBOROS_CLI_CRT_DIR@/<name>/key.pem
+# server_sec_file=/path/to/sec.conf Default: @OUROBOROS_SRV_CRT_DIR@/<name>/sec.conf
+# server_crt_file=/path/to/crt.pem Default: @OUROBOROS_SRV_CRT_DIR@/<name>/crt.pem
+# server_key_file=/path/to/key.pem Default: @OUROBOROS_SRV_CRT_DIR@/<name>/key.pem
+# client_sec_file=/path/to/sec.conf Default: @OUROBOROS_CLI_CRT_DIR@/<name>/sec.conf
+# client_crt_file=/path/to/crt.pem Default: @OUROBOROS_CLI_CRT_DIR@/<name>/crt.pem
+# client_key_file=/path/to/key.pem Default: @OUROBOROS_CLI_CRT_DIR@/<name>/key.pem
[local.local1]
bootstrap="local1" # Defaults to not set.
@@ -70,34 +109,6 @@ bootstrap="local1" # Defaults to not set.
# NAMES KNOWN
reg=["bc1", "LAN"] # Defaults to [].
-[eth-llc.ethl]
-bootstrap="ethl1" # Defaults to not set.
- # BOOTSTRAP CONFIGURATION
-dev="lo"
-# hash="SHA3_224" # Defaults to SHA3_256.
-
-[eth-dix.eth1]
-bootstrap="eth1" # Defaults to not set.
- # BOOTSTRAP CONFIGURATION
-dev="lo"
-# ethertype=0xA007 # Defaults to 0xA000.
-# hash="SHA3_224" # Defaults to SHA3_256.
-reg=["lan1"]
-
-[udp4.udp1]
-bootstrap="udp" # Defaults to not set.
- # BOOTSTRAP CONFIGURATION
-ip="127.0.0.1"
-# port=9000 # Defaults to 3435.
-# dns="127.0.0.1" # Requires a DDNS server. Disables DDNS support if not set.
-
-[udp6.udp2]
-bootstrap="udp2" # Defaults to not set.
- # BOOTSTRAP CONFIGURATION
-ip="::1"
-# port=9000 # Defaults to 3435.
-# dns="::1" # Requires a DDNS server. Disables DDNS support if not set.
-
[broadcast.bc1]
bootstrap="broadcast" # Defaults to not set.
# autobind=true # Defaults to false.
@@ -113,6 +124,7 @@ autobind=true # Defaults to false.
# addr_size=4 # Defaults to 4 (32-bit addresses).
# eid_size=8 # Defaults to 8 (64-bit endpoint IDs, only accepted option).
# max_ttl=60 # Defaults to 60 (max 255).
+# max_rtt=200 # Defaults to 200 (max 65535).
# addr-auth="flat" # Defaults to flat (currently only option).
# routing="lfa" # Defaults to link-state (options: link-state, lfa, ecmp).
# ls_t_recalc=4 # Forwarding Function update interval (s) (Default 4).
@@ -129,7 +141,19 @@ autobind=true # Defaults to false.
# NAMES KNOWN
reg=["oping"] # Defaults to [].
+ # POINTS OF ATTACHMENT
+udp = ["127.0.0.1", "127.0.0.1:3436"] # Two PoAs; port defaults to 3435.
+# udp = ["127.0.0.1:9000"] # Or pick a port explicitly.
+
+[[unicast.lan1.eth]]
+dev="lo"
+# ethertype=0xA007 # Defaults to 0xA000.
+
[unicast.lan2]
enrol="LAN" # Defaults to not set.
+# enrol={dst="LAN", eth={dev="eth0"}} # Or enrol over a specific PoA.
conn=["lan1"] # Defaults to [].
+# conn=["lan1",
+# {dst="lan3", eth={}},
+# {dst="lan4", udp="10.0.0.1:3435"}] # Or connect over specific PoAs.
autobind=true # Defaults to false.
diff --git a/enc.conf.in b/sec.conf.in
index 8f91d717..efd07878 100644
--- a/enc.conf.in
+++ b/sec.conf.in
@@ -1,19 +1,19 @@
-### Example Ouroboros encryption configuration file
+### Example Ouroboros security configuration file
#
-# This file specifies the key exchange (KEX) algorithm and cipher to use
-# for encrypted flows.
+# This file specifies the security parameters for a service: the key
+# exchange (KEX) algorithm, cipher, key derivation, and peer authentication.
#
# File Locations:
# ---------------
#
# This file should be placed at one of:
-# @OUROBOROS_CONFIG_DIR@/security/server/<name>/enc.conf (server-side config)
-# @OUROBOROS_CONFIG_DIR@/security/client/<name>/enc.conf (client-side config)
+# @OUROBOROS_CONFIG_DIR@/security/server/<name>/sec.conf (server-side config)
+# @OUROBOROS_CONFIG_DIR@/security/client/<name>/sec.conf (client-side config)
#
# Where <name> is the service name registered with 'irm name create'.
#
# You can override the default paths using:
-# irm name create <name> sencpath <server-enc-path> cencpath <client-enc-path>
+# irm name create <name> ssecpath <server-sec-path> csecpath <client-sec-path>
#
# Configuration Options:
# ----------------------
@@ -22,7 +22,8 @@
# cipher=<cipher> Symmetric cipher algorithm
# kdf=<hash> Key derivation function hash algorithm
# kem_mode=<mode> KEM encapsulation mode (server or client)
-# none Explicitly disable encryption
+# auth=<policy> Peer authentication policy (required or optional)
+# encryption=none Explicitly disable encryption
#
# Supported KEX algorithms (kex=):
# --------------------------------
@@ -45,8 +46,10 @@
# ML-KEM-1024 CRYSTALS-Kyber-1024
#
# Hybrid KEMs:
-# X25519MLKEM768 X25519 + ML-KEM-768
-# X448MLKEM1024 X448 + ML-KEM-1024
+# X25519MLKEM768 X25519 + ML-KEM-768
+# X448MLKEM1024 X448 + ML-KEM-1024
+# SecP256r1MLKEM768 NIST P-256 + ML-KEM-768
+# SecP384r1MLKEM1024 NIST P-384 + ML-KEM-1024
#
# Supported cipher algorithms (cipher=):
# --------------------------------------
@@ -57,11 +60,6 @@
# aes-256-gcm AES-256 in GCM mode (default)
# chacha20-poly1305 ChaCha20-Poly1305
#
-# Stream ciphers (not recommended):
-# aes-128-ctr AES-128 in CTR mode
-# aes-192-ctr AES-192 in CTR mode
-# aes-256-ctr AES-256 in CTR mode
-#
# Key Derivation Functions (kdf=):
# ---------------------------------
#
@@ -76,6 +74,33 @@
# blake2b512 BLAKE2b-512
# blake2s256 BLAKE2s-256
#
+# Peer Authentication (auth=):
+# ----------------------------
+#
+# optional Accept unauthenticated peers
+# required Reject peers that do not present a valid certificate
+#
+# This setting applies to the *peer*: in a client config it requires
+# the server to authenticate; in a server config it requires the
+# client. The defaults mirror the web: a client config defaults to
+# required (the server must authenticate), a server config defaults
+# to optional (client authentication is opt-in). Set auth=required on
+# the server too for mutual authentication. Combine encryption=none
+# with auth=required for authenticated but unencrypted flows.
+#
+# Issuer Pinning (cacert=):
+# -------------------------
+#
+# cacert=<path> Path to a CA certificate that must be part of the
+# peer certificate's verified chain
+#
+# The peer certificate is always validated against the trusted CA
+# store; cacert= further restricts which CA must have issued it: a
+# certificate, if presented, must chain through the pinned CA. Whether
+# a certificate is mandatory is controlled by auth= alone: under
+# auth=optional a peer may still connect without one. The pinned CA
+# must load when the config is read, otherwise flow allocation fails.
+#
# KEM Mode (kem_mode=):
# ---------------------
#
@@ -114,7 +139,7 @@
#
# File formats:
# - Pure ML-KEM: PEM format (.pem extension)
-# - Hybrid KEMs: Raw bytes (.raw extension)
+# - Hybrid KEMs: Raw key bytes (.raw extension), for the kex= algorithm
#
# Examples:
# ---------
@@ -147,4 +172,8 @@ kdf=sha256
# kdf=sha512
#
# Disable encryption:
-# none
+# encryption=none
+#
+# Authentication required, no encryption:
+# encryption=none
+# auth=required
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(&eth_data.fd_to_ef[i].r_addr, 0, MAC_SIZE);
-#ifdef IPCP_ETH_FLOW_STATS
- memset(&eth_data.fd_to_ef[i].stat, 0,
- sizeof(eth_data.fd_to_ef[i].stat));
-#endif
- }
-#ifdef IPCP_ETH_FLOW_STATS
- memset(&eth_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(&eth_data.flows_lock, NULL))
- goto fail_flows_lock;
-
- if (pthread_mutex_init(&eth_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(&eth_data.mgmt_cond, &cattr))
- goto fail_mgmt_cond;
-
- pthread_condattr_destroy(&cattr);
-
- list_head_init(&eth_data.mgmt_frames);
-
- return 0;
-
- fail_mgmt_cond:
- pthread_condattr_destroy(&cattr);
- fail_condattr:
- pthread_mutex_destroy(&eth_data.mgmt_lock);
- fail_mgmt_lock:
- pthread_rwlock_destroy(&eth_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(&eth_data.mgmt_cond);
- pthread_mutex_destroy(&eth_data.mgmt_lock);
- pthread_rwlock_destroy(&eth_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(&eth_data.stat.kern_rcv,
- tp_stats.tp_packets);
- FETCH_ADD_RELAXED(&eth_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(&eth_data.stat.n_flows),
- LOAD_RELAXED(&eth_data.stat.n_rcv),
- LOAD_RELAXED(&eth_data.stat.n_snd),
- LOAD_RELAXED(&eth_data.stat.n_mgmt_rcv),
- LOAD_RELAXED(&eth_data.stat.n_mgmt_snd),
- LOAD_RELAXED(&eth_data.stat.n_bad_id),
- LOAD_RELAXED(&eth_data.stat.n_dlv_f),
- LOAD_RELAXED(&eth_data.stat.n_buf_f),
- LOAD_RELAXED(&eth_data.stat.n_rcv_f),
- LOAD_RELAXED(&eth_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(&eth_data.stat.kern_rcv),
- LOAD_RELAXED(&eth_data.stat.kern_drp));
-# endif
-#endif
- return n;
- }
-
- fd = atoi(entry);
-
- if (fd < 0 || fd >= SYS_MAX_FLOWS)
- return -1;
-
- flow = &eth_data.fd_to_ef[fd];
-
- pthread_rwlock_rdlock(&eth_data.flows_lock);
-
- stamp = flow->stat.stamp;
- if (stamp == 0) {
- pthread_rwlock_unlock(&eth_data.flows_lock);
- return 0;
- }
-
- pthread_rwlock_unlock(&eth_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(&eth_data.flows_lock);
-
- n_entries = (int) LOAD_RELAXED(&eth_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(&eth_data.flows_lock);
-
- return idx;
-
- fail_entry:
- while (idx-- > 0)
- free((*buf)[idx]);
- free(*buf);
- fail_entries:
- pthread_rwlock_unlock(&eth_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 = &eth_data.fd_to_ef[fd];
-
- pthread_rwlock_rdlock(&eth_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(&eth_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(&eth_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 *) &eth_data.device,
- sizeof(eth_data.device)) <= 0) {
- log_dbg("Failed to send message: %s.", strerror(errno));
- return -1;
- }
-#endif /* HAVE_NETMAP */
-
- FETCH_ADD_RELAXED(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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, &eth_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(&eth_data.mgmt_lock);
-
- while (list_is_empty(&eth_data.mgmt_frames) &&
- ret != -ETIMEDOUT)
- ret = -pthread_cond_timedwait(&eth_data.mgmt_cond,
- &eth_data.mgmt_lock,
- &abstime);
- if (ret != -ETIMEDOUT)
- frame = list_first_entry((&eth_data.mgmt_frames),
- struct mgmt_frame, next);
- if (frame != NULL)
- list_del(&frame->next);
-
- pthread_mutex_unlock(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_data.mgmt_lock);
- list_add(&frame->next, &eth_data.mgmt_frames);
- pthread_cond_signal(&eth_data.mgmt_cond);
- pthread_mutex_unlock(&eth_data.mgmt_lock);
- FETCH_ADD_RELAXED(&eth_data.stat.n_rcv, 1);
- FETCH_ADD_RELAXED(&eth_data.stat.n_mgmt_rcv, 1);
- } else {
- pthread_rwlock_rdlock(&eth_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(&eth_data.flows_lock);
- FETCH_ADD_RELAXED(&eth_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(&eth_data.flows_lock);
- FETCH_ADD_RELAXED(&eth_data.stat.n_bad_id, 1);
- goto fail_frame;
- }
-#endif
- FETCH_ADD_RELAXED(&eth_data.fd_to_ef[fd].stat.p_rcv, 1);
- FETCH_ADD_RELAXED(&eth_data.fd_to_ef[fd].stat.b_rcv,
- length);
- FETCH_ADD_RELAXED(&eth_data.stat.n_rcv, 1);
- pthread_rwlock_unlock(&eth_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(
- &eth_data.fd_to_ef[fd].stat.p_dlv_f,
- 1);
- FETCH_ADD_RELAXED(&eth_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(&eth_data.stat.n_buf_f, 1);
- continue;
- }
-
- pthread_rwlock_rdlock(&eth_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(&eth_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(
- &eth_data.fd_to_ef[fd].stat.p_snd_f,
- 1);
- FETCH_ADD_RELAXED(&eth_data.stat.n_snd_f, 1);
- } else {
- FETCH_ADD_RELAXED(
- &eth_data.fd_to_ef[fd].stat.p_snd,
- 1);
- FETCH_ADD_RELAXED(
- &eth_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(&eth_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(&eth_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(&eth_data.poll_in, 0, sizeof(eth_data.poll_in));
- memset(&eth_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 *) &eth_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, &eth_r_ops)) {
- log_err("Failed to register RIB.");
- goto fail_rib_reg;
- }
-#endif
-#if defined(__linux__)
- if (pthread_create(&eth_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(&eth_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(&eth_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(&eth_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(&eth_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(&eth_data.flows_lock);
- ssap = bmp_allocate(eth_data.saps);
- if (!bmp_is_id_valid(eth_data.saps, ssap)) {
- pthread_rwlock_unlock(&eth_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(&eth_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(&eth_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(&eth_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(&eth_data.flows_lock);
- memset(&eth_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(&eth_data.stat.n_flows, 1);
- pthread_rwlock_unlock(&eth_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(&eth_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(&eth_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(&eth_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(&eth_data.flows_lock);
- bmp_release(eth_data.saps, eth_data.fd_to_ef[fd].sap);
- pthread_rwlock_unlock(&eth_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(&eth_data.flows_lock);
- memset(&eth_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(&eth_data.stat.n_flows, 1);
- pthread_rwlock_unlock(&eth_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(&eth_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(&eth_data.fd_to_ef[fd].r_addr, 0, MAC_SIZE);
-
-#ifdef IPCP_ETH_FLOW_STATS
- memset(&eth_data.fd_to_ef[fd].stat, 0,
- sizeof(eth_data.fd_to_ef[fd].stat));
- FETCH_SUB_RELAXED(&eth_data.stat.n_flows, 1);
-#endif
-
- pthread_rwlock_unlock(&eth_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, &eth_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, &reg->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(&reg->names);
+
+ if (pthread_rwlock_init(&reg->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(&reg->lock);
+
+ while (!list_is_empty(&reg->names)) {
+ struct reg_entry * e;
+
+ e = list_first_entry(&reg->names, struct reg_entry, list);
+
+ list_del(&e->list);
+
+ reg_entry_destroy(e);
+ }
+
+ pthread_rwlock_unlock(&reg->lock);
+
+ pthread_rwlock_destroy(&reg->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(&reg->lock);
+
+ if (reg_find(reg, hash) != NULL) {
+ pthread_rwlock_unlock(&reg->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, &reg->names);
+
+ pthread_rwlock_unlock(&reg->lock);
+
+ return 0;
+
+ fail:
+ pthread_rwlock_unlock(&reg->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(&reg->lock);
+
+ e = reg_find(reg, hash);
+ if (e == NULL) {
+ pthread_rwlock_unlock(&reg->lock);
+ return 0; /* nothing to do */
+ }
+
+ list_del(&e->list);
+
+ pthread_rwlock_unlock(&reg->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(&reg->lock);
+
+ ret = reg_find(reg, hash) != NULL;
+
+ pthread_rwlock_unlock(&reg->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(&reg.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(&reg.mtx);
+
+ f = __reg_get_flow(flow_id);
+ if (f != NULL)
+ strcpy(f->name, name);
+
+ pthread_mutex_unlock(&reg.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(&reg.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(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+
+ ret = flow != NULL && flow->poa;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_set_poa(int flow_id)
+{
+ struct reg_flow * flow;
+ int ret = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ flow->poa = true;
+ ret = 0;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_set_oap_ctx(int flow_id,
+ void * ctx)
+{
+ struct reg_flow * flow;
+ int ret = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ flow->oap_ctx = ctx;
+ ret = 0;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+void * reg_flow_take_oap_ctx(int flow_id)
+{
+ struct reg_flow * flow;
+ void * ctx = NULL;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ ctx = flow->oap_ctx;
+ flow->oap_ctx = NULL;
+ }
+
+ pthread_mutex_unlock(&reg.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(&reg.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(&reg.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(&reg.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(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_get_epoch(int flow_id)
+{
+ struct reg_flow * flow;
+ int epoch = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted)
+ epoch = flow->rk.epoch;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return epoch;
+}
+
+bool reg_flow_rekey_pending(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.has_pending;
+
+ pthread_mutex_unlock(&reg.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(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ pid = flow->info.n_1_pid;
+
+ pthread_mutex_unlock(&reg.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(&reg.mtx);
+
+ llist_for_each(p, &reg.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(&reg.mtx);
+
+ return n;
+}
+
+void reg_flow_clear_in_flight(int flow_id)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ flow->rk.in_flight = false;
+
+ pthread_mutex_unlock(&reg.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(&reg.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(&reg.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(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.initiator && flow->rk.in_flight;
+
+ pthread_mutex_unlock(&reg.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(&reg.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(&reg.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(&reg.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(&reg.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(&reg.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(&reg.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(&reg.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(&reg.mtx);
+
+ return admit;
+}
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.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(&reg.mtx);
+}
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = uid_may_access(uid, flow->info.uid);
+
+ pthread_mutex_unlock(&reg.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(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ __notify_proc(flow->info.n_pid, flow_id, event);
+
+ pthread_mutex_unlock(&reg.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(&reg.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(&reg.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, &eth_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), &ethertype) < 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), &ethertype) < 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), &ethertype) < 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), &ethertype) < 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);