summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/woodpecker/00-check-version.yaml13
-rw-r--r--.ci/woodpecker/01-build.yaml111
-rw-r--r--.ci/woodpecker/10-build.yaml93
-rw-r--r--.ci/woodpecker/20-sanitizer.yaml40
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt249
-rw-r--r--VERSION1
-rw-r--r--cmake/AddCompileFlags.cmake17
-rw-r--r--cmake/CmakeUninstall.cmake.in29
-rw-r--r--cmake/OuroborosConfig.cmake.in23
-rw-r--r--cmake/compiler.cmake64
-rw-r--r--cmake/config/global.cmake47
-rw-r--r--cmake/config/ipcp/broadcast.cmake9
-rw-r--r--cmake/config/ipcp/common.cmake58
-rw-r--r--cmake/config/ipcp/eth.cmake22
-rw-r--r--cmake/config/ipcp/local.cmake39
-rw-r--r--cmake/config/ipcp/udp.cmake16
-rw-r--r--cmake/config/ipcp/unicast.cmake35
-rw-r--r--cmake/config/irmd.cmake46
-rw-r--r--cmake/config/lib.cmake139
-rw-r--r--cmake/config/ssm.cmake168
-rw-r--r--cmake/config/tests.cmake17
-rw-r--r--cmake/dependencies.cmake44
-rw-r--r--cmake/dependencies/coverage/gcov.cmake21
-rw-r--r--cmake/dependencies/coverage/lcov.cmake17
-rw-r--r--cmake/dependencies/crypt/libgcrypt.cmake55
-rw-r--r--cmake/dependencies/crypt/openssl.cmake56
-rw-r--r--cmake/dependencies/eth/bpf.cmake20
-rw-r--r--cmake/dependencies/eth/netmap.cmake18
-rw-r--r--cmake/dependencies/eth/rawsockets.cmake12
-rw-r--r--cmake/dependencies/irmd/libtoml.cmake29
-rw-r--r--cmake/dependencies/system/explicit_bzero.cmake4
-rw-r--r--cmake/dependencies/system/fuse.cmake46
-rw-r--r--cmake/dependencies/system/libraries.cmake17
-rw-r--r--cmake/dependencies/system/liburcu.cmake45
-rw-r--r--cmake/dependencies/system/protobufc.cmake13
-rw-r--r--cmake/dependencies/system/robustmutex.cmake18
-rw-r--r--cmake/dependencies/system/sysrandom.cmake14
-rw-r--r--cmake/dependencies/udp/ddns.cmake31
-rw-r--r--cmake/doc.cmake (renamed from doc/man/CMakeLists.txt)24
-rw-r--r--cmake/include.cmake21
-rw-r--r--cmake/install.cmake101
-rw-r--r--cmake/package.cmake29
-rw-r--r--cmake/tags.cmake21
-rw-r--r--cmake/tests.cmake30
-rw-r--r--cmake/utils/AddCompileFlags.cmake6
-rw-r--r--cmake/utils/CMakeUninstall.cmake.in21
-rw-r--r--cmake/utils/CPUUtils.cmake82
-rw-r--r--cmake/utils/CompilerUtils.cmake (renamed from cmake/CompilerUtils.cmake)10
-rw-r--r--cmake/utils/DebugTargets.cmake16
-rw-r--r--cmake/utils/DisableTestLogging.cmake11
-rw-r--r--cmake/utils/FindProtobufC.cmake (renamed from cmake/FindProtobufC.cmake)36
-rw-r--r--cmake/utils/GenCoverage.cmake94
-rw-r--r--cmake/utils/GenVersionHeader.cmake12
-rw-r--r--cmake/utils/GetGitHash.cmake40
-rw-r--r--cmake/utils/HumanReadable.cmake17
-rw-r--r--cmake/utils/ParseCoverage.cmake120
-rw-r--r--cmake/utils/ParseGitTag.cmake56
-rw-r--r--cmake/utils/PrintCoverage.cmake333
-rw-r--r--cmake/utils/PrintVersion.cmake8
-rw-r--r--cmake/utils/TestUtils.cmake38
-rw-r--r--cmake/version.cmake34
-rw-r--r--doc/CMakeLists.txt1
-rw-r--r--doc/man/flow_alloc.388
-rw-r--r--doc/man/flow_read.346
-rw-r--r--doc/man/fqueue.321
-rw-r--r--doc/man/ouroboros.86
-rw-r--r--include/CMakeLists.txt1
-rw-r--r--include/ouroboros/CMakeLists.txt25
-rw-r--r--include/ouroboros/atomics.h39
-rw-r--r--include/ouroboros/bitmap.h2
-rw-r--r--include/ouroboros/btree.h2
-rw-r--r--include/ouroboros/cdefs.h2
-rw-r--r--include/ouroboros/cep.h2
-rw-r--r--include/ouroboros/crc16.h43
-rw-r--r--include/ouroboros/crc32.h2
-rw-r--r--include/ouroboros/crc64.h44
-rw-r--r--include/ouroboros/crc8.h43
-rw-r--r--include/ouroboros/crypt.h373
-rw-r--r--include/ouroboros/dev.h2
-rw-r--r--include/ouroboros/endian.h2
-rw-r--r--include/ouroboros/errno.h3
-rw-r--r--include/ouroboros/fccntl.h15
-rw-r--r--include/ouroboros/flow.h11
-rw-r--r--include/ouroboros/fqueue.h5
-rw-r--r--include/ouroboros/hash.h8
-rw-r--r--include/ouroboros/ipcp-dev.h25
-rw-r--r--include/ouroboros/ipcp.h2
-rw-r--r--include/ouroboros/irm.h8
-rw-r--r--include/ouroboros/list.h95
-rw-r--r--include/ouroboros/local-dev.h10
-rw-r--r--include/ouroboros/lockfile.h2
-rw-r--r--include/ouroboros/logs.h35
-rw-r--r--include/ouroboros/md5.h2
-rw-r--r--include/ouroboros/name.h8
-rw-r--r--include/ouroboros/notifier.h2
-rw-r--r--include/ouroboros/np1_flow.h8
-rw-r--r--include/ouroboros/proc.h7
-rw-r--r--include/ouroboros/proto.h2
-rw-r--r--include/ouroboros/protobuf.h2
-rw-r--r--include/ouroboros/pthread.h8
-rw-r--r--include/ouroboros/qos.h59
-rw-r--r--include/ouroboros/qoscube.h2
-rw-r--r--include/ouroboros/random.h2
-rw-r--r--include/ouroboros/rcu.h110
-rw-r--r--include/ouroboros/rib.h2
-rw-r--r--include/ouroboros/serdes-irm.h25
-rw-r--r--include/ouroboros/serdes-oep.h10
-rw-r--r--include/ouroboros/sha3.h2
-rw-r--r--include/ouroboros/shm_du_buff.h58
-rw-r--r--include/ouroboros/shm_rbuff.h69
-rw-r--r--include/ouroboros/shm_rdrbuff.h67
-rw-r--r--include/ouroboros/sockets.h.in2
-rw-r--r--include/ouroboros/ssm_flow_set.h (renamed from include/ouroboros/shm_flow_set.h)30
-rw-r--r--include/ouroboros/ssm_pk_buff.h58
-rw-r--r--include/ouroboros/ssm_pool.h74
-rw-r--r--include/ouroboros/ssm_rbuff.h76
-rw-r--r--include/ouroboros/time.h8
-rw-r--r--include/ouroboros/tpm.h3
-rw-r--r--include/ouroboros/tw.h77
-rw-r--r--include/ouroboros/utils.h15
-rw-r--r--include/ouroboros/version.h.in3
-rw-r--r--include/test/certs/ecdsa.h162
-rw-r--r--include/test/certs/ml_dsa.h656
-rw-r--r--include/test/certs/slh_dsa.h754
-rw-r--r--include/test/test.h (renamed from include/ouroboros/test.h)38
-rw-r--r--irmd.conf.in18
-rw-r--r--sec.conf.in177
-rw-r--r--src/CMakeLists.txt4
-rw-r--r--src/ipcpd/CMakeLists.txt72
-rw-r--r--src/ipcpd/broadcast/CMakeLists.txt41
-rw-r--r--src/ipcpd/broadcast/connmgr.c2
-rw-r--r--src/ipcpd/broadcast/dt.c64
-rw-r--r--src/ipcpd/broadcast/dt.h2
-rw-r--r--src/ipcpd/broadcast/main.c9
-rw-r--r--src/ipcpd/common/comp.h2
-rw-r--r--src/ipcpd/common/connmgr.c2
-rw-r--r--src/ipcpd/common/connmgr.h2
-rw-r--r--src/ipcpd/common/enroll.c2
-rw-r--r--src/ipcpd/common/enroll.h2
-rw-r--r--src/ipcpd/config.h.in32
-rw-r--r--src/ipcpd/eth/CMakeLists.txt155
-rw-r--r--src/ipcpd/eth/dix.c2
-rw-r--r--src/ipcpd/eth/eth.c734
-rw-r--r--src/ipcpd/eth/llc.c2
-rw-r--r--src/ipcpd/ipcp.c97
-rw-r--r--src/ipcpd/ipcp.h6
-rw-r--r--src/ipcpd/local/CMakeLists.txt43
-rw-r--r--src/ipcpd/local/main.c96
-rw-r--r--src/ipcpd/np1.h (renamed from src/irmd/tests/irm_test.c)28
-rw-r--r--src/ipcpd/shim-data.c2
-rw-r--r--src/ipcpd/shim-data.h2
-rw-r--r--src/ipcpd/udp/CMakeLists.txt89
-rw-r--r--src/ipcpd/udp/udp.c188
-rw-r--r--src/ipcpd/udp/udp4.c2
-rw-r--r--src/ipcpd/udp/udp6.c2
-rw-r--r--src/ipcpd/unicast/CMakeLists.txt72
-rw-r--r--src/ipcpd/unicast/addr-auth.c2
-rw-r--r--src/ipcpd/unicast/addr-auth.h2
-rw-r--r--src/ipcpd/unicast/addr-auth/flat.c2
-rw-r--r--src/ipcpd/unicast/addr-auth/flat.h2
-rw-r--r--src/ipcpd/unicast/addr-auth/ops.h2
-rw-r--r--src/ipcpd/unicast/addr-auth/pol.h2
-rw-r--r--src/ipcpd/unicast/ca.c180
-rw-r--r--src/ipcpd/unicast/ca.h30
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.c828
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.h19
-rw-r--r--src/ipcpd/unicast/ca/nop.c37
-rw-r--r--src/ipcpd/unicast/ca/nop.h19
-rw-r--r--src/ipcpd/unicast/ca/ops.h22
-rw-r--r--src/ipcpd/unicast/ca/pol.h2
-rw-r--r--src/ipcpd/unicast/ca/tests/CMakeLists.txt44
-rw-r--r--src/ipcpd/unicast/ca/tests/ca_test.c392
-rw-r--r--src/ipcpd/unicast/ca/tests/mb_ecn_test.c2588
-rw-r--r--src/ipcpd/unicast/cap.c291
-rw-r--r--src/ipcpd/unicast/cap.h54
-rw-r--r--src/ipcpd/unicast/connmgr.c2
-rw-r--r--src/ipcpd/unicast/dir.c2
-rw-r--r--src/ipcpd/unicast/dir.h2
-rw-r--r--src/ipcpd/unicast/dir/dht.c479
-rw-r--r--src/ipcpd/unicast/dir/dht.h2
-rw-r--r--src/ipcpd/unicast/dir/dht.proto2
-rw-r--r--src/ipcpd/unicast/dir/ops.h2
-rw-r--r--src/ipcpd/unicast/dir/pol.h2
-rw-r--r--src/ipcpd/unicast/dir/tests/CMakeLists.txt44
-rw-r--r--src/ipcpd/unicast/dir/tests/dht_test.c47
-rw-r--r--src/ipcpd/unicast/dt.c387
-rw-r--r--src/ipcpd/unicast/dt.h9
-rw-r--r--src/ipcpd/unicast/fa.c290
-rw-r--r--src/ipcpd/unicast/fa.h8
-rw-r--r--src/ipcpd/unicast/main.c7
-rw-r--r--src/ipcpd/unicast/pff.c2
-rw-r--r--src/ipcpd/unicast/pff.h2
-rw-r--r--src/ipcpd/unicast/pff/alternate.c4
-rw-r--r--src/ipcpd/unicast/pff/alternate.h2
-rw-r--r--src/ipcpd/unicast/pff/multipath.c4
-rw-r--r--src/ipcpd/unicast/pff/multipath.h2
-rw-r--r--src/ipcpd/unicast/pff/ops.h2
-rw-r--r--src/ipcpd/unicast/pff/pft.c21
-rw-r--r--src/ipcpd/unicast/pff/pft.h6
-rw-r--r--src/ipcpd/unicast/pff/pol.h2
-rw-r--r--src/ipcpd/unicast/pff/simple.c4
-rw-r--r--src/ipcpd/unicast/pff/simple.h2
-rw-r--r--src/ipcpd/unicast/pff/tests/CMakeLists.txt38
-rw-r--r--src/ipcpd/unicast/pff/tests/pft_test.c12
-rw-r--r--src/ipcpd/unicast/psched.c207
-rw-r--r--src/ipcpd/unicast/psched.h10
-rw-r--r--src/ipcpd/unicast/routing.c2
-rw-r--r--src/ipcpd/unicast/routing.h2
-rw-r--r--src/ipcpd/unicast/routing/graph.c92
-rw-r--r--src/ipcpd/unicast/routing/graph.h2
-rw-r--r--src/ipcpd/unicast/routing/link-state.c90
-rw-r--r--src/ipcpd/unicast/routing/link-state.h2
-rw-r--r--src/ipcpd/unicast/routing/ops.h2
-rw-r--r--src/ipcpd/unicast/routing/pol.h2
-rw-r--r--src/ipcpd/unicast/routing/tests/CMakeLists.txt38
-rw-r--r--src/ipcpd/unicast/routing/tests/graph_test.c2
-rw-r--r--src/ipcpd/unicast/tests/CMakeLists.txt34
-rw-r--r--src/ipcpd/unicast/tests/cap_test.c593
-rw-r--r--src/irmd/CMakeLists.txt137
-rw-r--r--src/irmd/config.h.in14
-rw-r--r--src/irmd/configfile.c16
-rw-r--r--src/irmd/configfile.h2
-rw-r--r--src/irmd/ipcp.c40
-rw-r--r--src/irmd/ipcp.h5
-rw-r--r--src/irmd/irmd.h2
-rw-r--r--src/irmd/main.c2043
-rw-r--r--src/irmd/oap.c288
-rw-r--r--src/irmd/oap.h108
-rw-r--r--src/irmd/oap/auth.c558
-rw-r--r--src/irmd/oap/auth.h68
-rw-r--r--src/irmd/oap/cli.c682
-rw-r--r--src/irmd/oap/hdr.c761
-rw-r--r--src/irmd/oap/hdr.h199
-rw-r--r--src/irmd/oap/internal.h119
-rw-r--r--src/irmd/oap/io.c158
-rw-r--r--src/irmd/oap/io.h40
-rw-r--r--src/irmd/oap/srv.c592
-rw-r--r--src/irmd/oap/tests/CMakeLists.txt64
-rw-r--r--src/irmd/oap/tests/common.c717
-rw-r--r--src/irmd/oap/tests/common.h122
-rw-r--r--src/irmd/oap/tests/oap_test.c2109
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c777
-rw-r--r--src/irmd/reg/CMakeLists.txt9
-rw-r--r--src/irmd/reg/flow.c65
-rw-r--r--src/irmd/reg/flow.h38
-rw-r--r--src/irmd/reg/ipcp.c3
-rw-r--r--src/irmd/reg/ipcp.h2
-rw-r--r--src/irmd/reg/name.c65
-rw-r--r--src/irmd/reg/name.h17
-rw-r--r--src/irmd/reg/pool.c97
-rw-r--r--src/irmd/reg/pool.h48
-rw-r--r--src/irmd/reg/proc.c41
-rw-r--r--src/irmd/reg/proc.h11
-rw-r--r--src/irmd/reg/prog.c27
-rw-r--r--src/irmd/reg/prog.h5
-rw-r--r--src/irmd/reg/reg.c1070
-rw-r--r--src/irmd/reg/reg.h91
-rw-r--r--src/irmd/reg/tests/CMakeLists.txt38
-rw-r--r--src/irmd/reg/tests/flow_test.c79
-rw-r--r--src/irmd/reg/tests/ipcp_test.c4
-rw-r--r--src/irmd/reg/tests/name_test.c12
-rw-r--r--src/irmd/reg/tests/proc_test.c25
-rw-r--r--src/irmd/reg/tests/prog_test.c8
-rw-r--r--src/irmd/reg/tests/reg_test.c578
-rw-r--r--src/irmd/tests/CMakeLists.txt27
-rw-r--r--src/irmd/tests/oap_test.c285
-rw-r--r--src/lib/CMakeLists.txt412
-rw-r--r--src/lib/bitmap.c2
-rw-r--r--src/lib/btree.c2
-rw-r--r--src/lib/cep.c2
-rw-r--r--src/lib/config.h.in53
-rw-r--r--src/lib/crc/crc16.c61
-rw-r--r--src/lib/crc/crc32.c (renamed from src/lib/crc32.c)2
-rw-r--r--src/lib/crc/crc64.c363
-rw-r--r--src/lib/crc/crc8.c62
-rw-r--r--src/lib/crc/tests/CMakeLists.txt21
-rw-r--r--src/lib/crc/tests/crc16_test.c67
-rw-r--r--src/lib/crc/tests/crc32_test.c (renamed from src/lib/tests/crc32_test.c)2
-rw-r--r--src/lib/crc/tests/crc64_test.c126
-rw-r--r--src/lib/crc/tests/crc8_test.c67
-rw-r--r--src/lib/crypt.c1123
-rw-r--r--src/lib/crypt/keyrot.c775
-rw-r--r--src/lib/crypt/keyrot.h74
-rw-r--r--src/lib/crypt/openssl.c1631
-rw-r--r--src/lib/crypt/openssl.h161
-rw-r--r--src/lib/dev.c2289
-rw-r--r--src/lib/frct.c4292
-rw-r--r--src/lib/hash.c54
-rw-r--r--src/lib/irm.c4
-rw-r--r--src/lib/list.c78
-rw-r--r--src/lib/lockfile.c2
-rw-r--r--src/lib/logs.c2
-rw-r--r--src/lib/md5.c2
-rw-r--r--src/lib/notifier.c2
-rw-r--r--src/lib/pb/cep.proto2
-rw-r--r--src/lib/pb/enroll.proto2
-rw-r--r--src/lib/pb/ipcp.proto6
-rw-r--r--src/lib/pb/ipcp_config.proto2
-rw-r--r--src/lib/pb/irm.proto18
-rw-r--r--src/lib/pb/model.proto18
-rw-r--r--src/lib/protobuf.c32
-rw-r--r--src/lib/qoscube.c14
-rw-r--r--src/lib/random.c27
-rw-r--r--src/lib/rib.c20
-rw-r--r--src/lib/serdes-irm.c164
-rw-r--r--src/lib/serdes-oep.c2
-rw-r--r--src/lib/sha3.c2
-rw-r--r--src/lib/shm_rbuff.c215
-rw-r--r--src/lib/shm_rbuff_ll.c249
-rw-r--r--src/lib/shm_rbuff_pthr.c304
-rw-r--r--src/lib/shm_rdrbuff.c610
-rw-r--r--src/lib/sockets.c2
-rw-r--r--src/lib/ssm/flow_set.c (renamed from src/lib/shm_flow_set.c)95
-rw-r--r--src/lib/ssm/pool.c919
-rw-r--r--src/lib/ssm/rbuff.c519
-rw-r--r--src/lib/ssm/ssm.h.in190
-rw-r--r--src/lib/ssm/tests/CMakeLists.txt21
-rw-r--r--src/lib/ssm/tests/flow_set_test.c255
-rw-r--r--src/lib/ssm/tests/pool_sharding_test.c474
-rw-r--r--src/lib/ssm/tests/pool_test.c1068
-rw-r--r--src/lib/ssm/tests/rbuff_test.c675
-rw-r--r--src/lib/tests/CMakeLists.txt31
-rw-r--r--src/lib/tests/auth_test.c361
-rw-r--r--src/lib/tests/auth_test_ml_dsa.c356
-rw-r--r--src/lib/tests/auth_test_slh_dsa.c367
-rw-r--r--src/lib/tests/bitmap_test.c2
-rw-r--r--src/lib/tests/btree_test.c2
-rw-r--r--src/lib/tests/crypt_test.c570
-rw-r--r--src/lib/tests/hash_test.c151
-rw-r--r--src/lib/tests/kex_test.c1248
-rw-r--r--src/lib/tests/kex_test_ml_kem.c549
-rw-r--r--src/lib/tests/keyrot_test.c1234
-rw-r--r--src/lib/tests/md5_test.c2
-rw-r--r--src/lib/tests/sha3_test.c2
-rw-r--r--src/lib/tests/shm_rbuff_test.c113
-rw-r--r--src/lib/tests/sockets_test.c8
-rw-r--r--src/lib/tests/time_test.c4
-rw-r--r--src/lib/tests/tpm_test.c6
-rw-r--r--src/lib/tests/tw_test.c663
-rw-r--r--src/lib/timerwheel.c414
-rw-r--r--src/lib/tpm.c4
-rw-r--r--src/lib/tw.c307
-rw-r--r--src/lib/utils.c94
-rw-r--r--src/tools/CMakeLists.txt85
-rw-r--r--src/tools/irm/CMakeLists.txt44
-rw-r--r--src/tools/irm/irm.c2
-rw-r--r--src/tools/irm/irm_bind.c2
-rw-r--r--src/tools/irm/irm_bind_ipcp.c2
-rw-r--r--src/tools/irm/irm_bind_process.c2
-rw-r--r--src/tools/irm/irm_bind_program.c2
-rw-r--r--src/tools/irm/irm_ipcp.c2
-rw-r--r--src/tools/irm/irm_ipcp_bootstrap.c2
-rw-r--r--src/tools/irm/irm_ipcp_connect.c24
-rw-r--r--src/tools/irm/irm_ipcp_create.c2
-rw-r--r--src/tools/irm/irm_ipcp_destroy.c2
-rw-r--r--src/tools/irm/irm_ipcp_disconnect.c2
-rw-r--r--src/tools/irm/irm_ipcp_enroll.c2
-rw-r--r--src/tools/irm/irm_ipcp_list.c2
-rw-r--r--src/tools/irm/irm_name.c2
-rw-r--r--src/tools/irm/irm_name_create.c26
-rw-r--r--src/tools/irm/irm_name_destroy.c2
-rw-r--r--src/tools/irm/irm_name_list.c2
-rw-r--r--src/tools/irm/irm_name_reg.c2
-rw-r--r--src/tools/irm/irm_name_unreg.c2
-rw-r--r--src/tools/irm/irm_ops.h2
-rw-r--r--src/tools/irm/irm_unbind.c2
-rw-r--r--src/tools/irm/irm_unbind_ipcp.c2
-rw-r--r--src/tools/irm/irm_unbind_process.c2
-rw-r--r--src/tools/irm/irm_unbind_program.c2
-rw-r--r--src/tools/irm/irm_utils.c2
-rw-r--r--src/tools/irm/irm_utils.h2
-rw-r--r--src/tools/obc/CMakeLists.txt16
-rw-r--r--src/tools/obc/obc.c2
-rw-r--r--src/tools/ocbr/CMakeLists.txt21
-rw-r--r--src/tools/ocbr/ocbr.c2
-rw-r--r--src/tools/ocbr/ocbr_client.c38
-rw-r--r--src/tools/ocbr/ocbr_server.c4
-rw-r--r--src/tools/oecho/CMakeLists.txt16
-rw-r--r--src/tools/oecho/oecho.c10
-rw-r--r--src/tools/oftp/oftp.c445
-rw-r--r--src/tools/operf/CMakeLists.txt26
-rw-r--r--src/tools/operf/operf.c5
-rw-r--r--src/tools/operf/operf_client.c8
-rw-r--r--src/tools/operf/operf_server.c2
-rw-r--r--src/tools/oping/CMakeLists.txt28
-rw-r--r--src/tools/oping/oping.c79
-rw-r--r--src/tools/oping/oping_client.c271
-rw-r--r--src/tools/oping/oping_server.c91
-rw-r--r--src/tools/ovpn/CMakeLists.txt21
-rw-r--r--src/tools/ovpn/ovpn.c2
-rw-r--r--src/tools/time_utils.h2
393 files changed, 43887 insertions, 8976 deletions
diff --git a/.ci/woodpecker/00-check-version.yaml b/.ci/woodpecker/00-check-version.yaml
new file mode 100644
index 00000000..cc2a24a8
--- /dev/null
+++ b/.ci/woodpecker/00-check-version.yaml
@@ -0,0 +1,13 @@
+steps:
+ - name: check-version
+ image: alpine
+ pull: true
+ when:
+ branch: [be]
+ event: [push, pull_request]
+ commands:
+ - |
+ grep -q '$Format:' VERSION || (
+ echo "ERROR: VERSION must contain \$Format placeholder for export-subst"
+ exit 1
+ )
diff --git a/.ci/woodpecker/01-build.yaml b/.ci/woodpecker/01-build.yaml
deleted file mode 100644
index f8109c94..00000000
--- a/.ci/woodpecker/01-build.yaml
+++ /dev/null
@@ -1,111 +0,0 @@
-matrix:
- IMAGE:
- - dstaesse/debian:o7s
- - dstaesse/ubuntu:o7s
- FLAGS:
- - ''
- - -m32
- COMPILER:
- - clang
- - gcc
- BUILD_TYPE:
- - Debug
- - Release
- DISABLE_FUSE:
- - TRUE
- - FALSE
- DISABLE_OPENSSL:
- - TRUE
- - FALSE
- DISABLE_LIBGCRYPT:
- - TRUE
- - FALSE
- SANITIZER:
- - DebugASan
- - DebugUSan
- - DebugLSan
-
-steps:
- - name: build
- image: ${IMAGE}
- pull: true
- when:
- branch: [testing, be]
- event: [push, pull_request]
- commands:
- - apt-get update -y
- - apt-get install bash clang -y
- - apt-get install git protobuf-c-compiler cmake -y
- - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
- - apt-get install libprotobuf-c-dev -y || true
- - mkdir build
- - cd build
- - CC=${COMPILER} cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DDISABLE_FUSE=${DISABLE_FUSE} \
- -DDISABLE_OPENSSL=${DISABLE_OPENSSL} -DDISABLE_LIBGCRYPT=${DISABLE_LIBGCRYPT}
- - make CFLAGS="${FLAGS}" -s -j2
- - env CTEST_OUTPUT_ON_FAILURE=1 make CFLAGS="${FLAGS}" -s check
- - cd ..
- - rm -rf build
-
- - name: sanitizers
- image: ${IMAGE}
- pull: true
- when:
- branch: [testing, be]
- event: [push, pull_request]
- commands:
- - apt-get update -y
- - apt-get install bash clang -y
- - apt-get install git protobuf-c-compiler cmake -y
- - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
- - apt-get install libprotobuf-c-dev -y || true
- - mkdir build
- - cd build
- - CC=${COMPILER} cmake .. -DCMAKE_BUILD_TYPE=${SANITIZER} -DDISABLE_FUSE=${DISABLE_FUSE} \
- -DDISABLE_OPENSSL=${DISABLE_OPENSSL} -DDISABLE_LIBGCRYPT=${DISABLE_LIBGCRYPT} \
- - make -s -j2
- - env CTEST_OUTPUT_ON_FAILURE=1 make -s check
- - cd ..
- - rm -rf build
-
- - name: build (manual)
- image: ${IMAGE}
- pull: true
- when:
- event: manual
- commands:
- - apt-get update -y
- - apt-get install bash clang -y
- - apt-get install git protobuf-c-compiler cmake -y
- - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
- - apt-get install libprotobuf-c-dev -y || true
- - mkdir build
- - cd build
- - CC=${COMPILER} cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DDISABLE_FUSE=${DISABLE_FUSE} \
- -DDISABLE_OPENSSL=${DISABLE_OPENSSL} -DDISABLE_LIBGCRYPT=${DISABLE_LIBGCRYPT}
- - make CFLAGS="${FLAGS}" -s -j2
- - env CTEST_OUTPUT_ON_FAILURE=1 make CFLAGS="${FLAGS}" -s check
- - cd ..
- - rm -rf build
-
- - name: sanitizers (manual)
- image: ${IMAGE}
- pull: true
- when:
- event: manual
- commands:
- - apt-get update -y
- - apt-get install bash clang -y
- - apt-get install git protobuf-c-compiler cmake -y
- - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
- - apt-get install libprotobuf-c-dev -y || true
- - mkdir build
- - cd build
- - CC=${COMPILER} cmake .. -DCMAKE_BUILD_TYPE=${SANITIZER} -DDISABLE_FUSE=${DISABLE_FUSE} \
- -DDISABLE_OPENSSL=${DISABLE_OPENSSL} -DDISABLE_LIBGCRYPT=${DISABLE_LIBGCRYPT} \
- - make -s -j2
- - env CTEST_OUTPUT_ON_FAILURE=1 make -s check
- - cd ..
- - rm -rf build
-
-
diff --git a/.ci/woodpecker/10-build.yaml b/.ci/woodpecker/10-build.yaml
new file mode 100644
index 00000000..31b9b9b4
--- /dev/null
+++ b/.ci/woodpecker/10-build.yaml
@@ -0,0 +1,93 @@
+depends_on:
+ - 00-check-version
+
+matrix:
+ IMAGE:
+ - dstaesse/debian:o7s
+ - dstaesse/ubuntu:o7s
+ COMPILER:
+ - clang
+ - gcc
+
+steps:
+ - name: build
+ image: ${IMAGE}
+ pull: true
+ when:
+ - branch: be
+ event: [push, pull_request]
+ - event: manual
+ commands:
+ - apt-get update -y
+ - apt-get install bash clang -y
+ - apt-get install git protobuf-c-compiler cmake -y
+ - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
+ - apt-get install libprotobuf-c-dev -y || true
+ - |
+ set -e
+
+ run_build() {
+ mkdir build && cd build
+ CC=${COMPILER} cmake .. "$@"
+ make CFLAGS="${CFLAGS_EXTRA}" -s -j2
+ env CTEST_OUTPUT_ON_FAILURE=1 \
+ make CFLAGS="${CFLAGS_EXTRA}" -s check
+ cd .. && rm -rf build
+ }
+
+ for build_type in Release Debug; do
+ for flags in '' -m32; do
+ echo "--- ${COMPILER} $build_type $flags ---"
+ CFLAGS_EXTRA="$flags"
+ run_build \
+ -DCMAKE_BUILD_TYPE=$build_type
+ done
+ done
+
+ CFLAGS_EXTRA=""
+
+ for flow_stats in TRUE FALSE; do
+ echo "--- IPCP_FLOW_STATS=$flow_stats ---"
+ run_build \
+ -DIPCP_FLOW_STATS=$flow_stats
+ done
+
+ for disable_fuse in TRUE FALSE; do
+ echo "--- DISABLE_FUSE=$disable_fuse ---"
+ run_build \
+ -DDISABLE_FUSE=$disable_fuse
+ done
+
+ for disable_cf in TRUE FALSE; do
+ for build_type in Release Debug; do
+ echo "--- DISABLE_CONFIGFILE=$disable_cf $build_type ---"
+ run_build \
+ -DCMAKE_BUILD_TYPE=$build_type \
+ -DDISABLE_CONFIGFILE=$disable_cf
+ done
+ done
+
+ for disable_ddns in TRUE FALSE; do
+ for build_type in Release Debug; do
+ echo "--- DISABLE_DDNS=$disable_ddns $build_type ---"
+ run_build \
+ -DCMAKE_BUILD_TYPE=$build_type \
+ -DDISABLE_DDNS=$disable_ddns
+ done
+ done
+
+ for disable_ssl in TRUE FALSE; do
+ for disable_gc in TRUE FALSE; do
+ for build_type in Release Debug; do
+ echo "--- OPENSSL=$disable_ssl GCRYPT=$disable_gc $build_type ---"
+ run_build \
+ -DCMAKE_BUILD_TYPE=$build_type \
+ -DDISABLE_OPENSSL=$disable_ssl \
+ -DDISABLE_LIBGCRYPT=$disable_gc
+ done
+ done
+ done
+
+ run_build
+
+
diff --git a/.ci/woodpecker/20-sanitizer.yaml b/.ci/woodpecker/20-sanitizer.yaml
new file mode 100644
index 00000000..b3d9f6af
--- /dev/null
+++ b/.ci/woodpecker/20-sanitizer.yaml
@@ -0,0 +1,40 @@
+depends_on:
+ - 00-check-version
+
+matrix:
+ IMAGE:
+ - dstaesse/debian:o7s
+ - dstaesse/ubuntu:o7s
+ COMPILER:
+ - clang
+ - gcc
+
+steps:
+ - name: sanitizers
+ image: ${IMAGE}
+ pull: true
+ when:
+ - branch: be
+ event: [push, pull_request]
+ - event: manual
+ commands:
+ - apt-get update -y
+ - apt-get install bash clang -y
+ - apt-get install git protobuf-c-compiler cmake -y
+ - apt-get install libgcrypt20-dev libssl-dev libfuse-dev dnsutils cmake-curses-gui -y
+ - apt-get install libprotobuf-c-dev -y || true
+ - |
+ set -e
+
+ for sanitizer in DebugASan DebugUSan DebugLSan; do
+ echo "--- ${COMPILER} $sanitizer ---"
+ mkdir build && cd build
+ CC=${COMPILER} cmake .. \
+ -DCMAKE_BUILD_TYPE=$sanitizer
+ make -s -j2
+ env CTEST_OUTPUT_ON_FAILURE=1 \
+ make -s check
+ cd .. && rm -rf build
+ done
+
+
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..5f72683f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+VERSION export-subst
diff --git a/.gitignore b/.gitignore
index 43f47a46..b10e8173 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
*~
*#
build/
+/tags
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b5632ad9..bfabd711 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,225 +1,74 @@
-cmake_minimum_required(VERSION 2.8.12.2...4.0.3.0)
-cmake_policy(VERSION ${CMAKE_VERSION})
+cmake_minimum_required(VERSION 3.19)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
project(ouroboros C)
-include(GNUInstallDirs)
-
-set(PACKAGE_VERSION_MAJOR 0)
-set(PACKAGE_VERSION_MINOR 21)
-set(PACKAGE_VERSION_PATCH 4)
-
-set(PACKAGE_NAME "${CMAKE_PROJECT_NAME}")
-set(PACKAGE_DESCRIPTION "The Ouroboros prototype")
-set(PACKAGE_URL "http://ouroboros.rocks")
-set(PACKAGE_BUGREPORT "http://ouroboros.rocks/bugzilla/")
-set(PACKAGE_VERSION
- "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}")
-
-if (NOT CMAKE_BUILD_TYPE)
- set(CMAKE_BUILD_TYPE "Release" CACHE STRING
- "Build type (Release, Debug, DebugASan, DebugTSan, DebugLSan, DebugUSan, DebugAnalyzer)" FORCE)
-endif()
-
-if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
- if (APPLE)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ if(APPLE)
set(CMAKE_INSTALL_PREFIX "/usr/local" CACHE STRING
"Installation Prefix" FORCE)
else()
set(CMAKE_INSTALL_PREFIX "/usr" CACHE STRING
"Installation Prefix" FORCE)
endif()
-endif ()
+endif()
-if (APPLE)
+include(GNUInstallDirs)
+
+include(utils/DebugTargets)
+
+include(version)
+include(package)
+
+include(compiler)
+
+if(APPLE)
set(CMAKE_MACOSX_RPATH 1)
- include_directories("/usr/local/include/")
+ # Homebrew installs to /usr/local/include on Intel, /opt/homebrew/include on ARM
+ set(APPLE_INCLUDE_DIRS "/usr/local/include" "/opt/homebrew/include"
+ CACHE INTERNAL "Apple system include directories")
endif()
-if (CMAKE_INSTALL_PREFIX STREQUAL "/usr")
+if(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
set(RPATH_PREFIX "")
-else ()
+else()
set(RPATH_PREFIX ${CMAKE_INSTALL_PREFIX})
-endif ()
+endif()
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES
- "${RPATH_PREFIX}/lib" isSystemDir)
-IF ("${isSystemDir}" STREQUAL "-1")
- set(CMAKE_INSTALL_RPATH "${RPATH_PREFIX}/lib")
-ENDIF ("${isSystemDir}" STREQUAL "-1")
-
-message(STATUS "Package name is: ${PACKAGE_NAME}")
-message(STATUS "Package description is: ${PACKAGE_DESCRIPTION}")
-message(STATUS "Package version is: ${PACKAGE_VERSION}")
-message(STATUS "Package URL is: ${PACKAGE_URL}")
-message(STATUS "Package bug-report address: ${PACKAGE_BUGREPORT}")
-message(STATUS "Package install prefix: ${CMAKE_INSTALL_PREFIX}")
-
-include(FindPkgConfig)
-
-include(CompilerUtils)
-test_and_set_c_compiler_flag_global(-std=c89)
-test_and_set_c_compiler_flag_global(-Wall)
-# -Wextra may fail on clobbered warning due to pthread_cleanup
-test_and_set_c_compiler_flag_global(-Wno-clobbered)
-test_and_set_c_compiler_flag_global(-Wextra)
-# explicitly add other flags in -Wextra
-test_and_set_c_compiler_flag_global(-Wempty-body)
-test_and_set_c_compiler_flag_global(-Wignored-qualifiers)
-test_and_set_c_compiler_flag_global(-Wimplicit-fallthrough=4)
-test_and_set_c_compiler_flag_global(-Wmissing-field-initializers)
-test_and_set_c_compiler_flag_global(-Wmissing-parameter-type)
-test_and_set_c_compiler_flag_global(-Wold-style-declaration)
-test_and_set_c_compiler_flag_global(-Woverride-init)
-test_and_set_c_compiler_flag_global(-Wsign-compare)
-test_and_set_c_compiler_flag_global(-Wtype-limits)
-test_and_set_c_compiler_flag_global(-Wuninitialized)
-test_and_set_c_compiler_flag_global(-Wshift-negative-value)
-test_and_set_c_compiler_flag_global(-Wunused-parameter)
-test_and_set_c_compiler_flag_global(-Wunused-but-set-parameter)
-test_and_set_c_compiler_flag_global(-Werror)
-test_and_set_c_compiler_flag_global(-Wundef)
-test_and_set_c_compiler_flag_global(-Wpointer-arith)
-test_and_set_c_compiler_flag_global(-Wstrict-prototypes)
-test_and_set_c_compiler_flag_global(-Wvla)
-test_and_set_c_compiler_flag_global(-Wswitch-default)
-test_and_set_c_compiler_flag_global(-Wreturn-type)
-test_and_set_c_compiler_flag_global(-Wunreachable-code)
-test_and_set_c_compiler_flag_global(-Wdeclaration-after-statement)
-test_and_set_c_compiler_flag_global(-Winfinite-recursion)
-test_and_set_c_compiler_flag_global(-fmax-errors=5)
-
-if (CMAKE_BUILD_TYPE STREQUAL "Release")
- test_and_set_c_compiler_flag_global(-O3)
-elseif (CMAKE_BUILD_TYPE STREQUAL "Debug")
- test_and_set_c_compiler_flag_global(-g)
-elseif (CMAKE_BUILD_TYPE STREQUAL "DebugASan")
- test_and_set_c_compiler_flag_global(-g)
- test_and_set_c_compiler_flag_global(-fsanitize=address)
-elseif (CMAKE_BUILD_TYPE STREQUAL "DebugTSan")
- test_and_set_c_compiler_flag_global(-g)
- test_and_set_c_compiler_flag_global(-fsanitize=thread)
-elseif (CMAKE_BUILD_TYPE STREQUAL "DebugLSan")
- test_and_set_c_compiler_flag_global(-g)
- test_and_set_c_compiler_flag_global(-fsanitize=leak)
-elseif (CMAKE_BUILD_TYPE STREQUAL "DebugUSan")
- test_and_set_c_compiler_flag_global(-g)
- test_and_set_c_compiler_flag_global(-fsanitize=undefined)
-elseif (CMAKE_BUILD_TYPE STREQUAL "DebugAnalyzer")
- test_and_set_c_compiler_flag_global(-g)
- test_and_set_c_compiler_flag_global(-fanalyzer)
-else ()
- message(FATAL_ERROR "Unkown build type ${CMAKE_BUILD_TYPE}")
-endif ()
-
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ouroboros-dev.pc.in"
- "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-dev.pc" @ONLY)
-
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ouroboros-irm.pc.in"
- "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-irm.pc" @ONLY)
-
-install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-dev.pc"
- DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
-
-install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-irm.pc"
- DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
-
-# modified from https://github.com/ximion/limba
-set(SYSTEMD_INSTALL_FILES "DETECT" CACHE STRING
- "Install systemd .service files (NO (never), DETECT (use pkg-config - default),\
- FORCE (always - see SYSTEMD_UNITDIR_OVERRIDE))")
-set(SYSTEMD_UNITDIR_OVERRIDE "" CACHE PATH
- "Path to install systemd files. When SYSTEMD_INSTALL_FILES == DETECT, this\
- can be empty to automatically determine the path. Cannot be empty when FORCE.")
-
-if (SYSTEMD_INSTALL_FILES STREQUAL "DETECT" OR SYSTEMD_INSTALL_FILES STREQUAL "FORCE")
- if (SYSTEMD_INSTALL_FILES STREQUAL "DETECT")
- pkg_check_modules(SYSTEMD "systemd")
- if (SYSTEMD_FOUND)
- if ("${SYSTEMD_UNITDIR_OVERRIDE}" STREQUAL "")
- execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
- --variable=systemdsystemunitdir systemd
- OUTPUT_VARIABLE SYSTEMD_UNITDIR_INTERNAL)
- string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_UNITDIR_INTERNAL
- "${SYSTEMD_UNITDIR_INTERNAL}"
- )
- else ()
- set(SYSTEMD_UNITDIR_INTERNAL "${SYSTEMD_UNITDIR_OVERRIDE}")
- endif ()
- else ()
- set(SYSTEMD_UNITDIR_INTERNAL "")
- endif ()
- elseif (SYSTEMD_INSTALL_FILES STREQUAL "FORCE")
- if ("${SYSTEMD_UNITDIR_OVERRIDE}" STREQUAL "")
- message(FATAL_ERROR "Systemd installation required by user, but no path\
- provided with SYSTEMD_UNITDIR_OVERRIDE.")
- else ()
- set(SYSTEMD_UNITDIR_INTERNAL "${SYSTEMD_UNITDIR_OVERRIDE}")
- endif ()
- endif()
- if (NOT ${SYSTEMD_UNITDIR_INTERNAL} STREQUAL "")
- message(STATUS "Systemd service installation enabled to: ${SYSTEMD_UNITDIR_INTERNAL}")
- if (LIBTOML_LIBRARIES AND NOT DISABLE_CONFIGFILE)
- set (CONFIGURE_STRING "--config ${OUROBOROS_CONFIG_DIR}${OUROBOROS_CONFIG_FILE}")
- else ()
- set (CONFIGURE_STRING "")
- endif ()
- configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ouroboros.service.in"
- "${CMAKE_CURRENT_BINARY_DIR}/ouroboros.service" @ONLY)
- install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ouroboros.service"
- DESTINATION "${SYSTEMD_UNITDIR_INTERNAL}")
- endif ()
-else ()
- message(STATUS "Systemd service installation disabled by user")
+ "${RPATH_PREFIX}/${CMAKE_INSTALL_LIBDIR}" isSystemDir)
+if(isSystemDir STREQUAL "-1")
+ set(CMAKE_INSTALL_RPATH "${RPATH_PREFIX}/${CMAKE_INSTALL_LIBDIR}")
endif()
-include(CTest) # Sets BUILD_TESTING by default to on.
-# BUILD_TESTS: control if tests are included by CMAKE for this project.
-if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
- set(BUILD_TESTS ON)
-else ()
- set(BUILD_TESTS OFF)
-endif()
-add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND})
-
-find_package(ProtobufC QUIET)
-if (NOT (PROTOBUF_C_INCLUDE_DIRS AND PROTOBUF_C_LIBRARY
- AND PROTOBUF_PROTOC_C_EXECUTABLE))
- message(FATAL_ERROR "Protobuf C compiler required but not found. "
- "Please install Google Protocol Buffers.")
-else ()
- message(STATUS "Found protobuf C compiler in ${PROTOBUF_PROTOC_C_EXECUTABLE}")
-endif ()
-include_directories(${PROTOBUF_C_INCLUDE_DIRS})
-
-add_subdirectory(include)
-add_subdirectory(src)
-add_subdirectory(doc)
-
-# Uninstall target
-configure_file("${CMAKE_SOURCE_DIR}/cmake/CmakeUninstall.cmake.in"
- "${CMAKE_BINARY_DIR}/cmake/CmakeUninstall.cmake" IMMEDIATE @ONLY)
-
-add_custom_target(uninstall
- COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/cmake/CmakeUninstall.cmake)
-
-set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PACKAGE_DESCRIPTION}")
-set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README")
-set(CPACK_PACKAGE_VERSION_MAJOR "${PACKAGE_VERSION_MAJOR}")
-set(CPACK_PACKAGE_VERSION_MINOR "${PACKAGE_VERSION_MINOR}")
-set(CPACK_PACKAGE_VERSION_PATCH "${PACKAGE_VERSION_PATCH}")
-set(CPACK_PACKAGE_INSTALL_DIRECTORY
- "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}")
-set(CPACK_GENERATOR "TGZ")
-set(CPACK_SOURCE_GENERATOR "TGZ")
-
-include(CPack)
-
-#include(FeatureSummary)
-#print_enabled_features()
+# Configuration options (must be loaded before dependencies)
+include(config/global)
+
+include(dependencies)
+
+include(config/lib)
+include(config/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)
+add_subdirectory(src/lib)
+add_subdirectory(src/irmd)
+add_subdirectory(src/ipcpd)
+add_subdirectory(src/tools)
+setup_coverage_target()
+include(doc)
+include(tags)
+
+include(install)
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..4c3f2e25
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+$Format:%(describe:tags=true,match=[0-9]*.[0-9]*.[0-9]*)$
diff --git a/cmake/AddCompileFlags.cmake b/cmake/AddCompileFlags.cmake
deleted file mode 100644
index 8f3877d9..00000000
--- a/cmake/AddCompileFlags.cmake
+++ /dev/null
@@ -1,17 +0,0 @@
-# - MACRO_ADD_COMPILE_FLAGS(<_target> "flags...")
-
-# Copyright (c) 2006, Oswald Buddenhagen, <ossi@kde.org>
-#
-# Redistribution and use is allowed according to the terms of the BSD license.
-
-macro(add_compile_flags _target _flg)
-
- get_target_property(_flags ${_target} COMPILE_FLAGS)
- if (_flags)
- set(_flags "${_flags} ${_flg}")
- else (_flags)
- set(_flags "${_flg}")
- endif (_flags)
- set_target_properties(${_target} PROPERTIES COMPILE_FLAGS "${_flags}")
-
-endmacro(add_compile_flags)
diff --git a/cmake/CmakeUninstall.cmake.in b/cmake/CmakeUninstall.cmake.in
deleted file mode 100644
index 985b31b2..00000000
--- a/cmake/CmakeUninstall.cmake.in
+++ /dev/null
@@ -1,29 +0,0 @@
-if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt")
- message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt")
-endif(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt")
-
-file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files)
-string(REGEX REPLACE "\n" ";" files "${files}")
-foreach(file ${files})
- message(STATUS "Uninstalling $ENV{DESTDIR}${file}")
- if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
- if(CMAKE_VERSION VERSION_LESS "3.28.0")
- exec_program(
- "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
- OUTPUT_VARIABLE rm_out
- RETURN_VALUE rm_retval
- )
- else()
- execute_process(
- COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}"
- RESULT_VARIABLE rm_out
- ERROR_VARIABLE rm_retval
- )
- endif ()
- if(NOT "${rm_retval}" STREQUAL "" AND NOT "${rm_retval}" STREQUAL 0)
- message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}")
- endif(NOT "${rm_retval}" STREQUAL "" AND NOT "${rm_retval}" STREQUAL 0)
- else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
- message(STATUS "File $ENV{DESTDIR}${file} does not exist.")
- endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
-endforeach(file)
diff --git a/cmake/OuroborosConfig.cmake.in b/cmake/OuroborosConfig.cmake.in
new file mode 100644
index 00000000..41ff7fdd
--- /dev/null
+++ b/cmake/OuroborosConfig.cmake.in
@@ -0,0 +1,23 @@
+# Ouroboros CMake Package Configuration
+#
+# This file allows other CMake projects to find and use Ouroboros libraries
+# via find_package(Ouroboros).
+#
+# Exported targets:
+# Ouroboros::dev - Development library (libouroboros-dev)
+# Ouroboros::irm - IRM management library (libouroboros-irm)
+#
+# Example usage:
+# find_package(Ouroboros REQUIRED)
+# target_link_libraries(myapp Ouroboros::dev)
+
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+
+# Required dependencies for Ouroboros libraries
+find_dependency(Threads)
+
+include("${CMAKE_CURRENT_LIST_DIR}/OuroborosTargets.cmake")
+
+check_required_components(Ouroboros)
diff --git a/cmake/compiler.cmake b/cmake/compiler.cmake
new file mode 100644
index 00000000..5d452e02
--- /dev/null
+++ b/cmake/compiler.cmake
@@ -0,0 +1,64 @@
+include(utils/CompilerUtils)
+
+test_and_set_c_compiler_flag_global(-std=c89)
+test_and_set_c_compiler_flag_global(-Wall)
+# -Wextra may fail on clobbered warning due to pthread_cleanup
+test_and_set_c_compiler_flag_global(-Wno-clobbered)
+test_and_set_c_compiler_flag_global(-Wextra)
+# explicitly add other flags in -Wextra
+test_and_set_c_compiler_flag_global(-Wempty-body)
+test_and_set_c_compiler_flag_global(-Wignored-qualifiers)
+test_and_set_c_compiler_flag_global(-Wimplicit-fallthrough=4)
+test_and_set_c_compiler_flag_global(-Wmissing-field-initializers)
+test_and_set_c_compiler_flag_global(-Wmissing-parameter-type)
+test_and_set_c_compiler_flag_global(-Wold-style-declaration)
+test_and_set_c_compiler_flag_global(-Woverride-init)
+test_and_set_c_compiler_flag_global(-Wsign-compare)
+test_and_set_c_compiler_flag_global(-Wtype-limits)
+test_and_set_c_compiler_flag_global(-Wuninitialized)
+test_and_set_c_compiler_flag_global(-Wshift-negative-value)
+test_and_set_c_compiler_flag_global(-Wunused-parameter)
+test_and_set_c_compiler_flag_global(-Wunused-but-set-parameter)
+test_and_set_c_compiler_flag_global(-Werror)
+test_and_set_c_compiler_flag_global(-Wundef)
+test_and_set_c_compiler_flag_global(-Wpointer-arith)
+test_and_set_c_compiler_flag_global(-Wstrict-prototypes)
+test_and_set_c_compiler_flag_global(-Wvla)
+test_and_set_c_compiler_flag_global(-Wswitch-default)
+test_and_set_c_compiler_flag_global(-Wreturn-type)
+test_and_set_c_compiler_flag_global(-Wunreachable-code)
+test_and_set_c_compiler_flag_global(-Wdeclaration-after-statement)
+test_and_set_c_compiler_flag_global(-Winfinite-recursion)
+test_and_set_c_compiler_flag_global(-fmax-errors=5)
+
+if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE "Release" CACHE STRING
+ "Build type (Release, Debug, DebugASan, DebugTSan, DebugLSan, DebugUSan, DebugAnalyzer)" FORCE)
+endif()
+
+if(CMAKE_BUILD_TYPE STREQUAL "Release")
+ test_and_set_c_compiler_flag_global(-O3)
+elseif(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ test_and_set_c_compiler_flag_global(-g)
+elseif(CMAKE_BUILD_TYPE STREQUAL "DebugASan")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(-fsanitize=address)
+ add_link_options(-fsanitize=address)
+elseif(CMAKE_BUILD_TYPE STREQUAL "DebugTSan")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(-fsanitize=thread)
+ add_link_options(-fsanitize=thread)
+elseif(CMAKE_BUILD_TYPE STREQUAL "DebugLSan")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(-fsanitize=leak)
+ add_link_options(-fsanitize=leak)
+elseif(CMAKE_BUILD_TYPE STREQUAL "DebugUSan")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(-fsanitize=undefined)
+ add_link_options(-fsanitize=undefined)
+elseif(CMAKE_BUILD_TYPE STREQUAL "DebugAnalyzer")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(-fanalyzer)
+else()
+ message(FATAL_ERROR "Unknown build type ${CMAKE_BUILD_TYPE}")
+endif()
diff --git a/cmake/config/global.cmake b/cmake/config/global.cmake
new file mode 100644
index 00000000..1e172724
--- /dev/null
+++ b/cmake/config/global.cmake
@@ -0,0 +1,47 @@
+# Global configuration options for Ouroboros
+# These options affect the entire framework
+
+# Installation directories
+set(OUROBOROS_CONFIG_DIR "${CMAKE_INSTALL_FULL_SYSCONFDIR}/ouroboros" CACHE PATH
+ "Configuration directory")
+
+# Security directories
+set(OUROBOROS_SECURITY_DIR "${OUROBOROS_CONFIG_DIR}/security" CACHE PATH
+ "Security directory holding authentication information")
+set(OUROBOROS_CA_CRT_DIR "${OUROBOROS_SECURITY_DIR}/cacert" CACHE PATH
+ "Directory holding trusted CA certificates")
+set(OUROBOROS_SRV_CRT_DIR "${OUROBOROS_SECURITY_DIR}/server" CACHE PATH
+ "Directory holding server certificates")
+set(OUROBOROS_CLI_CRT_DIR "${OUROBOROS_SECURITY_DIR}/client" CACHE PATH
+ "Directory holding client certificates")
+set(OUROBOROS_UNTRUSTED_DIR "${OUROBOROS_SECURITY_DIR}/untrusted" CACHE PATH
+ "Directory holding untrusted intermediate certificates")
+
+# Shared memory naming
+set(SHM_PREFIX "ouroboros" CACHE STRING
+ "String to prepend to POSIX shared memory filenames")
+set(SHM_LOCKFILE_NAME "/${SHM_PREFIX}.lockfile" CACHE INTERNAL
+ "Filename for the POSIX shared memory lockfile")
+
+# Secure memory configuration
+set(IRMD_SECMEM_MAX 1048576 CACHE STRING "IRMd secure heap 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
+ "Disable thread priority setting for container compatibility")
+set(DISABLE_CORE_LOCK TRUE CACHE BOOL
+ "Disable locking performance threads to a core")
+
+# IPC socket configuration
+set(SOCK_BUF_SIZE 10240 CACHE STRING
+ "Size of the buffer used by the UNIX sockets for local IPC")
diff --git a/cmake/config/ipcp/broadcast.cmake b/cmake/config/ipcp/broadcast.cmake
new file mode 100644
index 00000000..f521ed8e
--- /dev/null
+++ b/cmake/config/ipcp/broadcast.cmake
@@ -0,0 +1,9 @@
+# Broadcast IPCP configuration options for Ouroboros
+
+set(IPCP_BROADCAST_TARGET ipcpd-broadcast)
+
+set(IPCP_BROADCAST_MPL 100 CACHE STRING
+ "Default maximum packet lifetime for the Broadcast IPCP, in ms")
+
+set(IPCP_BROADCAST_MTU 1400 CACHE STRING
+ "Layer MTU advertised by the Broadcast IPCP, in bytes")
diff --git a/cmake/config/ipcp/common.cmake b/cmake/config/ipcp/common.cmake
new file mode 100644
index 00000000..7dbc252b
--- /dev/null
+++ b/cmake/config/ipcp/common.cmake
@@ -0,0 +1,58 @@
+# Common IPCP configuration options for Ouroboros
+# Options affecting all IPC Process types
+
+# Connection manager
+set(CONNMGR_RCV_TIMEOUT 1000 CACHE STRING
+ "Timeout for the connection manager to wait for OCEP info (ms).")
+
+# Debugging
+set(IPCP_DEBUG_LOCAL FALSE CACHE BOOL
+ "Use PID as address for local debugging")
+
+# QoS cube priorities (0-99, higher = more priority)
+set(IPCP_QOS_CUBE_BE_PRIO 50 CACHE STRING
+ "Priority for best effort QoS cube (0-99)")
+set(IPCP_QOS_CUBE_VIDEO_PRIO 90 CACHE STRING
+ "Priority for video QoS cube (0-99)")
+set(IPCP_QOS_CUBE_VOICE_PRIO 99 CACHE STRING
+ "Priority for voice QoS cube (0-99)")
+
+# Validate QoS cube priorities
+if((IPCP_QOS_CUBE_BE_PRIO LESS 0) OR (IPCP_QOS_CUBE_BE_PRIO GREATER 99))
+ message(FATAL_ERROR "Invalid priority for best effort QoS cube (must be 0-99)")
+endif()
+if((IPCP_QOS_CUBE_VIDEO_PRIO LESS 0) OR (IPCP_QOS_CUBE_VIDEO_PRIO GREATER 99))
+ message(FATAL_ERROR "Invalid priority for video QoS cube (must be 0-99)")
+endif()
+if((IPCP_QOS_CUBE_VOICE_PRIO LESS 0) OR (IPCP_QOS_CUBE_VOICE_PRIO GREATER 99))
+ message(FATAL_ERROR "Invalid priority for voice QoS cube (must be 0-99)")
+endif()
+
+# Threading
+set(IPCP_MIN_THREADS 4 CACHE STRING
+ "Minimum number of worker threads in the IPCP")
+set(IPCP_ADD_THREADS 4 CACHE STRING
+ "Number of extra threads to start when an IPCP faces thread starvation")
+set(IPCP_SCHED_THR_MUL 2 CACHE STRING
+ "Number of scheduler threads per QoS cube")
+
+# Linux-specific
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ set(IPCP_LINUX_TIMERSLACK_NS 100 CACHE STRING
+ "Slack value for high resolution timers on Linux systems.")
+endif()
+
+# Per-flow statistics exposed via the RIB (requires FUSE).
+if(HAVE_FUSE)
+ 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()
+
+ 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")
+ endif()
+endif()
diff --git a/cmake/config/ipcp/eth.cmake b/cmake/config/ipcp/eth.cmake
new file mode 100644
index 00000000..6a044d4b
--- /dev/null
+++ b/cmake/config/ipcp/eth.cmake
@@ -0,0 +1,22 @@
+# 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 4096 CACHE STRING
+ "Raw socket SO_SNDBUF in bytes (floored to one frame); 0 = kernel 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/local.cmake b/cmake/config/ipcp/local.cmake
new file mode 100644
index 00000000..70423cd1
--- /dev/null
+++ b/cmake/config/ipcp/local.cmake
@@ -0,0 +1,39 @@
+# Local IPCP configuration options for Ouroboros
+
+set(IPCP_LOCAL_TARGET ipcpd-local)
+
+set(IPCP_LOCAL_MPL 50 CACHE STRING
+ "Default maximum packet lifetime for the Local IPCP, in ms")
+
+set(IPCP_LOCAL_MTU 65000 CACHE STRING
+ "Layer MTU advertised by the Local IPCP, in bytes")
+
+set(IPCP_LOCAL_POLLING FALSE CACHE BOOL
+ "Enable active polling in the Local IPCP for low-latency mode")
+
+# IPCP_LOCAL_MTU must fit in the largest enabled GSPP and PUP class
+# (sender-side allocation: daemons use GSPP, apps use PUP). Reserve a
+# margin for sizeof(struct ssm_pk_buff) + HEADSPACE + TAILSPACE.
+math(EXPR _ssm_pk_overhead
+ "${SSM_PK_BUFF_HEADSPACE} + ${SSM_PK_BUFF_TAILSPACE} + 64")
+
+foreach(_pool GSPP PUP)
+ set(_largest 0)
+ foreach(_pair "256;256" "512;512" "1K;1024" "2K;2048" "4K;4096"
+ "16K;16384" "64K;65536" "256K;262144" "1M;1048576")
+ list(GET _pair 0 _name)
+ list(GET _pair 1 _bytes)
+ if(SSM_${_pool}_${_name}_BLOCKS GREATER 0
+ AND _bytes GREATER _largest)
+ set(_largest ${_bytes})
+ endif()
+ endforeach()
+ math(EXPR _avail "${_largest} - ${_ssm_pk_overhead}")
+ if(IPCP_LOCAL_MTU GREATER _avail)
+ message(FATAL_ERROR
+ "IPCP_LOCAL_MTU (${IPCP_LOCAL_MTU}) exceeds largest enabled "
+ "SSM_${_pool} class minus per-block overhead "
+ "(${_largest} - ${_ssm_pk_overhead} = ${_avail} bytes). "
+ "Lower IPCP_LOCAL_MTU or enable a larger SSM_${_pool}_*_BLOCKS.")
+ endif()
+endforeach()
diff --git a/cmake/config/ipcp/udp.cmake b/cmake/config/ipcp/udp.cmake
new file mode 100644
index 00000000..af84a844
--- /dev/null
+++ b/cmake/config/ipcp/udp.cmake
@@ -0,0 +1,16 @@
+# 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
new file mode 100644
index 00000000..92eeae65
--- /dev/null
+++ b/cmake/config/ipcp/unicast.cmake
@@ -0,0 +1,35 @@
+# Unicast IPCP configuration options for Ouroboros
+
+set(IPCP_UNICAST_TARGET ipcpd-unicast)
+
+set(IPCP_UNICAST_MPL 100 CACHE STRING
+ "Default maximum packet lifetime for the Unicast IPCP, in ms")
+set(IPCP_UNICAST_MTU 1400 CACHE STRING
+ "Layer MTU advertised by the Unicast IPCP, in bytes (TODO: derive per-flow from n-1 path MTU minus DT PCI)")
+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")
+set(DEBUG_PROTO_LS FALSE CACHE BOOL
+ "Add link state protocol debug logging")
diff --git a/cmake/config/irmd.cmake b/cmake/config/irmd.cmake
new file mode 100644
index 00000000..79e24bae
--- /dev/null
+++ b/cmake/config/irmd.cmake
@@ -0,0 +1,46 @@
+# IRMd configuration options for Ouroboros
+# Options affecting the IPC Resource Manager daemon
+
+# Timeouts (all in milliseconds unless noted)
+set(IRMD_REQ_ARR_TIMEOUT 1000 CACHE STRING
+ "Timeout for an application to respond to a new flow (ms)")
+set(BOOTSTRAP_TIMEOUT 5000 CACHE STRING
+ "Timeout for an IPCP to bootstrap (ms)")
+set(ENROLL_TIMEOUT 20000 CACHE STRING
+ "Timeout for an IPCP to enroll (ms)")
+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")
+set(CONNECT_TIMEOUT 20000 CACHE STRING
+ "Timeout to connect an IPCP to another IPCP (ms)")
+set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
+ "Timeout for a flow allocation response (ms)")
+
+# 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")
+
+# Threading
+set(IRMD_MIN_THREADS 8 CACHE STRING
+ "Minimum number of worker threads in the IRMd")
+set(IRMD_ADD_THREADS 8 CACHE STRING
+ "Number of extra threads to start when the IRMD faces thread starvation")
+
+# Direct IPC
+set(DISABLE_DIRECT_IPC FALSE CACHE BOOL
+ "Disable direct inter-process communication between local applications")
+
+# Process management
+set(IRMD_PKILL_TIMEOUT 30 CACHE STRING
+ "Number of seconds to wait before sending SIGKILL to subprocesses on exit")
+set(IRMD_KILL_ALL_PROCESSES TRUE CACHE BOOL
+ "Kill all processes on exit")
diff --git a/cmake/config/lib.cmake b/cmake/config/lib.cmake
new file mode 100644
index 00000000..81a7d6ba
--- /dev/null
+++ b/cmake/config/lib.cmake
@@ -0,0 +1,139 @@
+# 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_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()
+
+# 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/ssm.cmake b/cmake/config/ssm.cmake
new file mode 100644
index 00000000..589171ea
--- /dev/null
+++ b/cmake/config/ssm.cmake
@@ -0,0 +1,168 @@
+# 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
+ "Prefix for secure shared memory pools")
+
+# Pool naming (internal)
+set(SSM_GSPP_NAME "/${SSM_PREFIX}.gspp" CACHE INTERNAL
+ "Name for the Global Shared Packet Pool")
+set(SSM_PUP_NAME_FMT "/${SSM_PREFIX}.pup.%d" CACHE INTERNAL
+ "Format string for Per-User Pool names (uid as argument)")
+
+# Packet buffer configuration
+set(SSM_POOL_NAME "/${SHM_PREFIX}.pool" CACHE INTERNAL
+ "Name for the main POSIX shared memory pool")
+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")
+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_PREFIX "/${SHM_PREFIX}.rbuff." CACHE INTERNAL
+ "Prefix for rbuff POSIX shared memory filenames")
+set(SSM_FLOW_SET_PREFIX "/${SHM_PREFIX}.set." CACHE INTERNAL
+ "Prefix for the POSIX shared memory flow set")
+
+# 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)
+set(SSM_GSPP_256_BLOCKS 1024 CACHE STRING
+ "GSPP: Number of 256B blocks")
+set(SSM_GSPP_512_BLOCKS 2048 CACHE STRING
+ "GSPP: Number of 512B blocks")
+set(SSM_GSPP_1K_BLOCKS 512 CACHE STRING
+ "GSPP: Number of 1KB blocks")
+set(SSM_GSPP_2K_BLOCKS 384 CACHE STRING
+ "GSPP: Number of 2KB blocks")
+set(SSM_GSPP_4K_BLOCKS 256 CACHE STRING
+ "GSPP: Number of 4KB blocks")
+set(SSM_GSPP_16K_BLOCKS 128 CACHE STRING
+ "GSPP: Number of 16KB blocks")
+set(SSM_GSPP_64K_BLOCKS 64 CACHE STRING
+ "GSPP: Number of 64KB blocks")
+set(SSM_GSPP_256K_BLOCKS 32 CACHE STRING
+ "GSPP: Number of 256KB blocks")
+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)
+set(SSM_PUP_256_BLOCKS 512 CACHE STRING
+ "PUP: Number of 256B blocks")
+set(SSM_PUP_512_BLOCKS 512 CACHE STRING
+ "PUP: Number of 512B blocks")
+set(SSM_PUP_1K_BLOCKS 512 CACHE STRING
+ "PUP: Number of 1KB blocks")
+set(SSM_PUP_2K_BLOCKS 512 CACHE STRING
+ "PUP: Number of 2KB blocks")
+set(SSM_PUP_4K_BLOCKS 32 CACHE STRING
+ "PUP: Number of 4KB blocks")
+set(SSM_PUP_16K_BLOCKS 16 CACHE STRING
+ "PUP: Number of 16KB blocks")
+set(SSM_PUP_64K_BLOCKS 8 CACHE STRING
+ "PUP: Number of 64KB blocks")
+set(SSM_PUP_256K_BLOCKS 2 CACHE STRING
+ "PUP: Number of 256KB blocks")
+set(SSM_PUP_1M_BLOCKS 0 CACHE STRING
+ "PUP: Number of 1MB blocks")
+
+# Zero classes too small for spb header + HEADSPACE + TAILSPACE + 1 B.
+math(EXPR _SSM_MIN_USEFUL_CLASS
+ "32 + ${SSM_PK_BUFF_HEADSPACE} + ${SSM_PK_BUFF_TAILSPACE}")
+foreach(_pair "256:256" "512:512" "1K:1024" "2K:2048")
+ string(REPLACE ":" ";" _p "${_pair}")
+ list(GET _p 0 _suffix)
+ list(GET _p 1 _size)
+ if(_size LESS _SSM_MIN_USEFUL_CLASS)
+ set(SSM_GSPP_${_suffix}_BLOCKS 0)
+ set(SSM_PUP_${_suffix}_BLOCKS 0)
+ endif()
+endforeach()
+unset(_SSM_MIN_USEFUL_CLASS)
+unset(_p)
+unset(_suffix)
+unset(_size)
+
+# SSM pool size calculations
+include(utils/HumanReadable)
+
+math(EXPR SSM_GSPP_TOTAL_SIZE
+ "(1 << 8) * ${SSM_GSPP_256_BLOCKS} + \
+ (1 << 9) * ${SSM_GSPP_512_BLOCKS} + \
+ (1 << 10) * ${SSM_GSPP_1K_BLOCKS} + \
+ (1 << 11) * ${SSM_GSPP_2K_BLOCKS} + \
+ (1 << 12) * ${SSM_GSPP_4K_BLOCKS} + \
+ (1 << 14) * ${SSM_GSPP_16K_BLOCKS} + \
+ (1 << 16) * ${SSM_GSPP_64K_BLOCKS} + \
+ (1 << 18) * ${SSM_GSPP_256K_BLOCKS} + \
+ (1 << 20) * ${SSM_GSPP_1M_BLOCKS}")
+
+set(SSM_GSPP_TOTAL_SIZE ${SSM_GSPP_TOTAL_SIZE} CACHE INTERNAL
+ "GSPP total size in bytes")
+
+math(EXPR SSM_PUP_TOTAL_SIZE
+ "(1 << 8) * ${SSM_PUP_256_BLOCKS} + \
+ (1 << 9) * ${SSM_PUP_512_BLOCKS} + \
+ (1 << 10) * ${SSM_PUP_1K_BLOCKS} + \
+ (1 << 11) * ${SSM_PUP_2K_BLOCKS} + \
+ (1 << 12) * ${SSM_PUP_4K_BLOCKS} + \
+ (1 << 14) * ${SSM_PUP_16K_BLOCKS} + \
+ (1 << 16) * ${SSM_PUP_64K_BLOCKS} + \
+ (1 << 18) * ${SSM_PUP_256K_BLOCKS} + \
+ (1 << 20) * ${SSM_PUP_1M_BLOCKS}")
+
+set(SSM_PUP_TOTAL_SIZE ${SSM_PUP_TOTAL_SIZE} CACHE INTERNAL
+ "PUP total size in bytes")
+
+set(SSM_POOL_TOTAL_SIZE ${SSM_GSPP_TOTAL_SIZE} CACHE INTERNAL
+ "Total shared memory pool size in bytes")
+
+format_bytes_human_readable(${SSM_GSPP_TOTAL_SIZE} SSM_GSPP_SIZE_DISPLAY)
+format_bytes_human_readable(${SSM_PUP_TOTAL_SIZE} SSM_PUP_SIZE_DISPLAY)
+
+message(STATUS "Secure Shared Memory Pool Configuration:")
+message(STATUS " Pool prefix: ${SSM_PREFIX}")
+message(STATUS " Size classes: "
+ "256B, 512B, 1KiB, 2KiB, 4KiB, 16KiB, 64KiB, 256KiB, 1MiB")
+message(STATUS " Max allocation: 1 MB")
+message(STATUS " Shards per class: ${SSM_POOL_SHARDS}")
+message(STATUS " GSPP (privileged): ${SSM_GSPP_SIZE_DISPLAY} "
+ "(${SSM_GSPP_TOTAL_SIZE} bytes)")
+message(STATUS " Blocks: ${SSM_GSPP_256_BLOCKS}, ${SSM_GSPP_512_BLOCKS}, "
+ "${SSM_GSPP_1K_BLOCKS}, ${SSM_GSPP_2K_BLOCKS}, ${SSM_GSPP_4K_BLOCKS}, "
+ "${SSM_GSPP_16K_BLOCKS}, ${SSM_GSPP_64K_BLOCKS}, ${SSM_GSPP_256K_BLOCKS}, "
+ "${SSM_GSPP_1M_BLOCKS}")
+message(STATUS " PUP (unprivileged): ${SSM_PUP_SIZE_DISPLAY} "
+ "(${SSM_PUP_TOTAL_SIZE} bytes)")
+message(STATUS " Blocks: ${SSM_PUP_256_BLOCKS}, ${SSM_PUP_512_BLOCKS}, "
+ "${SSM_PUP_1K_BLOCKS}, ${SSM_PUP_2K_BLOCKS}, ${SSM_PUP_4K_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.
+foreach(_class 256 512 1K 2K)
+ if(SSM_PUP_${_class}_BLOCKS GREATER 0
+ AND NOT FRCT_REORDER_QUEUE_SIZE LESS SSM_PUP_${_class}_BLOCKS)
+ message(FATAL_ERROR
+ "FRCT_REORDER_QUEUE_SIZE (${FRCT_REORDER_QUEUE_SIZE}) must be "
+ "< SSM_PUP_${_class}_BLOCKS (${SSM_PUP_${_class}_BLOCKS}): "
+ "the FC window cannot exceed the pool that backs OOO stashing.")
+ endif()
+ if(SSM_GSPP_${_class}_BLOCKS GREATER 0
+ AND NOT FRCT_REORDER_QUEUE_SIZE LESS SSM_GSPP_${_class}_BLOCKS)
+ message(FATAL_ERROR
+ "FRCT_REORDER_QUEUE_SIZE (${FRCT_REORDER_QUEUE_SIZE}) must be "
+ "< SSM_GSPP_${_class}_BLOCKS (${SSM_GSPP_${_class}_BLOCKS}).")
+ endif()
+endforeach()
diff --git a/cmake/config/tests.cmake b/cmake/config/tests.cmake
new file mode 100644
index 00000000..37c06ca9
--- /dev/null
+++ b/cmake/config/tests.cmake
@@ -0,0 +1,17 @@
+# Test configuration options
+
+set(DISABLE_TESTS_LOGGING TRUE CACHE BOOL
+ "Disable Ouroboros log output in tests")
+if(DISABLE_TESTS_LOGGING)
+ message(STATUS "Ouroboros logging in test output disabled")
+else()
+ message(STATUS "Ouroboros logging in test output enabled")
+endif()
+
+set(DISABLE_TESTS_CORE_DUMPS TRUE CACHE BOOL
+ "Disable core dumps for tests")
+if(DISABLE_TESTS_CORE_DUMPS)
+ message(STATUS "Core dumps in tests disabled")
+else()
+ message(STATUS "Core dumps in tests enabled")
+endif()
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
new file mode 100644
index 00000000..ff44ad68
--- /dev/null
+++ b/cmake/dependencies.cmake
@@ -0,0 +1,44 @@
+find_package(PkgConfig QUIET)
+include(CheckSymbolExists)
+
+# System libraries and features
+include(dependencies/system/protobufc)
+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
+include(dependencies/crypt/openssl)
+include(dependencies/crypt/libgcrypt)
+
+# IRMd
+include(dependencies/irmd/libtoml)
+
+# Ethernet IPCP backends
+include(dependencies/eth/rawsockets)
+include(dependencies/eth/bpf)
+include(dependencies/eth/netmap)
+if(HAVE_RAW_SOCKETS OR HAVE_BPF OR HAVE_NETMAP)
+ set(HAVE_ETH TRUE CACHE INTERNAL "Ethernet IPCP support available")
+else()
+ unset(HAVE_ETH CACHE)
+endif()
+
+# UDP IPCP
+include(dependencies/udp/ddns)
+
+# Coverage tools
+include(dependencies/coverage/gcov)
+include(dependencies/coverage/lcov)
+
+# Validate that at least one secure random generator is available
+if(NOT ((CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") OR APPLE OR
+ HAVE_SYS_RANDOM OR HAVE_OPENSSL_RNG OR HAVE_LIBGCRYPT))
+ message(FATAL_ERROR "No secure random generator found, "
+ "please install libgcrypt (> 1.7.0) or OpenSSL")
+endif()
+
+
diff --git a/cmake/dependencies/coverage/gcov.cmake b/cmake/dependencies/coverage/gcov.cmake
new file mode 100644
index 00000000..1b593155
--- /dev/null
+++ b/cmake/dependencies/coverage/gcov.cmake
@@ -0,0 +1,21 @@
+include(utils/CompilerUtils)
+
+find_program(GCOV_PATH gcov)
+
+if(GCOV_PATH)
+ set(HAVE_GCOV TRUE CACHE INTERNAL "gcov coverage tool available")
+ set(DISABLE_COVERAGE ON CACHE BOOL "Disable code coverage analysis")
+ if(DISABLE_COVERAGE)
+ message(STATUS "gcov found - coverage analysis available (disabled by user)")
+ else()
+ message(STATUS "Code coverage analysis enabled")
+ test_and_set_c_compiler_flag_global(-g)
+ test_and_set_c_compiler_flag_global(--coverage)
+ add_link_options(--coverage)
+ endif()
+else()
+ set(HAVE_GCOV FALSE CACHE INTERNAL "gcov coverage tool available")
+ message(STATUS "gcov not found - coverage analysis not available")
+endif()
+
+mark_as_advanced(GCOV_PATH)
diff --git a/cmake/dependencies/coverage/lcov.cmake b/cmake/dependencies/coverage/lcov.cmake
new file mode 100644
index 00000000..65ed316e
--- /dev/null
+++ b/cmake/dependencies/coverage/lcov.cmake
@@ -0,0 +1,17 @@
+find_program(LCOV_PATH lcov)
+find_program(GENHTML_PATH genhtml)
+
+if(LCOV_PATH AND GENHTML_PATH)
+ set(HAVE_LCOV TRUE CACHE INTERNAL "lcov HTML coverage reports available")
+ message(STATUS "lcov and genhtml found - HTML coverage reports available")
+else()
+ set(HAVE_LCOV FALSE CACHE INTERNAL "lcov HTML coverage reports available")
+ if(NOT LCOV_PATH)
+ message(STATUS "lcov not found - HTML coverage reports not available")
+ endif()
+ if(NOT GENHTML_PATH)
+ message(STATUS "genhtml not found - HTML coverage reports not available")
+ endif()
+endif()
+
+mark_as_advanced(LCOV_PATH GENHTML_PATH)
diff --git a/cmake/dependencies/crypt/libgcrypt.cmake b/cmake/dependencies/crypt/libgcrypt.cmake
new file mode 100644
index 00000000..4f8a4cfe
--- /dev/null
+++ b/cmake/dependencies/crypt/libgcrypt.cmake
@@ -0,0 +1,55 @@
+# Try pkg-config first, fall back to find_library
+if(PkgConfig_FOUND)
+ pkg_check_modules(LIBGCRYPT QUIET IMPORTED_TARGET libgcrypt>=1.7.0)
+ if(LIBGCRYPT_FOUND AND NOT TARGET Gcrypt::Gcrypt)
+ add_library(Gcrypt::Gcrypt ALIAS PkgConfig::LIBGCRYPT)
+ endif()
+endif()
+
+if(NOT LIBGCRYPT_FOUND)
+ find_library(LIBGCRYPT_LIBRARIES gcrypt QUIET)
+ if(LIBGCRYPT_LIBRARIES)
+ find_path(LIBGCRYPT_INCLUDE_DIR gcrypt.h
+ HINTS /usr/include /usr/local/include)
+ if(LIBGCRYPT_INCLUDE_DIR)
+ file(STRINGS ${LIBGCRYPT_INCLUDE_DIR}/gcrypt.h GCSTR
+ REGEX "^#define GCRYPT_VERSION ")
+ string(REGEX REPLACE "^#define GCRYPT_VERSION \"(.*)\".*$" "\\1"
+ LIBGCRYPT_VERSION "${GCSTR}")
+ if(NOT LIBGCRYPT_VERSION VERSION_LESS "1.7.0")
+ set(LIBGCRYPT_FOUND TRUE)
+ if(NOT TARGET Gcrypt::Gcrypt)
+ add_library(Gcrypt::Gcrypt UNKNOWN IMPORTED)
+ set_target_properties(Gcrypt::Gcrypt PROPERTIES
+ IMPORTED_LOCATION "${LIBGCRYPT_LIBRARIES}"
+ INTERFACE_INCLUDE_DIRECTORIES "${LIBGCRYPT_INCLUDE_DIR}")
+ endif()
+ endif()
+ endif()
+ endif()
+endif()
+
+if(LIBGCRYPT_FOUND)
+ set(DISABLE_LIBGCRYPT FALSE CACHE BOOL "Disable libgcrypt support")
+ if(NOT DISABLE_LIBGCRYPT)
+ if(LIBGCRYPT_VERSION)
+ message(STATUS "libgcrypt support enabled (version ${LIBGCRYPT_VERSION})")
+ else()
+ message(STATUS "libgcrypt support enabled")
+ endif()
+ set(HAVE_LIBGCRYPT TRUE CACHE INTERNAL "libgcrypt cryptography support available")
+ else()
+ message(STATUS "libgcrypt support disabled by user")
+ unset(HAVE_LIBGCRYPT CACHE)
+ endif()
+else()
+ message(STATUS "Install libgcrypt >= 1.7.0 to enable libgcrypt support")
+ unset(HAVE_LIBGCRYPT CACHE)
+endif()
+
+if(NOT HAVE_LIBGCRYPT)
+ set(LIBGCRYPT_LIBRARIES "")
+ set(LIBGCRYPT_INCLUDE_DIR "")
+endif()
+
+mark_as_advanced(LIBGCRYPT_LIBRARIES LIBGCRYPT_INCLUDE_DIR)
diff --git a/cmake/dependencies/crypt/openssl.cmake b/cmake/dependencies/crypt/openssl.cmake
new file mode 100644
index 00000000..b0fa8d11
--- /dev/null
+++ b/cmake/dependencies/crypt/openssl.cmake
@@ -0,0 +1,56 @@
+find_package(OpenSSL QUIET)
+if(NOT OPENSSL_FOUND)
+ message(STATUS "Install OpenSSL version >= 3.0.0 to enable OpenSSL support")
+ unset(HAVE_OPENSSL_RNG)
+ unset(HAVE_OPENSSL CACHE)
+ return()
+endif()
+
+set(HAVE_OPENSSL_RNG TRUE)
+
+if(OPENSSL_VERSION VERSION_LESS "3.0.0")
+ message(STATUS "Install version >= 3.0.0 to enable OpenSSL support "
+ "(found version \"${OPENSSL_VERSION}\")")
+ return()
+endif()
+
+set(DISABLE_OPENSSL FALSE CACHE BOOL "Disable OpenSSL support")
+if(DISABLE_OPENSSL)
+ message(STATUS "OpenSSL support disabled")
+ unset(HAVE_OPENSSL CACHE)
+ return()
+endif()
+
+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.5.0")
+ 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_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_SLH CACHE)
+ endif()
+else()
+ message(STATUS
+ "Install OpenSSL >= 3.5.0 for PQC (ML-KEM/ML-DSA/SLH-DSA)")
+ unset(HAVE_ML CACHE)
+ unset(HAVE_SLH CACHE)
+endif()
+
+# Secure memory options are in cmake/config/global.cmake
diff --git a/cmake/dependencies/eth/bpf.cmake b/cmake/dependencies/eth/bpf.cmake
new file mode 100644
index 00000000..c2f69e79
--- /dev/null
+++ b/cmake/dependencies/eth/bpf.cmake
@@ -0,0 +1,20 @@
+# Berkeley Packet Filter support (BSD/macOS only)
+if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ find_path(BPF_C_INCLUDE_DIR
+ net/bpf.h
+ HINTS /usr/include /usr/local/include)
+
+ mark_as_advanced(BPF_C_INCLUDE_DIR)
+
+ if(BPF_C_INCLUDE_DIR)
+ set(DISABLE_BPF FALSE CACHE BOOL
+ "Disable Berkeley Packet Filter support for Ethernet IPCPs")
+ if(NOT DISABLE_BPF)
+ message(STATUS "Berkeley Packet Filter support for Ethernet IPCPs enabled")
+ set(HAVE_BPF TRUE)
+ else()
+ message(STATUS "Berkeley Packet Filter support for Ethernet IPCPs disabled by user")
+ unset(HAVE_BPF)
+ endif()
+ endif()
+endif()
diff --git a/cmake/dependencies/eth/netmap.cmake b/cmake/dependencies/eth/netmap.cmake
new file mode 100644
index 00000000..94ecd634
--- /dev/null
+++ b/cmake/dependencies/eth/netmap.cmake
@@ -0,0 +1,18 @@
+# netmap support (optional acceleration)
+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)
+ endif()
+endif()
diff --git a/cmake/dependencies/eth/rawsockets.cmake b/cmake/dependencies/eth/rawsockets.cmake
new file mode 100644
index 00000000..395d9efb
--- /dev/null
+++ b/cmake/dependencies/eth/rawsockets.cmake
@@ -0,0 +1,12 @@
+# Raw sockets support (Linux only)
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ set(DISABLE_RAW_SOCKETS FALSE CACHE BOOL
+ "Disable raw socket support for Ethernet IPCPs")
+ if(NOT DISABLE_RAW_SOCKETS)
+ message(STATUS "Raw socket support for Ethernet IPCPs enabled")
+ set(HAVE_RAW_SOCKETS TRUE)
+ else()
+ message(STATUS "Raw socket support for Ethernet IPCPs disabled by user")
+ unset(HAVE_RAW_SOCKETS)
+ endif()
+endif()
diff --git a/cmake/dependencies/irmd/libtoml.cmake b/cmake/dependencies/irmd/libtoml.cmake
new file mode 100644
index 00000000..dcbc17e4
--- /dev/null
+++ b/cmake/dependencies/irmd/libtoml.cmake
@@ -0,0 +1,29 @@
+find_library(LIBTOML_LIBRARY toml QUIET)
+if(LIBTOML_LIBRARY)
+ find_path(LIBTOML_INCLUDE_DIR toml.h)
+ set(DISABLE_CONFIGFILE FALSE CACHE BOOL
+ "Disable configuration file support")
+ if(NOT DISABLE_CONFIGFILE)
+ set(OUROBOROS_CONFIG_FILE irmd.conf CACHE STRING
+ "Name of the IRMd configuration file")
+ set(HAVE_TOML TRUE CACHE INTERNAL "TOML configuration file support available")
+ message(STATUS "Configuration file support enabled")
+ message(STATUS "Configuration directory: ${OUROBOROS_CONFIG_DIR}")
+ # Create imported target for consistency with other dependencies
+ if(NOT TARGET toml::toml)
+ add_library(toml::toml UNKNOWN IMPORTED)
+ set_target_properties(toml::toml PROPERTIES
+ IMPORTED_LOCATION "${LIBTOML_LIBRARY}"
+ INTERFACE_INCLUDE_DIRECTORIES "${LIBTOML_INCLUDE_DIR}")
+ endif()
+ else()
+ message(STATUS "Configuration file support disabled by user")
+ unset(OUROBOROS_CONFIG_FILE CACHE)
+ unset(HAVE_TOML CACHE)
+ endif()
+ mark_as_advanced(LIBTOML_LIBRARY LIBTOML_INCLUDE_DIR)
+else()
+ message(STATUS "Install tomlc99 for config file support")
+ message(STATUS " https://github.com/cktan/tomlc99")
+ unset(HAVE_TOML CACHE)
+endif()
diff --git a/cmake/dependencies/system/explicit_bzero.cmake b/cmake/dependencies/system/explicit_bzero.cmake
new file mode 100644
index 00000000..89ab3abc
--- /dev/null
+++ b/cmake/dependencies/system/explicit_bzero.cmake
@@ -0,0 +1,4 @@
+# Check for explicit_bzero in string.h
+# glibc requires _DEFAULT_SOURCE to expose it; harmless on other platforms
+list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_DEFAULT_SOURCE)
+check_symbol_exists(explicit_bzero "string.h" HAVE_EXPLICIT_BZERO)
diff --git a/cmake/dependencies/system/fuse.cmake b/cmake/dependencies/system/fuse.cmake
new file mode 100644
index 00000000..007d82fd
--- /dev/null
+++ b/cmake/dependencies/system/fuse.cmake
@@ -0,0 +1,46 @@
+# Try pkg-config first, fall back to find_library
+# Use FUSE_PKG prefix to avoid collision with FUSE_PREFIX
+if(PkgConfig_FOUND)
+ pkg_check_modules(FUSE_PKG QUIET IMPORTED_TARGET fuse>=2.6)
+ if(FUSE_PKG_FOUND AND NOT TARGET Fuse::Fuse)
+ add_library(Fuse::Fuse ALIAS PkgConfig::FUSE_PKG)
+ endif()
+endif()
+
+if(NOT FUSE_PKG_FOUND)
+ find_library(FUSE_PKG_LIBRARIES fuse QUIET)
+ if(FUSE_PKG_LIBRARIES)
+ set(FUSE_PKG_FOUND TRUE)
+ if(NOT TARGET Fuse::Fuse)
+ add_library(Fuse::Fuse UNKNOWN IMPORTED)
+ set_target_properties(Fuse::Fuse PROPERTIES
+ IMPORTED_LOCATION "${FUSE_PKG_LIBRARIES}")
+ endif()
+ endif()
+endif()
+
+if(FUSE_PKG_FOUND)
+ set(DISABLE_FUSE FALSE CACHE BOOL "Disable FUSE support")
+ if(NOT DISABLE_FUSE)
+ if(FUSE_PKG_VERSION)
+ message(STATUS "FUSE support enabled (version ${FUSE_PKG_VERSION})")
+ else()
+ message(STATUS "FUSE support enabled")
+ endif()
+ set(HAVE_FUSE TRUE CACHE INTERNAL "FUSE filesystem support available")
+ set(FUSE_PREFIX "/tmp/ouroboros" CACHE STRING
+ "Mountpoint for RIB filesystem")
+ else()
+ message(STATUS "FUSE support disabled by user")
+ unset(HAVE_FUSE CACHE)
+ endif()
+else()
+ message(STATUS "Install FUSE version >= 2.6 to enable RIB access")
+ unset(HAVE_FUSE CACHE)
+endif()
+
+if(NOT HAVE_FUSE)
+ set(FUSE_PKG_LIBRARIES "")
+endif()
+
+mark_as_advanced(FUSE_PKG_LIBRARIES)
diff --git a/cmake/dependencies/system/libraries.cmake b/cmake/dependencies/system/libraries.cmake
new file mode 100644
index 00000000..55c22d6a
--- /dev/null
+++ b/cmake/dependencies/system/libraries.cmake
@@ -0,0 +1,17 @@
+if(NOT APPLE)
+ find_library(LIBRT_LIBRARIES rt)
+ mark_as_advanced(LIBRT_LIBRARIES)
+ if(NOT LIBRT_LIBRARIES)
+ message(FATAL_ERROR "Could not find librt")
+ endif()
+else()
+ set(LIBRT_LIBRARIES "")
+endif()
+
+find_package(Threads REQUIRED)
+
+find_library(LIBM_LIBRARIES m)
+mark_as_advanced(LIBM_LIBRARIES)
+if(NOT LIBM_LIBRARIES)
+ message(FATAL_ERROR "Could not find libm")
+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/system/protobufc.cmake b/cmake/dependencies/system/protobufc.cmake
new file mode 100644
index 00000000..b7e0062a
--- /dev/null
+++ b/cmake/dependencies/system/protobufc.cmake
@@ -0,0 +1,13 @@
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/utils")
+
+find_package(ProtobufC QUIET)
+if(NOT (PROTOBUF_C_INCLUDE_DIRS AND PROTOBUF_C_LIBRARY
+ AND PROTOBUF_PROTOC_C_EXECUTABLE))
+ message(FATAL_ERROR "Protobuf C compiler required but not found. "
+ "Please install Google Protocol Buffers.")
+else()
+ message(STATUS "Found protobuf C compiler in ${PROTOBUF_PROTOC_C_EXECUTABLE}")
+endif()
+
+# Note: Include dirs are added per-target via target_include_directories
+# using ${PROTOBUF_C_INCLUDE_DIRS}
diff --git a/cmake/dependencies/system/robustmutex.cmake b/cmake/dependencies/system/robustmutex.cmake
new file mode 100644
index 00000000..89b7325b
--- /dev/null
+++ b/cmake/dependencies/system/robustmutex.cmake
@@ -0,0 +1,18 @@
+list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_POSIX_C_SOURCE=200809L)
+list(APPEND CMAKE_REQUIRED_DEFINITIONS -D__XSI_VISIBLE=500)
+list(APPEND CMAKE_REQUIRED_LIBRARIES pthread)
+check_symbol_exists(pthread_mutexattr_setrobust pthread.h HAVE_ROBUST_MUTEX)
+
+if(HAVE_ROBUST_MUTEX)
+ set(DISABLE_ROBUST_MUTEXES FALSE CACHE BOOL "Disable robust mutex support")
+ if(NOT DISABLE_ROBUST_MUTEXES)
+ message(STATUS "Robust mutex support enabled")
+ set(HAVE_ROBUST_MUTEX TRUE)
+ else()
+ message(STATUS "Robust mutex support disabled by user")
+ unset(HAVE_ROBUST_MUTEX)
+ endif()
+else()
+ message(STATUS "Robust mutex support not available")
+ unset(HAVE_ROBUST_MUTEX)
+endif()
diff --git a/cmake/dependencies/system/sysrandom.cmake b/cmake/dependencies/system/sysrandom.cmake
new file mode 100644
index 00000000..dc8443a1
--- /dev/null
+++ b/cmake/dependencies/system/sysrandom.cmake
@@ -0,0 +1,14 @@
+if(APPLE OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+ set(SYS_RND_HDR "")
+else()
+ find_path(SYS_RND_HDR sys/random.h PATH /usr/include/ /usr/local/include/)
+ if(SYS_RND_HDR)
+ message(STATUS "Found sys/random.h in ${SYS_RND_HDR}")
+ set(HAVE_SYS_RANDOM TRUE)
+ else()
+ set(SYS_RND_HDR "")
+ unset(HAVE_SYS_RANDOM)
+ endif()
+endif()
+
+mark_as_advanced(SYS_RND_HDR)
diff --git a/cmake/dependencies/udp/ddns.cmake b/cmake/dependencies/udp/ddns.cmake
new file mode 100644
index 00000000..e8208e47
--- /dev/null
+++ b/cmake/dependencies/udp/ddns.cmake
@@ -0,0 +1,31 @@
+# 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/CMakeLists.txt b/cmake/doc.cmake
index add68d62..f75cde5b 100644
--- a/doc/man/CMakeLists.txt
+++ b/cmake/doc.cmake
@@ -1,3 +1,6 @@
+set(DOC_SOURCE_DIR "${CMAKE_SOURCE_DIR}/doc")
+set(DOC_BINARY_DIR "${CMAKE_BINARY_DIR}/doc")
+
set(MAN_NAMES
# Add man page sources here
flow_accept.3
@@ -25,27 +28,22 @@ set(MAN_NAMES
irm.8
)
-macro(INSTALL_MAN __mans)
- foreach (_man ${ARGV})
- string(REGEX REPLACE "^.+[.]([1-9]).gz" "\\1" _mansect ${_man})
- install(FILES ${_man} DESTINATION "${CMAKE_INSTALL_MANDIR}/man${_mansect}")
- endforeach (_man)
-endmacro(INSTALL_MAN __mans)
-
find_program(GZIP_EXECUTABLE
NAMES gzip
DOC "Will gzip the man pages")
-
mark_as_advanced(GZIP_EXECUTABLE)
-if (GZIP_EXECUTABLE)
+if(GZIP_EXECUTABLE)
+ # Create the doc output directory
+ file(MAKE_DIRECTORY ${DOC_BINARY_DIR})
+
foreach (m ${MAN_NAMES})
- set(md ${CMAKE_CURRENT_BINARY_DIR}/${m}.gz)
+ set(md ${DOC_BINARY_DIR}/${m}.gz)
add_custom_command(
OUTPUT ${md}
COMMAND ${GZIP_EXECUTABLE}
- ARGS -c ${CMAKE_CURRENT_SOURCE_DIR}/${m} > ${md}
+ ARGS -c ${DOC_SOURCE_DIR}/man/${m} > ${md}
COMMENT "Compressing manpage ${m}"
VERBATIM)
@@ -53,6 +51,4 @@ if (GZIP_EXECUTABLE)
endforeach ()
add_custom_target(man ALL DEPENDS ${MAN_FILES})
-
- INSTALL_MAN(${MAN_FILES})
-endif ()
+endif()
diff --git a/cmake/include.cmake b/cmake/include.cmake
new file mode 100644
index 00000000..414ea0ad
--- /dev/null
+++ b/cmake/include.cmake
@@ -0,0 +1,21 @@
+set(HEADERS_SOURCE_DIR "${CMAKE_SOURCE_DIR}/include/ouroboros")
+
+# SOCK_BUF_SIZE is in cmake/config/global.cmake
+
+configure_file("${CMAKE_SOURCE_DIR}/include/ouroboros/sockets.h.in"
+ "${CMAKE_BINARY_DIR}/include/ouroboros/sockets.h" @ONLY)
+
+set(PUBLIC_HEADER_FILES
+ ${HEADERS_SOURCE_DIR}/cep.h
+ ${HEADERS_SOURCE_DIR}/cdefs.h
+ ${HEADERS_SOURCE_DIR}/dev.h
+ ${HEADERS_SOURCE_DIR}/errno.h
+ ${HEADERS_SOURCE_DIR}/fccntl.h
+ ${HEADERS_SOURCE_DIR}/fqueue.h
+ ${HEADERS_SOURCE_DIR}/ipcp.h
+ ${HEADERS_SOURCE_DIR}/irm.h
+ ${HEADERS_SOURCE_DIR}/name.h
+ ${HEADERS_SOURCE_DIR}/proto.h
+ ${HEADERS_SOURCE_DIR}/qos.h
+ ${CMAKE_BINARY_DIR}/include/ouroboros/version.h
+)
diff --git a/cmake/install.cmake b/cmake/install.cmake
new file mode 100644
index 00000000..79714df2
--- /dev/null
+++ b/cmake/install.cmake
@@ -0,0 +1,101 @@
+# Installation configuration
+
+include(CMakePackageConfigHelpers)
+
+# Public headers
+install(FILES ${PUBLIC_HEADER_FILES}
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ouroboros)
+
+# Man pages
+if(GZIP_EXECUTABLE)
+ foreach(_man ${MAN_FILES})
+ string(REGEX REPLACE "^.+[.]([1-9]).gz" "\\1" _mansect ${_man})
+ install(FILES ${_man} DESTINATION "${CMAKE_INSTALL_MANDIR}/man${_mansect}")
+ endforeach()
+endif()
+
+# pkg-config files
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-dev.pc"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
+
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ouroboros-irm.pc"
+ DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
+
+set(OUROBOROS_CMAKE_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/Ouroboros")
+
+install(EXPORT OuroborosTargets
+ FILE OuroborosTargets.cmake
+ NAMESPACE Ouroboros::
+ DESTINATION ${OUROBOROS_CMAKE_DIR})
+
+configure_package_config_file(
+ "${CMAKE_SOURCE_DIR}/cmake/OuroborosConfig.cmake.in"
+ "${CMAKE_BINARY_DIR}/OuroborosConfig.cmake"
+ INSTALL_DESTINATION ${OUROBOROS_CMAKE_DIR})
+
+write_basic_package_version_file(
+ "${CMAKE_BINARY_DIR}/OuroborosConfigVersion.cmake"
+ VERSION ${PACKAGE_VERSION}
+ COMPATIBILITY SameMajorVersion)
+
+install(FILES
+ "${CMAKE_BINARY_DIR}/OuroborosConfig.cmake"
+ "${CMAKE_BINARY_DIR}/OuroborosConfigVersion.cmake"
+ DESTINATION ${OUROBOROS_CMAKE_DIR})
+
+# Systemd service file installation
+set(SYSTEMD_INSTALL_FILES "DETECT" CACHE STRING
+ "Install systemd .service files (NO (never), DETECT (use pkg-config - default),\
+ FORCE (always - see SYSTEMD_UNITDIR_OVERRIDE))")
+set(SYSTEMD_UNITDIR_OVERRIDE "" CACHE PATH
+ "Path to install systemd files. When SYSTEMD_INSTALL_FILES == DETECT, this\
+ can be empty to automatically determine the path. Cannot be empty when FORCE.")
+
+if(SYSTEMD_INSTALL_FILES STREQUAL "DETECT" OR SYSTEMD_INSTALL_FILES STREQUAL "FORCE")
+ if(SYSTEMD_INSTALL_FILES STREQUAL "DETECT")
+ if(PkgConfig_FOUND)
+ pkg_check_modules(SYSTEMD "systemd")
+ endif()
+ if(SYSTEMD_FOUND)
+ if(SYSTEMD_UNITDIR_OVERRIDE STREQUAL "")
+ execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
+ --variable=systemdsystemunitdir systemd
+ OUTPUT_VARIABLE SYSTEMD_UNITDIR_INTERNAL)
+ string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_UNITDIR_INTERNAL
+ "${SYSTEMD_UNITDIR_INTERNAL}"
+ )
+ else()
+ set(SYSTEMD_UNITDIR_INTERNAL "${SYSTEMD_UNITDIR_OVERRIDE}")
+ endif()
+ else()
+ set(SYSTEMD_UNITDIR_INTERNAL "")
+ endif()
+ elseif(SYSTEMD_INSTALL_FILES STREQUAL "FORCE")
+ if(SYSTEMD_UNITDIR_OVERRIDE STREQUAL "")
+ message(FATAL_ERROR "Systemd installation required by user, but no path\
+ provided with SYSTEMD_UNITDIR_OVERRIDE.")
+ else()
+ set(SYSTEMD_UNITDIR_INTERNAL "${SYSTEMD_UNITDIR_OVERRIDE}")
+ endif()
+ endif()
+ if(NOT SYSTEMD_UNITDIR_INTERNAL STREQUAL "")
+ message(STATUS "Systemd service installation enabled to: ${SYSTEMD_UNITDIR_INTERNAL}")
+ if(LIBTOML_LIBRARIES AND NOT DISABLE_CONFIGFILE)
+ set (CONFIGURE_STRING "--config ${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}")
+ else()
+ set (CONFIGURE_STRING "")
+ endif()
+ configure_file("${CMAKE_SOURCE_DIR}/ouroboros.service.in"
+ "${CMAKE_BINARY_DIR}/ouroboros.service" @ONLY)
+ install(FILES "${CMAKE_BINARY_DIR}/ouroboros.service"
+ DESTINATION "${SYSTEMD_UNITDIR_INTERNAL}")
+ endif()
+else()
+ message(STATUS "Systemd service installation disabled by user")
+endif()
+
+configure_file("${CMAKE_SOURCE_DIR}/cmake/utils/CMakeUninstall.cmake.in"
+ "${CMAKE_BINARY_DIR}/cmake/cmakeuninstall.cmake" @ONLY)
+
+add_custom_target(uninstall
+ COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/cmake/cmakeuninstall.cmake)
diff --git a/cmake/package.cmake b/cmake/package.cmake
new file mode 100644
index 00000000..71105ee8
--- /dev/null
+++ b/cmake/package.cmake
@@ -0,0 +1,29 @@
+set(PACKAGE_NAME "${CMAKE_PROJECT_NAME}")
+set(PACKAGE_DESCRIPTION "The Ouroboros prototype")
+set(PACKAGE_URL "http://ouroboros.rocks")
+set(PACKAGE_BUGREPORT "http://ouroboros.rocks/bugzilla/")
+
+message(STATUS "Package name is: ${PACKAGE_NAME}")
+message(STATUS "Package description is: ${PACKAGE_DESCRIPTION}")
+message(STATUS "Package version is: ${PACKAGE_VERSION}")
+message(STATUS "Package URL is: ${PACKAGE_URL}")
+message(STATUS "Package bug-report address: ${PACKAGE_BUGREPORT}")
+message(STATUS "Package install prefix: ${CMAKE_INSTALL_PREFIX}")
+
+configure_file("${CMAKE_SOURCE_DIR}/ouroboros-dev.pc.in"
+ "${CMAKE_BINARY_DIR}/ouroboros-dev.pc" @ONLY)
+
+configure_file("${CMAKE_SOURCE_DIR}/ouroboros-irm.pc.in"
+ "${CMAKE_BINARY_DIR}/ouroboros-irm.pc" @ONLY)
+
+set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PACKAGE_DESCRIPTION}")
+set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README")
+set(CPACK_PACKAGE_VERSION_MAJOR "${PACKAGE_VERSION_MAJOR}")
+set(CPACK_PACKAGE_VERSION_MINOR "${PACKAGE_VERSION_MINOR}")
+set(CPACK_PACKAGE_VERSION_PATCH "${PACKAGE_VERSION_PATCH}")
+set(CPACK_PACKAGE_INSTALL_DIRECTORY
+ "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}")
+set(CPACK_GENERATOR "TGZ")
+set(CPACK_SOURCE_GENERATOR "TGZ")
+
+include(CPack)
diff --git a/cmake/tags.cmake b/cmake/tags.cmake
new file mode 100644
index 00000000..00e6f0d6
--- /dev/null
+++ b/cmake/tags.cmake
@@ -0,0 +1,21 @@
+find_program(CTAGS_EXECUTABLE
+ NAMES ctags-universal universal-ctags ctags
+ DOC "Generate a ctags index for source navigation: make tags")
+mark_as_advanced(CTAGS_EXECUTABLE)
+
+if(CTAGS_EXECUTABLE)
+ add_custom_target(tags
+ COMMAND ${CTAGS_EXECUTABLE}
+ -R
+ --languages=C
+ --c-kinds=+p
+ --fields=+S
+ --exclude=build
+ --exclude=build-claude
+ --exclude=build_tmp
+ --exclude=.git
+ -f ${CMAKE_SOURCE_DIR}/tags
+ ${CMAKE_SOURCE_DIR}
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ COMMENT "Generating ctags index at ${CMAKE_SOURCE_DIR}/tags")
+endif()
diff --git a/cmake/tests.cmake b/cmake/tests.cmake
new file mode 100644
index 00000000..edc2987c
--- /dev/null
+++ b/cmake/tests.cmake
@@ -0,0 +1,30 @@
+include(CTest) # Sets BUILD_TESTING by default to on.
+include(utils/TestUtils)
+
+# Test configuration options
+include(config/tests)
+include(utils/DisableTestLogging)
+
+if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
+ set(BUILD_TESTS ON)
+else()
+ set(BUILD_TESTS OFF)
+endif()
+
+add_custom_target(build_tests)
+
+if(BUILD_TESTS)
+ add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND})
+ add_dependencies(check build_tests)
+endif()
+
+# Test subdirectories are added from their parent CMakeLists.txt files
+# via add_subdirectory(tests) - keeping tests with their source code
+
+# Coverage target setup (called after all targets are defined)
+function(setup_coverage_target)
+ if(BUILD_TESTS)
+ include(utils/GenCoverage)
+ create_coverage_target()
+ endif()
+endfunction()
diff --git a/cmake/utils/AddCompileFlags.cmake b/cmake/utils/AddCompileFlags.cmake
new file mode 100644
index 00000000..b81ccc67
--- /dev/null
+++ b/cmake/utils/AddCompileFlags.cmake
@@ -0,0 +1,6 @@
+# - add_compile_flags(<target> <flags>...)
+# Add compile flags to a target using modern CMake
+
+macro(add_compile_flags _target)
+ target_compile_options(${_target} PRIVATE ${ARGN})
+endmacro()
diff --git a/cmake/utils/CMakeUninstall.cmake.in b/cmake/utils/CMakeUninstall.cmake.in
new file mode 100644
index 00000000..b8bba5fd
--- /dev/null
+++ b/cmake/utils/CMakeUninstall.cmake.in
@@ -0,0 +1,21 @@
+if (NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt")
+ message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt")
+endif()
+
+file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files)
+string(REGEX REPLACE "\n" ";" files "${files}")
+foreach (file ${files})
+ message(STATUS "Uninstalling $ENV{DESTDIR}${file}")
+ if (IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
+ execute_process(
+ COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}"
+ RESULT_VARIABLE rm_out
+ ERROR_VARIABLE rm_retval
+ )
+ if (NOT "${rm_retval}" STREQUAL "" AND NOT "${rm_retval}" STREQUAL 0)
+ message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}")
+ endif()
+ else()
+ message(STATUS "File $ENV{DESTDIR}${file} does not exist.")
+ endif()
+endforeach ()
diff --git a/cmake/utils/CPUUtils.cmake b/cmake/utils/CPUUtils.cmake
new file mode 100644
index 00000000..8ca7683a
--- /dev/null
+++ b/cmake/utils/CPUUtils.cmake
@@ -0,0 +1,82 @@
+include(CheckCSourceRuns)
+
+# Compile + run a probe so we only enable a feature the host CPU
+# actually implements (toolchains accept flags the silicon may lack).
+# Cross-compile without an emulator: feature off.
+function(detect_cpu_feature _result_var _flags _source)
+ set(_save_flags "${CMAKE_REQUIRED_FLAGS}")
+ set(_save_quiet "${CMAKE_REQUIRED_QUIET}")
+ set(CMAKE_REQUIRED_FLAGS "${_save_flags} ${_flags}")
+ set(CMAKE_REQUIRED_QUIET TRUE)
+ if(CMAKE_CROSSCOMPILING AND NOT CMAKE_CROSSCOMPILING_EMULATOR)
+ set(${_result_var} FALSE CACHE INTERNAL
+ "${_result_var} (cross-compile without emulator: off)")
+ else()
+ check_c_source_runs("${_source}" ${_result_var})
+ endif()
+ set(CMAKE_REQUIRED_FLAGS "${_save_flags}")
+ set(CMAKE_REQUIRED_QUIET "${_save_quiet}")
+endfunction()
+
+# x86 PCLMULQDQ + SSE4.1. argc-derived input defeats constant folding;
+# SIGILL handler exits cleanly so the kernel skips the core dump.
+function(detect_pclmul)
+ detect_cpu_feature(_HAVE_PCLMUL "-mpclmul"
+"#include <wmmintrin.h>
+#include <signal.h>
+#include <unistd.h>
+static void on_sigill(int sig) { (void) sig; _exit(1); }
+int main(int argc, char ** argv) {
+ __m128i a;
+ __m128i b;
+ (void) argv;
+ signal(SIGILL, on_sigill);
+ a = _mm_set1_epi32(argc);
+ b = _mm_clmulepi64_si128(a, a, 0);
+ return _mm_cvtsi128_si32(b) & 0;
+}")
+ detect_cpu_feature(_HAVE_SSE41 "-msse4.1"
+"#include <smmintrin.h>
+#include <signal.h>
+#include <unistd.h>
+static void on_sigill(int sig) { (void) sig; _exit(1); }
+int main(int argc, char ** argv) {
+ __m128i a;
+ (void) argv;
+ signal(SIGILL, on_sigill);
+ a = _mm_set1_epi32(argc);
+ return _mm_extract_epi32(a, 0) & 0;
+}")
+ if(_HAVE_PCLMUL AND _HAVE_SSE41)
+ set(HAVE_PCLMUL TRUE CACHE INTERNAL
+ "x86 PCLMUL + SSE4.1 intrinsics available")
+ else()
+ unset(HAVE_PCLMUL CACHE)
+ endif()
+endfunction()
+
+# aarch64 FEAT_PMULL (vmull_p64). Pi 4's BCM2711 accepts +crypto at
+# compile time but lacks the hardware — the runtime probe catches that.
+function(detect_pmull)
+ detect_cpu_feature(_HAVE_PMULL "-march=armv8-a+crypto"
+"#include <arm_neon.h>
+#include <signal.h>
+#include <stdint.h>
+#include <unistd.h>
+static void on_sigill(int sig) { (void) sig; _exit(1); }
+int main(int argc, char ** argv) {
+ poly64_t a;
+ poly128_t c;
+ (void) argv;
+ signal(SIGILL, on_sigill);
+ a = (poly64_t) (uint64_t) argc;
+ c = vmull_p64(a, a);
+ return (int) (vgetq_lane_u64((uint64x2_t) c, 0) & 0);
+}")
+ if(_HAVE_PMULL)
+ set(HAVE_PMULL TRUE CACHE INTERNAL
+ "aarch64 PMULL intrinsics available")
+ else()
+ unset(HAVE_PMULL CACHE)
+ endif()
+endfunction()
diff --git a/cmake/CompilerUtils.cmake b/cmake/utils/CompilerUtils.cmake
index 7c8b022f..35548f4f 100644
--- a/cmake/CompilerUtils.cmake
+++ b/cmake/utils/CompilerUtils.cmake
@@ -1,13 +1,13 @@
include(CheckCCompilerFlag)
function(test_and_set_c_compiler_flag_global _flag)
-
string(REGEX REPLACE "-" "_" _sflag ${_flag})
- set(CMAKE_REQUIRED_FLAGS ${_flag})
+ string(REGEX REPLACE "=" "_" _sflag ${_sflag})
+ # Use -Werror during test so clang rejects unknown flags
+ set(CMAKE_REQUIRED_FLAGS "-Werror ${_flag}")
check_c_compiler_flag(${_flag} COMPILER_SUPPORTS_FLAG_${_sflag})
if(COMPILER_SUPPORTS_FLAG_${_sflag})
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_flag}" PARENT_SCOPE)
+ add_compile_options(${_flag})
endif()
-
-endfunction(test_and_set_c_compiler_flag_global)
+endfunction()
diff --git a/cmake/utils/DebugTargets.cmake b/cmake/utils/DebugTargets.cmake
new file mode 100644
index 00000000..78899213
--- /dev/null
+++ b/cmake/utils/DebugTargets.cmake
@@ -0,0 +1,16 @@
+# Utility functions for Ouroboros target configuration
+set(OUROBOROS_DEBUG_CONFIGS
+ Debug
+ DebugASan
+ DebugTSan
+ DebugLSan
+ DebugUSan
+ DebugAnalyzer
+)
+
+# Add CONFIG_OUROBOROS_DEBUG definition for debug build types
+function(ouroboros_target_debug_definitions target)
+ list(JOIN OUROBOROS_DEBUG_CONFIGS "," _configs)
+ target_compile_definitions(${target} PRIVATE
+ "$<$<CONFIG:${_configs}>:CONFIG_OUROBOROS_DEBUG>")
+endfunction()
diff --git a/cmake/utils/DisableTestLogging.cmake b/cmake/utils/DisableTestLogging.cmake
new file mode 100644
index 00000000..40c16668
--- /dev/null
+++ b/cmake/utils/DisableTestLogging.cmake
@@ -0,0 +1,11 @@
+# Macro to apply test logging settings to a target
+# Configuration options are in cmake/config/tests.cmake
+
+macro(disable_test_logging_for_target target)
+ if(DISABLE_TESTS_LOGGING)
+ target_compile_definitions(${target} PRIVATE OUROBOROS_DISABLE_LOGGING)
+ endif()
+ if(DISABLE_TESTS_CORE_DUMPS)
+ target_compile_definitions(${target} PRIVATE DISABLE_TESTS_CORE_DUMPS)
+ endif()
+endmacro()
diff --git a/cmake/FindProtobufC.cmake b/cmake/utils/FindProtobufC.cmake
index ff892b5b..4632dfcb 100644
--- a/cmake/FindProtobufC.cmake
+++ b/cmake/utils/FindProtobufC.cmake
@@ -1,22 +1,22 @@
-function(PROTOBUF_GENERATE_C SRCS HDRS)
- if (NOT ARGN)
- message(SEND_ERROR "Error: PROTOBUF_GENERATE_C() called without any proto files")
+function(protobuf_generate_c SRCS HDRS)
+ if(NOT ARGN)
+ message(SEND_ERROR "Error: protobuf_generate_c() called without any proto files")
return()
- endif ()
+ endif()
- if (PROTOBUF_GENERATE_C_APPEND_PATH)
+ if(PROTOBUF_GENERATE_C_APPEND_PATH)
# Create an include path for each file specified
foreach (FIL ${ARGN})
get_filename_component(ABS_FIL ${FIL} ABSOLUTE)
get_filename_component(ABS_PATH ${ABS_FIL} PATH)
list(FIND _protobuf_include_path ${ABS_PATH} _contains_already)
- if (${_contains_already} EQUAL -1)
+ if(${_contains_already} EQUAL -1)
list(APPEND _protobuf_include_path -I ${ABS_PATH})
- endif ()
+ endif()
endforeach ()
- else ()
+ else()
set(_protobuf_include_path -I ${CMAKE_CURRENT_SOURCE_DIR})
- endif ()
+ endif()
set(${SRCS})
set(${HDRS})
@@ -42,33 +42,37 @@ function(PROTOBUF_GENERATE_C SRCS HDRS)
set(${HDRS} ${${HDRS}} PARENT_SCOPE)
endfunction()
-# By default have PROTOBUF_GENERATE_C macro pass -I to protoc
+# By default have protobuf_generate_c function pass -I to protoc
# for each directory where a proto file is referenced.
-if (NOT DEFINED PROTOBUF_GENERATE_C_APPEND_PATH)
+if(NOT DEFINED PROTOBUF_GENERATE_C_APPEND_PATH)
set(PROTOBUF_GENERATE_C_APPEND_PATH TRUE)
-endif ()
+endif()
-# Find library
find_library(PROTOBUF_C_LIBRARY
NAMES libprotobuf-c.so libprotobuf-c libprotobuf-c.dylib
)
mark_as_advanced(PROTOBUF_C_LIBRARY)
-# Find the include directory
find_path(PROTOBUF_C_INCLUDE_DIR
google/protobuf-c/protobuf-c.h
)
mark_as_advanced(PROTOBUF_C_INCLUDE_DIR)
-# Find the protoc-c Executable
find_program(PROTOBUF_PROTOC_C_EXECUTABLE
NAMES protoc protoc-c
DOC "The Google Protocol Buffers C Compiler"
)
mark_as_advanced(PROTOBUF_PROTOC_C_EXECUTABLE)
-find_package(PackageHandleStandardArgs)
+include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ProtobufC DEFAULT_MSG
PROTOBUF_C_LIBRARY PROTOBUF_C_INCLUDE_DIR PROTOBUF_PROTOC_C_EXECUTABLE)
set(PROTOBUF_C_INCLUDE_DIRS ${PROTOBUF_C_INCLUDE_DIR})
+
+if(ProtobufC_FOUND AND NOT TARGET ProtobufC::ProtobufC)
+ add_library(ProtobufC::ProtobufC UNKNOWN IMPORTED)
+ set_target_properties(ProtobufC::ProtobufC PROPERTIES
+ IMPORTED_LOCATION "${PROTOBUF_C_LIBRARY}"
+ INTERFACE_INCLUDE_DIRECTORIES "${PROTOBUF_C_INCLUDE_DIR}")
+endif()
diff --git a/cmake/utils/GenCoverage.cmake b/cmake/utils/GenCoverage.cmake
new file mode 100644
index 00000000..0ae11ee8
--- /dev/null
+++ b/cmake/utils/GenCoverage.cmake
@@ -0,0 +1,94 @@
+# Creates the coverage target for test coverage analysis
+# Requires HAVE_GCOV to be set (from dependencies/gcov.cmake)
+# Uses HAVE_LCOV for optional HTML generation (from dependencies/lcov.cmake)
+
+# Filter patterns for lcov --remove
+set(LCOV_FILTERS '*_test.c' '*.h')
+
+# Ignore inconsistent coverage: legitimate gaps in error paths and
+# edge cases that are difficult to exercise in unit tests.
+function(get_html_coverage_commands OUTPUT_VAR)
+ if(HAVE_LCOV)
+ set(COMMANDS
+ COMMAND ${LCOV_PATH}
+ --capture --directory .
+ --output-file coverage.info
+ > /dev/null 2>&1
+ COMMAND ${LCOV_PATH}
+ --remove coverage.info ${LCOV_FILTERS}
+ --output-file coverage_filtered.info
+ --ignore-errors inconsistent
+ > /dev/null 2>&1
+ COMMAND ${GENHTML_PATH}
+ coverage_filtered.info
+ --output-directory coverage_html
+ > /dev/null 2>&1
+ COMMAND ${CMAKE_COMMAND} -E echo ""
+ COMMAND ${CMAKE_COMMAND} -E echo "HTML report: ${CMAKE_BINARY_DIR}/coverage_html/index.html"
+ COMMAND ${CMAKE_COMMAND} -E echo ""
+ )
+ set(${OUTPUT_VAR} "${COMMANDS}" PARENT_SCOPE)
+ else()
+ set(${OUTPUT_VAR} "" PARENT_SCOPE)
+ endif()
+endfunction()
+
+function(create_informational_target)
+ # MESSAGE lines are passed as ARGV, last arg is COMMENT
+ list(LENGTH ARGV NUM_ARGS)
+ math(EXPR COMMENT_IDX "${NUM_ARGS} - 1")
+ list(GET ARGV ${COMMENT_IDX} COMMENT_TEXT)
+
+ # Build command list
+ set(COMMANDS
+ COMMAND ${CMAKE_COMMAND} -E echo ""
+ )
+ foreach(i RANGE 0 ${COMMENT_IDX})
+ if(NOT i EQUAL ${COMMENT_IDX})
+ list(GET ARGV ${i} LINE)
+ list(APPEND COMMANDS COMMAND ${CMAKE_COMMAND} -E echo "${LINE}")
+ endif()
+ endforeach()
+ list(APPEND COMMANDS COMMAND ${CMAKE_COMMAND} -E echo "")
+
+ add_custom_target(coverage
+ ${COMMANDS}
+ COMMENT "${COMMENT_TEXT}"
+ )
+endfunction()
+
+function(create_coverage_target)
+ if(HAVE_GCOV AND NOT DISABLE_COVERAGE)
+ get_html_coverage_commands(HTML_COVERAGE_COMMANDS)
+
+ add_custom_target(coverage
+ COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalStart > /dev/null 2>&1
+ COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalConfigure > /dev/null 2>&1
+ COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalBuild > /dev/null 2>&1
+ COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalTest > /dev/null 2>&1
+ COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalCoverage > /dev/null 2>&1
+ COMMAND ${CMAKE_COMMAND}
+ -DPROJECT_SOURCE_DIR=${CMAKE_SOURCE_DIR}
+ -DPROJECT_BINARY_DIR=${CMAKE_BINARY_DIR}
+ -P ${CMAKE_SOURCE_DIR}/cmake/utils/PrintCoverage.cmake
+ ${HTML_COVERAGE_COMMANDS}
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ DEPENDS build_tests
+ COMMENT "Running tests with coverage analysis using CTest"
+ )
+ elseif(HAVE_GCOV)
+ create_informational_target(
+ "Coverage is currently disabled"
+ "To enable coverage analysis, reconfigure with"
+ " cmake -DDISABLE_COVERAGE=OFF .."
+ "Coverage disabled"
+ )
+ message(STATUS "Coverage disabled. Use 'make coverage' for instructions to enable.")
+ else()
+ create_informational_target(
+ "Coverage analysis is not available"
+ "Install gcov to enable coverage support"
+ "Coverage not available"
+ )
+ endif()
+endfunction()
diff --git a/cmake/utils/GenVersionHeader.cmake b/cmake/utils/GenVersionHeader.cmake
new file mode 100644
index 00000000..2dfe6a2b
--- /dev/null
+++ b/cmake/utils/GenVersionHeader.cmake
@@ -0,0 +1,12 @@
+include(${CMAKE_CURRENT_LIST_DIR}/GetGitHash.cmake)
+get_git_hash(${GIT_DIR} ${PACKAGE_VERSION_MAJOR} ${PACKAGE_VERSION_MINOR}
+ ${PACKAGE_VERSION_PATCH} PACKAGE_VERSION_STRING)
+
+configure_file(${INPUT_FILE} ${OUTPUT_FILE}.tmp @ONLY)
+
+execute_process(
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${OUTPUT_FILE}.tmp ${OUTPUT_FILE}
+)
+
+file(REMOVE ${OUTPUT_FILE}.tmp)
diff --git a/cmake/utils/GetGitHash.cmake b/cmake/utils/GetGitHash.cmake
new file mode 100644
index 00000000..e399216d
--- /dev/null
+++ b/cmake/utils/GetGitHash.cmake
@@ -0,0 +1,40 @@
+function(get_git_hash WORKING_DIR VERSION_MAJ VERSION_MIN VERSION_PAT OUTPUT_VAR)
+ execute_process(
+ COMMAND git describe --tags --always --dirty
+ --match "[0-9]*.[0-9]*.[0-9]*"
+ WORKING_DIRECTORY ${WORKING_DIR}
+ OUTPUT_VARIABLE _hash
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+
+ if(_hash MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+")
+ # git describe returned a tag-based version string
+ elseif(_hash)
+ # No version tag found, construct full version string
+ execute_process(
+ COMMAND git rev-list --count HEAD
+ WORKING_DIRECTORY ${WORKING_DIR}
+ OUTPUT_VARIABLE _count
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+ execute_process(
+ COMMAND git describe --always --dirty
+ WORKING_DIRECTORY ${WORKING_DIR}
+ OUTPUT_VARIABLE _desc
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+ set(_hash
+ "${VERSION_MAJ}.${VERSION_MIN}.${VERSION_PAT}-${_count}-g${_desc}")
+ elseif(EXISTS "${WORKING_DIR}/VERSION")
+ # No git, use VERSION file (git archive / cgit snapshot)
+ file(READ "${WORKING_DIR}/VERSION" _hash)
+ string(STRIP "${_hash}" _hash)
+ else()
+ set(_hash "${VERSION_MAJ}.${VERSION_MIN}.${VERSION_PAT}")
+ endif()
+
+ set(${OUTPUT_VAR} "${_hash}" PARENT_SCOPE)
+endfunction()
diff --git a/cmake/utils/HumanReadable.cmake b/cmake/utils/HumanReadable.cmake
new file mode 100644
index 00000000..8bc1722f
--- /dev/null
+++ b/cmake/utils/HumanReadable.cmake
@@ -0,0 +1,17 @@
+# Human-readable size conversion utilities
+
+# Convert bytes to human-readable format (GB, MB, KB)
+# Usage: format_bytes_human_readable(<bytes> <output_var>)
+function(format_bytes_human_readable bytes output_var)
+ math(EXPR size_gb "${bytes} / 1073741824")
+ math(EXPR size_mb "${bytes} / 1048576")
+ math(EXPR size_kb "${bytes} / 1024")
+
+ if(size_gb GREATER 0)
+ set(${output_var} "${size_gb} GB" PARENT_SCOPE)
+ elseif(size_mb GREATER 0)
+ set(${output_var} "${size_mb} MB" PARENT_SCOPE)
+ else()
+ set(${output_var} "${size_kb} KB" PARENT_SCOPE)
+ endif()
+endfunction()
diff --git a/cmake/utils/ParseCoverage.cmake b/cmake/utils/ParseCoverage.cmake
new file mode 100644
index 00000000..0bbed0de
--- /dev/null
+++ b/cmake/utils/ParseCoverage.cmake
@@ -0,0 +1,120 @@
+# Parse CTest Coverage.xml file and extract structured coverage data.
+#
+# Sets in PARENT_SCOPE:
+# LOC_TESTED, LOC_UNTESTED - Overall metrics
+# FILE_COVERAGE_LIST - "path|name|tested|untested|total|percent" entries
+# COVERED_FILES - Absolute paths of covered files
+
+# Regex building blocks
+set(R_NUMBER "[0-9]+")
+set(R_NON_TAG "[^<]*")
+set(R_NON_BRACKET "[^>]*")
+set(R_NON_QUOTE "[^\"]+")
+
+# XML element patterns
+set(R_LOC_TESTED_ELEM "<LOCTested>(${R_NUMBER})</LOCTested>")
+set(R_LOC_TESTED_SKIP "<LOCTested>${R_NUMBER}</LOCTested>")
+set(R_LOC_UNTESTED_ELEM "<LOCUntested>(${R_NUMBER})</LOCUntested>")
+set(R_LOC_UNTESTED_CAP "<LOCUnTested>(${R_NUMBER})</LOCUnTested>")
+
+# Regex patterns for XML parsing
+set(REGEX_LOC_TESTED "</File>${R_NON_TAG}${R_LOC_TESTED_ELEM}")
+set(REGEX_LOC_UNTESTED "</File>${R_NON_TAG}${R_LOC_TESTED_SKIP}${R_NON_TAG}${R_LOC_UNTESTED_ELEM}")
+set(REGEX_FILE_ENTRY "<File${R_NON_BRACKET}Name=\"(${R_NON_QUOTE})\"${R_NON_BRACKET}>${R_NON_TAG}${R_LOC_TESTED_ELEM}${R_NON_TAG}${R_LOC_UNTESTED_CAP}")
+
+# Regex patterns for filtering
+set(REGEX_TEST_FILE "test")
+set(REGEX_TEST_DIR "/tests/")
+set(REGEX_PROTOBUF_C "\\.pb-c\\.c$")
+set(REGEX_PROTOBUF_ALT "\\.pb\\.c$")
+set(REGEX_DOTSLASH_SRC "^\\.\\/src\\/")
+
+function(should_skip_file FILE_PATH OUTPUT_VAR)
+ if(FILE_PATH MATCHES "${REGEX_TEST_FILE}" OR FILE_PATH MATCHES "${REGEX_TEST_DIR}" OR
+ FILE_PATH MATCHES "${REGEX_PROTOBUF_C}" OR FILE_PATH MATCHES "${REGEX_PROTOBUF_ALT}")
+ set(${OUTPUT_VAR} TRUE PARENT_SCOPE)
+ else()
+ set(${OUTPUT_VAR} FALSE PARENT_SCOPE)
+ endif()
+endfunction()
+
+function(normalize_coverage_path FILE_PATH PROJECT_SOURCE_DIR OUTPUT_VAR)
+ if(NOT IS_ABSOLUTE "${FILE_PATH}")
+ string(REGEX REPLACE "${REGEX_DOTSLASH_SRC}" "src/" FILE_PATH "${FILE_PATH}")
+ get_filename_component(FILE_PATH "${PROJECT_SOURCE_DIR}/${FILE_PATH}" ABSOLUTE)
+ endif()
+ set(${OUTPUT_VAR} "${FILE_PATH}" PARENT_SCOPE)
+endfunction()
+
+function(extract_xml_attribute XML_STRING ATTRIBUTE OUTPUT_VAR)
+ string(REGEX MATCH "${ATTRIBUTE}=\"([^\"]+)\"" _ "${XML_STRING}")
+ set(${OUTPUT_VAR} "${CMAKE_MATCH_1}" PARENT_SCOPE)
+endfunction()
+
+function(extract_xml_element XML_STRING ELEMENT OUTPUT_VAR)
+ string(REGEX MATCH "<${ELEMENT}>([^<]+)</${ELEMENT}>" _ "${XML_STRING}")
+ set(${OUTPUT_VAR} "${CMAKE_MATCH_1}" PARENT_SCOPE)
+endfunction()
+
+function(build_coverage_entry PATH NAME TESTED UNTESTED OUTPUT_VAR)
+ math(EXPR TOTAL "${TESTED} + ${UNTESTED}")
+ if(NOT TOTAL GREATER 0)
+ set(${OUTPUT_VAR} "" PARENT_SCOPE)
+ return()
+ endif()
+ math(EXPR PERCENT "(${TESTED} * 100) / ${TOTAL}")
+ set(${OUTPUT_VAR} "${PATH}|${NAME}|${TESTED}|${UNTESTED}|${TOTAL}|${PERCENT}" PARENT_SCOPE)
+endfunction()
+
+function(parse_coverage_xml COVERAGE_FILE PROJECT_SOURCE_DIR)
+ if(NOT EXISTS "${COVERAGE_FILE}")
+ return()
+ endif()
+
+ file(READ "${COVERAGE_FILE}" COVERAGE_XML)
+
+ string(REGEX MATCH "${REGEX_LOC_TESTED}" _ "${COVERAGE_XML}")
+ set(TESTED "${CMAKE_MATCH_1}")
+
+ string(REGEX MATCH "${REGEX_LOC_UNTESTED}" _ "${COVERAGE_XML}")
+ set(UNTESTED "${CMAKE_MATCH_1}")
+
+ if(NOT TESTED OR NOT UNTESTED)
+ return()
+ endif()
+
+ string(REGEX MATCHALL "${REGEX_FILE_ENTRY}" FILE_MATCHES "${COVERAGE_XML}")
+
+ set(COVERED_LIST "")
+ set(COVERAGE_DATA "")
+
+ foreach(MATCH ${FILE_MATCHES})
+ extract_xml_attribute("${MATCH}" "FullPath" PATH)
+ extract_xml_attribute("${MATCH}" "Name" NAME)
+
+ should_skip_file("${PATH}" SKIP)
+ if(SKIP)
+ continue()
+ endif()
+
+ normalize_coverage_path("${PATH}" "${PROJECT_SOURCE_DIR}" ABS_PATH)
+ list(APPEND COVERED_LIST "${ABS_PATH}")
+
+ extract_xml_element("${MATCH}" "LOCTested" TESTED_LINES)
+ extract_xml_element("${MATCH}" "LOCUnTested" UNTESTED_LINES)
+
+ if(NOT TESTED_LINES OR NOT UNTESTED_LINES)
+ continue()
+ endif()
+
+ build_coverage_entry("${PATH}" "${NAME}" "${TESTED_LINES}" "${UNTESTED_LINES}" ENTRY)
+ if(ENTRY)
+ list(APPEND COVERAGE_DATA "${ENTRY}")
+ endif()
+ endforeach()
+
+ set(LOC_TESTED "${TESTED}" PARENT_SCOPE)
+ set(LOC_UNTESTED "${UNTESTED}" PARENT_SCOPE)
+ set(FILE_COVERAGE_LIST "${COVERAGE_DATA}" PARENT_SCOPE)
+ set(COVERED_FILES "${COVERED_LIST}" PARENT_SCOPE)
+endfunction()
diff --git a/cmake/utils/ParseGitTag.cmake b/cmake/utils/ParseGitTag.cmake
new file mode 100644
index 00000000..80e47608
--- /dev/null
+++ b/cmake/utils/ParseGitTag.cmake
@@ -0,0 +1,56 @@
+function(parse_git_tag WORKING_DIR OUTPUT_MAJOR OUTPUT_MINOR OUTPUT_PATCH)
+
+ # Check if we're in a git repo
+ execute_process(
+ COMMAND git rev-parse --git-dir
+ WORKING_DIRECTORY ${WORKING_DIR}
+ OUTPUT_VARIABLE _git_dir
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+
+ if(_git_dir)
+ # Get the latest version tag reachable from the current commit
+ execute_process(
+ COMMAND git describe --tags --abbrev=0
+ --match "[0-9]*.[0-9]*.[0-9]*"
+ WORKING_DIRECTORY ${WORKING_DIR}
+ OUTPUT_VARIABLE _latest_tag
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ )
+
+ if(_latest_tag MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
+ set(_major "${CMAKE_MATCH_1}")
+ set(_minor "${CMAKE_MATCH_2}")
+ set(_patch "${CMAKE_MATCH_3}")
+ message(STATUS "Version from git tag: ${_latest_tag}")
+ else()
+ string(ASCII 27 _esc)
+ set(_W "${_esc}[38;5;208m")
+ set(_R "${_esc}[0m")
+ message(STATUS "${_W}WARNING: No version tags found. "
+ "Try: git fetch --tags${_R}")
+ endif()
+ elseif(EXISTS "${WORKING_DIR}/VERSION")
+ # Not a git repo, try VERSION file (release tarball / git archive)
+ file(READ "${WORKING_DIR}/VERSION" _ver)
+ string(STRIP "${_ver}" _ver)
+ if(_ver MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)")
+ set(_major "${CMAKE_MATCH_1}")
+ set(_minor "${CMAKE_MATCH_2}")
+ set(_patch "${CMAKE_MATCH_3}")
+ message(STATUS "Version from VERSION file: ${_ver}")
+ endif()
+ endif()
+
+ if(NOT DEFINED _major)
+ set(_major "0")
+ set(_minor "0")
+ set(_patch "0")
+ endif()
+
+ set(${OUTPUT_MAJOR} "${_major}" PARENT_SCOPE)
+ set(${OUTPUT_MINOR} "${_minor}" PARENT_SCOPE)
+ set(${OUTPUT_PATCH} "${_patch}" PARENT_SCOPE)
+endfunction()
diff --git a/cmake/utils/PrintCoverage.cmake b/cmake/utils/PrintCoverage.cmake
new file mode 100644
index 00000000..4b75fa09
--- /dev/null
+++ b/cmake/utils/PrintCoverage.cmake
@@ -0,0 +1,333 @@
+# Script to parse and display coverage results from CTest
+#
+# This script is invoked by the 'make coverage' target and parses the CTest
+# Coverage.xml file to generate a formatted coverage report grouped by component.
+#
+# This script is run with cmake -P, so CMAKE_SOURCE_DIR won't be set correctly.
+# Use PROJECT_SOURCE_DIR and PROJECT_BINARY_DIR passed as -D arguments.
+
+if(NOT DEFINED PROJECT_SOURCE_DIR)
+ message(FATAL_ERROR "PROJECT_SOURCE_DIR must be defined")
+endif()
+if(NOT DEFINED PROJECT_BINARY_DIR)
+ message(FATAL_ERROR "PROJECT_BINARY_DIR must be defined")
+endif()
+
+# Include coverage parsing functions
+include(${CMAKE_CURRENT_LIST_DIR}/ParseCoverage.cmake)
+
+# Create padding strings (CMake 2.8 compatible)
+function(make_padding LENGTH OUTPUT_VAR)
+ set(RESULT "")
+ if(LENGTH GREATER 0)
+ foreach(i RANGE 1 ${LENGTH})
+ set(RESULT "${RESULT} ")
+ endforeach()
+ endif()
+ set(${OUTPUT_VAR} "${RESULT}" PARENT_SCOPE)
+endfunction()
+
+# Format a number with padding for right alignment
+function(format_number VALUE WIDTH OUTPUT_VAR)
+ string(LENGTH "${VALUE}" VALUE_LEN)
+ math(EXPR PAD_LEN "${WIDTH} - ${VALUE_LEN}")
+ make_padding(${PAD_LEN} PADDING)
+ set(${OUTPUT_VAR} "${PADDING}${VALUE}" PARENT_SCOPE)
+endfunction()
+
+# Format a complete coverage row with consistent alignment
+function(format_coverage_row LABEL TESTED TESTED_FC UNTESTED UNTESTED_FC TOTAL PERCENT OUTPUT_VAR)
+ string(LENGTH "${LABEL}" LABEL_LEN)
+ math(EXPR LABEL_PAD "28 - ${LABEL_LEN}")
+ make_padding(${LABEL_PAD} LP)
+
+ format_number(${TESTED} 6 TS)
+ format_number(${TESTED_FC} 3 TFC)
+ format_number(${UNTESTED} 8 US)
+ format_number(${UNTESTED_FC} 3 UFC)
+ format_number(${TOTAL} 5 TT)
+ format_number(${PERCENT} 3 PC)
+ set(${OUTPUT_VAR} " ${LABEL}${LP}${TS}[${TFC}] ${US}[${UFC}] ${TT} ${PC}%" PARENT_SCOPE)
+endfunction()
+
+# Format the header row to align with data columns
+function(format_coverage_header OUTPUT_VAR)
+ set(HEADER " Component Tested Untested Total %")
+ set(${OUTPUT_VAR} "${HEADER}" PARENT_SCOPE)
+endfunction()
+
+# Calculate metrics from entry list (pipe-delimited: path|name|tested|untested|total|percent)
+function(calculate_metrics ENTRIES OUT_TESTED OUT_UNTESTED OUT_TESTED_FC OUT_UNTESTED_FC)
+ set(TESTED 0)
+ set(UNTESTED 0)
+ set(TESTED_FC 0)
+ set(UNTESTED_FC 0)
+
+ foreach(ENTRY ${ENTRIES})
+ string(REPLACE "|" ";" PARTS "${ENTRY}")
+ list(GET PARTS 2 ENTRY_TESTED)
+ list(GET PARTS 3 ENTRY_UNTESTED)
+
+ math(EXPR TESTED "${TESTED} + ${ENTRY_TESTED}")
+ math(EXPR UNTESTED "${UNTESTED} + ${ENTRY_UNTESTED}")
+
+ if(ENTRY_TESTED EQUAL 0)
+ math(EXPR UNTESTED_FC "${UNTESTED_FC} + 1")
+ else()
+ math(EXPR TESTED_FC "${TESTED_FC} + 1")
+ endif()
+ endforeach()
+
+ set(${OUT_TESTED} "${TESTED}" PARENT_SCOPE)
+ set(${OUT_UNTESTED} "${UNTESTED}" PARENT_SCOPE)
+ set(${OUT_TESTED_FC} "${TESTED_FC}" PARENT_SCOPE)
+ set(${OUT_UNTESTED_FC} "${UNTESTED_FC}" PARENT_SCOPE)
+endfunction()
+
+# Discover components and sub-components from source tree
+function(discover_components PROJECT_SOURCE_DIR OUT_COMPONENTS OUT_COMP_SUBCOMPS)
+ file(GLOB COMPONENT_DIRS "${PROJECT_SOURCE_DIR}/src/*")
+ set(COMPONENTS "")
+ set(SKIP_DIRS "include;doc;tests")
+
+ foreach(DIR ${COMPONENT_DIRS})
+ if(IS_DIRECTORY ${DIR})
+ get_filename_component(COMP_NAME ${DIR} NAME)
+ list(FIND SKIP_DIRS ${COMP_NAME} SKIP_IDX)
+ if(SKIP_IDX EQUAL -1)
+ list(APPEND COMPONENTS ${COMP_NAME})
+
+ file(GLOB SUBCOMP_DIRS "${DIR}/*")
+ set(COMP_${COMP_NAME}_SUBCOMPS "")
+ foreach(SUBDIR ${SUBCOMP_DIRS})
+ if(IS_DIRECTORY ${SUBDIR})
+ get_filename_component(SUBCOMP_NAME ${SUBDIR} NAME)
+ list(FIND SKIP_DIRS ${SUBCOMP_NAME} SKIP_IDX2)
+ if(SKIP_IDX2 EQUAL -1)
+ list(APPEND COMP_${COMP_NAME}_SUBCOMPS ${SUBCOMP_NAME})
+ set(COMP_${COMP_NAME}_${SUBCOMP_NAME} "")
+ endif()
+ endif()
+ endforeach()
+ endif()
+ endif()
+ endforeach()
+
+ set(${OUT_COMPONENTS} "${COMPONENTS}" PARENT_SCOPE)
+ foreach(COMP ${COMPONENTS})
+ set(COMP_${COMP}_SUBCOMPS "${COMP_${COMP}_SUBCOMPS}" PARENT_SCOPE)
+ endforeach()
+endfunction()
+
+# Print a coverage line
+function(print_coverage_line LABEL TESTED TESTED_FC UNTESTED UNTESTED_FC TOTAL PERCENT)
+ format_coverage_row("${LABEL}" ${TESTED} ${TESTED_FC} ${UNTESTED} ${UNTESTED_FC} ${TOTAL} ${PERCENT} ROW)
+ execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${ROW}")
+endfunction()
+
+# Locate the coverage file
+if(NOT EXISTS "${CMAKE_BINARY_DIR}/Testing/TAG")
+ message(WARNING "Testing/TAG file not found. Coverage may not have run successfully.")
+ return()
+endif()
+
+file(STRINGS "${CMAKE_BINARY_DIR}/Testing/TAG" TAG_CONTENTS LIMIT_COUNT 1)
+string(STRIP "${TAG_CONTENTS}" TEST_DIR)
+set(COVERAGE_FILE "${CMAKE_BINARY_DIR}/Testing/${TEST_DIR}/Coverage.xml")
+
+if(NOT EXISTS "${COVERAGE_FILE}")
+ message(WARNING "Coverage file not found: ${COVERAGE_FILE}")
+ execute_process(COMMAND ${CMAKE_COMMAND} -E echo "Note: Coverage results should be in Testing/${TEST_DIR}/")
+ return()
+endif()
+
+# Parse the coverage XML file
+parse_coverage_xml("${COVERAGE_FILE}" "${PROJECT_SOURCE_DIR}")
+
+if(NOT LOC_TESTED OR NOT LOC_UNTESTED)
+ message(WARNING "Could not parse coverage metrics")
+ return()
+endif()
+
+math(EXPR TOTAL_LOC "${LOC_TESTED} + ${LOC_UNTESTED}")
+if(NOT TOTAL_LOC GREATER 0)
+ message(WARNING "No coverage data found")
+ return()
+endif()
+
+math(EXPR COVERAGE_PERCENT "(${LOC_TESTED} * 100) / ${TOTAL_LOC}")
+
+# Discover and group files by component
+if(NOT FILE_COVERAGE_LIST)
+ execute_process(COMMAND ${CMAKE_COMMAND} -E echo "No coverage data for source files (excluding tests)")
+ return()
+endif()
+
+# Discover components from source tree (only src/ directory)
+discover_components("${PROJECT_SOURCE_DIR}" COMPONENTS COMP_SUBCOMPS)
+
+# Find all source files and identify completely untested ones
+file(GLOB_RECURSE ALL_SOURCE_FILES "${PROJECT_SOURCE_DIR}/src/*.c")
+set(UNCOVERED_FILES "")
+foreach(SRC_FILE ${ALL_SOURCE_FILES})
+ if(SRC_FILE MATCHES "test" OR SRC_FILE MATCHES "/tests/")
+ continue()
+ endif()
+
+ list(FIND COVERED_FILES "${SRC_FILE}" FILE_IDX)
+ if(FILE_IDX EQUAL -1)
+ file(STRINGS "${SRC_FILE}" FILE_LINES)
+ list(LENGTH FILE_LINES LINE_COUNT)
+ if(LINE_COUNT GREATER 0)
+ get_filename_component(FILE_NAME "${SRC_FILE}" NAME)
+ list(APPEND UNCOVERED_FILES "${SRC_FILE}|${FILE_NAME}|0|${LINE_COUNT}|${LINE_COUNT}|0")
+ endif()
+ endif()
+endforeach()
+
+# Combine covered and uncovered files
+set(ALL_FILES ${FILE_COVERAGE_LIST})
+if(UNCOVERED_FILES)
+ list(APPEND ALL_FILES ${UNCOVERED_FILES})
+endif()
+
+# Group files into components and sub-components
+foreach(ENTRY ${ALL_FILES})
+ string(REPLACE "|" ";" ENTRY_PARTS "${ENTRY}")
+ list(GET ENTRY_PARTS 0 F_PATH)
+
+ # Normalize path and extract relative path from src/
+ # Remove leading ./ if present, then ensure path starts with src/
+ string(REGEX REPLACE "^\\./" "" REL_PATH "${F_PATH}")
+ string(REGEX REPLACE "^.*/src/" "src/" REL_PATH "${REL_PATH}")
+
+ # Tokenize on /: ["src", "component", "subcomponent", "file.c"]
+ string(REPLACE "/" ";" PATH_TOKENS "${REL_PATH}")
+ list(LENGTH PATH_TOKENS TOKEN_COUNT)
+
+ if(TOKEN_COUNT EQUAL 3)
+ # src/component/file.c - add to component main
+ list(GET PATH_TOKENS 1 FILE_COMP)
+ list(FIND COMPONENTS ${FILE_COMP} COMP_IDX)
+ if(COMP_IDX GREATER -1)
+ list(APPEND COMP_${FILE_COMP} "${ENTRY}")
+ list(APPEND COMP_${FILE_COMP}_main "${ENTRY}")
+ endif()
+ elseif(TOKEN_COUNT GREATER 3)
+ # src/component/subdir/.../file.c - check if subdir is a known subcomponent
+ list(GET PATH_TOKENS 1 FILE_COMP)
+ list(GET PATH_TOKENS 2 FILE_SUBCOMP)
+
+ list(FIND COMPONENTS ${FILE_COMP} COMP_IDX)
+ if(COMP_IDX GREATER -1)
+ list(APPEND COMP_${FILE_COMP} "${ENTRY}")
+
+ # Check if subdir is a recognized subcomponent
+ list(FIND COMP_${FILE_COMP}_SUBCOMPS ${FILE_SUBCOMP} SUBCOMP_IDX)
+ if(SUBCOMP_IDX GREATER -1)
+ list(APPEND COMP_${FILE_COMP}_${FILE_SUBCOMP} "${ENTRY}")
+ else()
+ # Subdir not recognized, treat as main
+ list(APPEND COMP_${FILE_COMP}_main "${ENTRY}")
+ endif()
+ endif()
+ endif()
+endforeach()
+
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "")
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================")
+format_coverage_header(HEADER)
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${HEADER}")
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================")
+
+# Process and display each component
+foreach(COMP_NAME ${COMPONENTS})
+ set(COMP_LIST ${COMP_${COMP_NAME}})
+
+ if(NOT COMP_LIST)
+ continue()
+ endif()
+
+ calculate_metrics("${COMP_LIST}" COMP_TESTED COMP_UNTESTED COMP_TESTED_FILE_COUNT COMP_UNTESTED_FILE_COUNT)
+ math(EXPR COMP_TOTAL "${COMP_TESTED} + ${COMP_UNTESTED}")
+ if(COMP_TOTAL GREATER 0)
+ math(EXPR COMP_PERCENT "(${COMP_TESTED} * 100) / ${COMP_TOTAL}")
+ else()
+ set(COMP_PERCENT 0)
+ endif()
+
+ print_coverage_line("${COMP_NAME}" ${COMP_TESTED} ${COMP_TESTED_FILE_COUNT} ${COMP_UNTESTED} ${COMP_UNTESTED_FILE_COUNT} ${COMP_TOTAL} ${COMP_PERCENT})
+
+ # Display "main" sub-component first if it has files
+ set(MAIN_LIST ${COMP_${COMP_NAME}_main})
+ if(MAIN_LIST)
+ calculate_metrics("${MAIN_LIST}" MAIN_TESTED MAIN_UNTESTED MAIN_TESTED_FILE_COUNT MAIN_UNTESTED_FILE_COUNT)
+ math(EXPR MAIN_TOTAL "${MAIN_TESTED} + ${MAIN_UNTESTED}")
+ if(MAIN_TOTAL GREATER 0)
+ math(EXPR MAIN_PERCENT "(${MAIN_TESTED} * 100) / ${MAIN_TOTAL}")
+ else()
+ set(MAIN_PERCENT 0)
+ endif()
+
+ print_coverage_line(" ." ${MAIN_TESTED} ${MAIN_TESTED_FILE_COUNT} ${MAIN_UNTESTED} ${MAIN_UNTESTED_FILE_COUNT} ${MAIN_TOTAL} ${MAIN_PERCENT})
+ endif()
+
+ # Display sub-components
+ foreach(SUBCOMP ${COMP_${COMP_NAME}_SUBCOMPS})
+ set(SUBCOMP_LIST ${COMP_${COMP_NAME}_${SUBCOMP}})
+ if(NOT SUBCOMP_LIST)
+ continue()
+ endif()
+
+ calculate_metrics("${SUBCOMP_LIST}" SUBCOMP_TESTED SUBCOMP_UNTESTED SUBCOMP_TESTED_FILE_COUNT SUBCOMP_UNTESTED_FILE_COUNT)
+ math(EXPR SUBCOMP_TOTAL "${SUBCOMP_TESTED} + ${SUBCOMP_UNTESTED}")
+ if(SUBCOMP_TOTAL GREATER 0)
+ math(EXPR SUBCOMP_PERCENT "(${SUBCOMP_TESTED} * 100) / ${SUBCOMP_TOTAL}")
+ else()
+ set(SUBCOMP_PERCENT 0)
+ endif()
+
+ print_coverage_line(" ${SUBCOMP}" ${SUBCOMP_TESTED} ${SUBCOMP_TESTED_FILE_COUNT} ${SUBCOMP_UNTESTED} ${SUBCOMP_UNTESTED_FILE_COUNT} ${SUBCOMP_TOTAL} ${SUBCOMP_PERCENT})
+ endforeach()
+
+ execute_process(COMMAND ${CMAKE_COMMAND} -E echo "")
+endforeach()
+
+# Calculate overall coverage
+set(TOTAL_TESTED ${LOC_TESTED})
+set(TOTAL_UNTESTED ${LOC_UNTESTED})
+
+# Count file coverage for totals
+set(TOTAL_TESTED_FILE_COUNT 0)
+set(TOTAL_UNTESTED_FILE_COUNT 0)
+
+foreach(ENTRY ${FILE_COVERAGE_LIST})
+ string(REPLACE "|" ";" PARTS "${ENTRY}")
+ list(GET PARTS 2 ENTRY_TESTED)
+ if(ENTRY_TESTED EQUAL 0)
+ math(EXPR TOTAL_UNTESTED_FILE_COUNT "${TOTAL_UNTESTED_FILE_COUNT} + 1")
+ else()
+ math(EXPR TOTAL_TESTED_FILE_COUNT "${TOTAL_TESTED_FILE_COUNT} + 1")
+ endif()
+endforeach()
+
+# Add untested file counts
+foreach(ENTRY ${UNCOVERED_FILES})
+ string(REPLACE "|" ";" PARTS "${ENTRY}")
+ list(GET PARTS 3 ENTRY_UNTESTED)
+ math(EXPR TOTAL_UNTESTED "${TOTAL_UNTESTED} + ${ENTRY_UNTESTED}")
+ math(EXPR TOTAL_UNTESTED_FILE_COUNT "${TOTAL_UNTESTED_FILE_COUNT} + 1")
+endforeach()
+
+math(EXPR TOTAL_ALL_LOC "${TOTAL_TESTED} + ${TOTAL_UNTESTED}")
+if(TOTAL_ALL_LOC GREATER 0)
+ math(EXPR OVERALL_COVERAGE_PERCENT "(${TOTAL_TESTED} * 100) / ${TOTAL_ALL_LOC}")
+else()
+ set(OVERALL_COVERAGE_PERCENT 0)
+endif()
+
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================")
+print_coverage_line("Total" ${TOTAL_TESTED} ${TOTAL_TESTED_FILE_COUNT} ${TOTAL_UNTESTED} ${TOTAL_UNTESTED_FILE_COUNT} ${TOTAL_ALL_LOC} ${OVERALL_COVERAGE_PERCENT})
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================")
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "")
+execute_process(COMMAND ${CMAKE_COMMAND} -E echo "Detailed XML report: ${COVERAGE_FILE}")
diff --git a/cmake/utils/PrintVersion.cmake b/cmake/utils/PrintVersion.cmake
new file mode 100644
index 00000000..34148eb7
--- /dev/null
+++ b/cmake/utils/PrintVersion.cmake
@@ -0,0 +1,8 @@
+file(STRINGS "${VERSION_HEADER}" lines REGEX "OUROBOROS_VERSION_STRING")
+foreach(line ${lines})
+ if(line MATCHES "define OUROBOROS_VERSION_STRING \"(.*)\"")
+ set(version "${CMAKE_MATCH_1}")
+ message("Ouroboros ${version}")
+ break()
+ endif()
+endforeach()
diff --git a/cmake/utils/TestUtils.cmake b/cmake/utils/TestUtils.cmake
new file mode 100644
index 00000000..e602690c
--- /dev/null
+++ b/cmake/utils/TestUtils.cmake
@@ -0,0 +1,38 @@
+# Compute test name prefix from directory structure
+function(compute_test_prefix)
+ file(RELATIVE_PATH _prefix "${CMAKE_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}")
+ string(REGEX REPLACE "/tests$" "" _prefix "${_prefix}")
+ set(TEST_PREFIX "${_prefix}" PARENT_SCOPE)
+endfunction()
+
+# Register tests from a test executable with the test framework
+# Usage: ouroboros_register_tests(TARGET <target> TESTS <test_list> [ENVIRONMENT <env>])
+# The TESTS argument should be the test list variable created by create_test_sourcelist
+function(ouroboros_register_tests)
+ cmake_parse_arguments(PARSE_ARGV 0 ARG "" "TARGET;ENVIRONMENT" "TESTS")
+
+ if(NOT ARG_TARGET)
+ message(FATAL_ERROR "ouroboros_register_tests: TARGET required")
+ endif()
+
+ if(NOT ARG_TESTS)
+ message(FATAL_ERROR "ouroboros_register_tests: TESTS required")
+ endif()
+
+ # First entry is the test driver, skip it
+ set(_tests ${ARG_TESTS})
+ list(POP_FRONT _tests)
+
+ foreach (test_src ${_tests})
+ get_filename_component(test_name ${test_src} NAME_WE)
+ add_test(${TEST_PREFIX}/${test_name}
+ ${CMAKE_CURRENT_BINARY_DIR}/${ARG_TARGET} ${test_name})
+ # All Ouroboros tests support skip return code
+ set_property(TEST ${TEST_PREFIX}/${test_name} PROPERTY SKIP_RETURN_CODE 1)
+ # Optional environment variables
+ if(ARG_ENVIRONMENT)
+ set_property(TEST ${TEST_PREFIX}/${test_name}
+ PROPERTY ENVIRONMENT "${ARG_ENVIRONMENT}")
+ endif()
+ endforeach ()
+endfunction()
diff --git a/cmake/version.cmake b/cmake/version.cmake
new file mode 100644
index 00000000..75a4ac59
--- /dev/null
+++ b/cmake/version.cmake
@@ -0,0 +1,34 @@
+include(utils/ParseGitTag)
+
+# Parse version from git tag or use custom version if unavailable
+parse_git_tag(${CMAKE_SOURCE_DIR} PACKAGE_VERSION_MAJOR PACKAGE_VERSION_MINOR
+ PACKAGE_VERSION_PATCH)
+
+set(PACKAGE_VERSION
+ "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}")
+
+include(utils/GetGitHash)
+get_git_hash(${CMAKE_SOURCE_DIR} ${PACKAGE_VERSION_MAJOR} ${PACKAGE_VERSION_MINOR}
+ ${PACKAGE_VERSION_PATCH} PACKAGE_VERSION_STRING)
+
+configure_file("${CMAKE_SOURCE_DIR}/include/ouroboros/version.h.in"
+ "${CMAKE_BINARY_DIR}/include/ouroboros/version.h" @ONLY)
+
+add_custom_target(version_header ALL
+ COMMAND ${CMAKE_COMMAND}
+ -DGIT_DIR=${CMAKE_SOURCE_DIR}
+ -DINPUT_FILE=${CMAKE_SOURCE_DIR}/include/ouroboros/version.h.in
+ -DOUTPUT_FILE=${CMAKE_BINARY_DIR}/include/ouroboros/version.h
+ -DPACKAGE_VERSION_MAJOR=${PACKAGE_VERSION_MAJOR}
+ -DPACKAGE_VERSION_MINOR=${PACKAGE_VERSION_MINOR}
+ -DPACKAGE_VERSION_PATCH=${PACKAGE_VERSION_PATCH}
+ -P ${CMAKE_SOURCE_DIR}/cmake/utils/GenVersionHeader.cmake
+ COMMENT "Updating git hash in version.h"
+)
+
+add_custom_target(version
+ COMMAND ${CMAKE_COMMAND}
+ -DVERSION_HEADER=${CMAKE_BINARY_DIR}/include/ouroboros/version.h
+ -P ${CMAKE_SOURCE_DIR}/cmake/utils/PrintVersion.cmake
+ DEPENDS version_header
+)
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
deleted file mode 100644
index 5cf30050..00000000
--- a/doc/CMakeLists.txt
+++ /dev/null
@@ -1 +0,0 @@
-add_subdirectory(man)
diff --git a/doc/man/flow_alloc.3 b/doc/man/flow_alloc.3
index dbe5323c..8a9b5f5b 100644
--- a/doc/man/flow_alloc.3
+++ b/doc/man/flow_alloc.3
@@ -62,10 +62,60 @@ The \fBflow_dealloc\fR() function will release any resources
associated with the flow. This call may block and keep reliable flows
active until all packets are acknowledged.
-A \fBqosspec_t\fR specifies the following QoS characteristics of a
-flow:
-
-TODO: specify a qosspec_t
+A \fBqosspec_t\fR specifies the QoS characteristics of a flow.
+The fields are:
+
+.TP
+\fBdelay\fR (ms)
+Maximum one-way delay.
+.TP
+\fBbandwidth\fR (bits/s)
+Minimum bandwidth.
+.TP
+\fBavailability\fR
+Class of 9s (e.g. 5 = 99.999%).
+.TP
+\fBloss\fR
+Tolerated packet loss; 0 selects reliable delivery.
+.TP
+\fBber\fR
+Tolerated bit error rate (errors per billion bits); 0 enables an
+end-to-end integrity check (corrupted packets are dropped).
+.TP
+\fBservice\fR
+Framing / reliability class: \fBSVC_RAW\fR (0) disables FRCT;
+\fBSVC_MESSAGE\fR (1) preserves SDU boundaries; \fBSVC_STREAM\fR (2) is
+a byte stream with no SDU boundaries. \fBSVC_STREAM\fR requires
+\fIloss\fR = 0; otherwise
+\fBflow_alloc\fR()/\fBflow_accept\fR() returns \fB-EINVAL\fR.
+.TP
+\fBmax_gap\fR (ms)
+Maximum tolerated inter-packet gap. Packets exceeding the gap
+budget are dropped under the real-time cubes.
+.TP
+\fBtimeout\fR (ms)
+Peer-liveness timeout; 0 disables. Only applies when FRCT is
+enabled (service > 0).
+
+.PP
+The library provides predefined cubes:
+
+.TP
+\fBqos_raw\fR
+No guarantees, no integrity check.
+.TP
+\fBqos_raw_safe\fR
+Best-effort with end-to-end integrity (ber = 0).
+.TP
+\fBqos_rt\fR / \fBqos_rt_safe\fR
+Real-time messages, optimised for latency over reliability;
+\fBqos_rt_safe\fR adds an end-to-end integrity check.
+.TP
+\fBqos_msg\fR
+Reliable, SDU-preserving delivery.
+.TP
+\fBqos_stream\fR
+Reliable byte stream; no SDU boundaries are preserved.
.SH RETURN VALUE
@@ -117,13 +167,39 @@ _
\fBflow_dealloc\fR() & Thread safety & MT-Safe
.TE
+.SH NOTES
+The returned file descriptor is subject to a single-reader and
+single-writer discipline \(em at most one thread may call
+.BR flow_read ()
+(or monitor the fd via
+.BR fevent ())
+and at most one thread may call
+.BR flow_write ()
+concurrently. See
+.BR flow_read (3),
+.BR flow_write (3),
+and
+.BR fevent (3)
+for details.
+.PP
+.BR flow_dealloc ()
+must not be called concurrently with any thread that is inside
+.BR flow_read (),
+.BR flow_write (),
+.BR fevent (),
+or any other Ouroboros library call on the same fd; the result is
+undefined behaviour. Applications must serialise teardown with
+in-flight use, e.g. by signalling worker threads to drop the fd
+before calling
+.BR flow_dealloc ().
+
.SH TERMINOLOGY
Please see \fBouroboros-glossary\fR(7).
.SH SEE ALSO
-.BR fccntl "(3), " flow_read "(3), " fqueue "(3), " fset "(3), " \
-ouroboros (8)
+.BR fccntl "(3), " fevent "(3), " flow_read "(3), " flow_write "(3), " \
+fqueue "(3), " fset "(3), " ouroboros (8)
.SH COLOPHON
This page is part of the Ouroboros project, found at
diff --git a/doc/man/flow_read.3 b/doc/man/flow_read.3
index acc1f61e..d4a5e883 100644
--- a/doc/man/flow_read.3
+++ b/doc/man/flow_read.3
@@ -39,8 +39,7 @@ end of the datagram.
On success, \fBflow_write\fR() returns the number of bytes written. On
failure, a negative value indicating the error will be returned.
-Partial writes needs to be explicitly enabled. Passing a
-NULL pointer for \fIbuf\fR returns 0 with no other effects.
+Passing a NULL pointer for \fIbuf\fR returns 0 with no other effects.
.SH ERRORS
.B -EINVAL
@@ -62,7 +61,8 @@ The flow has been reported down.
The flow's peer is unresponsive (flow timed out).
.B -EMSGSIZE
-The buffer was too large to be written.
+The received packet does not fit in the caller's buffer and partial
+reads are disabled (see \fBfccntl\fR(3), \fBFLOWFRNOPART\fR).
.SH ATTRIBUTES
@@ -74,11 +74,47 @@ LB|LB|LB
L|L|L.
Interface & Attribute & Value
_
-\fBflow_read\fR() & Thread safety & MT-Safe
+\fBflow_read\fR() & Thread safety & MT-Safe race:fd
_
-\fBflow_write\fR() & Thread safety & MT-Safe
+\fBflow_write\fR() & Thread safety & MT-Safe race:fd
.TE
+.SH THREAD SAFETY
+Only one thread may call
+.BR flow_read ()
+on a given file descriptor at any time. Partial-read state kept
+across calls assumes a single logical reader; two threads racing
+.BR flow_read ()
+on the same fd is undefined behaviour. Likewise, only one thread
+may call
+.BR flow_write ()
+on a given fd at a time; two writer threads on the same fd is
+undefined behaviour.
+.PP
+Combining a writer thread with a reader thread (one thread calling
+.BR flow_write (),
+another calling
+.BR flow_read ()
+or
+.BR fevent ())
+is permitted and safe. The writer does not need a dedicated reader
+thread \(em when the FRCT send window fills,
+.BR flow_write ()
+drives its own inbound rx draining internally to process incoming
+ACKs and reopen the window, clamped by the caller's
+.BR fccntl (3)
+send-timeout if any.
+.PP
+Monitoring the same fd via
+.BR fevent ()
+from a different thread is well-defined but races: events reported
+by
+.BR fevent ()
+may already have been consumed by the racing
+.BR flow_read (),
+so the second reader may then block. See
+.BR fevent (3).
+
.SH TERMINOLOGY
Please see \fBouroboros-glossary\fR(7).
diff --git a/doc/man/fqueue.3 b/doc/man/fqueue.3
index 72a0bc25..f2fb8c9f 100644
--- a/doc/man/fqueue.3
+++ b/doc/man/fqueue.3
@@ -116,6 +116,27 @@ _
\fBfevent\fR() & Thread safety & MT-Safe
.TE
+.SH THREAD SAFETY
+.BR fevent ()
+and
+.BR flow_read ()
+on the same fd from distinct threads is well-defined but races:
+events reported by
+.BR fevent ()
+may already have been consumed by the racing
+.BR flow_read (),
+so the reader may then block. Same shape as
+.BR select (2)
++
+.BR read (2)
+from distinct threads. The intended pattern is that the thread
+invoking
+.BR fevent ()
+is the same thread that calls
+.BR flow_read ()
+on the fds returned by
+.BR fqueue_next ().
+
.SH TERMINOLOGY
Please see \fBouroboros-glossary\fR(7).
diff --git a/doc/man/ouroboros.8 b/doc/man/ouroboros.8
index 5d3d9475..759b1433 100644
--- a/doc/man/ouroboros.8
+++ b/doc/man/ouroboros.8
@@ -389,11 +389,11 @@ not accept future flow allocation requests for \fIname\fR.
.SH IRM NAME COMMANDS
.PP
\fBirm name create \fIname\fR lb \fIpolicy\fR
-[sencpath \fI/path/to/server/enc.cfg\fR]
+[sencpath \fI/path/to/server/enc.conf\fR]
[scrtpath \fI/path/to/server/crt.pem\fR]
[skeypath \fI/path/to/server/key.pem\fR]
-[cencpath \fI/path/to/client/enc.cfg\fR]
+[cencpath \fI/path/to/client/enc.conf\fR]
[ccrtpath \fI/path/to/client/crt.pem\fR]
[ckeypath \fI/path/to/client/key.pem\fR]
.RS 4
@@ -401,7 +401,7 @@ Create a name \fIname\fR with a load-balancing policy and security credentials
.br
\fIpolicy\fR: round-robin, spillover
.br
-\fI/path/to/enc.cfg\fR: The path to the server and client encryption configuration.
+\fI/path/to/enc.conf\fR: The path to the server and client encryption configuration.
\fI/path/to/pem\fR: The path to the server and client certificates and
private keys, in pem format.
.br
diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt
deleted file mode 100644
index 8895c582..00000000
--- a/include/CMakeLists.txt
+++ /dev/null
@@ -1 +0,0 @@
-add_subdirectory(ouroboros)
diff --git a/include/ouroboros/CMakeLists.txt b/include/ouroboros/CMakeLists.txt
deleted file mode 100644
index 4e90bc59..00000000
--- a/include/ouroboros/CMakeLists.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.h.in"
- "${CMAKE_CURRENT_BINARY_DIR}/version.h" @ONLY)
-
-set(SOCK_BUF_SIZE 10240 CACHE STRING
- "Size of the buffer used by the UNIX sockets for local IPC")
-
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/sockets.h.in"
- "${CMAKE_CURRENT_BINARY_DIR}/sockets.h" @ONLY)
-
-set(HEADER_FILES
- cep.h
- cdefs.h
- dev.h
- errno.h
- fccntl.h
- fqueue.h
- ipcp.h
- irm.h
- name.h
- proto.h
- qos.h
- ${CMAKE_CURRENT_BINARY_DIR}/version.h
- )
-
-install(FILES ${HEADER_FILES} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ouroboros)
diff --git a/include/ouroboros/atomics.h b/include/ouroboros/atomics.h
new file mode 100644
index 00000000..8e667522
--- /dev/null
+++ b/include/ouroboros/atomics.h
@@ -0,0 +1,39 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Atomic helpers
+ *
+ * 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_ATOMICS_H
+#define OUROBOROS_LIB_ATOMICS_H
+
+#define LOAD_RELAXED(p) (__atomic_load_n(p, __ATOMIC_RELAXED))
+#define LOAD_ACQUIRE(p) (__atomic_load_n(p, __ATOMIC_ACQUIRE))
+#define LOAD(p) (__atomic_load_n(p, __ATOMIC_SEQ_CST))
+
+#define STORE_RELAXED(p, v) (__atomic_store_n(p, v, __ATOMIC_RELAXED))
+#define STORE_RELEASE(p, v) (__atomic_store_n(p, v, __ATOMIC_RELEASE))
+#define STORE(p, v) (__atomic_store_n(p, v, __ATOMIC_SEQ_CST))
+
+#define FETCH_ADD_RELAXED(p, v) (__atomic_fetch_add(p, v, __ATOMIC_RELAXED))
+#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))
+
+#endif /* OUROBOROS_LIB_ATOMICS_H */
diff --git a/include/ouroboros/bitmap.h b/include/ouroboros/bitmap.h
index 04467a8a..259fb920 100644
--- a/include/ouroboros/bitmap.h
+++ b/include/ouroboros/bitmap.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bitmap implementation
*
diff --git a/include/ouroboros/btree.h b/include/ouroboros/btree.h
index cf982856..e79599a6 100644
--- a/include/ouroboros/btree.h
+++ b/include/ouroboros/btree.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* B-trees
*
diff --git a/include/ouroboros/cdefs.h b/include/ouroboros/cdefs.h
index f4a5dc65..76f80062 100644
--- a/include/ouroboros/cdefs.h
+++ b/include/ouroboros/cdefs.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* C Definitions
*
diff --git a/include/ouroboros/cep.h b/include/ouroboros/cep.h
index 4c1737f0..df30366a 100644
--- a/include/ouroboros/cep.h
+++ b/include/ouroboros/cep.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The Ouroboros Connection Establishment Protocol
*
diff --git a/include/ouroboros/crc16.h b/include/ouroboros/crc16.h
new file mode 100644
index 00000000..df4d4f57
--- /dev/null
+++ b/include/ouroboros/crc16.h
@@ -0,0 +1,43 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 16-bit Cyclic Redundancy Check (CCITT-FALSE variant)
+ *
+ * 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/.
+ */
+
+/*
+ * Polynomial: ITU-T V.41 / CCITT-FALSE, CRC-16/IBM-3740.
+ * reveng catalog: https://reveng.sourceforge.io/crc-catalogue
+ *
+ * Intended for medium-size header check sequences (typ. <= 4 KiB).
+ * Hamming distance HD=4 up to 32751 message bits.
+ */
+
+#ifndef OUROBOROS_LIB_CRC16_H
+#define OUROBOROS_LIB_CRC16_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define CRC16_HASH_LEN 2
+
+void crc16_ccitt_false(uint16_t * crc,
+ const void * buf,
+ size_t len);
+
+#endif /* OUROBOROS_LIB_CRC16_H */
diff --git a/include/ouroboros/crc32.h b/include/ouroboros/crc32.h
index eb610797..cbcee893 100644
--- a/include/ouroboros/crc32.h
+++ b/include/ouroboros/crc32.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* 32-bit Cyclic Redundancy Check
*
diff --git a/include/ouroboros/crc64.h b/include/ouroboros/crc64.h
new file mode 100644
index 00000000..f6e407a0
--- /dev/null
+++ b/include/ouroboros/crc64.h
@@ -0,0 +1,44 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 64-bit Cyclic Redundancy Check (NVMe variant)
+ *
+ * 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/.
+ */
+
+/*
+ * Polynomial: NVM Express Base Spec, CRC-64/NVMe.
+ * reveng catalog: https://reveng.sourceforge.io/crc-catalogue
+ *
+ * Fold-by-N (PCLMUL/PMULL) algorithm:
+ * V. Gopal et al., "Fast CRC Computation for Generic Polynomials
+ * Using PCLMULQDQ", Intel white paper, 2009.
+ */
+
+#ifndef OUROBOROS_LIB_CRC64_H
+#define OUROBOROS_LIB_CRC64_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define CRC64_HASH_LEN 8
+
+void crc64_nvme(uint64_t * crc,
+ const void * buf,
+ size_t len);
+
+#endif /* OUROBOROS_LIB_CRC64_H */
diff --git a/include/ouroboros/crc8.h b/include/ouroboros/crc8.h
new file mode 100644
index 00000000..97502a25
--- /dev/null
+++ b/include/ouroboros/crc8.h
@@ -0,0 +1,43 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 8-bit Cyclic Redundancy Check (AUTOSAR variant)
+ *
+ * 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/.
+ */
+
+/*
+ * Polynomial: AUTOSAR SWS_CRC, CRC-8/AUTOSAR.
+ * reveng catalog: https://reveng.sourceforge.io/crc-catalogue
+ *
+ * Intended for short header check sequences (typ. <= 32 bytes).
+ * Hamming distance HD=4 up to 119 message bits, HD=3 up to 247.
+ */
+
+#ifndef OUROBOROS_LIB_CRC8_H
+#define OUROBOROS_LIB_CRC8_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define CRC8_HASH_LEN 1
+
+void crc8_autosar(uint8_t * crc,
+ const void * buf,
+ size_t len);
+
+#endif /* OUROBOROS_LIB_CRC8_H */
diff --git a/include/ouroboros/crypt.h b/include/ouroboros/crypt.h
index 2d7cda6d..9feaa610 100644
--- a/include/ouroboros/crypt.h
+++ b/include/ouroboros/crypt.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Cryptography
*
@@ -23,28 +23,306 @@
#ifndef OUROBOROS_LIB_CRYPT_H
#define OUROBOROS_LIB_CRYPT_H
-#include <ouroboros/shm_du_buff.h>
+#include <ouroboros/ssm_pk_buff.h>
#include <ouroboros/utils.h>
-#define IVSZ 16
-#define SYMMKEYSZ 32
-#define MSGBUFSZ 2048
+#include <assert.h>
+
+#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
+
+/* Cipher NIDs (match OpenSSL values) */
+#define NID_aes_128_gcm 895
+#define NID_aes_192_gcm 898
+#define NID_aes_256_gcm 901
+#define NID_aes_128_ctr 904
+#define NID_aes_192_ctr 905
+#define NID_aes_256_ctr 906
+#define NID_chacha20_poly1305 1018
+
+#if !defined (__APPLE__) || !defined ( HAVE_OPENSSL )
+/* KEX algorithm NIDs (match OpenSSL values) */
+#define NID_X9_62_prime256v1 415
+#define NID_secp384r1 715
+#define NID_secp521r1 716
+#define NID_X25519 1034
+#define NID_X448 1035
+#define NID_ffdhe2048 1126
+#define NID_ffdhe3072 1127
+#define NID_ffdhe4096 1128
+#endif /* __APPLE__ */
+#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 */
+
+/* KDF NIDs (match OpenSSL values) */
+#define NID_hkdf 1036
+#define NID_sha256 672
+#define NID_sha384 673
+#define NID_sha512 674
+#if !defined (__APPLE__) || !defined ( HAVE_OPENSSL )
+#define NID_sha3_256 1096
+#define NID_sha3_384 1097
+#define NID_sha3_512 1098
+#endif /* __APPLE__ */
+#define NID_blake2b512 1056
+#define NID_blake2s256 1057
+
+
+#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 CRYPT_KEY_BUFSZ 4096 /* Safe buffer for key material */
+
+#define KEM_MODE_SERVER_ENCAP 0 /* Server encapsulates (default) */
+#define KEM_MODE_CLIENT_ENCAP 1 /* Client encapsulates */
+#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 epoch; /* installed batch epoch */
+ uint8_t role; /* CRYPT_ROLE_INIT / _RESP */
+};
+
+struct sec_config {
+ struct {
+ const char * str;
+ 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 */
+#define SET_KEX_ALGO(cfg, algo_str) do { \
+ (cfg)->x.nid = kex_str_to_nid(algo_str); \
+ (cfg)->x.str = kex_nid_to_str((cfg)->x.nid); \
+ assert((cfg)->x.nid != NID_undef || (cfg)->x.str == NULL); \
+} while (0)
+
+#define SET_KEX_ALGO_NID(cfg, nid_val) do { \
+ (cfg)->x.nid = (nid_val); \
+ (cfg)->x.str = kex_nid_to_str((cfg)->x.nid); \
+ assert((cfg)->x.nid != NID_undef || (cfg)->x.str == NULL); \
+} while (0)
+
+#define SET_KEX_KEM_MODE(cfg, mode_val) do { \
+ (cfg)->x.mode = (mode_val); \
+} while (0)
+
+#define SET_KEX_KDF(cfg, kdf_str) do { \
+ (cfg)->k.nid = md_str_to_nid(kdf_str); \
+ (cfg)->k.str = md_nid_to_str((cfg)->k.nid); \
+ assert((cfg)->k.nid != NID_undef || (cfg)->k.str == NULL); \
+} while (0)
+
+#define SET_KEX_KDF_NID(cfg, nid_val) do { \
+ (cfg)->k.nid = (nid_val); \
+ (cfg)->k.str = md_nid_to_str((cfg)->k.nid); \
+ assert((cfg)->k.nid != NID_undef || (cfg)->k.str == NULL); \
+} while (0)
+
+#define SET_KEX_CIPHER(cfg, cipher_str) do { \
+ (cfg)->c.nid = crypt_str_to_nid(cipher_str); \
+ (cfg)->c.str = crypt_nid_to_str((cfg)->c.nid); \
+ assert((cfg)->c.nid != NID_undef || (cfg)->c.str == NULL); \
+} while (0)
+
+#define SET_KEX_CIPHER_NID(cfg, nid_val) do { \
+ (cfg)->c.nid = (nid_val); \
+ (cfg)->c.str = crypt_nid_to_str((cfg)->c.nid); \
+ assert((cfg)->c.nid != NID_undef || (cfg)->c.str == NULL); \
+} while (0)
+
+#define SET_KEX_DIGEST(cfg, digest_str) do { \
+ (cfg)->d.nid = md_str_to_nid(digest_str); \
+ (cfg)->d.str = md_nid_to_str((cfg)->d.nid); \
+ assert((cfg)->d.nid != NID_undef || (cfg)->d.str == NULL); \
+} while (0)
+
+#define SET_KEX_DIGEST_NID(cfg, nid_val) do { \
+ (cfg)->d.nid = (nid_val); \
+ (cfg)->d.str = md_nid_to_str((cfg)->d.nid); \
+ assert((cfg)->d.nid != NID_undef || (cfg)->d.str == NULL); \
+} while (0)
+
+#define CLEAR_KEX_ALGO(cfg) do { \
+ (cfg)->x.nid = NID_undef; \
+ (cfg)->x.str = NULL; \
+} while (0)
+
+#define CLEAR_KEX_KDF(cfg) do { \
+ (cfg)->k.nid = NID_undef; \
+ (cfg)->k.str = NULL; \
+} while (0)
+
+#define CLEAR_KEX_CIPHER(cfg) do { \
+ (cfg)->c.nid = NID_undef; \
+ (cfg)->c.str = NULL; \
+} while (0)
+
+#define CLEAR_KEX_DIGEST(cfg) do { \
+ (cfg)->d.nid = NID_undef; \
+ (cfg)->d.str = NULL; \
+} while (0)
struct auth_ctx;
struct crypt_ctx;
-struct crypt_ctx * crypt_create_ctx(const uint8_t * key);
+struct auth_ctx * auth_create_ctx(void);
+
+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,
+ buffer_t * sig);
+
+int auth_verify_sig(void * pk,
+ int md_nid,
+ buffer_t msg,
+ buffer_t sig);
+
+int load_sec_config_file(struct sec_config * cfg,
+ const char * path);
+
+int kex_pkp_create(struct sec_config * cfg,
+ void ** pkp,
+ uint8_t * pk);
+
+void kex_pkp_destroy(void * pkp);
+
+int kex_dhe_derive(struct sec_config * cfg,
+ void * pkp,
+ buffer_t pk,
+ uint8_t * s);
+
+ssize_t kex_kem_encap(buffer_t pk,
+ uint8_t * ct,
+ int kdf_nid,
+ uint8_t * s);
+
+ssize_t kex_kem_encap_raw(buffer_t pk,
+ uint8_t * ct,
+ int kdf_nid,
+ uint8_t * s);
+
+int kex_kem_decap(void * pkp,
+ buffer_t ct,
+ int kdf_nid,
+ uint8_t * s);
+
+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);
+
+const char * kex_nid_to_str(uint16_t nid);
+
+uint16_t kex_str_to_nid(const char * algo);
+
+struct crypt_ctx * crypt_create_ctx(struct crypt_sk * sk);
void crypt_destroy_ctx(struct crypt_ctx * ctx);
-int crypt_dh_pkp_create(void ** pkp,
- uint8_t * pk);
+int crypt_validate_nid(int nid);
+
+const char * crypt_nid_to_str(uint16_t nid);
+
+uint16_t crypt_str_to_nid(const char * cipher);
+
+int crypt_cipher_rank(int nid);
+
+int crypt_kdf_rank(int nid);
+
+int crypt_kex_rank(int nid);
+
+int md_validate_nid(int nid);
+
+const char * md_nid_to_str(uint16_t nid);
+
+uint16_t md_str_to_nid(const char * kdf);
-void crypt_dh_pkp_destroy(void * pkp);
+ssize_t md_digest(int md_nid,
+ buffer_t in,
+ uint8_t * out);
-int crypt_dh_derive(void * pkp,
- buffer_t pk,
- uint8_t * s);
+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,
@@ -54,6 +332,37 @@ int crypt_decrypt(struct crypt_ctx * ctx,
buffer_t in,
buffer_t * out);
+/* 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);
@@ -77,6 +386,22 @@ int crypt_load_privkey_str(const char * str,
int crypt_load_pubkey_str(const char * str,
void ** key);
+int crypt_load_pubkey_file(const char * path,
+ void ** key);
+
+int crypt_load_pubkey_file_to_der(const char * path,
+ buffer_t * buf);
+
+int crypt_load_pubkey_raw_file(const char * path,
+ buffer_t * buf);
+
+int crypt_load_privkey_raw_file(const char * path,
+ 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);
@@ -91,24 +416,22 @@ int crypt_crt_der(const void * crt,
int crypt_check_crt_name(void * crt,
const char * name);
-struct auth_ctx * auth_create_ctx(void);
+int crypt_get_crt_name(void * crt,
+ char * name);
-void auth_destroy_ctx(struct auth_ctx * ctx);
+void crypt_cleanup(void);
-int auth_add_crt_to_store(struct auth_ctx * ctx,
- void * crt);
+/* Secure memory allocation for sensitive data (keys, secrets) */
+int crypt_secure_malloc_init(size_t max);
-void auth_destroy_ctx(struct auth_ctx * ctx);
+void crypt_secure_malloc_fini(void);
-int auth_verify_crt(struct auth_ctx * ctx,
- void * crt);
+void * crypt_secure_malloc(size_t size);
-int auth_sign(void * pkp,
- buffer_t msg,
- buffer_t * sig);
+void crypt_secure_free(void * ptr,
+ size_t size);
-int auth_verify_sig(void * pk,
- buffer_t msg,
- buffer_t sig);
+void crypt_secure_clear(void * ptr,
+ size_t size);
#endif /* OUROBOROS_LIB_CRYPT_H */
diff --git a/include/ouroboros/dev.h b/include/ouroboros/dev.h
index 61464fbf..b25657d8 100644
--- a/include/ouroboros/dev.h
+++ b/include/ouroboros/dev.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* API for applications
*
diff --git a/include/ouroboros/endian.h b/include/ouroboros/endian.h
index 6c3493d9..1bcda3f2 100644
--- a/include/ouroboros/endian.h
+++ b/include/ouroboros/endian.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Endianness
*
diff --git a/include/ouroboros/errno.h b/include/ouroboros/errno.h
index 6b808241..eedd978f 100644
--- a/include/ouroboros/errno.h
+++ b/include/ouroboros/errno.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros specific error numbers
*
@@ -37,5 +37,6 @@
#ifndef EAUTH /* Exists on BSD */
#define EAUTH 1009 /* Authentication error */
#endif
+#define EREPLAY 1010 /* OAP replay detected */
#endif /* OUROBOROS_ERRNO_H */
diff --git a/include/ouroboros/fccntl.h b/include/ouroboros/fccntl.h
index aa2b0d14..e91e91dd 100644
--- a/include/ouroboros/fccntl.h
+++ b/include/ouroboros/fccntl.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Flow and FRCT connection control
*
@@ -50,6 +50,12 @@
#define FRCTFRESCNTL 00000002 /* Feedback from receiver */
#define FRCTFLINGER 00000004 /* Send unsent data */
+/* All user-visible bits (readable via FRCTGFLAGS). */
+#define FRCTFMASK (FRCTFRTX | FRCTFRESCNTL | FRCTFLINGER)
+
+/* Subset writable via FRCTSFLAGS; FRCTFRTX is fixed at flow_alloc. */
+#define FRCTFSETMASK (FRCTFRESCNTL | FRCTFLINGER)
+
/* Flow operations */
#define FLOWSRCVTIMEO 00000001 /* Set read timeout */
#define FLOWGRCVTIMEO 00000002 /* Get read timeout */
@@ -60,10 +66,17 @@
#define FLOWGFLAGS 00000007 /* Get flags for flow */
#define FLOWGRXQLEN 00000010 /* Get queue length on rx */
#define FLOWGTXQLEN 00000011 /* Get queue length on tx */
+#define FLOWGMTU 00000012 /* Get per-packet MTU */
/* FRCT operations */
#define FRCTSFLAGS 00001000 /* Set flags for FRCT */
#define FRCTGFLAGS 00002000 /* Get flags for FRCT */
+#define FRCTSMAXSDU 00003000 /* Set max recv SDU size */
+#define FRCTGMAXSDU 00004000 /* Get max recv SDU size */
+#define FRCTSRRINGSZ 00005000 /* Set stream rcv ring sz */
+#define FRCTGRRINGSZ 00006000 /* Get stream rcv ring sz */
+#define FRCTSRTOMIN 00007000 /* Set RTO floor (ns) */
+#define FRCTGRTOMIN 00010000 /* Get RTO floor (ns) */
__BEGIN_DECLS
diff --git a/include/ouroboros/flow.h b/include/ouroboros/flow.h
index 77b7737e..8b096410 100644
--- a/include/ouroboros/flow.h
+++ b/include/ouroboros/flow.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Flows
*
@@ -25,9 +25,12 @@
#include <ouroboros/qos.h>
+#include <stdint.h>
#include <sys/types.h>
- enum flow_state { /* DO NOT CHANGE ORDER! */
+#define SYMMKEYSZ 32
+
+enum flow_state { /* DO NOT CHANGE ORDER! */
FLOW_INIT = 0,
FLOW_ALLOC_PENDING,
FLOW_ACCEPT_PENDING,
@@ -44,8 +47,12 @@ struct flow_info {
pid_t n_pid;
pid_t n_1_pid;
+ uid_t uid; /* 0 = privileged (GSPP), > 0 = PUP uid */
+
time_t mpl;
+ uint32_t mtu; /* n-1 layer MTU in bytes, 0 = unknown */
+
struct qos_spec qs;
enum flow_state state;
diff --git a/include/ouroboros/fqueue.h b/include/ouroboros/fqueue.h
index 8eb2ff50..322da3ea 100644
--- a/include/ouroboros/fqueue.h
+++ b/include/ouroboros/fqueue.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Flow queues
*
@@ -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 c44c2c8a..c6609ffc 100644
--- a/include/ouroboros/hash.h
+++ b/include/ouroboros/hash.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Hashing functions
*
@@ -38,6 +38,9 @@ enum hash_algo {
HASH_SHA3_512 = DIR_HASH_SHA3_512,
HASH_CRC32,
HASH_MD5,
+ HASH_CRC64,
+ HASH_CRC8,
+ HASH_CRC16,
};
#define HASH_FMT32 "%02x%02x%02x%02x"
@@ -86,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 35e07414..d00d6f08 100644
--- a/include/ouroboros/ipcp-dev.h
+++ b/include/ouroboros/ipcp-dev.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Additional API for IPCPs
*
@@ -25,32 +25,41 @@
#include <ouroboros/ipcp.h>
#include <ouroboros/qoscube.h>
-#include <ouroboros/shm_rdrbuff.h>
+#include <ouroboros/ssm_pool.h>
#include <ouroboros/utils.h>
+#include <stdint.h>
+
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);
+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 shm_du_buff ** sdb);
+ struct ssm_pk_buff ** spb);
int ipcp_flow_write(int fd,
- struct shm_du_buff * sdb);
+ struct ssm_pk_buff * spb);
int np1_flow_read(int fd,
- struct shm_du_buff ** sdb);
+ struct ssm_pk_buff ** spb,
+ struct ssm_pool * pool);
int np1_flow_write(int fd,
- struct shm_du_buff * sdb);
+ struct ssm_pk_buff * spb,
+ struct ssm_pool * pool);
int ipcp_flow_dealloc(int fd);
@@ -61,9 +70,9 @@ int ipcp_flow_get_qoscube(int fd,
size_t ipcp_flow_queued(int fd);
-int ipcp_sdb_reserve(struct shm_du_buff ** sdb,
+int ipcp_spb_reserve(struct ssm_pk_buff ** spb,
size_t len);
-void ipcp_sdb_release(struct shm_du_buff * sdb);
+void ipcp_spb_release(struct ssm_pk_buff * spb);
#endif /* OUROBOROS_LIB_IPCP_DEV_H */
diff --git a/include/ouroboros/ipcp.h b/include/ouroboros/ipcp.h
index c397f250..e29b080a 100644
--- a/include/ouroboros/ipcp.h
+++ b/include/ouroboros/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPCP definitions and policies
*
diff --git a/include/ouroboros/irm.h b/include/ouroboros/irm.h
index 70a21ed7..7cb71c21 100644
--- a/include/ouroboros/irm.h
+++ b/include/ouroboros/irm.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API to instruct the IPC Resource Manager
*
@@ -53,13 +53,13 @@ int irm_bootstrap_ipcp(pid_t pid,
const struct ipcp_config * conf);
int irm_connect_ipcp(pid_t pid,
- const char * component,
const char * dst,
+ const char * component,
qosspec_t qs);
int irm_disconnect_ipcp(pid_t pid,
- const char * component,
- const char * dst);
+ const char * dst,
+ const char * component);
int irm_bind_program(const char * prog,
const char * name,
diff --git a/include/ouroboros/list.h b/include/ouroboros/list.h
index f3ea0e46..26138b28 100644
--- a/include/ouroboros/list.h
+++ b/include/ouroboros/list.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Simple doubly linked list implementation.
*
@@ -33,6 +33,11 @@ struct list_head {
struct list_head * prv;
};
+struct llist {
+ struct list_head list;
+ size_t len;
+};
+
#define list_entry(ptr, type, mbr) \
((type *)((uint8_t *)(ptr) - offsetof(type, mbr)))
@@ -48,19 +53,89 @@ struct list_head {
#define list_for_each_safe(p, t, h) \
for (p = (h)->nxt, t = p->nxt; p != (h); p = t, t = p->nxt)
-void list_head_init(struct list_head * h);
+#define list_head_init(h) do { \
+ (h)->nxt = (h); \
+ (h)->prv = (h); \
+} while (0)
+
+#define __list_add(_n, _prv, _nxt) do { \
+ struct list_head * __nxt = (_nxt); \
+ struct list_head * __prv = (_prv); \
+ struct list_head * __n = (_n); \
+ __nxt->prv = __n; \
+ __n->nxt = __nxt; \
+ __n->prv = __prv; \
+ __prv->nxt = __n; \
+} while (0)
+
+#define __list_del(_prv, _nxt) do { \
+ struct list_head * __nxt = (_nxt); \
+ struct list_head * __prv = (_prv); \
+ __nxt->prv = __prv; \
+ __prv->nxt = __nxt; \
+} while (0)
+
+#define list_add(n, h) do { \
+ __list_add(n, h, (h)->nxt); \
+} while (0)
+
+#define list_add_tail(n, h) do { \
+ __list_add(n, (h)->prv, h); \
+} while (0)
+
+#define list_del(e) do { \
+ __list_del((e)->prv, (e)->nxt); \
+ (e)->nxt = (e)->prv = (e); \
+} while (0)
+
+#define list_move(n, h) do { \
+ __list_del((n)->prv, (n)->nxt); \
+ __list_add(n, h, (h)->nxt); \
+} while (0)
+
+#define list_is_empty(h) ((h)->nxt == (h))
+
+#define llist_init(l) do { \
+ list_head_init(&(l)->list); \
+ (l)->len = 0; \
+} while (0)
+
+#define llist_add(e, l) do { \
+ list_add(e, &(l)->list); \
+ (l)->len++; \
+} while (0)
+
+#define llist_add_tail(e, l) do { \
+ list_add_tail(e, &(l)->list); \
+ (l)->len++; \
+} while (0)
+
+#define llist_add_at(e, pos, l) do { \
+ list_add(e, pos); \
+ (l)->len++; \
+} while (0)
+
+#define llist_add_tail_at(e, pos, l) do { \
+ list_add_tail(e, pos); \
+ (l)->len++; \
+} while (0)
+
+#define llist_del(e, l) do { \
+ list_del(e); \
+ (l)->len--; \
+} while (0)
-void list_add(struct list_head * e,
- struct list_head * h);
+#define llist_is_empty(l) ((l)->len == 0)
-void list_add_tail(struct list_head * e,
- struct list_head * h);
+#define llist_first_entry(l, type, mbr) \
+ list_first_entry(&(l)->list, type, mbr)
-void list_del(struct list_head * e);
+#define llist_last_entry(l, type, mbr) \
+ list_last_entry(&(l)->list, type, mbr)
-void list_move(struct list_head * dst,
- struct list_head * src);
+#define llist_for_each(p, l) list_for_each(p, &(l)->list)
-bool list_is_empty(const struct list_head * h);
+#define llist_for_each_safe(p, t, l) \
+ list_for_each_safe(p, t, &(l)->list)
#endif /* OUROBOROS_LIB_LIST_H */
diff --git a/include/ouroboros/local-dev.h b/include/ouroboros/local-dev.h
index da62e31c..0d74ea78 100644
--- a/include/ouroboros/local-dev.h
+++ b/include/ouroboros/local-dev.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Optimized calls for the local IPCPs
*
@@ -23,9 +23,11 @@
#ifndef OUROBOROS_LIB_LOCAL_DEV_H
#define OUROBOROS_LIB_LOCAL_DEV_H
-ssize_t local_flow_read(int fd);
+#include <ouroboros/ssm_pool.h>
-int local_flow_write(int fd,
- size_t idx);
+int local_flow_transfer(int src_fd,
+ int dst_fd,
+ struct ssm_pool * src_pool,
+ struct ssm_pool * dst_pool);
#endif /* OUROBOROS_LIB_LOCAL_DEV_H */
diff --git a/include/ouroboros/lockfile.h b/include/ouroboros/lockfile.h
index 85a57313..7c9edffc 100644
--- a/include/ouroboros/lockfile.h
+++ b/include/ouroboros/lockfile.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Lockfile for Ouroboros
*
diff --git a/include/ouroboros/logs.h b/include/ouroboros/logs.h
index f1c401fa..1ae77673 100644
--- a/include/ouroboros/logs.h
+++ b/include/ouroboros/logs.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Logging facilities
*
@@ -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,8 +85,10 @@ void log_fini(void);
printf(CLR_RESET "\n"); \
fflush(stdout); \
} \
+ pthread_setcancelstate(__cs, NULL); \
} while (0)
+#ifndef OUROBOROS_DISABLE_LOGGING
#define log_err(...) \
__olog(CLR_RED, ERROR_CODE, LOG_ERR, __VA_ARGS__)
#define log_warn(...) \
@@ -95,20 +103,29 @@ void log_fini(void);
__olog_id(CLR_YELLOW, WARN_CODE, LOG_WARNING, id, fmt, ## __VA_ARGS__)
#define log_info_id(id, fmt, ...) \
__olog_id(CLR_GREEN, INFO_CODE, LOG_INFO, id, fmt, ## __VA_ARGS__)
+#else /* OUROBOROS_DISABLE_LOGGING: all logging disabled */
+#define log_err(...) do { } while (0)
+#define log_warn(...) do { } while (0)
+#define log_info(...) do { } while (0)
-#ifdef CONFIG_OUROBOROS_DEBUG
+#define log_err_id(id, fmt, ...) do { (void)(id); } while (0)
+#define log_warn_id(id, fmt, ...) do { (void)(id); } while (0)
+#define log_info_id(id, fmt, ...) do { (void)(id); } while (0)
+
+#endif /* OUROBOROS_DISABLE_LOGGING */
+
+#if defined(OUROBOROS_DISABLE_LOGGING) || !defined(CONFIG_OUROBOROS_DEBUG)
+#define log_dbg(...) do { } while (0)
+#define log_dbg_id(id, ...) do { (void)(id); } while (0)
+#define log_proto(...) do { } while (0)
+#define log_proto_id(id, ...) do { (void)(id); } while (0)
+#else
#define log_dbg(...) __olog("", DEBUG_CODE, LOG_DEBUG, __VA_ARGS__)
#define log_dbg_id(id, fmt, ...) \
__olog_id("", DEBUG_CODE, LOG_DEBUG, id, fmt, ## __VA_ARGS__)
#define log_proto(...) __olog(CLR_BLUE, PROTO_CODE, LOG_DEBUG, __VA_ARGS__)
-#define log_proto_id(id, fmt, ...) \
+#define log_proto_id(id, fmt, ...) \
__olog_id(CLR_BLUE, INFO_CODE, LOG_INFO, id, fmt, ## __VA_ARGS__)
-
-#else
-#define log_dbg(...) do { } while (0)
-#define log_dbg_id(...) do { } while (0)
-#define log_proto(...) do { } while (0)
-#define log_proto_id(...) do { } while (0)
#endif
#endif /* OUROBOROS_LIB_LOGS_H */
diff --git a/include/ouroboros/md5.h b/include/ouroboros/md5.h
index 85a22544..5b8d9e8f 100644
--- a/include/ouroboros/md5.h
+++ b/include/ouroboros/md5.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* MD5 algorithm
*
diff --git a/include/ouroboros/name.h b/include/ouroboros/name.h
index 14fdd504..a3aac8c4 100644
--- a/include/ouroboros/name.h
+++ b/include/ouroboros/name.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Names
*
@@ -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/notifier.h b/include/ouroboros/notifier.h
index db945f1e..2cd87221 100644
--- a/include/ouroboros/notifier.h
+++ b/include/ouroboros/notifier.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Notifier event system using callbacks
*
diff --git a/include/ouroboros/np1_flow.h b/include/ouroboros/np1_flow.h
index 4110ab6a..758b6db8 100644
--- a/include/ouroboros/np1_flow.h
+++ b/include/ouroboros/np1_flow.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Adapter functions for N + 1 flow descriptors
*
@@ -36,13 +36,17 @@ 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,
.bandwidth = 0,
.availability = 0,
.loss = UINT32_MAX,
.ber = UINT32_MAX,
- .in_order = 0,
.max_gap = UINT32_MAX,
.timeout = 0
};
diff --git a/include/ouroboros/proc.h b/include/ouroboros/proc.h
index 80c67227..a424d64a 100644
--- a/include/ouroboros/proc.h
+++ b/include/ouroboros/proc.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Processes and Programs
*
@@ -31,8 +31,9 @@
/* Processes */
struct proc_info {
pid_t pid;
- char prog[PROG_NAME_SIZE + 1]; /* program instantiated */
-
+ char prog[PROG_NAME_SIZE + 1];
+ uid_t uid;
+ gid_t gid;
};
/* Programs */
diff --git a/include/ouroboros/proto.h b/include/ouroboros/proto.h
index 5c863c8b..6a5a9ba7 100644
--- a/include/ouroboros/proto.h
+++ b/include/ouroboros/proto.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Protocol syntax definitions
*
diff --git a/include/ouroboros/protobuf.h b/include/ouroboros/protobuf.h
index 780d58dc..951e8fd6 100644
--- a/include/ouroboros/protobuf.h
+++ b/include/ouroboros/protobuf.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Protobuf syntax conversion
*
diff --git a/include/ouroboros/pthread.h b/include/ouroboros/pthread.h
index 7044cb5e..3ca79d10 100644
--- a/include/ouroboros/pthread.h
+++ b/include/ouroboros/pthread.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Useful cleanup functions for pthreads
*
@@ -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 2be31305..7980ad00 100644
--- a/include/ouroboros/qos.h
+++ b/include/ouroboros/qos.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Quality of Service specification
*
@@ -28,79 +28,88 @@
#define DEFAULT_PEER_TIMEOUT 120000
+/* 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 */
+};
+
typedef struct qos_spec {
+ uint8_t service; /* enum qos_service; gates FRCT (>0). */
uint32_t delay; /* In ms. */
uint64_t bandwidth; /* In bits/s. */
uint8_t availability; /* Class of 9s. */
uint32_t loss; /* Packet loss. */
uint32_t ber; /* Bit error rate, errors per billion bits. */
- uint8_t in_order; /* In-order delivery, enables FRCT. */
uint32_t max_gap; /* In ms. */
uint32_t timeout; /* Peer timeout time, in ms, 0 = no timeout. */
} qosspec_t;
+/* "_safe" = integrity check (ber=0). "rt" = latency over reliability. */
+
static const qosspec_t qos_raw = {
+ .service = SVC_RAW,
.delay = UINT32_MAX,
.bandwidth = 0,
.availability = 0,
.loss = 1,
.ber = 1,
- .in_order = 0,
.max_gap = UINT32_MAX,
- .timeout = DEFAULT_PEER_TIMEOUT
+ .timeout = 0
};
-static const qosspec_t qos_raw_no_errors = {
+static const qosspec_t qos_raw_safe = {
+ .service = SVC_RAW,
.delay = UINT32_MAX,
.bandwidth = 0,
.availability = 0,
.loss = 1,
.ber = 0,
- .in_order = 0,
.max_gap = UINT32_MAX,
- .timeout = DEFAULT_PEER_TIMEOUT
+ .timeout = 0
};
-static const qosspec_t qos_best_effort = {
- .delay = UINT32_MAX,
- .bandwidth = 0,
- .availability = 0,
+static const qosspec_t qos_rt = {
+ .service = SVC_MESSAGE,
+ .delay = 100,
+ .bandwidth = UINT64_MAX,
+ .availability = 3,
.loss = 1,
- .ber = 0,
- .in_order = 1,
- .max_gap = UINT32_MAX,
+ .ber = 1,
+ .max_gap = 100,
.timeout = DEFAULT_PEER_TIMEOUT
};
-static const qosspec_t qos_video = {
+static const qosspec_t qos_rt_safe = {
+ .service = SVC_MESSAGE,
.delay = 100,
.bandwidth = UINT64_MAX,
.availability = 3,
.loss = 1,
.ber = 0,
- .in_order = 1,
.max_gap = 100,
.timeout = DEFAULT_PEER_TIMEOUT
};
-static const qosspec_t qos_voice = {
- .delay = 50,
- .bandwidth = 100000,
- .availability = 5,
- .loss = 1,
+static const qosspec_t qos_msg = {
+ .service = SVC_MESSAGE,
+ .delay = 1000,
+ .bandwidth = 0,
+ .availability = 0,
+ .loss = 0,
.ber = 0,
- .in_order = 1,
- .max_gap = 50,
+ .max_gap = 2000,
.timeout = DEFAULT_PEER_TIMEOUT
};
-static const qosspec_t qos_data = {
+static const qosspec_t qos_stream = {
+ .service = SVC_STREAM,
.delay = 1000,
.bandwidth = 0,
.availability = 0,
.loss = 0,
.ber = 0,
- .in_order = 1,
.max_gap = 2000,
.timeout = DEFAULT_PEER_TIMEOUT
};
diff --git a/include/ouroboros/qoscube.h b/include/ouroboros/qoscube.h
index ed20484c..799aa22c 100644
--- a/include/ouroboros/qoscube.h
+++ b/include/ouroboros/qoscube.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Quality of Service cubes
*
diff --git a/include/ouroboros/random.h b/include/ouroboros/random.h
index e1b25e5d..df4b7758 100644
--- a/include/ouroboros/random.h
+++ b/include/ouroboros/random.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Pseudo random generator
*
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/rib.h b/include/ouroboros/rib.h
index cdc5a9d5..c2ef0f52 100644
--- a/include/ouroboros/rib.h
+++ b/include/ouroboros/rib.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* RIB export using FUSE
*
diff --git a/include/ouroboros/serdes-irm.h b/include/ouroboros/serdes-irm.h
index 1d041541..a5854d5b 100644
--- a/include/ouroboros/serdes-irm.h
+++ b/include/ouroboros/serdes-irm.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros IRM Protocol - serialization/deserialization
*
@@ -23,12 +23,15 @@
#ifndef OUROBOROS_LIB_SERDES_IRM_H
#define OUROBOROS_LIB_SERDES_IRM_H
+#include <ouroboros/crypt.h>
#include <ouroboros/flow.h>
#include <ouroboros/ipcp.h>
+#include <ouroboros/proc.h>
#include <ouroboros/time.h>
#include <ouroboros/utils.h>
#include <inttypes.h>
+#include <stdbool.h>
int flow_alloc__irm_req_ser(buffer_t * buf,
const struct flow_info * flow,
@@ -49,28 +52,40 @@ int ipcp_flow_req_arr__irm_req_ser(buffer_t * buf,
const struct flow_info * flow,
const 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,
const buffer_t * data);
-/* response to alloc / join / accept / flow_req_arr */
int flow__irm_result_des(buffer_t * buf,
struct flow_info * flow,
- buffer_t * sk);
+ struct crypt_sk * sk);
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);
int ipcp_create_r__irm_req_ser(buffer_t * buf,
const struct ipcp_info * ipcp);
-int proc_announce__irm_req_ser(buffer_t * buf,
- const char * prog);
+int proc_announce__irm_req_ser(buffer_t * buf,
+ const struct proc_info * proc);
int proc_exit__irm_req_ser(buffer_t * buf);
diff --git a/include/ouroboros/serdes-oep.h b/include/ouroboros/serdes-oep.h
index af4446c1..2a3e00cb 100644
--- a/include/ouroboros/serdes-oep.h
+++ b/include/ouroboros/serdes-oep.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros Enrollment Protocol - serialization/deserialization
*
@@ -66,12 +66,4 @@ ssize_t enroll_ack_ser(const struct enroll_ack * ack,
int enroll_ack_des(struct enroll_ack * ack,
const buffer_t buf);
-#ifdef DEBUG_PROTO_OEP
-void debug_enroll_req(const struct enroll_req * req);
-
-void debug_enroll_resp(const struct enroll_resp * resp);
-
-void debug_enroll_ack(const struct enroll_ack * ack);
-#endif /* DEBUG_PROTO_OEP */
-
#endif /* OUROBOROS_LIB_SERDES_OEP_H*/
diff --git a/include/ouroboros/sha3.h b/include/ouroboros/sha3.h
index 04871f3b..08f993b7 100644
--- a/include/ouroboros/sha3.h
+++ b/include/ouroboros/sha3.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* SHA3 algorithm
*
diff --git a/include/ouroboros/shm_du_buff.h b/include/ouroboros/shm_du_buff.h
deleted file mode 100644
index c25d4b95..00000000
--- a/include/ouroboros/shm_du_buff.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Data Buffer element in Random Deletion Ring Buffer
- *
- * 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_SHM_DU_BUFF_H
-#define OUROBOROS_LIB_SHM_DU_BUFF_H
-
-#include <sys/types.h>
-#include <stdint.h>
-
-struct shm_du_buff;
-
-size_t shm_du_buff_get_idx(struct shm_du_buff * sdb);
-
-uint8_t * shm_du_buff_head(struct shm_du_buff * sdb);
-
-uint8_t * shm_du_buff_tail(struct shm_du_buff * sdb);
-
-size_t shm_du_buff_len(struct shm_du_buff * sdb);
-
-uint8_t * shm_du_buff_head_alloc(struct shm_du_buff * sdb,
- size_t size);
-
-uint8_t * shm_du_buff_tail_alloc(struct shm_du_buff * sdb,
- size_t size);
-
-uint8_t * shm_du_buff_head_release(struct shm_du_buff * sdb,
- size_t size);
-
-uint8_t * shm_du_buff_tail_release(struct shm_du_buff * sdb,
- size_t size);
-
-void shm_du_buff_truncate(struct shm_du_buff * sdb,
- size_t len);
-
-int shm_du_buff_wait_ack(struct shm_du_buff * sdb);
-
-int shm_du_buff_ack(struct shm_du_buff * sdb);
-
-#endif /* OUROBOROS_LIB_SHM_DU_BUFF_H */
diff --git a/include/ouroboros/shm_rbuff.h b/include/ouroboros/shm_rbuff.h
deleted file mode 100644
index 4323d4e1..00000000
--- a/include/ouroboros/shm_rbuff.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Ring buffer for incoming packets
- *
- * 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_SHM_RBUFF_H
-#define OUROBOROS_LIB_SHM_RBUFF_H
-
-#include <sys/types.h>
-#include <sys/time.h>
-
-#include <stdint.h>
-
-#define ACL_RDWR 0000
-#define ACL_RDONLY 0001
-#define ACL_FLOWDOWN 0002
-#define ACL_FLOWPEER 0004
-
-struct shm_rbuff;
-
-struct shm_rbuff * shm_rbuff_create(pid_t pid,
- int flow_id);
-
-struct shm_rbuff * shm_rbuff_open(pid_t pid,
- int flow_id);
-
-void shm_rbuff_close(struct shm_rbuff * rb);
-
-void shm_rbuff_destroy(struct shm_rbuff * rb);
-
-void shm_rbuff_set_acl(struct shm_rbuff * rb,
- uint32_t flags);
-
-uint32_t shm_rbuff_get_acl(struct shm_rbuff * rb);
-
-void shm_rbuff_fini(struct shm_rbuff * rb);
-
-int shm_rbuff_write(struct shm_rbuff * rb,
- size_t idx);
-
-int shm_rbuff_write_b(struct shm_rbuff * rb,
- size_t idx,
- const struct timespec * abstime);
-
-ssize_t shm_rbuff_read(struct shm_rbuff * rb);
-
-ssize_t shm_rbuff_read_b(struct shm_rbuff * rb,
- const struct timespec * abstime);
-
-size_t shm_rbuff_queued(struct shm_rbuff * rb);
-
-#endif /* OUROBOROS_LIB_SHM_RBUFF_H */
diff --git a/include/ouroboros/shm_rdrbuff.h b/include/ouroboros/shm_rdrbuff.h
deleted file mode 100644
index 4f9a215a..00000000
--- a/include/ouroboros/shm_rdrbuff.h
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Random Deletion Ring Buffer for Data Units
- *
- * 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_SHM_RDRBUFF_H
-#define OUROBOROS_LIB_SHM_RDRBUFF_H
-
-#include <ouroboros/shm_du_buff.h>
-#include <ouroboros/time.h>
-
-#include <pthread.h>
-#include <stdint.h>
-#include <sys/types.h>
-
-struct shm_rdrbuff;
-
-struct shm_rdrbuff * shm_rdrbuff_create(void);
-
-struct shm_rdrbuff * shm_rdrbuff_open(void);
-
-void shm_rdrbuff_close(struct shm_rdrbuff * rdrb);
-
-void shm_rdrbuff_destroy(struct shm_rdrbuff * rdrb);
-
-void shm_rdrbuff_purge(void);
-
-/* Returns block index, a ptr and du_buff. */
-ssize_t shm_rdrbuff_alloc(struct shm_rdrbuff * rdrb,
- size_t count,
- uint8_t ** ptr,
- struct shm_du_buff ** sdb);
-
-ssize_t shm_rdrbuff_alloc_b(struct shm_rdrbuff * rdrb,
- size_t count,
- uint8_t ** ptr,
- struct shm_du_buff ** sdb,
- const struct timespec * abstime);
-
-ssize_t shm_rdrbuff_read(uint8_t ** dst,
- struct shm_rdrbuff * rdrb,
- size_t idx);
-
-struct shm_du_buff * shm_rdrbuff_get(struct shm_rdrbuff * rdrb,
- size_t idx);
-
-int shm_rdrbuff_remove(struct shm_rdrbuff * rdrb,
- size_t idx);
-
-#endif /* OUROBOROS_LIB_SHM_RDRBUFF_H */
diff --git a/include/ouroboros/sockets.h.in b/include/ouroboros/sockets.h.in
index 1a6974ac..458f82b3 100644
--- a/include/ouroboros/sockets.h.in
+++ b/include/ouroboros/sockets.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The sockets layer to communicate between daemons
*
diff --git a/include/ouroboros/shm_flow_set.h b/include/ouroboros/ssm_flow_set.h
index 09e37649..80bab1c2 100644
--- a/include/ouroboros/shm_flow_set.h
+++ b/include/ouroboros/ssm_flow_set.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Management of flow_sets for fqueue
*
@@ -20,8 +20,8 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
-#ifndef OUROBOROS_LIB_SHM_FLOW_SET_H
-#define OUROBOROS_LIB_SHM_FLOW_SET_H
+#ifndef OUROBOROS_LIB_SSM_FLOW_SET_H
+#define OUROBOROS_LIB_SSM_FLOW_SET_H
#include <ouroboros/fqueue.h>
@@ -32,38 +32,38 @@ struct flowevent {
int event;
};
-struct shm_flow_set;
+struct ssm_flow_set;
-struct shm_flow_set * shm_flow_set_create(pid_t pid);
+struct ssm_flow_set * ssm_flow_set_create(pid_t pid);
-void shm_flow_set_destroy(struct shm_flow_set * set);
+void ssm_flow_set_destroy(struct ssm_flow_set * set);
-struct shm_flow_set * shm_flow_set_open(pid_t pid);
+struct ssm_flow_set * ssm_flow_set_open(pid_t pid);
-void shm_flow_set_close(struct shm_flow_set * set);
+void ssm_flow_set_close(struct ssm_flow_set * set);
-void shm_flow_set_zero(struct shm_flow_set * shm_set,
+void ssm_flow_set_zero(struct ssm_flow_set * set,
size_t idx);
-int shm_flow_set_add(struct shm_flow_set * shm_set,
+int ssm_flow_set_add(struct ssm_flow_set * set,
size_t idx,
int flow_id);
-int shm_flow_set_has(struct shm_flow_set * shm_set,
+int ssm_flow_set_has(struct ssm_flow_set * set,
size_t idx,
int flow_id);
-void shm_flow_set_del(struct shm_flow_set * shm_set,
+void ssm_flow_set_del(struct ssm_flow_set * set,
size_t idx,
int flow_id);
-void shm_flow_set_notify(struct shm_flow_set * set,
+void ssm_flow_set_notify(struct ssm_flow_set * set,
int flow_id,
int event);
-ssize_t shm_flow_set_wait(const struct shm_flow_set * shm_set,
+ssize_t ssm_flow_set_wait(const struct ssm_flow_set * set,
size_t idx,
struct flowevent * fqueue,
const struct timespec * abstime);
-#endif /* OUROBOROS_LIB_SHM_FLOW_SET_H */
+#endif /* OUROBOROS_LIB_SSM_FLOW_SET_H */
diff --git a/include/ouroboros/ssm_pk_buff.h b/include/ouroboros/ssm_pk_buff.h
new file mode 100644
index 00000000..1d5597c7
--- /dev/null
+++ b/include/ouroboros/ssm_pk_buff.h
@@ -0,0 +1,58 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Data Buffer element in Random Deletion Ring Buffer
+ *
+ * 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_SSM_PK_BUFF_H
+#define OUROBOROS_LIB_SSM_PK_BUFF_H
+
+#include <sys/types.h>
+#include <stdint.h>
+
+struct ssm_pk_buff;
+
+size_t ssm_pk_buff_get_off(const struct ssm_pk_buff * spb);
+
+uint8_t * ssm_pk_buff_head(const struct ssm_pk_buff * spb);
+
+uint8_t * ssm_pk_buff_tail(const struct ssm_pk_buff * spb);
+
+size_t ssm_pk_buff_len(const struct ssm_pk_buff * spb);
+
+uint8_t * ssm_pk_buff_push(struct ssm_pk_buff * spb,
+ size_t size);
+
+uint8_t * ssm_pk_buff_push_tail(struct ssm_pk_buff * spb,
+ size_t size);
+
+uint8_t * ssm_pk_buff_pop(struct ssm_pk_buff * spb,
+ size_t size);
+
+uint8_t * ssm_pk_buff_pop_tail(struct ssm_pk_buff * spb,
+ size_t size);
+
+void ssm_pk_buff_truncate(struct ssm_pk_buff * spb,
+ size_t len);
+
+int ssm_pk_buff_wait_ack(struct ssm_pk_buff * spb);
+
+int ssm_pk_buff_ack(struct ssm_pk_buff * spb);
+
+#endif /* OUROBOROS_LIB_SSM_PK_BUFF_H */
diff --git a/include/ouroboros/ssm_pool.h b/include/ouroboros/ssm_pool.h
new file mode 100644
index 00000000..bba76798
--- /dev/null
+++ b/include/ouroboros/ssm_pool.h
@@ -0,0 +1,74 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Secure Shared Memory infrastructure (SSM) Packet Buffer
+ *
+ * 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_SSM_POOL_H
+#define OUROBOROS_LIB_SSM_POOL_H
+
+#include <ouroboros/ssm_pk_buff.h>
+#include <ouroboros/time.h>
+
+#include <pthread.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+struct ssm_pool;
+
+/* Pool API: uid = 0 for GSPP (privileged), uid > 0 for PUP (per-user). */
+struct ssm_pool * ssm_pool_create(uid_t uid,
+ gid_t gid);
+
+struct ssm_pool * ssm_pool_open(uid_t uid);
+
+void ssm_pool_close(struct ssm_pool * pool);
+
+void ssm_pool_destroy(struct ssm_pool * pool);
+
+int ssm_pool_mlock(struct ssm_pool * pool);
+
+void ssm_pool_gspp_purge(void);
+
+/* Alloc count bytes, returns block offset, a ptr and pk_buff. */
+ssize_t ssm_pool_alloc(struct ssm_pool * pool,
+ size_t count,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb);
+
+ssize_t ssm_pool_alloc_b(struct ssm_pool * pool,
+ size_t count,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb,
+ const struct timespec * abstime);
+
+ssize_t ssm_pool_read(uint8_t ** dst,
+ struct ssm_pool * pool,
+ size_t off);
+
+struct ssm_pk_buff * ssm_pool_get(struct ssm_pool * pool,
+ size_t off);
+
+int ssm_pool_remove(struct ssm_pool * pool,
+ size_t off);
+
+void ssm_pool_reclaim_orphans(struct ssm_pool * pool,
+ pid_t pid);
+
+#endif /* OUROBOROS_LIB_SSM_POOL_H */
diff --git a/include/ouroboros/ssm_rbuff.h b/include/ouroboros/ssm_rbuff.h
new file mode 100644
index 00000000..e77eec09
--- /dev/null
+++ b/include/ouroboros/ssm_rbuff.h
@@ -0,0 +1,76 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Ring buffer for incoming packets
+ *
+ * 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_SSM_RBUFF_H
+#define OUROBOROS_LIB_SSM_RBUFF_H
+
+#include <sys/types.h>
+#include <sys/time.h>
+
+#include <stdint.h>
+
+#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) */
+
+struct ssm_rbuff;
+
+struct ssm_rbuff * ssm_rbuff_create(pid_t pid,
+ int flow_id);
+
+void ssm_rbuff_destroy(struct ssm_rbuff * rb);
+
+struct ssm_rbuff * ssm_rbuff_open(pid_t pid,
+ int flow_id);
+
+void ssm_rbuff_close(struct ssm_rbuff * rb);
+
+void ssm_rbuff_set_bits(struct ssm_rbuff * rb,
+ uint32_t bits);
+
+void ssm_rbuff_clr_bits(struct ssm_rbuff * rb,
+ uint32_t bits);
+
+uint32_t ssm_rbuff_get_flags(struct ssm_rbuff * rb);
+
+void ssm_rbuff_fini(struct ssm_rbuff * rb);
+
+int ssm_rbuff_mlock(struct ssm_rbuff * rb);
+
+int ssm_rbuff_write(struct ssm_rbuff * rb,
+ size_t off);
+
+int ssm_rbuff_write_b(struct ssm_rbuff * rb,
+ size_t off,
+ const struct timespec * abstime);
+
+ssize_t ssm_rbuff_read(struct ssm_rbuff * rb);
+
+ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb,
+ const struct timespec * abstime);
+
+size_t ssm_rbuff_queued(struct ssm_rbuff * rb);
+
+#endif /* OUROBOROS_LIB_SSM_RBUFF_H */
diff --git a/include/ouroboros/time.h b/include/ouroboros/time.h
index 3bd6a257..a4136e8e 100644
--- a/include/ouroboros/time.h
+++ b/include/ouroboros/time.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Time utilities
*
@@ -46,6 +46,12 @@
#define TS_TO_UINT64(ts) \
((uint64_t)(ts).tv_sec * BILLION + (uint64_t)(ts).tv_nsec)
+#define UINT64_TO_TS(ns, ts) \
+ do { \
+ (ts)->tv_sec = (time_t)((ns) / BILLION); \
+ (ts)->tv_nsec = (long)((ns) % BILLION); \
+ } while (0)
+
#define TIMEVAL_INIT_S(s) {(s), 0}
#define TIMEVAL_INIT_MS(ms) {(ms) / 1000, ((ms) % 1000) * 1000}
#define TIMEVAL_INIT_US(us) {(us) / MILLION, ((us) % MILLION)}
diff --git a/include/ouroboros/tpm.h b/include/ouroboros/tpm.h
index 3fb49b88..56c04701 100644
--- a/include/ouroboros/tpm.h
+++ b/include/ouroboros/tpm.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Threadpool management
*
@@ -24,6 +24,7 @@
#define OUROBOROS_LIB_TPM_H
#include <stdbool.h>
+#include <sys/types.h>
struct tpm;
diff --git a/include/ouroboros/tw.h b/include/ouroboros/tw.h
new file mode 100644
index 00000000..156f99db
--- /dev/null
+++ b/include/ouroboros/tw.h
@@ -0,0 +1,77 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Generic deadline-ordered callback queue (timing wheel)
+ *
+ * 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_TW_H
+#define OUROBOROS_TW_H
+
+#include <ouroboros/cdefs.h>
+#include <ouroboros/list.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <time.h>
+
+typedef void (*tw_fire_fn_t)(void * arg);
+
+struct tw_entry {
+ struct list_head next;
+ uint64_t deadline_ns;
+ tw_fire_fn_t fire;
+ void * arg;
+ size_t lvl;
+};
+
+__BEGIN_DECLS
+
+int tw_init(void);
+
+void tw_fini(void);
+
+void tw_init_entry(struct tw_entry * e);
+
+/*
+ * Schedule e to fire at deadline_ns. If e is already posted,
+ * the previous schedule is cancelled and replaced.
+ */
+void tw_post(struct tw_entry * e,
+ uint64_t deadline_ns,
+ tw_fire_fn_t fire,
+ void * arg);
+
+void tw_cancel(struct tw_entry * e);
+
+/*
+ * Advance the wheel and fire due callbacks. Callbacks run with the wheel
+ * unlocked and may call tw_post / tw_cancel on any entry, including the one
+ * currently firing. Concurrent tw_move from a second thread is a no-op.
+ */
+void tw_move(void);
+
+/*
+ * Write the absolute deadline of the earliest pending entry to *out.
+ * Empty wheel is signalled by out->tv_nsec == -1.
+ */
+void tw_next_expiry(struct timespec * out);
+
+__END_DECLS
+
+#endif /* OUROBOROS_TW_H */
diff --git a/include/ouroboros/utils.h b/include/ouroboros/utils.h
index b93b345d..b3de488e 100644
--- a/include/ouroboros/utils.h
+++ b/include/ouroboros/utils.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handy utilities
*
@@ -23,16 +23,18 @@
#ifndef OUROBOROS_LIB_UTILS_H
#define OUROBOROS_LIB_UTILS_H
+#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/types.h>
#include <unistd.h>
#define MIN(a,b) (((a) < (b)) ? (a) : (b))
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
#define ABS(a) ((a) > 0 ? (a) : -(a))
-#define clrbuf(buf) do { memset(&(buf), 0, sizeof(buf)); } while (0);
-#define freebuf(buf) do { free((buf).data); clrbuf(buf); } while (0);
+#define clrbuf(buf) do { memset(&(buf), 0, sizeof(buf)); } while (0)
+#define freebuf(buf) do { free((buf).data); clrbuf(buf); } while (0)
#define BUF_INIT { 0, NULL }
#define BUF_IS_EMPTY(buf) ((buf)->data == NULL && (buf)->len == 0)
@@ -50,9 +52,14 @@ int bufcmp(const buffer_t * a,
*/
int n_digits(unsigned i);
-/* gets the application name */
char * path_strip(const char * src);
+char * trim_whitespace(char * str);
+
+bool is_ouroboros_member_uid(uid_t uid);
+
+bool is_ouroboros_member(void);
+
/* functions for copying and destroying arguments list */
size_t argvlen(const char ** argv);
diff --git a/include/ouroboros/version.h.in b/include/ouroboros/version.h.in
index c006a095..13dd2921 100644
--- a/include/ouroboros/version.h.in
+++ b/include/ouroboros/version.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros version
*
@@ -26,5 +26,6 @@
#define OUROBOROS_VERSION_MAJOR @PACKAGE_VERSION_MAJOR@
#define OUROBOROS_VERSION_MINOR @PACKAGE_VERSION_MINOR@
#define OUROBOROS_VERSION_PATCH @PACKAGE_VERSION_PATCH@
+#define OUROBOROS_VERSION_STRING "@PACKAGE_VERSION_STRING@"
#endif /* OUROBOROS_VERSION_H */
diff --git a/include/test/certs/ecdsa.h b/include/test/certs/ecdsa.h
new file mode 100644
index 00000000..cbc4ed06
--- /dev/null
+++ b/include/test/certs/ecdsa.h
@@ -0,0 +1,162 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test certificates - ECDSA/P-256 signed certificates
+ *
+ * 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 TEST_CERTS_ECDSA_H
+#define TEST_CERTS_ECDSA_H
+
+/*
+* Certificates created following the guide
+* Building an openssl certificate authority
+* on
+* https://community.f5.com/kb/technicalarticles/
+*/
+
+/* Root certificate for CA ca.unittest.o7s */
+static const char * root_ca_crt_ec = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIICXTCCAgOgAwIBAgIURlENlCOy1OsA/AXFscPUQ2li8OYwCgYIKoZIzj0EAwIw\n"
+"fDELMAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAK\n"
+"BgNVBAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51\n"
+"bml0dGVzdC5vN3MxEDAOBgkqhkiG9w0BCQEWASAwHhcNMjUwODAzMTg1MzE1WhcN\n"
+"NDUwNzI5MTg1MzE1WjB8MQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYD\n"
+"VQQHDAVHaGVudDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n"
+"GDAWBgNVBAMMD2NhLnVuaXR0ZXN0Lm83czEQMA4GCSqGSIb3DQEJARYBIDBZMBMG\n"
+"ByqGSM49AgEGCCqGSM49AwEHA0IABEPMseCScbd/d5TlHmyYVszn/YGVeNdUCnFR\n"
+"naOr95WlTNo3MyKKBuoiEFwHhjPASgXr/VDVjJLSyM3JUPebAcGjYzBhMB0GA1Ud\n"
+"DgQWBBQkxjMILHH6lZ+rnCMnD/63GO3y1zAfBgNVHSMEGDAWgBQkxjMILHH6lZ+r\n"
+"nCMnD/63GO3y1zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAKBggq\n"
+"hkjOPQQDAgNIADBFAiEA1jVJWW4idkCgAYv0m2LT9C33Dq42aLyRkJ+9YdzDqLwC\n"
+"IHT6MS4I0k52YP/hxoqWVBbpOW79PKYMRLyXTk1r7+Fa\n"
+"-----END CERTIFICATE-----\n";
+
+/* Certificate for intermediary im.unittest.o7s used for signing */
+static const char * im_ca_crt_ec = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIICbTCCAhOgAwIBAgICEAMwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCQkUxDDAK\n"
+"BgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAKBgNVBAoMA283czEVMBMGA1UE\n"
+"CwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51bml0dGVzdC5vN3MxEDAOBgkq\n"
+"hkiG9w0BCQEWASAwHhcNMjUwODAzMTkwMjU3WhcNNDUwNzI5MTkwMjU3WjBaMQsw\n"
+"CQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQwwCgYDVQQKDANvN3MxFTATBgNVBAsM\n"
+"DHVuaXR0ZXN0Lm83czEYMBYGA1UEAwwPaW0udW5pdHRlc3QubzdzMFkwEwYHKoZI\n"
+"zj0CAQYIKoZIzj0DAQcDQgAEdlra08XItIPtVl5veaq4UF6LIcBXj2mZFqKNEXFh\n"
+"l9uAz6UAbIc+FUPNfom6dwKbg/AjQ82a100eh6K/jCY7eKOBpjCBozAdBgNVHQ4E\n"
+"FgQUy8Go8BIO6i0lJ+mgBr9lvh2L0eswHwYDVR0jBBgwFoAUJMYzCCxx+pWfq5wj\n"
+"Jw/+txjt8tcwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEQYD\n"
+"VR0fBAowCDAGoASgAoYAMCoGCCsGAQUFBwEBBB4wHDAMBggrBgEFBQcwAoYAMAwG\n"
+"CCsGAQUFBzABhgAwCgYIKoZIzj0EAwIDSAAwRQIhAN3ZYhqu6mVLGidmONsbANk5\n"
+"rzT6aHJcmvj19OxMusaXAiBKy0gBFCri/GLizi4wZo09wf31yZMqfr8IrApvPaLw\n"
+"qA==\n"
+"-----END CERTIFICATE-----\n";
+
+/* Server test-1.unittest.o7s private-public key pair */
+static const char * server_pkp_ec = \
+"-----BEGIN EC PRIVATE KEY-----\n"
+"MHcCAQEEIA4/bcmquVvGrY4+TtfnFSy1SpXs896r5xJjGuD6NmGRoAoGCCqGSM49\n"
+"AwEHoUQDQgAE4BSOhv36q4bCMLSkJaCvzwZ3pPy2M0YzRKFKeV48tG5eD+MBaTrT\n"
+"eoHUcRfpz0EO/inq3FVDzEoAQ2NWpnz0kA==\n"
+"-----END EC PRIVATE KEY-----\n";
+
+/* Public key for the Private key */
+static __attribute__((unused)) const char * server_pk_ec = \
+"-----BEGIN PUBLIC KEY-----\n"
+"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4BSOhv36q4bCMLSkJaCvzwZ3pPy2\n"
+"M0YzRKFKeV48tG5eD+MBaTrTeoHUcRfpz0EO/inq3FVDzEoAQ2NWpnz0kA==\n"
+"-----END PUBLIC KEY-----\n";
+
+/* Valid signed server certificate for test-1.unittest.o7s */
+#define SSC_TEXT_SIZE 2295 /* size of cleartext certificate */
+static const char * signed_server_crt_ec = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIIDiTCCAy+gAwIBAgICEAUwCgYIKoZIzj0EAwIwWjELMAkGA1UEBhMCQkUxDDAK\n"
+"BgNVBAgMA09WTDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n"
+"GDAWBgNVBAMMD2ltLnVuaXR0ZXN0Lm83czAeFw0yNTA4MDgxODQ4NTNaFw00NTA4\n"
+"MDMxODQ4NTNaMG4xCzAJBgNVBAYTAkJFMQwwCgYDVQQIDANPVkwxDjAMBgNVBAcM\n"
+"BUdoZW50MQwwCgYDVQQKDANvN3MxFTATBgNVBAsMDHVuaXR0ZXN0Lm83czEcMBoG\n"
+"A1UEAwwTdGVzdC0xLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49AwEH\n"
+"A0IABOAUjob9+quGwjC0pCWgr88Gd6T8tjNGM0ShSnlePLRuXg/jAWk603qB1HEX\n"
+"6c9BDv4p6txVQ8xKAENjVqZ89JCjggHPMIIByzAJBgNVHRMEAjAAMBEGCWCGSAGG\n"
+"+EIBAQQEAwIGQDA4BglghkgBhvhCAQ0EKxYpbzdzIHVuaXR0ZXN0IEdlbmVyYXRl\n"
+"ZCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFI+htsK0xxy6e1CqCyxn7mqi\n"
+"wRrpMIGoBgNVHSMEgaAwgZ2AFMvBqPASDuotJSfpoAa/Zb4di9HroYGApH4wfDEL\n"
+"MAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAKBgNV\n"
+"BAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51bml0\n"
+"dGVzdC5vN3MxEDAOBgkqhkiG9w0BCQEWASCCAhADMA4GA1UdDwEB/wQEAwIFoDAT\n"
+"BgNVHSUEDDAKBggrBgEFBQcDATAoBgNVHR8EITAfMB2gG6AZhhdodHRwczovL291\n"
+"cm9ib3Jvcy5yb2NrczBYBggrBgEFBQcBAQRMMEowIwYIKwYBBQUHMAKGF2h0dHBz\n"
+"Oi8vb3Vyb2Jvcm9zLnJvY2tzMCMGCCsGAQUFBzABhhdodHRwczovL291cm9ib3Jv\n"
+"cy5yb2NrczAKBggqhkjOPQQDAgNIADBFAiBZuw/Yb2pq925H7pEiOXr4fMo0wknz\n"
+"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"
+"MIIBfjCCASWgAwIBAgIUB5VYxp7i+sgYjvLiwfpf0W5NfqQwCgYIKoZIzj0EAwIw\n"
+"HjEcMBoGA1UEAwwTdGVzdC0xLnVuaXR0ZXN0Lm83czAeFw0yNTA4MDMxOTI4MzVa\n"
+"Fw00NTA3MjkxOTI4MzVaMB4xHDAaBgNVBAMME3Rlc3QtMS51bml0dGVzdC5vN3Mw\n"
+"WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATgFI6G/fqrhsIwtKQloK/PBnek/LYz\n"
+"RjNEoUp5Xjy0bl4P4wFpOtN6gdRxF+nPQQ7+KercVUPMSgBDY1amfPSQo0EwPzAe\n"
+"BgNVHREEFzAVghN0ZXN0LTEudW5pdHRlc3QubzdzMB0GA1UdDgQWBBSPobbCtMcc\n"
+"untQqgssZ+5qosEa6TAKBggqhkjOPQQDAgNHADBEAiAoFC/rqgrRXmMUx4y5cPbv\n"
+"jOKpoL3FpehRgGkPatmL/QIgMRHc2TSGo6q1SG22Xt1dHAIBsaN2AlSfhjKULMH5\n"
+"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/include/test/certs/ml_dsa.h b/include/test/certs/ml_dsa.h
new file mode 100644
index 00000000..fad888b3
--- /dev/null
+++ b/include/test/certs/ml_dsa.h
@@ -0,0 +1,656 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test certificates - ML-DSA-65 (post-quantum) signed certificates
+ *
+ * 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 TEST_CERTS_ML_DSA_H
+#define TEST_CERTS_ML_DSA_H
+
+/*
+ * ML-DSA-65 certificates for testing post-quantum cryptography
+ * Root CA: ca.unittest.o7s
+ * Intermediate CA: im.unittest.o7s (pathlen:0)
+ * Server: test-1.unittest.o7s
+ */
+
+/* PEM certificate strings will go here */
+static const char * root_ca_crt_ml = \
+"-----BEGIN CERTIFICATE-----\n"
+"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\n"
+"-----END CERTIFICATE-----\n";
+
+static const char * im_ca_crt_ml = \
+"-----BEGIN CERTIFICATE-----\n"
+"MIIVyDCCCMWgAwIBAgICEAAwCwYJYIZIAWUDBAMSMFMxCzAJBgNVBAYTAkJFMQww\n"
+"CgYDVQQIDANPVkwxDjAMBgNVBAcMBUdoZW50MQwwCgYDVQQKDANvN3MxGDAWBgNV\n"
+"BAMMD2NhLnVuaXR0ZXN0Lm83czAeFw0yNjAxMDUxOTE1MjJaFw0zNjAxMDMxOTE1\n"
+"MjJaMBoxGDAWBgNVBAMMD2ltLnVuaXR0ZXN0Lm83czCCB7IwCwYJYIZIAWUDBAMS\n"
+"A4IHoQA5Wkc2Mz19rGlaczLiVz+WqRHexHpdzgKP/wXNiduzFZtYxbXVPJHPybkX\n"
+"Jx8p18vxsTHBsUz1ST+nOqLKH0RAqqEQWBLmf2bg0To7aLkN53lg40xcZ8XszXBg\n"
+"H0cjl/agLFSvPoSpLv5R25FeR5mK5pkpaCzY29Wg2xppeAFeMPL1YBUtsxiDoLWG\n"
+"xnquhrDRWrFCHDcpcG+qFCrwSk30EJgIEFz8zAucZk++MWXINmm0rKzqAZHkh9zo\n"
+"IXHTulI6zFGxFDP19eRoke4AzmC6xJyw9W+WIU84TyU9o4bSfQtJjLFn7qk410a2\n"
+"/zm3mXS9fQiqqZm8M6mAk6UTFVHHr6/SzzwFutn652qz8xhSDkOlsd8jz9XixEpk\n"
+"wXQiZZMaai9MTW9m5Hzi+8mFMcMmGwsIgTWo/G0os8d8hxPWcRXO4lXfuHa83dUP\n"
+"KWHEcroEBjdN67R3qepCONhvq8VSHbw1/tkm8VjzTz2VCQUjFSkn/rxqJAi1cIoh\n"
+"ZEBuDuJ2Ilx3DlFAQTrMl6L6HwIqetYYyYoMUSL09MtAA1EZTIJm6CHM+gxnef20\n"
+"mLIvWDaXicm01FpBaxJwGqWD0fS+2Ii4+XwnxYh3kKLD1x1O4guIqPDPG3NVBdu5\n"
+"pKxYLAgcE9OJc3I7SfgimQCweGuEUGwHSqMjcYKYWiAodlo2z4+GtAN08Np+Av0/\n"
+"fVTOHW6aFMnvxfcpsZv0fBcwgOm+vvuxmKdIOTjJiQnLPqWWAykt3LM5AaoBT0v5\n"
+"Pd3OU7z4p/XXwkYgQXFV58nOttFHHE0ANKiLUF7iBmu2Vz3+n8ewTW/nYABUEZlC\n"
+"W+VfnhHwYhgmtg2krCBW9KNI1Vvfw9WutJxT/8+lAN3P/ZqyvF6vS9rECfn1zFIr\n"
+"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\n"
+"AAAAAAAABQoQFyIo\n"
+"-----END CERTIFICATE-----\n";
+
+static const char * server_pkp_ml = \
+"-----BEGIN PRIVATE KEY-----\n"
+"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\n"
+"DngTH/g89SEnGxqKXe7RxVof\n"
+"-----END PRIVATE KEY-----\n";
+
+static __attribute__((unused)) const char * server_pk_ml = \
+"-----BEGIN PUBLIC KEY-----\n"
+"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\n"
+"k1M8ksQo\n"
+"-----END PUBLIC KEY-----\n";
+
+static const char * signed_server_crt_ml = \
+"-----BEGIN CERTIFICATE-----\n"
+"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\n"
+"AAAAAAAAAAAAAAAABgkOExgc\n"
+"-----END CERTIFICATE-----\n";
+
+static __attribute__((unused)) const char * server_crt_ml = \
+"-----BEGIN CERTIFICATE-----\n"
+"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\n"
+"-----END CERTIFICATE-----\n";
+
+#endif /* TEST_CERTS_ML_DSA_H */
+
diff --git a/include/test/certs/slh_dsa.h b/include/test/certs/slh_dsa.h
new file mode 100644
index 00000000..42c21235
--- /dev/null
+++ b/include/test/certs/slh_dsa.h
@@ -0,0 +1,754 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test certificates - SLH-DSA-SHA2-128s signed certificates
+ *
+ * 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 TEST_CERTS_SLH_DSA_H
+#define TEST_CERTS_SLH_DSA_H
+
+/*
+ * SLH-DSA-SHA2-128s certificates for testing
+ * Root CA: ca.unittest.o7s
+ * Intermediate CA: im.unittest.o7s (pathlen:0)
+ * Server: test-1.unittest.o7s
+ */
+
+/* Root certificate for CA ca.unittest.o7s */
+static const char * root_ca_crt_slh = \
+"-----BEGIN CERTIFICATE-----\n"\
+"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\n"\
+"XiJ5AyeDQYokVQjo/KlCXgWrXg==\n"\
+"-----END CERTIFICATE-----\n";
+
+/* Certificate for intermediary im.unittest.o7s */
+static const char * im_ca_crt_slh = \
+"-----BEGIN CERTIFICATE-----\n"\
+"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\n"\
+"QYokVQjo/KlCXgWrXg==\n"\
+"-----END CERTIFICATE-----\n";
+
+/* Private key for test-1.unittest.o7s */
+static const char * server_pkp_slh = \
+"-----BEGIN PRIVATE KEY-----\n"\
+"MFICAQAwCwYJYIZIAWUDBAMUBEBYLONeDUFxVs+J2azu3Gjnd5NQcSSOWHAra6pQ\n"\
+"bYTw/6bmB198MoATF7UFKnl2gyPo3d++8Z9YYLOdbpEDWXmO\n"\
+"-----END PRIVATE KEY-----\n";
+
+/* Public key for test-1.unittest.o7s */
+static __attribute__((unused)) const char * server_pk_slh = \
+"-----BEGIN PUBLIC KEY-----\n"\
+"MDAwCwYJYIZIAWUDBAMUAyEApuYHX3wygBMXtQUqeXaDI+jd377xn1hgs51ukQNZ\n"\
+"eY4=\n"\
+"-----END PUBLIC KEY-----\n";
+
+/* Server certificate signed by im.unittest.o7s */
+static const char * signed_server_crt_slh = \
+"-----BEGIN CERTIFICATE-----\n"\
+"MIIf8DCCASqgAwIBAgIUKHXZdlpPXRsORtxdqhCJEXWfWsMwCwYJYIZIAWUDBAMU\n"\
+"MBoxGDAWBgNVBAMMD2ltLnVuaXR0ZXN0Lm83czAeFw0yNjAyMTcyMDU2MDNaFw0y\n"\
+"NzAyMTcyMDU2MDNaMB4xHDAaBgNVBAMME3Rlc3QtMS51bml0dGVzdC5vN3MwMDAL\n"\
+"BglghkgBZQMEAxQDIQCm5gdffDKAExe1BSp5doMj6N3fvvGfWGCznW6RA1l5jqNy\n"\
+"MHAwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH\n"\
+"AwEwHQYDVR0OBBYEFF1mV3niZJsxwZiUShvA/mkstMytMB8GA1UdIwQYMBaAFLGk\n"\
+"yCAvxd60/n5Bt6MxdS1NUhyVMAsGCWCGSAFlAwQDFAOCHrEAzVKLYC0HzegSAa52\n"\
+"bOarkBTzk7qSdpQ2z6P9Ci6CRf+akW0gvCt9SmxgTPd9gYGPS69ZdbeEQW84eChU\n"\
+"AG96lbnvtNuCtaWPAAoWeLJ2+2ckex5uYMEM8MtBKGKbp8TVJNqa0VTxdOclRT3j\n"\
+"2DaWMbBwphpUeX1grIFlW15ktH+sYXpzh+Y7cw85mRpMQRssqbUFpytYLfsMbGjN\n"\
+"Avp+lgz1EE7PISHILsWhKssxZd38u1m4gEScxI6S5S92/jqfDqEPJNlFDcxm/LM4\n"\
+"rgE1AZCinvnb65jF2rkZ8UAKPqnueHimd5oa53WJo/cdawe0g24BJbp4OfpWUSQ6\n"\
+"h2MFqFNd9TkUqa25BW9T62R70aakVj+n0+kutwJgPdxh8YpAEfWAxL8leyNPGvsc\n"\
+"6ao+Z4kvFZqDFr7zhr43i4YZl0Kvr7TJLKyhtvM+l03mkwblCsMT5DuWqGjUsRZx\n"\
+"VWw0lp6Biz5/LKao43FydLtQ9VWetyo1TKMnVWrbzGNFMXWLWUI3aKirOk3Jh9dp\n"\
+"a2X+zgbjVOPBfYuoN4Sf5sU9quOy/rjR7Ro4o6pBHOEm8MUKajg/TzY0UC0D9uy6\n"\
+"51QUBmXnXInaFsGf2IV25IQDCaZeGLBPV29fCFSvvsHS+u6aTU0/NPLE0a3PhV0Q\n"\
+"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\n"\
+"f0dXLBcT15qMrjnRYQmw1H39Jv8=\n"\
+"-----END CERTIFICATE-----\n";
+
+/* Self-signed server certificate for test-1.unittest.o7s */
+static __attribute__((unused)) const char * server_crt_slh = \
+"-----BEGIN CERTIFICATE-----\n"\
+"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\n"\
+"Ovqk2FmYmg4ys1oEdk3nx5jN5vDc0JJUaw==\n"\
+"-----END CERTIFICATE-----\n";
+
+#endif /* TEST_CERTS_SLH_DSA_H */
diff --git a/include/ouroboros/test.h b/include/test/test.h
index bccf9ccd..a76fe62a 100644
--- a/include/ouroboros/test.h
+++ b/include/test/test.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test macros
*
@@ -29,19 +29,30 @@
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
+#include <sys/resource.h>
+#ifdef __linux__
+#include <sys/prctl.h>
+#endif
#define TEST_RC_SUCCESS 0
#define TEST_RC_SKIP 1
#define TEST_RC_FAIL -1
-#define TEST_START() \
+#define TEST_START(...) \
do { \
- printf("%s started.\n", __func__); \
+ printf("%s", __func__); \
+ if (sizeof(#__VA_ARGS__) > 1) \
+ printf(" " __VA_ARGS__); \
+ printf(" started.\n"); \
fflush(stdout); \
} while (0)
-#define TEST_SUCCESS() \
+
+#define TEST_SUCCESS(...) \
do { \
- printf("\x1b[32m%s succeeded.\x1b[0m\n", __func__); \
+ printf("\x1b[32m%s", __func__); \
+ if (sizeof(#__VA_ARGS__) > 1) \
+ printf(" " __VA_ARGS__); \
+ printf(" succeeded.\x1b[0m\n"); \
fflush(stdout); \
} while (0)
@@ -51,9 +62,12 @@
fflush(stdout); \
} while (0)
-#define TEST_FAIL() \
+#define TEST_FAIL(...) \
do { \
- printf("\x1b[31m%s failed.\x1b[0m\n", __func__); \
+ printf("\x1b[31m%s", __func__); \
+ if (sizeof(#__VA_ARGS__) > 1) \
+ printf(" " __VA_ARGS__); \
+ printf(" failed.\x1b[0m\n"); \
fflush(stdout); \
} while (0)
@@ -71,8 +85,16 @@ static int __attribute__((unused)) test_assert_fail(int(* testfunc)(void))
return TEST_RC_FAIL;
}
- if (pid == 0)
+ if (pid == 0) {
+#ifdef DISABLE_TESTS_CORE_DUMPS
+ struct rlimit rl = { .rlim_cur = 0, .rlim_max = 0 };
+ setrlimit(RLIMIT_CORE, &rl);
+#ifdef __linux__
+ prctl(PR_SET_DUMPABLE, 0);
+#endif
+#endif
return testfunc(); /* should abort */
+ }
waitpid(pid, &wstatus, 0);
#ifdef CONFIG_OUROBOROS_DEBUG
diff --git a/irmd.conf.in b/irmd.conf.in
index 83ab19b5..b9b79782 100644
--- a/irmd.conf.in
+++ b/irmd.conf.in
@@ -1,20 +1,16 @@
### Example Ouroboros configuration file
#
-# This file contains a summary of current machine configuration
-# options for the O7s prototype. The IRMd will attempt to load its
-# configuration file during startup from the file
-# @OUROBOROS_CONFIG_DIR@@OUROBOROS_CONFIG_FILE@.
+# This file contains a summary of current machine configuration options
+# for the O7s prototype.
#
-# At the top level, it accepts configuration of
-# services via the "name" directive, and configuration of the network
-# specifying different IPCPs in the system. So, the list of accepted
-# top level stanza is:
+# The list of accepted top level stanza is:
#
# [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.
-# [eth-udp.<name for IPCP>] add a new IPCP over UDP/IPv4 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.
#
@@ -60,10 +56,10 @@ 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.cfg Default: @OUROBOROS_SRV_CRT_DIR@/<name>/enc.cfg
+# 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_enc_file=/path/to/enc.cfg Default: @OUROBOROS_CLI_CRT_DIR@/<name>/enc.cfg
+# 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
diff --git a/sec.conf.in b/sec.conf.in
new file mode 100644
index 00000000..4796b72d
--- /dev/null
+++ b/sec.conf.in
@@ -0,0 +1,177 @@
+### Example Ouroboros security configuration file
+#
+# 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>/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> ssecpath <server-sec-path> csecpath <client-sec-path>
+#
+# Configuration Options:
+# ----------------------
+#
+# kex=<algorithm> Key exchange/encapsulation algorithm
+# cipher=<cipher> Symmetric cipher algorithm
+# kdf=<hash> Key derivation function hash algorithm
+# kem_mode=<mode> KEM encapsulation mode (server or client)
+# auth=<policy> Peer authentication policy (required or optional)
+# encryption=none Explicitly disable encryption
+#
+# Supported KEX algorithms (kex=):
+# --------------------------------
+#
+# ECDH Curves:
+# prime256v1 NIST P-256 (default)
+# secp384r1 NIST P-384
+# secp521r1 NIST P-521
+# X25519 Curve25519
+# X448 Curve448
+#
+# Finite Field Diffie-Hellman (RFC 7919):
+# ffdhe2048 2048-bit MODP Group
+# ffdhe3072 3072-bit MODP Group
+# ffdhe4096 4096-bit MODP Group
+#
+# ML-KEM (FIPS 203):
+# ML-KEM-512 CRYSTALS-Kyber-512
+# ML-KEM-768 CRYSTALS-Kyber-768
+# ML-KEM-1024 CRYSTALS-Kyber-1024
+#
+# Hybrid KEMs:
+# X25519MLKEM768 X25519 + ML-KEM-768
+# X448MLKEM1024 X448 + ML-KEM-1024
+#
+# Supported cipher algorithms (cipher=):
+# --------------------------------------
+#
+# Authenticated encryption:
+# aes-128-gcm AES-128 in GCM mode
+# aes-192-gcm AES-192 in GCM mode
+# aes-256-gcm AES-256 in GCM mode (default)
+# chacha20-poly1305 ChaCha20-Poly1305
+#
+# Key Derivation Functions (kdf=):
+# ---------------------------------
+#
+# Hash algorithms for key derivation in KEX operations:
+#
+# sha256 SHA-256 (default)
+# sha384 SHA-384
+# sha512 SHA-512
+# sha3-256 SHA3-256
+# sha3-384 SHA3-384
+# sha3-512 SHA3-512
+# 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=):
+# ---------------------
+#
+# For KEM algorithms (ML-KEM-* and hybrid KEMs), specify which side
+# performs the encapsulation operation:
+#
+# server Server encapsulates to client's ephemeral public key (default, matches TLS 1.3)
+# - Client generates ephemeral keypair, sends public key in request
+# - Server encapsulates and sends ciphertext in response
+# - Client decapsulates with ephemeral private key
+# - Standard approach, no pre-shared keys needed
+#
+# client Client encapsulates to server's static public key (alternative)
+# - Requires cached server public key at:
+# @OUROBOROS_CONFIG_DIR@/security/client/<service>/kex.srv.pub.[pem|raw]
+# - Client encapsulates and sends ciphertext in initial request
+# - Server decapsulates with its static private key from:
+# @OUROBOROS_CONFIG_DIR@/security/server/kex.key.pem
+# - More efficient (0 round-trip) but requires key distribution
+# and forfeits forward secrecy
+#
+# Note: Both sides must use the same kem_mode setting.
+# This option is ignored for ECDH/DH key exchange algorithms.
+#
+# Key Management for Client Mode:
+# --------------------------------
+#
+# For client encapsulation mode, you must:
+# 1. Generate server KEM keypair:
+# openssl genpkey -algorithm ML-KEM-768 \
+# -out @OUROBOROS_CONFIG_DIR@/security/server/kex.key.pem
+# 2. Extract and distribute server public key:
+# openssl pkey -in kex.key.pem -pubout -out kex.srv.pub.pem
+# 3. Cache on clients at:
+# @OUROBOROS_CONFIG_DIR@/security/client/<service-name>/kex.srv.pub.pem
+#
+# File formats:
+# - Pure ML-KEM: PEM format (.pem extension)
+# - Hybrid KEMs: Raw bytes (.raw extension)
+#
+# Examples:
+# ---------
+#
+# Default configuration (NIST P-256 ECDH + AES-256-GCM):
+kex=prime256v1
+cipher=aes-256-gcm
+kdf=sha256
+#
+# Post-quantum KEX with server encapsulation (default, like TLS 1.3):
+# kex=ML-KEM-768
+# cipher=chacha20-poly1305
+# kdf=sha256
+# kem_mode=server
+#
+# Post-quantum KEX with client encapsulation (requires key distribution):
+# kex=ML-KEM-768
+# cipher=chacha20-poly1305
+# kdf=sha256
+# kem_mode=client
+#
+# Hybrid KEX (quantum-resistant):
+# kex=X25519MLKEM768
+# cipher=aes-256-gcm
+# kdf=sha256
+#
+# High security configuration:
+# kex=secp521r1
+# cipher=aes-256-gcm
+# kdf=sha512
+#
+# Disable encryption:
+# encryption=none
+#
+# Authentication required, no encryption:
+# encryption=none
+# auth=required
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
deleted file mode 100644
index 54fdd8ab..00000000
--- a/src/CMakeLists.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-add_subdirectory(lib)
-add_subdirectory(ipcpd)
-add_subdirectory(irmd)
-add_subdirectory(tools)
diff --git a/src/ipcpd/CMakeLists.txt b/src/ipcpd/CMakeLists.txt
index b3b049e3..609da54a 100644
--- a/src/ipcpd/CMakeLists.txt
+++ b/src/ipcpd/CMakeLists.txt
@@ -1,60 +1,30 @@
-set(CONNMGR_RCV_TIMEOUT 1000 CACHE STRING
- "Timeout for the connection manager to wait for OCEP info (ms).")
-set(IPCP_DEBUG_LOCAL FALSE CACHE BOOL
- "Use PID as address for local debugging")
-set(IPCP_QOS_CUBE_BE_PRIO 50 CACHE STRING
- "Priority for best effort QoS cube (0-99)")
-set(IPCP_QOS_CUBE_VIDEO_PRIO 90 CACHE STRING
- "Priority for video QoS cube (0-99)")
-set(IPCP_QOS_CUBE_VOICE_PRIO 99 CACHE STRING
- "Priority for voice QoS cube (0-99)")
-set(IPCP_MIN_THREADS 4 CACHE STRING
- "Minimum number of worker threads in the IPCP")
-set(IPCP_ADD_THREADS 4 CACHE STRING
- "Number of extra threads to start when an IPCP faces thread starvation")
-set(IPCP_SCHED_THR_MUL 2 CACHE STRING
- "Number of scheduler threads per QoS cube")
-set(DISABLE_CORE_LOCK TRUE CACHE BOOL
- "Disable locking performance threads to a core")
-set(DHT_ENROLL_SLACK 50 CACHE STRING
- "DHT enrollment waiting time (0-999, ms)")
-if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
- set(IPCP_LINUX_TIMERSLACK_NS 1000 CACHE STRING
- "Slack value for high resolution timers on Linux systems.")
-endif ()
-
-if ((IPCP_QOS_CUBE_BE_PRIO LESS 0) OR (IPCP_QOS_CUBE_BE_PRIO GREATER 99))
- message(FATAL_ERROR "Invalid priority for best effort QoS cube")
-endif ()
-
-if ((IPCP_QOS_CUBE_VIDEO_PRIO LESS 0) OR (IPCP_QOS_CUBE_VIDEO_PRIO GREATER 99))
- message(FATAL_ERROR "Invalid priority for video QoS cube")
-endif ()
-
-if ((IPCP_QOS_CUBE_VOICE_PRIO LESS 0) OR (IPCP_QOS_CUBE_VOICE_PRIO GREATER 99))
- message(FATAL_ERROR "Invalid priority for voice QoS cube")
-endif ()
-
-if ((DHT_ENROLL_SLACK LESS 0) OR (DHT_ENROLL_SLACK GREATER 999))
- message(FATAL_ERROR "Invalid DHT slack value")
-endif ()
-
+# IPCP (IPC Process) daemons build configuration
+# Configuration options and validation are in cmake/config/ipcp/*.cmake
+# Common sources shared by all IPCPs (absolute paths for subdirectories)
set(IPCP_SOURCES
- # Add source files here
${CMAKE_CURRENT_SOURCE_DIR}/ipcp.c
${CMAKE_CURRENT_SOURCE_DIR}/shim-data.c
- )
+)
-set (COMMON_SOURCES
- ${CMAKE_CURRENT_SOURCE_DIR}/common/enroll.c
- )
+set(COMMON_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/common/enroll.c
+)
-add_subdirectory(local)
-add_subdirectory(eth)
-add_subdirectory(udp)
-add_subdirectory(unicast)
-add_subdirectory(broadcast)
+set(IPCP_INCLUDE_DIRS
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
+
+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/CMakeLists.txt b/src/ipcpd/broadcast/CMakeLists.txt
index d85f335e..6749f660 100644
--- a/src/ipcpd/broadcast/CMakeLists.txt
+++ b/src/ipcpd/broadcast/CMakeLists.txt
@@ -1,35 +1,20 @@
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-get_filename_component(CURRENT_BINARY_PARENT_DIR
- ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+# Broadcast IPCP build configuration
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(IPCP_BROADCAST_TARGET ipcpd-broadcast CACHE INTERNAL "")
-set(IPCP_BROADCAST_MPL 60 CACHE STRING
- "Default maximum packet lifetime for the broadcast IPCP, in seconds")
-
-set(SOURCE_FILES
- # Add source files here
+set(BROADCAST_SOURCES
connmgr.c
dt.c
main.c
- )
+)
+
+add_executable(${IPCP_BROADCAST_TARGET}
+ ${BROADCAST_SOURCES}
+ ${IPCP_SOURCES}
+ ${COMMON_SOURCES}
+)
-add_executable(ipcpd-broadcast ${SOURCE_FILES} ${IPCP_SOURCES} ${COMMON_SOURCES}
- ${LAYER_CONFIG_PROTO_SRCS})
-target_link_libraries(ipcpd-broadcast LINK_PUBLIC ouroboros-dev)
+target_include_directories(${IPCP_BROADCAST_TARGET} PRIVATE ${IPCP_INCLUDE_DIRS})
+target_link_libraries(${IPCP_BROADCAST_TARGET} PRIVATE ouroboros-dev)
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ipcpd-broadcast -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+ouroboros_target_debug_definitions(${IPCP_BROADCAST_TARGET})
-install(TARGETS ipcpd-broadcast RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
+install(TARGETS ${IPCP_BROADCAST_TARGET} RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
diff --git a/src/ipcpd/broadcast/connmgr.c b/src/ipcpd/broadcast/connmgr.c
index f297175d..a4d20ee7 100644
--- a/src/ipcpd/broadcast/connmgr.c
+++ b/src/ipcpd/broadcast/connmgr.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handles connections between components
*
diff --git a/src/ipcpd/broadcast/dt.c b/src/ipcpd/broadcast/dt.c
index 938c9085..95483e33 100644
--- a/src/ipcpd/broadcast/dt.c
+++ b/src/ipcpd/broadcast/dt.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Forward loop for broadcast
*
@@ -28,7 +28,7 @@
#include "config.h"
-#define BROADCAST_MTU 1400 /* FIXME: avoid packet copy. */
+#define BROADCAST_MTU IPCP_BROADCAST_MTU /* FIXME: avoid packet copy. */
#define DT "dt"
#define OUROBOROS_PREFIX DT
@@ -58,14 +58,13 @@ struct nb {
};
struct {
- struct list_head nbs;
- size_t nbs_len;
- pthread_rwlock_t nbs_lock;
+ struct llist nbs;
+ pthread_rwlock_t lock;
- fset_t * set;
+ fset_t * set;
- pthread_t reader;
- pthread_t listener;
+ pthread_t reader;
+ pthread_t listener;
} fwd;
static int dt_add_nb(int fd)
@@ -73,12 +72,12 @@ static int dt_add_nb(int fd)
struct list_head * p;
struct nb * nb;
- pthread_rwlock_wrlock(&fwd.nbs_lock);
+ pthread_rwlock_wrlock(&fwd.lock);
- list_for_each(p, &fwd.nbs) {
+ llist_for_each(p, &fwd.nbs) {
struct nb * el = list_entry(p, struct nb, next);
if (el->fd == fd) {
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ pthread_rwlock_unlock(&fwd.lock);
log_warn("Already know neighbor on fd %d.", fd);
return 0;
}
@@ -86,18 +85,16 @@ static int dt_add_nb(int fd)
nb = malloc(sizeof(*nb));
if (nb == NULL) {
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ pthread_rwlock_unlock(&fwd.lock);
log_err("Failed to malloc neighbor struct.");
return -ENOMEM;
}
nb->fd = fd;
- list_add_tail(&nb->next, p);
+ llist_add_tail(&nb->next, &fwd.nbs);
- ++fwd.nbs_len;
-
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ pthread_rwlock_unlock(&fwd.lock);
log_dbg("Neighbor %d added.", fd);
@@ -109,21 +106,20 @@ static int dt_del_nb(int fd)
struct list_head * p;
struct list_head * h;
- pthread_rwlock_wrlock(&fwd.nbs_lock);
+ pthread_rwlock_wrlock(&fwd.lock);
- list_for_each_safe(p, h, &fwd.nbs) {
+ llist_for_each_safe(p, h, &fwd.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->fd == fd) {
- list_del(&nb->next);
- --fwd.nbs_len;
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ llist_del(&nb->next, &fwd.nbs);
+ pthread_rwlock_unlock(&fwd.lock);
log_dbg("Neighbor %d deleted.", nb->fd);
free(nb);
return 0;
}
}
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ pthread_rwlock_unlock(&fwd.lock);
log_err("Neighbor not found on fd %d.", fd);
@@ -157,11 +153,11 @@ static void dt_packet(uint8_t * buf,
{
struct list_head * p;
- pthread_rwlock_rdlock(&fwd.nbs_lock);
+ pthread_rwlock_rdlock(&fwd.lock);
- pthread_cleanup_push(__cleanup_rwlock_unlock, &fwd.nbs_lock);
+ pthread_cleanup_push(__cleanup_rwlock_unlock, &fwd.lock);
- list_for_each(p, &fwd.nbs) {
+ llist_for_each(p, &fwd.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->fd != in_fd)
flow_write(nb->fd, buf, len); /* FIXME: avoid copy. */
@@ -252,12 +248,12 @@ int dt_init(void)
strcpy(info.comp_name, DT);
strcpy(info.comp_name, DT_COMP);
- list_head_init(&fwd.nbs);
+ llist_init(&fwd.nbs);
if (notifier_reg(handle_event, NULL))
goto fail_notifier_reg;
- if (pthread_rwlock_init(&fwd.nbs_lock, NULL))
+ if (pthread_rwlock_init(&fwd.lock, NULL))
goto fail_lock_init;
fwd.set = fset_create();
@@ -273,8 +269,6 @@ int dt_init(void)
if (connmgr_comp_init(COMPID_DT, &info))
goto fail_connmgr_comp_init;
- fwd.nbs_len = 0;
-
return 0;
fail_connmgr_comp_init:
@@ -286,7 +280,7 @@ int dt_init(void)
fail_pthread_create_reader:
fset_destroy(fwd.set);
fail_fset_create:
- pthread_rwlock_destroy(&fwd.nbs_lock);
+ pthread_rwlock_destroy(&fwd.lock);
fail_lock_init:
notifier_unreg(handle_event);
fail_notifier_reg:
@@ -308,15 +302,15 @@ void dt_fini(void)
fset_destroy(fwd.set);
- pthread_rwlock_wrlock(&fwd.nbs_lock);
+ pthread_rwlock_wrlock(&fwd.lock);
- list_for_each_safe(p, h, &fwd.nbs) {
+ llist_for_each_safe(p, h, &fwd.nbs) {
struct nb * n = list_entry(p, struct nb, next);
- list_del(&n->next);
+ llist_del(&n->next, &fwd.nbs);
free(n);
}
- pthread_rwlock_unlock(&fwd.nbs_lock);
+ pthread_rwlock_unlock(&fwd.lock);
- pthread_rwlock_destroy(&fwd.nbs_lock);
+ pthread_rwlock_destroy(&fwd.lock);
}
diff --git a/src/ipcpd/broadcast/dt.h b/src/ipcpd/broadcast/dt.h
index 8d3b83f8..2472831e 100644
--- a/src/ipcpd/broadcast/dt.h
+++ b/src/ipcpd/broadcast/dt.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Forward loop for broadcast
*
diff --git a/src/ipcpd/broadcast/main.c b/src/ipcpd/broadcast/main.c
index 151b38c8..d18cac82 100644
--- a/src/ipcpd/broadcast/main.c
+++ b/src/ipcpd/broadcast/main.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Broadcast IPC Process
*
@@ -242,7 +242,7 @@ static int broadcast_ipcp_join(int fd,
notifier_event(NOTIFY_DT_CONN_ADD, &conn);
- ipcp_flow_alloc_reply(fd, 0, mpl, &data);
+ ipcp_flow_alloc_reply(fd, 0, mpl, IPCP_BROADCAST_MTU, &data);
return 0;
}
@@ -307,12 +307,13 @@ int main(int argc,
ipcp_sigwait();
if (ipcp_get_state() == IPCP_SHUTDOWN) {
+ ipcp_stop();
stop_components();
finalize_components();
+ } else {
+ ipcp_stop();
}
- ipcp_stop();
-
enroll_fini();
connmgr_fini();
diff --git a/src/ipcpd/common/comp.h b/src/ipcpd/common/comp.h
index f3790d9c..e1d025b6 100644
--- a/src/ipcpd/common/comp.h
+++ b/src/ipcpd/common/comp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Components for the unicast/broadcast IPC process
*
diff --git a/src/ipcpd/common/connmgr.c b/src/ipcpd/common/connmgr.c
index 1bb8c932..48ad79ba 100644
--- a/src/ipcpd/common/connmgr.c
+++ b/src/ipcpd/common/connmgr.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handles connections between components
*
diff --git a/src/ipcpd/common/connmgr.h b/src/ipcpd/common/connmgr.h
index 0710dbbf..f48ecd1b 100644
--- a/src/ipcpd/common/connmgr.h
+++ b/src/ipcpd/common/connmgr.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handles the different AP connections
*
diff --git a/src/ipcpd/common/enroll.c b/src/ipcpd/common/enroll.c
index 8e5384a5..959cca07 100644
--- a/src/ipcpd/common/enroll.c
+++ b/src/ipcpd/common/enroll.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Enrollment Task
*
diff --git a/src/ipcpd/common/enroll.h b/src/ipcpd/common/enroll.h
index f26c31a3..f56adfc8 100644
--- a/src/ipcpd/common/enroll.h
+++ b/src/ipcpd/common/enroll.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Enrollment Task
*
diff --git a/src/ipcpd/config.h.in b/src/ipcpd/config.h.in
index d2af6440..517016cc 100644
--- a/src/ipcpd/config.h.in
+++ b/src/ipcpd/config.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process configuration
*
@@ -23,17 +23,15 @@
#define PTHREAD_COND_CLOCK @PTHREAD_COND_CLOCK@
#define SYS_MAX_FLOWS @SYS_MAX_FLOWS@
-#define PROG_RES_FDS @PROG_RES_FDS@
-#define PROG_MAX_FLOWS @PROG_MAX_FLOWS@
+#define PROC_RES_FDS @PROC_RES_FDS@
+#define PROC_MAX_FLOWS @PROC_MAX_FLOWS@
#define SOCKET_TIMEOUT @SOCKET_TIMEOUT@
#define CONNECT_TIMEOUT @CONNECT_TIMEOUT@
-#define SHM_BUFFER_SIZE @SHM_BUFFER_SIZE@
-#define SHM_RDRB_BLOCK_SIZE @SHM_RDRB_BLOCK_SIZE@
+#define SSM_POOL_BLOCK_SIZE @SSM_POOL_BLOCK_SIZE@
#define DU_BUFF_HEADSPACE @DU_BUFF_HEADSPACE@
#define DU_BUFF_TAILSPACE @DU_BUFF_TAILSPACE@
-#cmakedefine SHM_RDRB_MULTI_BLOCK
#define IPCP_MIN_THREADS @IPCP_MIN_THREADS@
#define IPCP_ADD_THREADS @IPCP_ADD_THREADS@
@@ -47,12 +45,16 @@
#define QOS_PRIO_VOICE @IPCP_QOS_CUBE_VOICE_PRIO@
#define IPCP_SCHED_THR_MUL @IPCP_SCHED_THR_MUL@
#define PFT_SIZE @PFT_SIZE@
-#define DHT_ENROLL_SLACK @DHT_ENROLL_SLACK@
#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
@@ -67,20 +69,26 @@
#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_MPL @IPCP_ETH_MPL@
+#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@
/* broadcast */
-/* local */
#define IPCP_BROADCAST_MPL @IPCP_BROADCAST_MPL@
+#define IPCP_BROADCAST_MTU @IPCP_BROADCAST_MTU@
diff --git a/src/ipcpd/eth/CMakeLists.txt b/src/ipcpd/eth/CMakeLists.txt
index 44299a59..5a36352d 100644
--- a/src/ipcpd/eth/CMakeLists.txt
+++ b/src/ipcpd/eth/CMakeLists.txt
@@ -1,134 +1,21 @@
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-get_filename_component(CURRENT_BINARY_PARENT_DIR
- ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
-
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-find_path(NETMAP_C_INCLUDE_DIR
- net/netmap_user.h
- HINTS /usr/include /usr/local/include)
-
-mark_as_advanced(NETMAP_C_INCLUDE_DIR)
-
-# Check for raw sockets
-if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
- set(DISABLE_RAW_SOCKETS FALSE CACHE BOOL
- "Disable raw socket support for Ethernet IPCPs")
- if (NOT DISABLE_RAW_SOCKETS)
- message(STATUS "Raw socket support for Ethernet IPCPs enabled")
- set(HAVE_RAW_SOCKETS TRUE PARENT_SCOPE)
- set(HAVE_RAW_SOCKETS TRUE)
- set(HAVE_ETH TRUE)
- else ()
- message(STATUS "Raw socket support for Ethernet IPCPs disabled by user")
- unset(HAVE_RAW_SOCKETS PARENT_SCOPE)
- unset(HAVE_RAW_SOCKETS)
- endif ()
-endif ()
-
-# Check for BPF
-if (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
- find_path(BPF_C_INCLUDE_DIR
- net/bpf.h
- HINTS /usr/include /usr/local/include)
-
- mark_as_advanced(BPF_C_INCLUDE_DIR)
-
- if (BPF_C_INCLUDE_DIR)
- set(DISABLE_BPF FALSE CACHE BOOL
- "Disable Berkeley Packet Filter support for Ethernet IPCPs")
- if (NOT DISABLE_BPF)
- message(STATUS "Berkeley Packet Filter support "
- "for Ethernet IPCPs enabled")
- set(HAVE_BPF TRUE PARENT_SCOPE)
- set(HAVE_BPF TRUE)
- set(HAVE_ETH TRUE)
- else ()
- message(STATUS "Berkeley Packet Filter support "
- "for Ethernet IPCPs disabled by user")
- unset(HAVE_BPF PARENT_SCOPE)
- unset(HAVE_BPF)
- endif ()
- endif ()
-endif ()
-
-# Check for netmap exclusively
-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 PARENT_SCOPE)
- set(HAVE_ETH TRUE)
- else ()
- message(STATUS "Netmap support for Ethernet IPCPs disabled by user")
- unset(HAVE_NETMAP PARENT_SCOPE)
- endif ()
-endif ()
-
-if (HAVE_ETH)
- message(STATUS "Supported raw packet API found, building eth-llc and 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_LO_MTU 1500 CACHE STRING
- "Restrict Ethernet MTU over loopback interfaces")
- set(IPCP_ETH_MPL 100 CACHE STRING
- "Default maximum packet lifetime for the Ethernet IPCPs, in ms")
-
- set(ETH_LLC_SOURCES
- # Add source files here
- llc.c
- )
-
- set(ETH_DIX_SOURCES
- # Add source files here
- dix.c
- )
-
- set(IPCP_ETH_LLC_TARGET ipcpd-eth-llc CACHE INTERNAL "")
- set(IPCP_ETH_DIX_TARGET ipcpd-eth-dix CACHE INTERNAL "")
-
- add_executable(ipcpd-eth-llc ${ETH_LLC_SOURCES} ${IPCP_SOURCES})
- add_executable(ipcpd-eth-dix ${ETH_DIX_SOURCES} ${IPCP_SOURCES})
-
- if (HAVE_BPF AND NOT APPLE)
- target_include_directories(ipcpd-eth-llc PUBLIC ${BPF_C_INCLUDE_DIR})
- target_include_directories(ipcpd-eth-dix PUBLIC ${BPF_C_INCLUDE_DIR})
- endif ()
-
- if (HAVE_NETMAP AND NOT APPLE)
- set_target_properties(ipcpd-eth-llc PROPERTIES
- COMPILE_FLAGS "${CMAKE_C_FLAGS} -std=c99")
- set_target_properties(ipcpd-eth-dix PROPERTIES
- COMPILE_FLAGS "${CMAKE_C_FLAGS} -std=c99")
- target_include_directories(ipcpd-eth-llc PUBLIC
- ${NETMAP_C_INCLUDE_DIR})
- target_include_directories(ipcpd-eth-dix PUBLIC
- ${NETMAP_C_INCLUDE_DIR})
- endif ()
-
- target_link_libraries(ipcpd-eth-llc LINK_PUBLIC ouroboros-dev)
- target_link_libraries(ipcpd-eth-dix LINK_PUBLIC ouroboros-dev)
-
- include(AddCompileFlags)
- if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ipcpd-eth-llc -DCONFIG_OUROBOROS_DEBUG)
- add_compile_flags(ipcpd-eth-dix -DCONFIG_OUROBOROS_DEBUG)
- endif ()
-
- install(TARGETS ipcpd-eth-llc ipcpd-eth-dix RUNTIME DESTINATION
- ${CMAKE_INSTALL_SBINDIR})
-endif ()
+# 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/dix.c b/src/ipcpd/eth/dix.c
index 37b9896d..cf8253bd 100644
--- a/src/ipcpd/eth/dix.c
+++ b/src/ipcpd/eth/dix.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC processes over Ethernet - DIX
*
diff --git a/src/ipcpd/eth/eth.c b/src/ipcpd/eth/eth.c
index 0b6a91fb..2d3bd249 100644
--- a/src/ipcpd/eth/eth.c
+++ b/src/ipcpd/eth/eth.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC processes over Ethernet
*
@@ -37,21 +37,33 @@
#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/np1_flow.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>
@@ -71,6 +83,7 @@
#include <linux/if_ether.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
+#include <linux/sockios.h>
#endif
#ifdef __FreeBSD__
@@ -121,7 +134,8 @@
#define MGMT_EID 0
#define DIX_EID_SIZE sizeof(uint16_t)
#define DIX_LENGTH_SIZE sizeof(uint16_t)
-#define DIX_HEADER_SIZE (DIX_EID_SIZE + DIX_LENGTH_SIZE)
+#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)
@@ -129,21 +143,26 @@
#elif defined(BUILD_ETH_LLC)
#define THIS_TYPE IPCP_ETH_LLC
#define MGMT_SAP 0x01
-#define LLC_HEADER_SIZE 3
+#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 2000 /* ms */
-#define MGMT_TIMEO 100 /* ms */
-#define MGMT_FRAME_SIZE 2048
+#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
+#define FLOW_IRM_UPDATE 4
struct mgmt_msg {
#if defined(BUILD_ETH_DIX)
@@ -164,7 +183,7 @@ struct mgmt_msg {
uint32_t delay;
uint32_t timeout;
int32_t response;
- uint8_t in_order;
+ uint8_t service;
#if defined (BUILD_ETH_DIX)
uint8_t code;
uint8_t availability;
@@ -184,6 +203,7 @@ struct eth_frame {
uint8_t ssap;
uint8_t cf;
#endif
+ uint8_t hcs;
uint8_t payload;
} __attribute__((packed));
@@ -195,6 +215,17 @@ struct ef {
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 {
@@ -232,6 +263,22 @@ struct {
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];
@@ -283,7 +330,14 @@ static int eth_data_init(void)
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)
@@ -356,6 +410,232 @@ static void eth_data_fini(void)
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__)
+ int outq = 0;
+ 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__)
+ ioctl(eth_data.s_fd, SIOCOUTQ, &outq);
+ 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 ingress (bytes): %20d\n",
+ rcvbuf, sndbuf, queued);
+# if defined(__linux__)
+ n += sprintf(buf + n,
+ "Socket egress (bytes): %20d\n"
+ "Kernel frames received: %20zu\n"
+ "Kernel frames dropped: %20zu\n",
+ outq,
+ 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)
{
@@ -408,12 +688,18 @@ static int eth_ipcp_send_frame(const uint8_t * dst_addr,
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
@@ -439,10 +725,7 @@ static int eth_ipcp_send_frame(const uint8_t * dst_addr,
}
assert(FD_ISSET(eth_data.s_fd, &fds));
- if (sendto(eth_data.s_fd,
- frame,
- frame_len,
- 0,
+ 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));
@@ -450,6 +733,8 @@ static int eth_ipcp_send_frame(const uint8_t * dst_addr,
}
#endif /* HAVE_NETMAP */
+ FETCH_ADD_RELAXED(&eth_data.stat.n_snd, 1);
+
return 0;
}
@@ -474,6 +759,8 @@ static int eth_ipcp_alloc(const uint8_t * dst_addr,
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)
@@ -487,7 +774,7 @@ static int eth_ipcp_alloc(const uint8_t * dst_addr,
msg->availability = qs.availability;
msg->loss = hton32(qs.loss);
msg->ber = hton32(qs.ber);
- msg->in_order = qs.in_order;
+ msg->service = qs.service;
msg->max_gap = hton32(qs.max_gap);
msg->timeout = hton32(qs.timeout);
@@ -505,6 +792,9 @@ static int eth_ipcp_alloc(const uint8_t * dst_addr,
buf, len + data->len);
free(buf);
+ if (ret == 0)
+ FETCH_ADD_RELAXED(&eth_data.stat.n_mgmt_snd, 1);
+
return ret;
}
@@ -526,6 +816,8 @@ static int eth_ipcp_alloc_resp(uint8_t * dst_addr,
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;
@@ -553,11 +845,65 @@ static int eth_ipcp_alloc_resp(uint8_t * dst_addr,
return -1;
}
+ FETCH_ADD_RELAXED(&eth_data.stat.n_mgmt_snd, 1);
+
free(buf);
return 0;
}
+static int eth_ipcp_flow_update(int fd,
+ const buffer_t * data)
+{
+ struct mgmt_msg * msg;
+ struct ef * flow;
+ uint8_t * buf;
+ uint8_t r_addr[MAC_SIZE];
+ int ret;
+
+ 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_IRM_UPDATE;
+
+ pthread_rwlock_rdlock(&eth_data.flows_lock);
+
+ flow = &eth_data.fd_to_ef[fd];
+#if defined(BUILD_ETH_DIX)
+ msg->seid = htons((uint16_t) fd);
+ msg->deid = htons((uint16_t) flow->r_eid);
+#elif defined(BUILD_ETH_LLC)
+ msg->ssap = flow->sap;
+ msg->dsap = (uint8_t) flow->r_sap;
+#endif
+ memcpy(r_addr, flow->r_addr, MAC_SIZE);
+
+ pthread_rwlock_unlock(&eth_data.flows_lock);
+
+ if (data->len > 0)
+ memcpy(msg + 1, data->data, data->len);
+
+ ret = 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, sizeof(*msg) + data->len);
+ free(buf);
+
+ if (ret == 0)
+ FETCH_ADD_RELAXED(&eth_data.stat.n_mgmt_snd, 1);
+
+ return ret;
+}
+
static int eth_ipcp_req(uint8_t * r_addr,
#if defined(BUILD_ETH_DIX)
uint16_t r_eid,
@@ -570,7 +916,8 @@ static int eth_ipcp_req(uint8_t * r_addr,
{
int fd;
- fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_ETH_MPL, data);
+ 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;
@@ -617,7 +964,7 @@ static int eth_ipcp_alloc_reply(uint8_t * r_addr,
fd = eth_data.ef_to_fd[dsap];
#endif
if (fd < 0) {
- pthread_rwlock_unlock(& eth_data.flows_lock);
+ pthread_rwlock_unlock(&eth_data.flows_lock);
log_err("No flow found with that SAP.");
return -1; /* -EFLOWNOTFOUND */
}
@@ -642,7 +989,8 @@ static int eth_ipcp_alloc_reply(uint8_t * r_addr,
#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, data)) < 0) {
+ 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;
}
@@ -664,6 +1012,8 @@ static int eth_ipcp_name_query_req(const uint8_t * hash,
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;
@@ -682,6 +1032,8 @@ static int eth_ipcp_name_query_req(const uint8_t * hash,
return -1;
}
+ FETCH_ADD_RELAXED(&eth_data.stat.n_mgmt_snd, 1);
+
free(buf);
}
@@ -702,6 +1054,44 @@ static int eth_ipcp_name_query_reply(const uint8_t * hash,
return 0;
}
+static int eth_ipcp_flow_update_arr(const uint8_t * buf,
+ size_t len)
+{
+ struct mgmt_msg * msg;
+ buffer_t data;
+ int fd;
+ int flow_id;
+
+ msg = (struct mgmt_msg *) buf;
+
+ data.data = (uint8_t *) buf + sizeof(*msg);
+ data.len = len - sizeof(*msg);
+
+ pthread_rwlock_rdlock(&eth_data.flows_lock);
+#if defined(BUILD_ETH_DIX)
+ fd = ntohs(msg->deid);
+#elif defined(BUILD_ETH_LLC)
+ fd = eth_data.ef_to_fd[msg->dsap];
+#endif
+ pthread_rwlock_unlock(&eth_data.flows_lock);
+
+ if (fd < 0 || fd >= SYS_MAX_FLOWS) {
+ log_err("Flow update for unknown endpoint.");
+ return -1;
+ }
+
+ flow_id = np1_flow_id(fd);
+ if (flow_id < 0)
+ return -1;
+
+ if (ipcp_flow_update_arr(flow_id, &data) < 0) {
+ log_err("Failed to relay flow update on fd %d.", fd);
+ return -1;
+ }
+
+ return 0;
+}
+
static int eth_ipcp_mgmt_frame(const uint8_t * buf,
size_t len,
uint8_t * r_addr)
@@ -711,20 +1101,24 @@ static int eth_ipcp_mgmt_frame(const uint8_t * buf,
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();
- assert(len >= msg_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.in_order = msg->in_order;
+ qs.service = msg->service;
qs.max_gap = ntoh32(msg->max_gap);
qs.timeout = ntoh32(msg->timeout);
@@ -745,8 +1139,6 @@ static int eth_ipcp_mgmt_frame(const uint8_t * buf,
}
break;
case FLOW_REPLY:
- assert(len >= sizeof(*msg));
-
data.data = (uint8_t *) buf + sizeof(*msg);
data.len = len - sizeof(*msg);
@@ -761,10 +1153,17 @@ static int eth_ipcp_mgmt_frame(const uint8_t * buf,
ntoh32(msg->response),
&data);
break;
+ case FLOW_IRM_UPDATE:
+ eth_ipcp_flow_update_arr(buf, len);
+ 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:
@@ -833,10 +1232,16 @@ static void * eth_ipcp_packet_reader(void * o)
#if defined(HAVE_NETMAP)
struct nm_pkthdr hdr;
#else
- struct shm_du_buff * sdb;
+ 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;
@@ -865,33 +1270,67 @@ static void * eth_ipcp_packet_reader(void * o)
if (select(eth_data.bpf + 1, &fds, NULL, NULL, NULL))
continue;
assert(FD_ISSET(eth_data.bpf, &fds));
- if (ipcp_sdb_reserve(&sdb, BPF_LEN))
+ if (ipcp_spb_reserve(&spb, BPF_LEN))
continue;
- buf = shm_du_buff_head(sdb);
+ 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_sdb_reserve(&sdb, ETH_MTU))
+ if (ipcp_spb_reserve(&spb, ETH_MTU)) {
+ FETCH_ADD_RELAXED(&eth_data.stat.n_buf_f, 1);
continue;
- buf = shm_du_buff_head_alloc(sdb, ETH_HEADER_TOT_SIZE);
+ }
+ buf = ssm_pk_buff_push(spb, ETH_HEADER_TOT_SIZE);
if (buf == NULL) {
log_dbg("Failed to allocate header.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
+ FETCH_ADD_RELAXED(&eth_data.stat.n_buf_f, 1);
continue;
}
- frame_len = recv(eth_data.s_fd, buf,
- ETH_MTU + ETH_HEADER_TOT_SIZE, 0);
+ 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) {
- log_dbg("Failed to receive frame.");
- ipcp_sdb_release(sdb);
+ 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);
@@ -909,6 +1348,8 @@ static void * eth_ipcp_packet_reader(void * o)
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);
@@ -916,25 +1357,55 @@ static void * eth_ipcp_packet_reader(void * o)
if (e_frame->ethertype != eth_data.ethertype)
goto fail_frame;
+ if (length > ETH_MTU)
+ goto fail_frame;
+
deid = ntohs(e_frame->eid);
- if (deid == MGMT_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_sdb_release(sdb); /* No need for the N+1 buffer. */
+ 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.");
- goto fail_frame;
+ continue;
}
memcpy(frame->buf, &e_frame->payload, length);
@@ -945,6 +1416,8 @@ static void * eth_ipcp_packet_reader(void * o)
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);
@@ -955,6 +1428,7 @@ static void * eth_ipcp_packet_reader(void * o)
#endif
if (fd < 0) {
pthread_rwlock_unlock(&eth_data.flows_lock);
+ FETCH_ADD_RELAXED(&eth_data.stat.n_bad_id, 1);
goto fail_frame;
}
@@ -963,28 +1437,38 @@ static void * eth_ipcp_packet_reader(void * o)
|| 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
- shm_du_buff_head_release(sdb, ETH_HEADER_TOT_SIZE);
- shm_du_buff_truncate(sdb, length);
+ ssm_pk_buff_pop(spb, ETH_HEADER_TOT_SIZE);
+ ssm_pk_buff_truncate(spb, length);
#else
- if (ipcp_sdb_reserve(&sdb, length))
+ if (ipcp_spb_reserve(&spb, length))
continue;
- buf = shm_du_buff_head(sdb);
+ buf = ssm_pk_buff_head(spb);
memcpy(buf, &e_frame->payload, length);
#endif
- if (np1_flow_write(fd, sdb) < 0)
- ipcp_sdb_release(sdb);
+ 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_sdb_release(sdb);
+ ipcp_spb_release(spb);
#endif
}
}
@@ -1000,7 +1484,7 @@ static void cleanup_writer(void * o)
static void * eth_ipcp_packet_writer(void * o)
{
int fd;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
size_t len;
#if defined(BUILD_ETH_DIX)
uint16_t deid;
@@ -1028,17 +1512,18 @@ static void * eth_ipcp_packet_writer(void * o)
if (fqueue_type(fq) != FLOW_PKT)
continue;
- if (np1_flow_read(fd, &sdb)) {
+ if (np1_flow_read(fd, &spb, NP1_GET_POOL(fd))) {
log_dbg("Bad read from fd %d.", fd);
continue;
}
- len = shm_du_buff_len(sdb);
+ len = ssm_pk_buff_len(spb);
- if (shm_du_buff_head_alloc(sdb, ETH_HEADER_TOT_SIZE)
+ if (ssm_pk_buff_push(spb, ETH_HEADER_TOT_SIZE)
== NULL) {
log_dbg("Failed to allocate header.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
+ FETCH_ADD_RELAXED(&eth_data.stat.n_buf_f, 1);
continue;
}
@@ -1061,10 +1546,22 @@ static void * eth_ipcp_packet_writer(void * o)
#elif defined(BUILD_ETH_LLC)
dsap, ssap,
#endif
- shm_du_buff_head(sdb),
- len))
+ ssm_pk_buff_head(spb),
+ len)) {
log_dbg("Failed to send frame.");
- ipcp_sdb_release(sdb);
+ 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);
}
}
@@ -1330,14 +1827,7 @@ static int eth_set_mtu(struct ifreq * ifr)
IPCP_ETH_LO_MTU);
eth_data.mtu = IPCP_ETH_LO_MTU;
}
-#ifndef SHM_RDRB_MULTI_BLOCK
- maxsz = SHM_RDRB_BLOCK_SIZE - 5 * sizeof(size_t) -
- (DU_BUFF_HEADSPACE + DU_BUFF_TAILSPACE);
- if ((size_t) eth_data.mtu > maxsz ) {
- log_dbg("Layer MTU truncated to shm block size.");
- eth_data.mtu = maxsz;
- }
-#endif
+
log_dbg("Layer MTU is %d.", eth_data.mtu);
return 0;
@@ -1418,12 +1908,14 @@ static int eth_init_bpf(struct ifreq * ifr)
return -1;
}
#elif defined(HAVE_RAW_SOCKETS)
+#define SOCKOPT()
static int eth_init_raw_socket(struct ifreq * ifr)
{
int idx;
- int flags;
+ int sndbuf;
+ int rcvbuf;
#if defined(IPCP_ETH_QDISC_BYPASS)
- int qdisc_bypass = 1;
+ int qdisc_bypass = 1;
#endif /* ENABLE_QDISC_BYPASS */
idx = if_nametoindex(ifr->ifr_name);
@@ -1431,6 +1923,7 @@ static int eth_init_raw_socket(struct ifreq * ifr)
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;
@@ -1447,17 +1940,6 @@ static int eth_init_raw_socket(struct ifreq * ifr)
goto fail_socket;
}
- flags = fcntl(eth_data.s_fd, F_GETFL, 0);
- if (flags < 0) {
- log_err("Failed to get flags.");
- goto fail_device;
- }
-
- if (fcntl(eth_data.s_fd, F_SETFL, flags | O_NONBLOCK)) {
- log_err("Failed to set socket non-blocking.");
- goto fail_device;
- }
-
#if defined(IPCP_ETH_QDISC_BYPASS)
if (setsockopt(eth_data.s_fd, SOL_PACKET, PACKET_QDISC_BYPASS,
&qdisc_bypass, sizeof(qdisc_bypass))) {
@@ -1465,6 +1947,25 @@ static int eth_init_raw_socket(struct ifreq * ifr)
}
#endif
+ sndbuf = IPCP_ETH_SNDBUF;
+ if (sndbuf > 0) {
+ /* Never below one frame or sendto() cannot fit an SDU. */
+ sndbuf = MAX(sndbuf, (int) (ETH_HEADER_TOT_SIZE + ETH_MTU));
+ if (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) {
+ /* SO_RCVBUFFORCE bypasses rmem_max; SO_RCVBUF is fallback. */
+ if (setsockopt(eth_data.s_fd, SOL_SOCKET, SO_RCVBUFFORCE,
+ &rcvbuf, sizeof(rcvbuf))
+ && 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.");
@@ -1491,9 +1992,6 @@ static int eth_ipcp_bootstrap(struct ipcp_config * conf)
char ifn[IFNAMSIZ];
#endif /* HAVE_NETMAP */
-#ifndef SHM_RDRB_MULTI_BLOCK
- size_t maxsz;
-#endif
assert(conf);
assert(conf->type == THIS_TYPE);
@@ -1540,6 +2038,12 @@ static int eth_ipcp_bootstrap(struct ipcp_config * conf)
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)) {
@@ -1603,6 +2107,10 @@ static int eth_ipcp_bootstrap(struct ipcp_config * conf)
#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)
@@ -1634,12 +2142,14 @@ static int eth_ipcp_unreg(const uint8_t * hash)
static int eth_ipcp_query(const uint8_t * hash)
{
uint8_t r_addr[MAC_SIZE];
- struct timespec timeout = TIMESPEC_INIT_MS(NAME_QUERY_TIMEO);
+ 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;
@@ -1650,6 +2160,8 @@ static int eth_ipcp_query(const uint8_t * hash)
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;
@@ -1657,32 +2169,46 @@ static int eth_ipcp_query(const uint8_t * hash)
memset(r_addr, 0xff, MAC_SIZE);
- query = shim_data_dir_query_create(eth_data.shim_data, hash);
- if (query == NULL) {
- free(buf);
- return -1;
- }
+ per_ms = NAME_QUERY_TIMEO / (NAME_QUERY_RETRIES + 1);
- if (eth_ipcp_send_frame(r_addr,
+ 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,
+ MGMT_EID,
#elif defined(BUILD_ETH_LLC)
- reverse_bits(MGMT_SAP),
- reverse_bits(MGMT_SAP),
+ reverse_bits(MGMT_SAP),
+ reverse_bits(MGMT_SAP),
#endif
- buf, len)) {
- log_err("Failed to send management frame.");
+ 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);
- free(buf);
- return -1;
+
+ if (ret != -ETIMEDOUT)
+ break;
}
free(buf);
- ret = shim_data_dir_query_wait(query, &timeout);
-
- shim_data_dir_query_destroy(eth_data.shim_data, query);
-
return ret;
}
@@ -1743,6 +2269,14 @@ static int eth_ipcp_flow_alloc(int fd,
}
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
@@ -1803,6 +2337,14 @@ static int eth_ipcp_flow_alloc_resp(int fd,
}
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
@@ -1831,6 +2373,12 @@ static int eth_ipcp_flow_dealloc(int fd)
#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);
@@ -1849,7 +2397,8 @@ static struct ipcp_ops eth_ops = {
.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
+ .ipcp_flow_dealloc = eth_ipcp_flow_dealloc,
+ .ipcp_flow_update = eth_ipcp_flow_update
};
int main(int argc,
@@ -1897,6 +2446,9 @@ int main(int argc,
#ifdef __linux__
pthread_join(eth_data.if_monitor, NULL);
#endif
+#ifdef IPCP_ETH_FLOW_STATS
+ rib_unreg(ETH_RIB_PATH);
+#endif
}
ipcp_stop();
diff --git a/src/ipcpd/eth/llc.c b/src/ipcpd/eth/llc.c
index c900dcab..a772e86e 100644
--- a/src/ipcpd/eth/llc.c
+++ b/src/ipcpd/eth/llc.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC processes over Ethernet - LLC
*
diff --git a/src/ipcpd/ipcp.c b/src/ipcpd/ipcp.c
index ebb9b1c5..dcee4b9c 100644
--- a/src/ipcpd/ipcp.c
+++ b/src/ipcpd/ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process main loop
*
@@ -52,6 +52,7 @@
#include <ouroboros/utils.h>
#include "ipcp.h"
+#include "np1.h"
#include <signal.h>
#include <string.h>
@@ -131,6 +132,8 @@ struct {
pthread_t acceptor;
} ipcpd;
+struct np1_state np1;
+
struct cmd {
struct list_head next;
@@ -205,7 +208,7 @@ static int ipcp_rib_read(const char * path,
char * buf,
size_t len)
{
- char * entry;
+ const char * entry;
if (len < LAYER_NAME_SIZE + 2) /* trailing \n */
return 0;
@@ -360,6 +363,7 @@ static void * acceptloop(void * o)
int ipcp_wait_flow_req_arr(const uint8_t * dst,
qosspec_t qs,
time_t mpl,
+ uint32_t mtu,
const buffer_t * data)
{
struct timespec ts = TIMESPEC_INIT_MS(ALLOC_TIMEOUT);
@@ -389,7 +393,7 @@ int ipcp_wait_flow_req_arr(const uint8_t * dst,
assert(ipcpd.alloc_id == -1);
- fd = ipcp_flow_req_arr(&hash, qs, mpl, data);
+ fd = ipcp_flow_req_arr(&hash, qs, mpl, mtu, data);
if (fd < 0) {
pthread_mutex_unlock(&ipcpd.alloc_lock);
log_err("Failed to get fd for flow.");
@@ -633,9 +637,11 @@ static void do_flow_alloc(pid_t pid,
uint8_t * dst,
qosspec_t qs,
const buffer_t * data,
+ uid_t uid,
ipcp_msg_t * ret_msg)
{
- int fd;
+ int fd;
+ struct ssm_pool * pool = NULL;
log_info("Allocating flow %d for %d to " HASH_FMT32 ".",
flow_id, pid, HASH_VAL32(dst));
@@ -662,6 +668,17 @@ static void do_flow_alloc(pid_t pid,
return;
}
+ if (uid != 0) {
+ pool = ssm_pool_open(uid);
+ if (pool == NULL) {
+ log_err("Failed to open PUP for uid %d.", uid);
+ ret_msg->result = -ENOMEM;
+ return;
+ }
+ }
+
+ NP1_SET_POOL(fd, pool);
+
ret_msg->result = ipcpd.ops->ipcp_flow_alloc(fd, dst, qs, data);
log_info("Finished allocating flow %d to " HASH_FMT32 ".",
@@ -672,9 +689,11 @@ static void do_flow_alloc(pid_t pid,
static void do_flow_join(pid_t pid,
int flow_id,
const uint8_t * dst,
+ uid_t uid,
ipcp_msg_t * ret_msg)
{
- int fd;
+ int fd;
+ struct ssm_pool * pool = NULL;
log_info("Joining layer " HASH_FMT32 ".", HASH_VAL32(dst));
@@ -699,6 +718,17 @@ static void do_flow_join(pid_t pid,
return;
}
+ if (uid != 0) {
+ pool = ssm_pool_open(uid);
+ if (pool == NULL) {
+ log_err("Failed to open PUP for uid %d.", uid);
+ ret_msg->result = -ENOMEM;
+ return;
+ }
+ }
+
+ NP1_SET_POOL(fd, pool);
+
ret_msg->result = ipcpd.ops->ipcp_flow_join(fd, dst);
log_info("Finished joining layer " HASH_FMT32 ".", HASH_VAL32(dst));
@@ -706,10 +736,12 @@ static void do_flow_join(pid_t pid,
static void do_flow_alloc_resp(int resp,
int flow_id,
+ uid_t uid,
const buffer_t * data,
ipcp_msg_t * ret_msg)
{
- int fd = -1;
+ int fd = -1;
+ struct ssm_pool * pool = NULL;
log_info("Responding %d to alloc on flow_id %d.", resp, flow_id);
@@ -737,6 +769,17 @@ static void do_flow_alloc_resp(int resp,
return;
}
+ if (uid != 0) {
+ pool = ssm_pool_open(uid);
+ if (pool == NULL) {
+ log_err("Failed to open PUP for uid %d.", uid);
+ ret_msg->result = -ENOMEM;
+ return;
+ }
+ }
+
+ NP1_SET_POOL(fd, pool);
+
ret_msg->result = ipcpd.ops->ipcp_flow_alloc_resp(fd, resp, data);
log_info("Finished responding %d to allocation request.",
@@ -777,6 +820,33 @@ 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,
+ ipcp_msg_t * ret_msg)
+{
+ int fd;
+
+ if (ipcpd.ops->ipcp_flow_update == NULL) {
+ log_err("Failed to update flow: operation unsupported.");
+ ret_msg->result = -ENOTSUP;
+ return;
+ }
+
+ if (ipcp_get_state() != IPCP_OPERATIONAL) {
+ ret_msg->result = -EIPCPSTATE;
+ 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;
@@ -857,12 +927,12 @@ static void * mainloop(void * o)
qs = qos_spec_msg_to_s(msg->qosspec);
do_flow_alloc(msg->pid, msg->flow_id,
msg->hash.data, qs,
- &data, &ret_msg);
+ &data, msg->uid, &ret_msg);
break;
case IPCP_MSG_CODE__IPCP_FLOW_JOIN:
assert(msg->hash.len == ipcp_dir_hash_len());
do_flow_join(msg->pid, msg->flow_id,
- msg->hash.data, &ret_msg);
+ msg->hash.data, msg->uid, &ret_msg);
break;
case IPCP_MSG_CODE__IPCP_FLOW_ALLOC_RESP:
assert(msg->pk.len > 0 ? msg->pk.data != NULL
@@ -870,11 +940,18 @@ static void * mainloop(void * o)
data.len = msg->pk.len;
data.data = msg->pk.data;
do_flow_alloc_resp(msg->response, msg->flow_id,
- &data, &ret_msg);
+ msg->uid, &data, &ret_msg);
break;
case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC:
do_flow_dealloc(msg->flow_id, msg->timeo_sec, &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, &ret_msg);
+ break;
default:
ret_msg.result = -1;
log_err("Unknown message code: %d.", msg->code);
@@ -1035,6 +1112,8 @@ int ipcp_init(int argc,
ipcpd.alloc_id = -1;
+ memset(&np1, 0, sizeof(np1));
+
pthread_condattr_destroy(&cattr);
ipcp_set_state(IPCP_INIT);
diff --git a/src/ipcpd/ipcp.h b/src/ipcpd/ipcp.h
index e8c31a32..210157ec 100644
--- a/src/ipcpd/ipcp.h
+++ b/src/ipcpd/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process structure
*
@@ -68,6 +68,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,
@@ -98,6 +101,7 @@ enum ipcp_state ipcp_get_state(void);
int ipcp_wait_flow_req_arr(const uint8_t * dst,
qosspec_t qs,
time_t mpl,
+ uint32_t mtu,
const buffer_t * data);
int ipcp_wait_flow_resp(const int fd);
diff --git a/src/ipcpd/local/CMakeLists.txt b/src/ipcpd/local/CMakeLists.txt
index f661e9ae..91f300a3 100644
--- a/src/ipcpd/local/CMakeLists.txt
+++ b/src/ipcpd/local/CMakeLists.txt
@@ -1,36 +1,17 @@
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-get_filename_component(CURRENT_BINARY_PARENT_DIR
- ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+# Local IPCP build configuration
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
+add_executable(${IPCP_LOCAL_TARGET}
+ main.c
+ ${IPCP_SOURCES}
+)
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
+target_include_directories(${IPCP_LOCAL_TARGET} PRIVATE ${IPCP_INCLUDE_DIRS})
+target_link_libraries(${IPCP_LOCAL_TARGET} PRIVATE ouroboros-dev)
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
+ouroboros_target_debug_definitions(${IPCP_LOCAL_TARGET})
-set(IPCP_LOCAL_TARGET ipcpd-local CACHE INTERNAL "")
-set(IPCP_LOCAL_MPL 100 CACHE STRING
- "Default maximum packet lifetime for the Ethernet IPCPs, in ms")
+if(IPCP_LOCAL_POLLING)
+ target_compile_definitions(${IPCP_LOCAL_TARGET} PRIVATE CONFIG_IPCP_LOCAL_POLLING)
+endif()
-set(LOCAL_SOURCES
- # Add source files here
- ${CMAKE_CURRENT_SOURCE_DIR}/main.c)
-
-add_executable(ipcpd-local ${LOCAL_SOURCES} ${IPCP_SOURCES})
-target_link_libraries(ipcpd-local LINK_PUBLIC ouroboros-common ouroboros-dev)
-
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ipcpd-local -DCONFIG_OUROBOROS_DEBUG)
-endif ()
-
-install(TARGETS ipcpd-local RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
-
-# Enable once ipcp-local has tests
-# if(BUILD_TESTS)
-# add_subdirectory(tests)
-# endif ()
+install(TARGETS ${IPCP_LOCAL_TARGET} RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
diff --git a/src/ipcpd/local/main.c b/src/ipcpd/local/main.c
index ffa6dc5a..c0aeb51e 100644
--- a/src/ipcpd/local/main.c
+++ b/src/ipcpd/local/main.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Local IPC process
*
@@ -38,8 +38,10 @@
#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 <string.h>
@@ -103,34 +105,41 @@ static void local_data_fini(void){
static void * local_ipcp_packet_loop(void * o)
{
+ int src_fd;
+ int dst_fd;
+ struct timespec * timeout;
+#ifdef CONFIG_IPCP_LOCAL_POLLING
+ struct timespec ts_poll = {0, 0};
+#endif
(void) o;
ipcp_lock_to_core();
- while (true) {
- int fd;
- ssize_t idx;
+#ifdef CONFIG_IPCP_LOCAL_POLLING
+ timeout = &ts_poll; /* Spin poll with zero timeout */
+#else
+ timeout = NULL; /* Block until event */
+#endif
- fevent(local_data.flows, local_data.fq, NULL);
+ while (true) {
+ fevent(local_data.flows, local_data.fq, timeout);
- while ((fd = fqueue_next(local_data.fq)) >= 0) {
+ while ((src_fd = fqueue_next(local_data.fq)) >= 0) {
if (fqueue_type(local_data.fq) != FLOW_PKT)
continue;
- idx = local_flow_read(fd);
- if (idx < 0)
- continue;
-
- assert(idx < (SHM_BUFFER_SIZE));
-
pthread_rwlock_rdlock(&local_data.lock);
- fd = local_data.in_out[fd];
+ dst_fd = local_data.in_out[src_fd];
pthread_rwlock_unlock(&local_data.lock);
- if (fd != -1)
- local_flow_write(fd, idx);
+ if (dst_fd == -1)
+ continue;
+
+ local_flow_transfer(src_fd, dst_fd,
+ NP1_GET_POOL(src_fd),
+ NP1_GET_POOL(dst_fd));
}
}
@@ -195,7 +204,8 @@ static int local_ipcp_flow_alloc(int fd,
HASH_VAL32(dst), fd);
assert(dst);
- out_fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_LOCAL_MPL, data);
+ out_fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_LOCAL_MPL,
+ IPCP_LOCAL_MTU, data);
if (out_fd < 0) {
log_dbg("Flow allocation failed: %d", out_fd);
return -1;
@@ -228,15 +238,6 @@ static int local_ipcp_flow_alloc_resp(int fd,
return -1;
}
- if (response < 0) {
- pthread_rwlock_wrlock(&local_data.lock);
- if (local_data.in_out[fd] != -1)
- local_data.in_out[local_data.in_out[fd]] = fd;
- local_data.in_out[fd] = -1;
- pthread_rwlock_unlock(&local_data.lock);
- return 0;
- }
-
pthread_rwlock_rdlock(&local_data.lock);
out_fd = local_data.in_out[fd];
@@ -255,9 +256,17 @@ static int local_ipcp_flow_alloc_resp(int fd,
return -1;
}
+ if (response < 0) {
+ ipcp_flow_alloc_reply(out_fd, response, mpl,
+ IPCP_LOCAL_MTU, data);
+ log_info("Flow allocation rejected, fds (%d, %d).", out_fd, fd);
+ return 0;
+ }
+
fset_add(local_data.flows, fd);
- if (ipcp_flow_alloc_reply(out_fd, response, mpl, data) < 0) {
+ if (ipcp_flow_alloc_reply(out_fd, response, mpl,
+ IPCP_LOCAL_MTU, data) < 0) {
log_err("Failed to reply to allocation");
fset_del(local_data.flows, fd);
return -1;
@@ -289,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,
@@ -300,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/irmd/tests/irm_test.c b/src/ipcpd/np1.h
index d440289c..b7792cb9 100644
--- a/src/irmd/tests/irm_test.c
+++ b/src/ipcpd/np1.h
@@ -1,7 +1,8 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * N+1 flow pool tracking for IPCPs
*
- * Unit tests of IRMd functions
* Dimitri Staessens <dimitri@ouroboros.rocks>
* Sander Vrijders <sander@ouroboros.rocks>
*
@@ -19,15 +20,22 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
+#ifndef OUROBOROS_IPCPD_NP1_H
+#define OUROBOROS_IPCPD_NP1_H
+
+#include "config.h"
+
+#include <ouroboros/ssm_pool.h>
+#define NP1_LOAD(ptr) (__atomic_load_n((ptr), __ATOMIC_ACQUIRE))
+#define NP1_STORE(ptr, v) (__atomic_store_n((ptr), (v), __ATOMIC_RELEASE))
+#define NP1_GET_POOL(fd) (NP1_LOAD(&np1.pool[(fd)]))
+#define NP1_SET_POOL(fd, p) (NP1_STORE(&np1.pool[(fd)], (p)))
-int irm_test(int argc,
- char **argv)
-{
- int ret = 0;
+struct np1_state {
+ struct ssm_pool * pool[SYS_MAX_FLOWS];
+};
- (void) argc;
- (void) argv;
+extern struct np1_state np1;
- return ret;
-}
+#endif /* OUROBOROS_IPCPD_NP1_H */
diff --git a/src/ipcpd/shim-data.c b/src/ipcpd/shim-data.c
index 8801213a..90a676da 100644
--- a/src/ipcpd/shim-data.c
+++ b/src/ipcpd/shim-data.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process utilities
*
diff --git a/src/ipcpd/shim-data.h b/src/ipcpd/shim-data.h
index ea4ce413..fbadb4d4 100644
--- a/src/ipcpd/shim-data.h
+++ b/src/ipcpd/shim-data.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Utitilies for building IPC processes
*
diff --git a/src/ipcpd/udp/CMakeLists.txt b/src/ipcpd/udp/CMakeLists.txt
index 27e32094..a98f0919 100644
--- a/src/ipcpd/udp/CMakeLists.txt
+++ b/src/ipcpd/udp/CMakeLists.txt
@@ -1,81 +1,14 @@
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-get_filename_component(CURRENT_BINARY_PARENT_DIR
- ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+# UDP IPCPs build configuration (UDP4 and UDP6)
+# DDNS detection is in cmake/dependencies/udp/ddns.cmake
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
+add_executable(${IPCP_UDP4_TARGET} udp4.c ${IPCP_SOURCES})
+add_executable(${IPCP_UDP6_TARGET} udp6.c ${IPCP_SOURCES})
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(IPCP_UDP4_TARGET ipcpd-udp4 CACHE INTERNAL "")
-set(IPCP_UDP6_TARGET ipcpd-udp6 CACHE INTERNAL "")
-
-set(UDP4_SOURCES
- # Add source files here
- udp4.c
-)
-
-set(UDP6_SOURCES
- # Add source files here
- udp6.c
-)
-
-add_executable(ipcpd-udp4 ${UDP4_SOURCES} ${IPCP_SOURCES})
-target_link_libraries(ipcpd-udp4 LINK_PUBLIC ouroboros-dev)
-
-add_executable(ipcpd-udp6 ${UDP6_SOURCES} ${IPCP_SOURCES})
-target_link_libraries(ipcpd-udp6 LINK_PUBLIC ouroboros-dev)
-
-
-# Find the nsupdate executable
-find_program(NSUPDATE_EXECUTABLE
- NAMES nsupdate
- DOC "The nsupdate tool that enables DDNS")
-
-# Find the nslookup executable
-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_DNS)
- message(STATUS "DDNS support enabled")
- set(HAVE_DDNS TRUE CACHE INTERNAL "")
- 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 ()
-
-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")
-
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ipcpd-udp4 -DCONFIG_OUROBOROS_DEBUG)
- add_compile_flags(ipcpd-udp6 -DCONFIG_OUROBOROS_DEBUG)
-endif ()
-
-install(TARGETS ipcpd-udp4 RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
-install(TARGETS ipcpd-udp6 RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
+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
index be8069a4..db57e2f4 100644
--- a/src/ipcpd/udp/udp.c
+++ b/src/ipcpd/udp/udp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process over UDP
*
@@ -28,12 +28,15 @@
#include <ouroboros/list.h>
#include <ouroboros/utils.h>
#include <ouroboros/dev.h>
+#include <ouroboros/ipcp-dev.h>
+#include <ouroboros/np1_flow.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>
@@ -46,9 +49,14 @@
#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 FLOW_IRM_UPDATE 3
#define OUR_HEADER_LEN sizeof(uint32_t) /* adds eid */
@@ -86,7 +94,7 @@ struct mgmt_msg {
uint8_t code;
/* QoS parameters from spec */
uint8_t availability;
- uint8_t in_order;
+ uint8_t service;
} __attribute__((packed));
struct mgmt_frame {
@@ -129,6 +137,53 @@ static const char * __inet_ntop(const struct __ADDR * addr,
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;
@@ -208,6 +263,8 @@ static int udp_ipcp_port_alloc(const struct __SOCKADDR * r_saddr,
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;
@@ -217,7 +274,7 @@ static int udp_ipcp_port_alloc(const struct __SOCKADDR * r_saddr,
msg->availability = qs.availability;
msg->loss = hton32(qs.loss);
msg->ber = hton32(qs.ber);
- msg->in_order = qs.in_order;
+ msg->service = qs.service;
msg->max_gap = hton32(qs.max_gap);
msg->timeout = hton32(qs.timeout);
@@ -251,6 +308,8 @@ static int udp_ipcp_port_alloc_resp(const struct __SOCKADDR * r_saddr,
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);
@@ -272,6 +331,48 @@ static int udp_ipcp_port_alloc_resp(const struct __SOCKADDR * r_saddr,
return 0;
}
+static int udp_ipcp_flow_update(int fd,
+ const buffer_t * data)
+{
+ struct mgmt_msg * msg;
+ struct __SOCKADDR r_saddr;
+ uint32_t d_eid;
+
+ msg = malloc(sizeof(*msg) + data->len);
+ if (msg == NULL)
+ return -1;
+
+ memset(msg, 0, sizeof(*msg) + data->len);
+
+ pthread_rwlock_rdlock(&udp_data.flows_lock);
+
+ r_saddr = udp_data.fd_to_uf[fd].r_saddr;
+ d_eid = (uint32_t) udp_data.fd_to_uf[fd].d_eid;
+
+ pthread_rwlock_unlock(&udp_data.flows_lock);
+
+ msg->eid = hton32(MGMT_EID);
+ msg->code = FLOW_IRM_UPDATE;
+ msg->s_eid = hton32(d_eid);
+ msg->d_eid = hton32((uint32_t) fd);
+
+ 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) {
+ log_err("Failed to send flow update: %s.", strerror(errno));
+ 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,
@@ -280,7 +381,8 @@ static int udp_ipcp_port_req(struct __SOCKADDR * c_saddr,
{
int fd;
- fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UDP_MPL, data);
+ 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;
@@ -327,7 +429,8 @@ static int udp_ipcp_port_alloc_reply(const struct __SOCKADDR * saddr,
pthread_rwlock_unlock(&udp_data.flows_lock);
- if (ipcp_flow_alloc_reply(s_eid, response, mpl, data) < 0) {
+ 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;
}
@@ -338,6 +441,37 @@ static int udp_ipcp_port_alloc_reply(const struct __SOCKADDR * saddr,
return 0;
}
+static int udp_ipcp_flow_update_arr(const uint8_t * buf,
+ size_t len)
+{
+ struct mgmt_msg * msg;
+ buffer_t data;
+ int fd;
+ int flow_id;
+
+ msg = (struct mgmt_msg *) buf;
+
+ fd = (int) ntoh32(msg->s_eid);
+ if (fd < 0 || fd >= SYS_MAX_FLOWS) {
+ log_err("Flow update for invalid eid %d.", fd);
+ return -1;
+ }
+
+ data.len = len - sizeof(*msg);
+ data.data = (uint8_t *) buf + sizeof(*msg);
+
+ flow_id = np1_flow_id(fd);
+ if (flow_id < 0)
+ return -1;
+
+ if (ipcp_flow_update_arr(flow_id, &data) < 0) {
+ log_err("Failed to relay flow update on fd %d.", fd);
+ return -1;
+ }
+
+ return 0;
+}
+
static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr,
const uint8_t * buf,
size_t len)
@@ -347,13 +481,18 @@ static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr,
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();
- assert(len >= msg_len);
+ if (len < msg_len)
+ return -1;
data.len = len - msg_len;
data.data = (uint8_t *) buf + msg_len;
@@ -364,7 +503,7 @@ static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr,
qs.availability = msg->availability;
qs.loss = ntoh32(msg->loss);
qs.ber = ntoh32(msg->ber);
- qs.in_order = msg->in_order;
+ qs.service = msg->service;
qs.max_gap = ntoh32(msg->max_gap);
qs.timeout = ntoh32(msg->timeout);
@@ -372,8 +511,6 @@ static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr,
(uint8_t *) (msg + 1), qs,
&data);
case FLOW_REPLY:
- assert(len >= sizeof(*msg));
-
data.len = len - sizeof(*msg);
data.data = (uint8_t *) buf + sizeof(*msg);
@@ -382,6 +519,8 @@ static int udp_ipcp_mgmt_frame(struct __SOCKADDR c_saddr,
ntoh32(msg->d_eid),
ntoh32(msg->response),
&data);
+ case FLOW_IRM_UPDATE:
+ return udp_ipcp_flow_update_arr(buf, len);
default:
log_err("Unknown message received %d.", msg->code);
return -1;
@@ -439,7 +578,7 @@ static void * udp_ipcp_packet_reader(void * o)
struct mgmt_frame * frame;
struct __SOCKADDR r_saddr;
socklen_t len;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
uint8_t * head;
len = sizeof(r_saddr);
@@ -483,13 +622,13 @@ static void * udp_ipcp_packet_reader(void * o)
n-= sizeof(eid);
- if (ipcp_sdb_reserve(&sdb, n))
+ if (ipcp_spb_reserve(&spb, n))
continue;
- head = shm_du_buff_head(sdb);
+ head = ssm_pk_buff_head(spb);
memcpy(head, data, n);
- if (np1_flow_write(eid, sdb) < 0)
- ipcp_sdb_release(sdb);
+ if (np1_flow_write(eid, spb, NP1_GET_POOL(eid)) < 0)
+ ipcp_spb_release(spb);
}
return (void *) 0;
@@ -500,9 +639,9 @@ static void cleanup_fqueue(void * fq)
fqueue_destroy((fqueue_t *) fq);
}
-static void cleanup_sdb(void * sdb)
+static void cleanup_spb(void * spb)
{
- ipcp_sdb_release((struct shm_du_buff *) sdb);
+ ipcp_spb_release((struct ssm_pk_buff *) spb);
}
static void * udp_ipcp_packet_writer(void * o)
@@ -525,29 +664,29 @@ static void * udp_ipcp_packet_writer(void * o)
int fd;
fevent(udp_data.np1_flows, fq, NULL);
while ((fd = fqueue_next(fq)) >= 0) {
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
uint8_t * buf;
uint16_t len;
if (fqueue_type(fq) != FLOW_PKT)
continue;
- if (np1_flow_read(fd, &sdb)) {
+ if (np1_flow_read(fd, &spb, NP1_GET_POOL(fd))) {
log_dbg("Bad read from fd %d.", fd);
continue;
}
- len = shm_du_buff_len(sdb);
+ len = ssm_pk_buff_len(spb);
if (len > IPCP_UDP_MAX_PACKET_SIZE) {
log_dbg("Packet length exceeds MTU.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
continue;
}
- buf = shm_du_buff_head_alloc(sdb, OUR_HEADER_LEN);
+ buf = ssm_pk_buff_push(spb, OUR_HEADER_LEN);
if (buf == NULL) {
log_dbg("Failed to allocate header.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
continue;
}
@@ -560,7 +699,7 @@ static void * udp_ipcp_packet_writer(void * o)
memcpy(buf, &eid, sizeof(eid));
- pthread_cleanup_push(cleanup_sdb, sdb);
+ pthread_cleanup_push(cleanup_spb, spb);
if (sendto(udp_data.s_fd, buf, len + OUR_HEADER_LEN,
SENDTO_FLAGS,
@@ -1135,7 +1274,8 @@ static struct ipcp_ops udp_ops = {
.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
+ .ipcp_flow_dealloc = udp_ipcp_flow_dealloc,
+ .ipcp_flow_update = udp_ipcp_flow_update
};
int main(int argc,
diff --git a/src/ipcpd/udp/udp4.c b/src/ipcpd/udp/udp4.c
index 07d5f818..ff57bc09 100644
--- a/src/ipcpd/udp/udp4.c
+++ b/src/ipcpd/udp/udp4.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process over UDP/IPv4
*
diff --git a/src/ipcpd/udp/udp6.c b/src/ipcpd/udp/udp6.c
index b7924a3f..2ceb95f0 100644
--- a/src/ipcpd/udp/udp6.c
+++ b/src/ipcpd/udp/udp6.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* IPC process over UDP/IPv6
*
diff --git a/src/ipcpd/unicast/CMakeLists.txt b/src/ipcpd/unicast/CMakeLists.txt
index a9155353..2373f877 100644
--- a/src/ipcpd/unicast/CMakeLists.txt
+++ b/src/ipcpd/unicast/CMakeLists.txt
@@ -1,44 +1,12 @@
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-get_filename_component(CURRENT_BINARY_PARENT_DIR
- ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+# Unicast IPCP build configuration
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
+protobuf_generate_c(DHT_PROTO_SRCS DHT_PROTO_HDRS
+ "${CMAKE_CURRENT_SOURCE_DIR}/dir/dht.proto")
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(IPCP_UNICAST_TARGET ipcpd-unicast CACHE INTERNAL "")
-set(IPCP_UNICAST_MPL 10000 CACHE STRING
- "Default maximum packet lifetime for the unicast IPCP, in ms")
-set(DEBUG_PROTO_DHT FALSE CACHE BOOL
- "Add DHT protocol message output to debug logging")
-set(DEBUG_PROTO_LS FALSE CACHE BOOL
- "Add link state protocol message output to debug logging")
-
-protobuf_generate_c(DHT_PROTO_SRCS DHT_PROTO_HDRS dir/dht.proto)
-
-math(EXPR PFT_EXPR "1 << 12")
-set(PFT_SIZE ${PFT_EXPR} CACHE STRING
- "Size of the PDU forwarding table")
-if (HAVE_FUSE)
- set(IPCP_FLOW_STATS TRUE CACHE BOOL
- "Enable flow statistics tracking in IPCP")
- if (IPCP_FLOW_STATS)
- message(STATUS "IPCP flow statistics enabled")
- else ()
- message(STATUS "IPCP flow statistics disabled")
- endif ()
-endif ()
-
-set(IPCP_UNICAST_SOURCE_FILES
- # Add source files here
+set(UNICAST_SOURCES
addr-auth.c
ca.c
+ cap.c
connmgr.c
dir.c
dt.c
@@ -47,7 +15,6 @@ set(IPCP_UNICAST_SOURCE_FILES
pff.c
routing.c
psched.c
- # Add policies last
addr-auth/flat.c
ca/mb-ecn.c
ca/nop.c
@@ -58,21 +25,28 @@ set(IPCP_UNICAST_SOURCE_FILES
pff/pft.c
routing/link-state.c
routing/graph.c
- )
+)
+
+add_executable(${IPCP_UNICAST_TARGET}
+ ${UNICAST_SOURCES}
+ ${IPCP_SOURCES}
+ ${COMMON_SOURCES}
+ ${DHT_PROTO_SRCS}
+)
-add_executable(ipcpd-unicast ${IPCP_UNICAST_SOURCE_FILES} ${IPCP_SOURCES} ${COMMON_SOURCES}
- ${DHT_PROTO_SRCS} ${LAYER_CONFIG_PROTO_SRCS})
-target_link_libraries(ipcpd-unicast LINK_PUBLIC ouroboros-dev)
+target_include_directories(${IPCP_UNICAST_TARGET} PRIVATE ${IPCP_INCLUDE_DIRS})
+target_include_directories(${IPCP_UNICAST_TARGET} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+target_include_directories(${IPCP_UNICAST_TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(${IPCP_UNICAST_TARGET} PRIVATE ouroboros-dev)
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ipcpd-unicast -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+ouroboros_target_debug_definitions(${IPCP_UNICAST_TARGET})
-install(TARGETS ipcpd-unicast RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
+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(dir/tests)
-endif ()
+ add_subdirectory(tests)
+endif()
diff --git a/src/ipcpd/unicast/addr-auth.c b/src/ipcpd/unicast/addr-auth.c
index 908a4aa1..27671d09 100644
--- a/src/ipcpd/unicast/addr-auth.c
+++ b/src/ipcpd/unicast/addr-auth.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Address authority
*
diff --git a/src/ipcpd/unicast/addr-auth.h b/src/ipcpd/unicast/addr-auth.h
index 0d2cd4c0..65567dc3 100644
--- a/src/ipcpd/unicast/addr-auth.h
+++ b/src/ipcpd/unicast/addr-auth.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Address authority
*
diff --git a/src/ipcpd/unicast/addr-auth/flat.c b/src/ipcpd/unicast/addr-auth/flat.c
index 34ca1cef..cf832279 100644
--- a/src/ipcpd/unicast/addr-auth/flat.c
+++ b/src/ipcpd/unicast/addr-auth/flat.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for flat addresses in a distributed way
*
diff --git a/src/ipcpd/unicast/addr-auth/flat.h b/src/ipcpd/unicast/addr-auth/flat.h
index d4b672c7..b5c7e525 100644
--- a/src/ipcpd/unicast/addr-auth/flat.h
+++ b/src/ipcpd/unicast/addr-auth/flat.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for flat addresses in a distributed way
*
diff --git a/src/ipcpd/unicast/addr-auth/ops.h b/src/ipcpd/unicast/addr-auth/ops.h
index 06b24cec..f5c4c611 100644
--- a/src/ipcpd/unicast/addr-auth/ops.h
+++ b/src/ipcpd/unicast/addr-auth/ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Address authority policy ops
*
diff --git a/src/ipcpd/unicast/addr-auth/pol.h b/src/ipcpd/unicast/addr-auth/pol.h
index 844308c6..a33f1dd6 100644
--- a/src/ipcpd/unicast/addr-auth/pol.h
+++ b/src/ipcpd/unicast/addr-auth/pol.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Address Authority policies
*
diff --git a/src/ipcpd/unicast/ca.c b/src/ipcpd/unicast/ca.c
index 1fcc9bb2..d0ee2f73 100644
--- a/src/ipcpd/unicast/ca.c
+++ b/src/ipcpd/unicast/ca.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Congestion Avoidance
*
@@ -22,17 +22,47 @@
#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)
{
+#ifndef IPCP_CA_PER_FLOW
+ size_t i;
+#endif
switch(pol) {
case CA_NONE:
log_dbg("Disabling congestion control.");
@@ -46,63 +76,167 @@ int ca_init(enum pol_cong_avoid pol)
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, 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;
-void ca_ctx_update_ece(void * ctx,
- uint16_t ece)
-{
- return ca.ops->ctx_update_ece(ctx, ece);
+ return ca.ops->ctx_update_rcv(ctx->pol, len, ecn, cap, ece, fcap);
}
-void ca_wnd_wait(ca_wnd_t wnd)
+void ca_ctx_update_ece(void * _ctx,
+ uint16_t ece,
+ uint8_t cap)
{
- return ca.ops->wnd_wait(wnd);
+ struct ca_ctx * ctx = _ctx;
+
+ return ca.ops->ctx_update_ece(ctx->pol, ece, cap);
}
-int ca_calc_ecn(int fd,
+int ca_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
size_t len)
{
- return ca.ops->calc_ecn(fd, ecn, qc, len);
+ return ca.ops->calc_ecn(queued, ecn, qc, len);
}
-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 ea803e17..d73d35f5 100644
--- a/src/ipcpd/unicast/ca.h
+++ b/src/ipcpd/unicast/ca.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Congestion avoidance
*
@@ -29,38 +29,40 @@
#include <stdbool.h>
#include <sys/types.h>
-typedef union {
- time_t wait;
-} ca_wnd_t;
-
int ca_init(enum pol_cong_avoid ca);
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);
-void ca_wnd_wait(ca_wnd_t wnd);
-
-int ca_calc_ecn(int fd,
+int ca_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
size_t len);
+bool ca_marks_ecn(void);
+
ssize_t ca_print_stats(void * ctx,
char * buf,
size_t len);
diff --git a/src/ipcpd/unicast/ca/mb-ecn.c b/src/ipcpd/unicast/ca/mb-ecn.c
index d9a204b0..e59aac88 100644
--- a/src/ipcpd/unicast/ca/mb-ecn.c
+++ b/src/ipcpd/unicast/ca/mb-ecn.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Multi-bit ECN Congestion Avoidance
*
@@ -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,31 +39,125 @@
#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)
+/*
+ * The sender paces with a token bucket at a rate driven by Δt-scaled
+ * AIMD: each step changes the rate proportional to elapsed wall-clock
+ * time, so the per-second dynamics do not depend on how often packets
+ * arrive. The receiver's averaging window and the sender's feedback
+ * staleness both stretch with the flow's byte rate, so a slow flow
+ * is measured and controlled like a fast one; CA_RATE_MIN only
+ * bounds those horizons (window <= CA_TW_ABSMAX, TTL ~8 s). There
+ * is no per-flow timer; the control runs on packet sends.
+ *
+ * The floor and the AI slope scale with the path: forwarders stamp
+ * their measured link capacity into the PCI (cap.c), the receiver
+ * feeds the path MIN back with the ece, and the sender derives
+ * rate_min = ai_rate = C / 32, clamped to [CA_RATE_MIN, CA_RMIN_MAX],
+ * falling back to those defaults when the signal goes stale.
+ */
+
+#define CA_SHFT 5 /* ece fixed point: 32 * ecn */
+#define CA_TW_MIN (1ULL << 20) /* min mean window ~1.05 ms */
+#define CA_TW_INIT (1ULL << 26) /* initial mean window ~67ms */
+#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_N_TARGET 16 /* target packets per window */
+#define CA_RX_WBYTES (CA_N_TARGET * 1000ULL) /* target bytes/window */
+#define CA_RX_WCLOSE (2 * CA_RX_WBYTES) /* byte-triggered early close */
+#define CA_TW_SM_SHFT 2 /* window EWMA weight 1/4 */
+#define CA_MARK_Q 4 /* mark quantum (packets) */
+
+#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 << 16) /* 64 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 */
+#define CA_MD_KD_DIV 4 /* one-sided lead gain 1/4 */
+/* Must stay >= 1 ms: the MD term scales by dtc / MILLION (truncates). */
+#define CA_DT_CTRL (BILLION / 1000)
+#define CA_DT_CAP (BILLION / 20) /* idle-resume Δt clamp 50ms */
+/* Floor of the rate-relative feedback staleness (ctx->ece_ttl). */
+#define CA_ECE_TTL ((1 << CA_TW_GAP_SHFT) * CA_TW_INIT)
+#define CA_SS_TC (BILLION / 50)
+#define CA_WASH_BKT (BILLION / 32) /* washout bucket ~31 ms */
+#define CA_WASH_SHFT 2 /* damp 1/4 of bucket change */
+
+#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 */
+
+#define CA_SND_WIN CA_TW_INIT /* sender util window ~67ms */
+#define CA_USE_NUM 3 /* backlogged: offered >= */
+#define CA_USE_DEN 4 /* 3/4 * window-start rate */
+#define CA_HDRM_MARKS 4 /* ceiling ~2x offered load */
+#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 */
+
+/*
+ * Retuning invariants (pinned by the unit tests):
+ * - (1 << CA_TW_GAP_SHFT) * CA_TW_INIT > 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_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_DT_CTRL < CA_WASH_BKT < CA_DT_CAP: control cadence under the
+ * washout bucket under the sparse-step cutoff.
+ * - CA_RATE_MIN <= CA_RATE_INIT and CA_RMIN_MAX < CA_RATE_MAX.
+ */
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) */
+ uint8_t tx_cap; /* path capacity code fed back to us */
+ bool tx_cav; /* past slow start */
+ 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 r_bkt; /* rate snapshot at last washout bucket */
+ uint64_t wash_acc; /* washout bucket time accumulator (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 feedback applied (ns) */
+ uint64_t last_loc; /* last local mark seen (ns) */
+ uint64_t last_cap; /* last capacity applied (ns) */
+
+ 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_fb; /* feedback updates received */
+ uint64_t n_ttl; /* feedback aged out (TTL) */
+ uint64_t n_cap; /* capacity updates applied */
+ uint64_t ss_peak; /* peak rate in slow start (bytes/s) */
+
+ uint64_t snd_byt; /* bytes offered this window (capped) */
+ 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 */
};
struct ca_ops mb_ecn_ca_ops = {
@@ -71,14 +166,34 @@ struct ca_ops mb_ecn_ca_ops = {
.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,
.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;
+}
+
void * mb_ecn_ctx_create(void)
{
struct timespec now;
+ uint64_t t;
struct mb_ecn_ctx * ctx;
ctx = malloc(sizeof(*ctx));
@@ -89,10 +204,27 @@ 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->ece_ttl = mb_ecn_ece_ttl(CA_RATE_INIT);
+ ctx->r_bkt = CA_RATE_INIT;
+ ctx->inv_rate = mb_ecn_rate_inv(CA_RATE_INIT);
+ ctx->rx_ts = t;
+ ctx->rx_win = t;
+ ctx->rx_tw = CA_TW_INIT;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+ ctx->last_fb = 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->backlogged = true;
return (void *) ctx;
}
@@ -102,158 +234,519 @@ 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 / CA_SS_TC;
+
+ ctx->r_bkt = ctx->rate;
+}
+
+/* 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;
+
+ ctx->rate += ctx->ai_rate * dta / BILLION;
+ ctx->rate += ctx->rate * dta / CA_PROBE_TC;
+}
+
+/* Multiplicative decrease: proportional cut plus a one-sided lead. */
+static void mb_ecn_decrease(struct mb_ecn_ctx * ctx,
+ uint64_t dtc)
+{
+ uint64_t dtm;
+ uint64_t mark;
+ uint64_t rise;
+ uint64_t cut;
+ uint16_t m;
+
+ m = ctx->tx_ece > 0 ? ctx->tx_ece
+ : (uint16_t) (ctx->tx_loc << CA_SHFT);
+ if (m == 0) {
+ ctx->tx_ecp = 0;
+ return;
+ }
+
+ mark = MIN(m, CA_ECE_REF);
+ rise = 0;
+ if (m > ctx->tx_ecp)
+ rise = MIN(m - ctx->tx_ecp, CA_ECE_REF);
+
+ cut = ctx->rate * rise / (CA_ECE_REF * CA_MD_KD_DIV);
+
+ /* Honest elapsed ms, so a starved sender still cuts. */
+ dtm = dtc / 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;
+ ctx->tx_ecp = m;
+}
+
+/*
+ * Washout: once per wall-clock bucket, damp a fixed fraction of the
+ * rate change over that bucket. Bucketed (not per-step) so it stays
+ * cadence-independent; bounded so it cannot reverse a ramp. A sparse
+ * step resets it, so a starved sender keeps its cut.
+ */
+static void mb_ecn_washout(struct mb_ecn_ctx * ctx,
+ uint64_t dtc,
+ uint64_t dta)
+{
+ if (dtc > (uint64_t) CA_DT_CAP) {
+ ctx->r_bkt = ctx->rate;
+ ctx->wash_acc = 0;
+ return;
+ }
+
+ ctx->wash_acc += dta;
+ if (ctx->wash_acc < (uint64_t) CA_WASH_BKT)
+ return;
+
+ if (ctx->rate > ctx->r_bkt)
+ ctx->rate -= (ctx->rate - ctx->r_bkt) >> CA_WASH_SHFT;
+ else
+ ctx->rate += (ctx->r_bkt - ctx->rate) >> CA_WASH_SHFT;
+
+ ctx->r_bkt = ctx->rate;
+ ctx->wash_acc = 0;
+}
+
+/* Offered-load ceiling backstop while source-limited. */
+static void mb_ecn_ceiling(struct mb_ecn_ctx * ctx)
+{
+ unsigned code;
+ uint64_t hi;
+
+ if (ctx->backlogged) {
+ ctx->src_limited = false;
+ return;
+ }
+
+ code = (unsigned) cap_enc(ctx->snd_rate) + CA_HDRM_MARKS;
+ if (code > UINT8_MAX) /* keep the cast lossless */
+ code = UINT8_MAX;
+
+ hi = cap_dec((uint8_t) code);
+ if (hi > CA_RATE_MAX)
+ hi = CA_RATE_MAX;
+
+ if (hi < CA_RATE_MIN)
+ hi = CA_RATE_MIN;
+
+ ctx->src_limited = ctx->rate > hi;
+ if (ctx->src_limited) {
+ ctx->rate = hi;
+ ctx->r_bkt = ctx->rate;
+ }
+}
+
+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);
+ mb_ecn_washout(ctx, dtc, dta);
+ } 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;
+}
+
+/*
+ * 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;
+
+ offered = ctx->snd_byt * BILLION / elapsed;
+
+ ctx->backlogged = offered * CA_USE_DEN >= ctx->snd_r0 * CA_USE_NUM;
-ca_wnd_t mb_ecn_ctx_update_snd(void * _ctx,
- size_t len)
+ mb_ecn_offered(ctx, offered, elapsed);
+
+ if (ctx->backlogged)
+ ctx->src_limited = false;
+
+ ctx->snd_win = t;
+ ctx->snd_byt = 0;
+ ctx->snd_r0 = ctx->rate;
+}
+
+/* Age out congestion, local-mark and capacity signals once stale. */
+static void mb_ecn_age(struct mb_ecn_ctx * ctx,
+ uint64_t t)
+{
+ 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;
+
+ 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 s;
+
+ /* Lazy warm-up seed: packet #1 is never an idle resume. */
+ if (!ctx->started) {
+ ctx->started = true;
+ ctx->last_ts = t;
+ ctx->snd_win = t;
+ ctx->snd_r0 = ctx->rate;
+ }
+
+ dt = t - ctx->last_ts;
+ ctx->last_ts = t;
+
+ 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 (dt > (uint64_t) CA_DT_CAP)
+ ctx->backlogged = false;
+
+ 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;
+
+ 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,
+ 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_loc(ctx, lecn, t);
- if (ctx->tx_ctr > CA_WND)
- ctx->tx_ece = 0;
+ return mb_ecn_snd(ctx, len, t, ftag);
+}
- if (_slot_after(slot, ctx->tx_slot)) {
- bool carry = false; /* may carry over if window increases */
-
- ctx->tx_slot = slot;
-
- 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;
- }
-
- /* 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;
- }
-
- if (!carry) {
- ctx->tx_wbc = 0;
- ctx->tx_wpc = 0;
- }
- }
+/* Estimator idle, or a gap past ~4 current windows: restart fresh. */
+static bool mb_ecn_rcv_fresh(const struct mb_ecn_ctx * ctx,
+ uint64_t dt)
+{
+ if (ctx->rx_ece == 0 && ctx->rx_acc == 0)
+ return true;
- if (ctx->tx_wbc > ctx->tx_wbl)
- wnd.wait = ((ctx->tx_slot + 1) << ctx->tx_mul) - ts_to_ns(now);
+ return dt > ctx->rx_tw << CA_TW_GAP_SHFT;
+}
+
+/* Size the next averaging window to ~CA_N_TARGET packets at this rate. */
+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 < CA_TW_MIN)
+ ctx->rx_tw = CA_TW_MIN;
+
+ 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;
- nanosleep(&s, NULL);
+ dt = t - ctx->rx_ts;
+ ctx->rx_ts = t;
+
+ 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);
+
+ *ece = ctx->rx_ece;
+
+ 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)
+ return false;
+
+ if (win < CA_TW_MIN)
+ 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 mb_ecn_ctx* ctx = _ctx;
- bool update;
+ struct timespec now;
+ struct mb_ecn_ctx * ctx = _ctx;
- (void) len;
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
- if ((ctx->rx_ece | ecn) == 0)
- return false;
+ return mb_ecn_rcv(ctx, len, ecn, cap, ece, fcap, TS_TO_UINT64(now));
+}
- 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;
- }
+static void mb_ecn_ece(struct mb_ecn_ctx * ctx,
+ uint16_t ece,
+ uint8_t cap,
+ uint64_t t)
+{
+ uint64_t tgt;
+
+ ctx->tx_ece = ece;
+ ctx->tx_cav = true;
+ ctx->last_fb = 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 = 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;
- ctx->tx_ece = ece;
- ctx->tx_ctr = 0;
- ctx->tx_cav = true;
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ mb_ecn_ece(ctx, ece, cap, TS_TO_UINT64(now));
}
-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 q;
+ size_t q;
+ uint8_t mark;
(void) len;
(void) qc;
- q = ipcp_flow_queued(fd);
+ /* Saturate: a queue past 255 quanta must not wrap to a low mark. */
+ q = queued / CA_MARK_Q;
+ mark = q > 255 ? (uint8_t) 255 : (uint8_t) q;
- *ecn |= (uint8_t) (q >> ECN_Q_SHFT);
+ if (mark > *ecn)
+ *ecn = mark;
return 0;
}
@@ -262,35 +755,64 @@ 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)
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->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",
"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);
return strlen(buf);
}
diff --git a/src/ipcpd/unicast/ca/mb-ecn.h b/src/ipcpd/unicast/ca/mb-ecn.h
index 9a2c8b49..7bf0b29f 100644
--- a/src/ipcpd/unicast/ca/mb-ecn.h
+++ b/src/ipcpd/unicast/ca/mb-ecn.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Multi-bit ECN Congestion Avoidance
*
@@ -29,20 +29,23 @@ 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,
+ 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);
-
-void mb_ecn_wnd_wait(ca_wnd_t wnd);
+ uint16_t ece,
+ uint8_t cap);
-int mb_ecn_calc_ecn(int fd,
+int mb_ecn_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
size_t len);
diff --git a/src/ipcpd/unicast/ca/nop.c b/src/ipcpd/unicast/ca/nop.c
index 617fc15b..e6965297 100644
--- a/src/ipcpd/unicast/ca/nop.c
+++ b/src/ipcpd/unicast/ca/nop.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Dummy Congestion Avoidance
*
@@ -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,51 +45,52 @@ 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,
+ uint64_t * ftag)
{
- ca_wnd_t wnd;
-
(void) ctx;
(void) len;
+ (void) lecn;
+ (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)
{
- (void) fd;
+ (void) queued;
(void) len;
(void) ecn;
(void) qc;
diff --git a/src/ipcpd/unicast/ca/nop.h b/src/ipcpd/unicast/ca/nop.h
index 248b198d..6ca206df 100644
--- a/src/ipcpd/unicast/ca/nop.h
+++ b/src/ipcpd/unicast/ca/nop.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Dummy Congestion Avoidance
*
@@ -29,20 +29,23 @@ 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,
+ 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);
diff --git a/src/ipcpd/unicast/ca/ops.h b/src/ipcpd/unicast/ca/ops.h
index 3a7b7248..a86c1b3b 100644
--- a/src/ipcpd/unicast/ca/ops.h
+++ b/src/ipcpd/unicast/ca/ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Congestion avoidance policy ops
*
@@ -30,24 +30,30 @@ 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,
+ 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);
-
- void (* wnd_wait)(ca_wnd_t wnd);
+ uint16_t ece,
+ uint8_t cap);
- int (* calc_ecn)(int fd,
+ int (* calc_ecn)(size_t queued,
uint8_t * ecn,
qoscube_t qc,
size_t len);
+ /* True if calc_ecn inspects the queue; gates the lookup. */
+ bool marks_ecn;
+
/* Optional, can be NULL */
ssize_t (* print_stats)(void * ctx,
char * buf,
diff --git a/src/ipcpd/unicast/ca/pol.h b/src/ipcpd/unicast/ca/pol.h
index db0a1a11..bfb9cc2d 100644
--- a/src/ipcpd/unicast/ca/pol.h
+++ b/src/ipcpd/unicast/ca/pol.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Congestion avoidance policies
*
diff --git a/src/ipcpd/unicast/ca/tests/CMakeLists.txt b/src/ipcpd/unicast/ca/tests/CMakeLists.txt
new file mode 100644
index 00000000..6e42163d
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/CMakeLists.txt
@@ -0,0 +1,44 @@
+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})
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..4c44d29e
--- /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) < 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) == 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) < 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) < 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) < 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) < 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) < 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_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c
new file mode 100644
index 00000000..8e3a73da
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c
@@ -0,0 +1,2588 @@
+/*
+ * 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_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;
+
+ 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;
+}
+
+static int test_mb_ecn_calc_ecn(void)
+{
+ uint8_t ecn;
+
+ TEST_START();
+
+ /* A queue below one ECN quantum marks nothing. */
+ ecn = 0;
+ mb_ecn_calc_ecn(CA_MARK_Q - 1, &ecn, QOS_CUBE_BE, 0);
+ if (ecn != 0) {
+ printf("Sub-quantum queue marked %u.\n", ecn);
+ goto fail;
+ }
+
+ /* Queue depth maps to ecn = queued / CA_MARK_Q. */
+ ecn = 0;
+ mb_ecn_calc_ecn(5 * CA_MARK_Q, &ecn, QOS_CUBE_BE, 0);
+ if (ecn != 5) {
+ printf("Expected ecn 5, got %u.\n", ecn);
+ goto fail;
+ }
+
+ /* MAX keeps the larger value; a smaller mark cannot raise it. */
+ ecn = 0x80;
+ mb_ecn_calc_ecn(CA_MARK_Q, &ecn, QOS_CUBE_BE, 0);
+ if (ecn != 0x80) {
+ printf("Expected ecn 0x80, got 0x%x.\n", ecn);
+ goto fail;
+ }
+
+ ecn = 3;
+ mb_ecn_calc_ecn(4 * CA_MARK_Q, &ecn, QOS_CUBE_BE, 0);
+ if (ecn != 4) {
+ printf("Expected ecn 4, got %u.\n", ecn);
+ goto fail;
+ }
+
+ /* A queue past 255 quanta saturates, it does not wrap to a low mark. */
+ ecn = 0;
+ mb_ecn_calc_ecn(256 * CA_MARK_Q, &ecn, QOS_CUBE_BE, 0);
+ if (ecn != 255) {
+ printf("Deep queue wrapped: exp 255, 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;
+}
+
+/* A 50% duty mark square wave emits the time mean, not the last peak. */
+static int test_mb_ecn_rcv_window_mean(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ size_t upd;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, MS);
+
+ /* The byte trigger closes every ~32 packets: two windows. */
+ upd = 0;
+ for (i = 1; i <= 68; i++) {
+ time_t ecn = (i & 1) ? 8 : 0;
+ time_t t = MS + i * MS;
+ if (mb_ecn_rcv(ctx, LEN, ecn, 0, &ece, &fcap, t))
+ upd++;
+ }
+
+ if (upd != 2) {
+ printf("%zu updates in two windows.\n", upd);
+ goto fail_ctx;
+ }
+
+ if (ece < 120 || ece > 136) {
+ printf("window mean: exp ~128, got %u.\n", 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;
+}
+
+/*
+ * 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_INIT;
+ 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_INIT;
+ 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;
+}
+
+/* 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_INIT straddle the boundary. */
+ t = MS + CA_TW_INIT - 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_INIT - 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;
+}
+
+/* Scale-free density: the window holds ~CA_N_TARGET packets at any rate. */
+static int test_mb_ecn_rcv_window_holds_target(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint16_t last_ece;
+ uint64_t rates[4];
+ uint64_t ia;
+ uint64_t t;
+ size_t closes;
+ size_t since;
+ size_t count;
+ size_t ri;
+
+ TEST_START();
+
+ rates[0] = 5000000;
+ rates[1] = 10000000;
+ rates[2] = 50000000;
+ rates[3] = 100000000;
+
+ ctx = NULL;
+ for (ri = 0; ri < 4; ri++) {
+ ia = 8000ULL * BILLION / rates[ri];
+ t = 0;
+ closes = 0;
+ since = 0;
+ count = 0;
+ last_ece = 0;
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Warm past the ramp from CA_TW_INIT, then time one gap. */
+ while (closes < 42) {
+ t += ia;
+ since++;
+ if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t))
+ continue;
+ closes++;
+ if (closes == 41) {
+ since = 0;
+ } else if (closes == 42) {
+ count = since - 1;
+ last_ece = ece;
+ }
+ }
+
+ if (count < 8 || count > 32) {
+ printf("rate %" PRIu64 ": %zu pkts/window.\n",
+ rates[ri], count);
+ goto fail_ctx;
+ }
+
+ if (last_ece < 224 || last_ece > 288) {
+ printf("rate %" PRIu64 ": ece %u ~256.\n",
+ rates[ri], last_ece);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+ ctx = NULL;
+ }
+
+ 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_MIN, tracks the rate below the old 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_MIN. */
+ 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_MIN) {
+ printf("high-rate window: exp %" PRIu64 ", got %" PRIu64
+ ".\n", (uint64_t) CA_TW_MIN, ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ /* 1 Mbps: past the old 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_INIT);
+
+ 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_INIT - 2;
+
+ ok = mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, 2 * CA_TW_INIT - 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_INIT || ctx->rx_tw < CA_TW_MIN) {
+ 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_INIT) and a repeated mark must not re-fire the one-sided lead.
+ */
+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_INIT + 5 ms apart. */
+ t = MS;
+ for (i = 0; i < 4; i++) {
+ t += (2 * CA_TW_INIT + 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->r_bkt = a->rate;
+ b->r_bkt = b->rate;
+
+ /* a: one 30 ms step (under one washout bucket, so it stays out). */
+ 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;
+}
+
+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;
+ }
+
+ /* Cut larger than headroom must clamp; stay inside CA_ECE_TTL. */
+ ctx->rate = CA_RATE_MIN + 1000;
+ mb_ecn_ece(ctx, CA_ECE_REF, 0, 0);
+ mb_ecn_snd(ctx, LEN, 30 * 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;
+}
+
+/*
+ * One-sided lead: a mark rise cuts rate * rise / (REF * DEN) once;
+ * a flat or falling mark leaves only the dt-scaled proportional cut.
+ */
+static int test_mb_ecn_lead_cut(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t drop;
+ uint64_t ftag = 0;
+
+ 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;
+
+ /* Rise 0 -> 256: lead cuts ~rise/(REF*DEN) = 1/8 of the rate. */
+ prev = ctx->rate;
+ mb_ecn_ece(ctx, 256, 0, MS);
+ mb_ecn_snd(ctx, LEN, MS, &ftag);
+ drop = prev - ctx->rate;
+ if (drop < prev / 10) {
+ printf("lead cut missing: dropped %" PRIu64 ".\n", drop);
+ goto fail_ctx;
+ }
+
+ /* Flat mark: rise 0, only the ~0.05%% proportional cut. */
+ prev = ctx->rate;
+ mb_ecn_ece(ctx, 256, 0, 2 * MS);
+ mb_ecn_snd(ctx, LEN, 2 * MS, &ftag);
+ drop = prev - ctx->rate;
+ if (drop > prev / 100) {
+ printf("flat mark over-cut: dropped %" PRIu64 ".\n", drop);
+ goto fail_ctx;
+ }
+
+ /* Falling mark: one-sided lead must not fire. */
+ prev = ctx->rate;
+ mb_ecn_ece(ctx, 64, 0, 3 * MS);
+ mb_ecn_snd(ctx, LEN, 3 * MS, &ftag);
+ drop = prev > ctx->rate ? prev - ctx->rate : 0;
+ if (drop > prev / 100) {
+ printf("falling mark cut: dropped %" PRIu64 ".\n", drop);
+ 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;
+}
+
+/* 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->r_bkt = a->rate;
+ b->r_bkt = b->rate;
+ 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->r_bkt = ctx->rate;
+ 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);
+ }
+
+ /* Washout damps the probe to ~4/3 TC, so 8 s -> ~2.12x. */
+ ratio = (double) ctx->rate / r0;
+ if (ratio < 2.0 || ratio > 2.25) {
+ printf("probe TC off: exp ~2.12, 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_INIT;
+ 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_INIT;
+ 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_INIT;
+ 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 != 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->r_bkt = ctx->rate;
+ 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 next quarter-log2 headroom
+ * 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;
+
+ 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);
+
+ if (ctx->rate != ((uint64_t) 2 << 20)) {
+ printf("ceiling: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ (uint64_t) 2 << 20, 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);
+
+ 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->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;
+}
+
+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_calc_ecn();
+ ret |= test_mb_ecn_rcv_onset_immediate();
+ ret |= test_mb_ecn_rcv_window_mean();
+ 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_accum_bounds();
+ ret |= test_mb_ecn_rcv_window_holds_target();
+ 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_fixed_point();
+ ret |= test_mb_ecn_lead_cut();
+ 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_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();
+
+ return ret;
+}
diff --git a/src/ipcpd/unicast/cap.c b/src/ipcpd/unicast/cap.c
new file mode 100644
index 00000000..0d823dc6
--- /dev/null
+++ b/src/ipcpd/unicast/cap.c
@@ -0,0 +1,291 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Link capacity estimation
+ *
+ * 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 "config.h"
+
+#include <ouroboros/atomics.h>
+#include <ouroboros/time.h>
+
+#include "cap.h"
+
+#include <string.h>
+
+/*
+ * 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 ring buffer
+ * toward an n-1 flow (the flow to the layer below) while that ring
+ * is backlogged.
+ *
+ * Sampling is lock-free and off the fast path: the ring depth is
+ * read only at enqueue time, concurrently by many sender threads.
+ * Each enqueue bumps relaxed counters (packets, bytes, empty-ring
+ * hits). At most once per CAP_T_MIN, one thread wins a try-lock and
+ * closes a measurement window.
+ *
+ * Over a window, packet conservation gives the slots that drained:
+ * drained = queue at start (q0) + enqueued - queue now (q1)
+ * A window stays open until CAP_N_MIN slots have 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.
+ *
+ * The estimate is published as a quarter-log2 code: capacity is only
+ * ever needed to order-of-magnitude accuracy.
+ */
+
+#define CAP_T_MIN (BILLION / 1000) /* min fold spacing ~1 ms */
+#define CAP_T_MAX (1ULL << 27) /* stale window cap ~134 ms */
+#define CAP_N_MIN 16 /* drained slots to close */
+#define CAP_DEC_SHFT 4 /* max-filter decay 1/16 */
+#define CAP_IDL_SHFT 3 /* idle tolerance 1/8 */
+
+/* Try-lock on the busy flag: test-and-set acquire, store release. */
+#define CAP_TRY(p) (__atomic_exchange_n(p, 1, __ATOMIC_ACQUIRE) == 0)
+#define CAP_REL(p) (__atomic_store_n(p, 0, __ATOMIC_RELEASE))
+
+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 fold timestamp (ns) */
+ uint8_t busy; /* fold 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) */
+
+ uint8_t cap; /* published capacity code (0=none) */
+};
+
+struct {
+ struct cap_est est[PROC_MAX_FLOWS];
+} cap;
+
+int cap_init(void)
+{
+ memset(&cap, 0, sizeof(cap));
+
+ return 0;
+}
+
+void cap_fini(void)
+{
+}
+
+void cap_reset(int fd)
+{
+ /* A racing update seeds one bogus window; the filter absorbs. */
+ memset(&cap.est[fd], 0, sizeof(cap.est[fd]));
+}
+
+/*
+ * Rate <-> 8-bit code (cap_enc / cap_dec). The code is a tiny float:
+ * the high 6 bits are a band e = floor(log2 rate), the low 2 bits a
+ * quarter k that splits each band [2^e, 2^(e+1)) into four, so
+ * code = 4 * e + k. Each step is ~19% in rate; that coarseness is
+ * deliberate, capacity only needs order-of-magnitude accuracy.
+ *
+ * The quarter cut points are 256 * 2^(k/4) rounded to an integer:
+ * {256, 304, 362, 431} over the normalized range [256, 512).
+ */
+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++;
+ }
+
+ /* Top 9 bits: rate normalized to [256, 512). */
+ top = e >= 8 ? (uint16_t) (rate >> (e - 8))
+ : (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;
+}
+
+/* Fold flag held; q1 is the caller's pre-write ring sample. */
+static void cap_fold(struct cap_est * e,
+ uint64_t q1,
+ uint64_t now)
+{
+ 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; /* slots 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 + enq - q1);
+
+ if (e->t0 == 0 || dt > CAP_T_MAX || enq == 0)
+ goto reopen;
+
+ if (drained < (int64_t) CAP_N_MIN)
+ return; /* extend the window until enough drains */
+
+ if ((idl - e->idl0) << CAP_IDL_SHFT > enq)
+ goto reopen; /* mostly idle ring: not saturated */
+
+ avg = (byt - e->byt0) / enq;
+ r = (uint64_t) drained * avg * MILLION / (dt / 1000);
+
+ if (r >= e->rate) {
+ /* Empty-edged windows drain into buffers below. */
+ if (e->q0 > 0 && q1 > 0)
+ e->rate = r;
+ } else {
+ e->rate -= (e->rate - r) >> CAP_DEC_SHFT;
+ }
+
+ STORE_RELAXED(&e->cap, cap_enc(e->rate));
+ reopen:
+ e->t0 = now;
+ e->q0 = q1;
+ e->pkt0 = pkt;
+ e->byt0 = byt;
+ e->idl0 = idl;
+}
+
+/* Internal, timestamped entry point; tests drive this directly. */
+static void cap_update_at(int fd,
+ size_t qlen,
+ size_t len,
+ uint64_t now)
+{
+ struct cap_est * e = &cap.est[fd]; /* this flow's estimator */
+
+ FETCH_ADD_RELAXED(&e->c_pkt, 1);
+ FETCH_ADD_RELAXED(&e->c_byt, len);
+
+ if (qlen == 0)
+ FETCH_ADD_RELAXED(&e->c_idl, 1);
+
+ if (now - LOAD_RELAXED(&e->t_gate) < CAP_T_MIN)
+ return;
+
+ if (!CAP_TRY(&e->busy))
+ return;
+
+ if (now - e->t_gate >= CAP_T_MIN) {
+ cap_fold(e, qlen, now);
+ STORE_RELAXED(&e->t_gate, now);
+ }
+
+ CAP_REL(&e->busy);
+}
+
+void cap_update(int fd,
+ size_t qlen,
+ size_t len)
+{
+ struct timespec now;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ cap_update_at(fd, qlen, len, TS_TO_UINT64(now));
+}
+
+uint8_t cap_get(int fd)
+{
+ return LOAD_RELAXED(&cap.est[fd].cap);
+}
diff --git a/src/ipcpd/unicast/cap.h b/src/ipcpd/unicast/cap.h
new file mode 100644
index 00000000..df8c2ec1
--- /dev/null
+++ b/src/ipcpd/unicast/cap.h
@@ -0,0 +1,54 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Link capacity estimation
+ *
+ * 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_UNICAST_CAP_H
+#define OUROBOROS_IPCPD_UNICAST_CAP_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+int cap_init(void);
+
+void cap_fini(void);
+
+/* Account an egress packet; qlen is sampled before the write. */
+void cap_update(int fd,
+ size_t qlen,
+ size_t len);
+
+uint8_t cap_get(int fd);
+
+void cap_reset(int fd);
+
+/* 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/connmgr.c b/src/ipcpd/unicast/connmgr.c
index 07568fb5..f60f6fca 100644
--- a/src/ipcpd/unicast/connmgr.c
+++ b/src/ipcpd/unicast/connmgr.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handles connections between components
*
diff --git a/src/ipcpd/unicast/dir.c b/src/ipcpd/unicast/dir.c
index 2b305626..a31a562f 100644
--- a/src/ipcpd/unicast/dir.c
+++ b/src/ipcpd/unicast/dir.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Directory Management
*
diff --git a/src/ipcpd/unicast/dir.h b/src/ipcpd/unicast/dir.h
index dbfde19f..2be7b10f 100644
--- a/src/ipcpd/unicast/dir.h
+++ b/src/ipcpd/unicast/dir.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Directory
*
diff --git a/src/ipcpd/unicast/dir/dht.c b/src/ipcpd/unicast/dir/dht.c
index 6b06def9..c2cd33aa 100644
--- a/src/ipcpd/unicast/dir/dht.c
+++ b/src/ipcpd/unicast/dir/dht.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Distributed Hash Table based on Kademlia
*
@@ -148,15 +148,8 @@ struct dht_entry {
uint8_t * key;
- struct {
- struct list_head list;
- size_t len;
- } vals; /* We don't own these, only replicate */
-
- struct {
- struct list_head list;
- size_t len;
- } lvals; /* We own these, must be republished */
+ struct llist vals; /* We don't own these, only replicate */
+ struct llist lvals; /* We own these, must be republished */
};
struct contact {
@@ -183,38 +176,24 @@ struct peer_entry {
struct dht_req {
struct list_head next;
- uint8_t * key;
- time_t t_exp;
+ uint8_t * key;
+ time_t t_exp;
- struct {
- struct list_head list;
- size_t len;
- } peers;
-
- struct {
- struct list_head list;
- size_t len;
- } cache;
+ struct llist peers;
+ struct llist cache;
};
struct bucket {
- struct {
- struct list_head list;
- size_t len;
- } contacts;
-
- struct {
- struct list_head list;
- size_t len;
- } alts;
+ struct llist contacts;
+ struct llist alts;
- time_t t_refr;
+ time_t t_refr;
- size_t depth;
- uint8_t mask;
+ size_t depth;
+ uint8_t mask;
- struct bucket * parent;
- struct bucket * children[1L << DHT_BETA];
+ struct bucket * parent;
+ struct bucket * children[1L << DHT_BETA];
};
struct cmd {
@@ -236,8 +215,8 @@ struct {
struct { /* Kademlia parameters */
uint32_t alpha; /* Number of concurrent requests */
size_t k; /* Number of replicas to store */
- time_t t_expire; /* Expiry time for values (s) */
- time_t t_refresh; /* Refresh time for contacts (s) */
+ time_t t_exp; /* Expiry time for values (s) */
+ time_t t_refr; /* Refresh time for contacts (s) */
time_t t_repl; /* Replication time for values (s) */
};
@@ -261,8 +240,7 @@ struct {
} contacts;
struct {
- struct list_head list;
- size_t len;
+ struct llist ll;
size_t vals;
size_t lvals;
} kv;
@@ -271,10 +249,9 @@ struct {
} db;
struct {
- struct list_head list;
- size_t len;
- pthread_cond_t cond;
- pthread_mutex_t mtx;
+ struct llist ll;
+ pthread_cond_t cond;
+ pthread_mutex_t mtx;
} reqs;
struct {
@@ -321,7 +298,7 @@ static int dht_rib_statfile(char * buf,
pthread_rwlock_rdlock(&dht.db.lock);
- keys = dht.db.kv.len;
+ keys = dht.db.kv.ll.len;
lvals = dht.db.kv.lvals;
vals = dht.db.kv.vals;
@@ -335,7 +312,7 @@ static int dht_rib_statfile(char * buf,
tmstr,
ADDR_VAL32(&dht.addr),
dht.alpha, dht.k,
- dht.t_expire, dht.t_refresh, dht.t_repl,
+ dht.t_exp, dht.t_refr, dht.t_repl,
keys, vals, lvals);
return strlen(buf);
@@ -350,14 +327,14 @@ static size_t dht_db_file_len(void)
pthread_rwlock_rdlock(&dht.db.lock);
- if (dht.db.kv.len == 0) {
+ if (llist_is_empty(&dht.db.kv.ll)) {
pthread_rwlock_unlock(&dht.db.lock);
sz += 14; /* No entries */
return sz;
}
sz += 39 * 3 + 1; /* tally + extra newline */
- sz += dht.db.kv.len * (25 + 19 + 23 + 1);
+ sz += dht.db.kv.ll.len * (25 + 19 + 23 + 1);
vals = dht.db.kv.vals + dht.db.kv.lvals;
@@ -382,7 +359,7 @@ static int dht_rib_dbfile(char * buf,
pthread_rwlock_rdlock(&dht.db.lock);
- if (dht.db.kv.len == 0) {
+ if (llist_is_empty(&dht.db.kv.ll)) {
i += snprintf(buf, len, " No entries.\n");
pthread_rwlock_unlock(&dht.db.lock);
return i;
@@ -393,9 +370,9 @@ static int dht_rib_dbfile(char * buf,
"Number of keys: %10zu\n"
"Number of local values: %10zu\n"
"Number of non-local values: %10zu\n\n",
- dht.db.kv.len, dht.db.kv.vals, dht.db.kv.lvals);
+ dht.db.kv.ll.len, dht.db.kv.vals, dht.db.kv.lvals);
- list_for_each(p, &dht.db.kv.list) {
+ llist_for_each(p, &dht.db.kv.ll) {
struct dht_entry * e = list_entry(p, struct dht_entry, next);
struct list_head * h;
@@ -403,7 +380,7 @@ static int dht_rib_dbfile(char * buf,
KEY_VAL(e->key));
i += snprintf(buf + i, len - i, " Local entries:\n");
- list_for_each(h, &e->vals.list) {
+ llist_for_each(h, &e->vals) {
struct val_entry * v;
v = list_entry(h, struct val_entry, next);
@@ -416,7 +393,7 @@ static int dht_rib_dbfile(char * buf,
i += snprintf(buf + i, len - i,
" " VAL_FMT
- ", t_replicated=%.*s, t_expire=%.*s\n",
+ ", t_replicated=%.*s, t_exp=%.*s\n",
VAL_VAL(v->val),
RIB_TM_STRLEN, tmstr,
RIB_TM_STRLEN, exstr);
@@ -426,7 +403,7 @@ static int dht_rib_dbfile(char * buf,
i += snprintf(buf + i, len - i, " Non-local entries:\n");
- list_for_each(h, &e->lvals.list) {
+ llist_for_each(h, &e->lvals) {
struct val_entry * v;
v= list_entry(h, struct val_entry, next);
@@ -439,7 +416,7 @@ static int dht_rib_dbfile(char * buf,
i += snprintf(buf + i, len - i,
" " VAL_FMT
- ", t_replicated=%.*s, t_expire=%.*s\n",
+ ", t_replicated=%.*s, t_exp=%.*s\n",
VAL_VAL(v->val),
RIB_TM_STRLEN, tmstr,
RIB_TM_STRLEN, exstr);
@@ -694,11 +671,8 @@ static struct dht_entry * dht_entry_create(const uint8_t * key)
goto fail_entry;
list_head_init(&e->next);
- list_head_init(&e->vals.list);
- list_head_init(&e->lvals.list);
-
- e->vals.len = 0;
- e->lvals.len = 0;
+ llist_init(&e->vals);
+ llist_init(&e->lvals);
e->key = dht_dup_key(key);
if (e->key == NULL)
@@ -718,25 +692,23 @@ static void dht_entry_destroy(struct dht_entry * e)
assert(e != NULL);
- list_for_each_safe(p, h, &e->vals.list) {
+ llist_for_each_safe(p, h, &e->vals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
- list_del(&v->next);
+ llist_del(&v->next, &e->vals);
val_entry_destroy(v);
- --e->vals.len;
--dht.db.kv.vals;
}
- list_for_each_safe(p, h, &e->lvals.list) {
+ llist_for_each_safe(p, h, &e->lvals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
- list_del(&v->next);
+ llist_del(&v->next, &e->lvals);
val_entry_destroy(v);
- --e->lvals.len;
--dht.db.kv.lvals;
}
free(e->key);
- assert(e->vals.len == 0 && e->lvals.len == 0);
+ assert(llist_is_empty(&e->vals) && llist_is_empty(&e->lvals));
free(e);
}
@@ -750,7 +722,7 @@ static struct val_entry * dht_entry_get_lval(const struct dht_entry * e,
assert(val.data != NULL);
assert(val.len > 0);
- list_for_each(p, &e->lvals.list) {
+ llist_for_each(p, &e->lvals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
if (bufcmp(&v->val, &val) == 0)
return v;
@@ -768,7 +740,7 @@ static struct val_entry * dht_entry_get_val(const struct dht_entry * e,
assert(val.data != NULL);
assert(val.len > 0);
- list_for_each(p, &e->vals.list) {
+ llist_for_each(p, &e->vals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
if (bufcmp(&v->val, &val) == 0)
return v;
@@ -805,8 +777,7 @@ static int dht_entry_update_val(struct dht_entry * e,
if (v == NULL)
return -ENOMEM;
- list_add_tail(&v->next, &e->vals.list);
- ++e->vals.len;
+ llist_add_tail(&v->next, &e->vals);
++dht.db.kv.vals;
return 0;
@@ -833,12 +804,11 @@ static int dht_entry_update_lval(struct dht_entry * e,
v = dht_entry_get_lval(e, val);
if (v == NULL) {
log_dbg(KV_FMT " Adding lval.", KV_VAL(e->key, val));
- v = val_entry_create(val, now.tv_sec + dht.t_expire);
+ v = val_entry_create(val, now.tv_sec + dht.t_exp);
if (v == NULL)
return -ENOMEM;
- list_add_tail(&v->next, &e->lvals.list);
- ++e->lvals.len;
+ llist_add_tail(&v->next, &e->lvals);
++dht.db.kv.lvals;
return 0;
@@ -862,9 +832,8 @@ static int dht_entry_remove_lval(struct dht_entry * e,
log_dbg(KV_FMT " Removing lval.", KV_VAL(e->key, val));
- list_del(&v->next);
+ llist_del(&v->next, &e->lvals);
val_entry_destroy(v);
- --e->lvals.len;
--dht.db.kv.lvals;
return 0;
@@ -881,15 +850,14 @@ static void dht_entry_remove_expired_vals(struct dht_entry * e)
clock_gettime(CLOCK_REALTIME_COARSE, &now);
- list_for_each_safe(p, h, &e->vals.list) {
+ llist_for_each_safe(p, h, &e->vals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
if (!IS_EXPIRED(v, &now))
continue;
log_dbg(KV_FMT " Value expired." , KV_VAL(e->key, v->val));
- list_del(&v->next);
+ llist_del(&v->next, &e->vals);
val_entry_destroy(v);
- --e->vals.len;
--dht.db.kv.vals;
}
}
@@ -900,7 +868,7 @@ static struct dht_entry * __dht_kv_find_entry(const uint8_t * key)
assert(key != NULL);
- list_for_each(p, &dht.db.kv.list) {
+ llist_for_each(p, &dht.db.kv.ll) {
struct dht_entry * e = list_entry(p, struct dht_entry, next);
if (!memcmp(key, e->key, dht.id.len))
return e;
@@ -919,16 +887,15 @@ static void dht_kv_remove_expired_entries(void)
pthread_rwlock_wrlock(&dht.db.lock);
- list_for_each_safe(p, h, &dht.db.kv.list) {
+ llist_for_each_safe(p, h, &dht.db.kv.ll) {
struct dht_entry * e = list_entry(p, struct dht_entry, next);
dht_entry_remove_expired_vals(e);
if (e->lvals.len > 0 || e->vals.len > 0)
continue;
log_dbg(KEY_FMT " Entry removed. ", KEY_VAL(e->key));
- list_del(&e->next);
+ llist_del(&e->next, &dht.db.kv.ll);
dht_entry_destroy(e);
- --dht.db.kv.len;
}
pthread_rwlock_unlock(&dht.db.lock);
@@ -987,15 +954,13 @@ static struct dht_req * dht_req_create(const uint8_t * key)
req->t_exp = now.tv_sec + DHT_T_RESP;
- list_head_init(&req->peers.list);
- req->peers.len = 0;
+ llist_init(&req->peers);
req->key = dht_dup_key(key);
if (req->key == NULL)
goto fail_dup_key;
- list_head_init(&req->cache.list);
- req->cache.len = 0;
+ llist_init(&req->cache);
return req;
@@ -1013,34 +978,32 @@ static void dht_req_destroy(struct dht_req * req)
assert(req);
assert(req->key);
- list_for_each_safe(p, h, &req->peers.list) {
+ llist_for_each_safe(p, h, &req->peers) {
struct peer_entry * e = list_entry(p, struct peer_entry, next);
- list_del(&e->next);
+ llist_del(&e->next, &req->peers);
free(e->id);
free(e);
- --req->peers.len;
}
- list_for_each_safe(p, h, &req->cache.list) {
+ llist_for_each_safe(p, h, &req->cache) {
struct val_entry * e = list_entry(p, struct val_entry, next);
- list_del(&e->next);
+ llist_del(&e->next, &req->cache);
val_entry_destroy(e);
- --req->cache.len;
}
free(req->key);
- assert(req->peers.len == 0);
+ assert(llist_is_empty(&req->peers));
free(req);
}
-static struct peer_entry * dht_req_get_peer(struct dht_req * req,
- struct peer_entry * e)
+static struct peer_entry * dht_req_get_peer(struct dht_req * req,
+ const struct peer_entry * e)
{
struct list_head * p;
- list_for_each(p, &req->peers.list) {
+ llist_for_each(p, &req->peers) {
struct peer_entry * x = list_entry(p, struct peer_entry, next);
if (x->addr == e->addr)
return x;
@@ -1050,8 +1013,8 @@ static struct peer_entry * dht_req_get_peer(struct dht_req * req,
}
#define IS_MAGIC(peer) ((peer)->cookie == dht.magic)
-void dht_req_add_peer(struct dht_req * req,
- struct peer_entry * e)
+static int dht_req_add_peer(struct dht_req * req,
+ const struct peer_entry * e)
{
struct peer_entry * x; /* existing */
struct list_head * p; /* iterator */
@@ -1063,16 +1026,17 @@ void dht_req_add_peer(struct dht_req * req,
/*
* Dedupe messages to the same peer, unless
- * 1) The previous request was FIND_NODE and now it's FIND_VALUE
- * 2) We urgently need contacts from emergency peer (magic cookie)
+ * 1) The previous was FIND_NODE and now it's FIND_VALUE
+ * 2) We urgently need contacts (magic cookie)
*/
x = dht_req_get_peer(req, e);
if (x != NULL && x->code >= e->code && !IS_MAGIC(e))
- goto skip;
+ return -1;
/* Find how this contact ranks in distance to the key */
- list_for_each(p, &req->peers.list) {
- struct peer_entry * y = list_entry(p, struct peer_entry, next);
+ llist_for_each(p, &req->peers) {
+ struct peer_entry * y;
+ y = list_entry(p, struct peer_entry, next);
if (IS_CLOSER(y->id, e->id)) {
pos++;
continue;
@@ -1080,36 +1044,32 @@ void dht_req_add_peer(struct dht_req * req,
break;
}
- /* Add a new peer to this request if we need to */
- if (pos < dht.alpha || !IS_MAGIC(e)) {
- x = malloc(sizeof(*x));
- if (x == NULL) {
- log_err("Failed to malloc peer entry.");
- goto skip;
- }
+ if (pos >= dht.alpha && IS_MAGIC(e))
+ return -1;
- x->cookie = e->cookie;
- x->addr = e->addr;
- x->code = e->code;
- x->t_sent = e->t_sent;
- x->id = dht_dup_key(e->id);
- if (x->id == NULL) {
- log_err("Failed to dup peer ID.");
- free(x);
- goto skip;
- }
+ x = malloc(sizeof(*x));
+ if (x == NULL) {
+ log_err("Failed to malloc peer entry.");
+ return -1;
+ }
- if (IS_MAGIC(e))
- list_add(&x->next, p);
- else
- list_add_tail(&x->next, p);
- ++req->peers.len;
- return;
+ x->cookie = e->cookie;
+ x->addr = e->addr;
+ x->code = e->code;
+ x->t_sent = e->t_sent;
+ x->id = dht_dup_key(e->id);
+ if (x->id == NULL) {
+ log_err("Failed to dup peer ID.");
+ free(x);
+ return -1;
}
- skip:
- list_del(&e->next);
- free(e->id);
- free(e);
+
+ if (IS_MAGIC(e))
+ llist_add_at(&x->next, p, &req->peers);
+ else
+ llist_add_tail_at(&x->next, p, &req->peers);
+
+ return 0;
}
static size_t dht_req_add_peers(struct dht_req * req,
@@ -1123,8 +1083,13 @@ static size_t dht_req_add_peers(struct dht_req * req,
assert(pl != NULL);
list_for_each_safe(p, h, pl) {
- struct peer_entry * e = list_entry(p, struct peer_entry, next);
- dht_req_add_peer(req, e);
+ struct peer_entry * e;
+ e = list_entry(p, struct peer_entry, next);
+ if (dht_req_add_peer(req, e) < 0) {
+ list_del(&e->next);
+ free(e->id);
+ free(e);
+ }
}
return n;
@@ -1137,7 +1102,7 @@ static bool dht_req_has_peer(struct dht_req * req,
assert(req != NULL);
- list_for_each(p, &req->peers.list) {
+ llist_for_each(p, &req->peers) {
struct peer_entry * e = list_entry(p, struct peer_entry, next);
if (e->cookie == cookie)
return true;
@@ -1209,7 +1174,7 @@ static struct dht_req * __dht_kv_req_get_req(const uint8_t * key)
{
struct list_head * p;
- list_for_each(p, &dht.reqs.list) {
+ llist_for_each(p, &dht.reqs.ll) {
struct dht_req * r = list_entry(p, struct dht_req, next);
if (memcmp(r->key, key, dht.id.len) == 0)
return r;
@@ -1228,7 +1193,7 @@ static struct dht_req * __dht_kv_get_req_cache(const uint8_t * key)
if (req == NULL)
return NULL;
- if (req->cache.len == 0)
+ if (llist_is_empty(&req->cache))
return NULL;
return req;
@@ -1244,8 +1209,7 @@ static void __dht_kv_req_remove(const uint8_t * key)
if (req == NULL)
return;
- list_del(&req->next);
- --dht.reqs.len;
+ llist_del(&req->next, &dht.reqs.ll);
dht_req_destroy(req);
}
@@ -1301,9 +1265,9 @@ static int dht_kv_update_req(const uint8_t * key,
req = __dht_kv_req_get_req(key);
if (req == NULL) {
- if (dht.reqs.len == DHT_MAX_REQS) {
+ if (dht.reqs.ll.len == DHT_MAX_REQS) {
log_err(KEY_FMT " Max reqs reached (%zu).",
- KEY_VAL(key), dht.reqs.len);
+ KEY_VAL(key), dht.reqs.ll.len);
peer_list_destroy(pl);
goto fail_req;
}
@@ -1312,8 +1276,7 @@ static int dht_kv_update_req(const uint8_t * key,
log_err(KEY_FMT "Failed to create req.", KEY_VAL(key));
goto fail_req;
}
- list_add_tail(&req->next, &dht.reqs.list);
- ++dht.reqs.len;
+ llist_add_tail(&req->next, &dht.reqs.ll);
}
if (req->cache.len > 0) /* Already have values */
@@ -1322,9 +1285,9 @@ static int dht_kv_update_req(const uint8_t * key,
dht_req_add_peers(req, pl);
req->t_exp = now.tv_sec + DHT_T_RESP;
- if (dht.reqs.len > DHT_WARN_REQS) {
+ if (dht.reqs.ll.len > DHT_WARN_REQS) {
log_warn("Number of outstanding requests (%zu) exceeds %u.",
- dht.reqs.len, DHT_WARN_REQS);
+ dht.reqs.ll.len, DHT_WARN_REQS);
}
pthread_mutex_unlock(&dht.reqs.mtx);
@@ -1368,8 +1331,7 @@ static int dht_kv_respond_req(uint8_t * key,
continue;
}
- list_add_tail(&e->next, &req->cache.list);
- ++req->cache.len;
+ llist_add_tail(&e->next, &req->cache);
}
pthread_cond_broadcast(&dht.reqs.cond);
@@ -1434,7 +1396,7 @@ static ssize_t dht_kv_wait_req(const uint8_t * key,
memset(*vals, 0, max * sizeof(**vals));
- list_for_each(p, &req->cache.list) {
+ llist_for_each(p, &req->cache) {
struct val_entry * v;
if (i == max)
break; /* We have enough values */
@@ -1535,10 +1497,10 @@ static ssize_t dht_kv_contact_list(const uint8_t * key,
goto fail_bucket;
}
- b->t_refr = t.tv_sec + dht.t_refresh;
+ b->t_refr = t.tv_sec + dht.t_refr;
if (b->contacts.len == dht.k || b->parent == NULL) {
- list_for_each(p, &b->contacts.list) {
+ llist_for_each(p, &b->contacts) {
struct contact * c;
struct contact * d;
c = list_entry(p, struct contact, next);
@@ -1554,7 +1516,7 @@ static ssize_t dht_kv_contact_list(const uint8_t * key,
} else {
struct bucket * d = b->parent;
for (i = 0; i < (1L << DHT_BETA) && len < dht.k; ++i) {
- list_for_each(p, &d->children[i]->contacts.list) {
+ llist_for_each(p, &d->children[i]->contacts) {
struct contact * c;
struct contact * d;
c = list_entry(p, struct contact, next);
@@ -1635,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:
@@ -1661,11 +1624,11 @@ static void __dht_kv_bucket_refresh_list(struct bucket * b,
__dht_kv_bucket_refresh_list(b->children[i], t, r);
}
- if (b->contacts.len == 0)
+ if (llist_is_empty(&b->contacts))
return;
- c = list_first_entry(&b->contacts.list, struct contact, next);
- if (t > c->t_seen + dht.t_refresh) {
+ c = llist_first_entry(&b->contacts, struct contact, next);
+ if (t > c->t_seen + dht.t_refr) {
d = contact_create(c->id, c->addr);
if (d != NULL)
list_add(&d->next, r);
@@ -1682,14 +1645,12 @@ static struct bucket * bucket_create(void)
if (b == NULL)
return NULL;
- list_head_init(&b->contacts.list);
- b->contacts.len = 0;
+ llist_init(&b->contacts);
- list_head_init(&b->alts.list);
- b->alts.len = 0;
+ llist_init(&b->alts);
clock_gettime(CLOCK_REALTIME_COARSE, &t);
- b->t_refr = t.tv_sec + dht.t_refresh;
+ b->t_refr = t.tv_sec + dht.t_refr;
for (i = 0; i < (1L << DHT_BETA); ++i)
b->children[i] = NULL;
@@ -1713,18 +1674,16 @@ static void bucket_destroy(struct bucket * b)
if (b->children[i] != NULL)
bucket_destroy(b->children[i]);
- list_for_each_safe(p, h, &b->contacts.list) {
+ llist_for_each_safe(p, h, &b->contacts) {
struct contact * c = list_entry(p, struct contact, next);
- list_del(&c->next);
+ llist_del(&c->next, &b->contacts);
contact_destroy(c);
- --b->contacts.len;
}
- list_for_each_safe(p, h, &b->alts.list) {
+ llist_for_each_safe(p, h, &b->alts) {
struct contact * c = list_entry(p, struct contact, next);
- list_del(&c->next);
+ llist_del(&c->next, &b->alts);
contact_destroy(c);
- --b->alts.len;
}
free(b);
@@ -1759,13 +1718,11 @@ static int move_contacts(struct bucket * b,
assert(b != NULL);
assert(c != NULL);
- list_for_each_safe(p, h, &b->contacts.list) {
+ llist_for_each_safe(p, h, &b->contacts) {
d = list_entry(p, struct contact, next);
if (bucket_has_id(c, d->id)) {
- list_del(&d->next);
- --b->contacts.len;
- list_add_tail(&d->next, &c->contacts.list);
- ++c->contacts.len;
+ llist_del(&d->next, &b->contacts);
+ llist_add_tail(&d->next, &c->contacts);
}
}
@@ -1779,8 +1736,8 @@ static int split_bucket(struct bucket * b)
size_t b_len;
assert(b);
- assert(b->alts.len == 0);
- assert(b->contacts.len != 0);
+ assert(llist_is_empty(&b->alts));
+ assert(!llist_is_empty(&b->contacts));
assert(b->children[0] == NULL);
b_len = b->contacts.len;
@@ -1807,6 +1764,7 @@ static int split_bucket(struct bucket * b)
fail_child:
while (i-- > 0)
bucket_destroy(b->children[i]);
+
return -1;
}
@@ -1836,39 +1794,33 @@ static int dht_kv_update_contacts(const uint8_t * id,
goto fail_update;
}
- list_for_each_safe(p, h, &b->contacts.list) {
+ llist_for_each_safe(p, h, &b->contacts) {
struct contact * d = list_entry(p, struct contact, next);
if (d->addr == addr) {
- list_del(&d->next);
+ llist_del(&d->next, &b->contacts);
contact_destroy(d);
- --b->contacts.len;
}
}
if (b->contacts.len == dht.k) {
if (bucket_has_id(b, dht.id.data)) {
- list_add_tail(&c->next, &b->contacts.list);
- ++b->contacts.len;
+ llist_add_tail(&c->next, &b->contacts);
if (split_bucket(b)) {
- list_del(&c->next);
+ llist_del(&c->next, &b->contacts);
contact_destroy(c);
- --b->contacts.len;
}
} else if (b->alts.len == dht.k) {
struct contact * d;
- d = list_first_entry(&b->alts.list,
+ d = llist_first_entry(&b->alts,
struct contact, next);
- list_del(&d->next);
+ llist_del(&d->next, &b->alts);
contact_destroy(d);
- list_add_tail(&c->next, &b->alts.list);
- ++b->alts.len;
+ llist_add_tail(&c->next, &b->alts);
} else {
- list_add_tail(&c->next, &b->alts.list);
- ++b->alts.len;
+ llist_add_tail(&c->next, &b->alts);
}
} else {
- list_add_tail(&c->next, &b->contacts.list);
- ++b->contacts.len;
+ llist_add_tail(&c->next, &b->contacts);
}
pthread_rwlock_unlock(&dht.db.lock);
@@ -2116,7 +2068,7 @@ static ssize_t dht_kv_retrieve(const uint8_t * key,
i = 0;
- list_for_each(p, &e->vals.list) {
+ llist_for_each(p, &e->vals) {
struct val_entry * v;
if (i == n)
break; /* We have enough values */
@@ -2129,7 +2081,7 @@ static ssize_t dht_kv_retrieve(const uint8_t * key,
memcpy((*vals)[i++].data, v->val.data, v->val.len);
}
- list_for_each(p, &e->lvals.list) {
+ llist_for_each(p, &e->lvals) {
struct val_entry * v;
if (i == n)
break; /* We have enough values */
@@ -2266,7 +2218,7 @@ static int dht_send_msg(dht_msg_t * msg,
uint64_t addr)
{
size_t len;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
if (msg == NULL)
return 0;
@@ -2279,21 +2231,21 @@ static int dht_send_msg(dht_msg_t * msg,
goto fail_msg;
}
- if (ipcp_sdb_reserve(&sdb, len)) {
- log_warn("%s failed to get sdb.", DHT_CODE(msg));
+ if (ipcp_spb_reserve(&spb, len)) {
+ log_warn("%s failed to get spb.", DHT_CODE(msg));
goto fail_msg;
}
- dht_msg__pack(msg, shm_du_buff_head(sdb));
+ dht_msg__pack(msg, ssm_pk_buff_head(spb));
- if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, sdb) < 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;
}
return 0;
fail_send:
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
fail_msg:
return -1;
}
@@ -2584,15 +2536,14 @@ static void __add_dht_kv_entry(struct dht_entry * e)
assert(e != NULL);
- list_for_each(p, &dht.db.kv.list) {
+ llist_for_each(p, &dht.db.kv.ll) {
struct dht_entry * d = list_entry(p, struct dht_entry, next);
if (IS_CLOSER(d->key, e->key))
continue;
break;
}
- list_add_tail(&e->next, p);
- ++dht.db.kv.len;
+ llist_add_tail_at(&e->next, p, &dht.db.kv.ll);
}
/* incoming store message */
@@ -2629,9 +2580,8 @@ static int dht_kv_store(const uint8_t * key,
return 0;
fail_add:
if (new) {
- list_del(&e->next);
+ llist_del(&e->next, &dht.db.kv.ll);
dht_entry_destroy(e);
- --dht.db.kv.len;
}
fail:
pthread_rwlock_unlock(&dht.db.lock);
@@ -2669,14 +2619,13 @@ static int dht_kv_publish(const uint8_t * key,
pthread_rwlock_unlock(&dht.db.lock);
- dht_kv_store_remote(key, val, now.tv_sec + dht.t_expire);
+ dht_kv_store_remote(key, val, now.tv_sec + dht.t_exp);
return 0;
fail_add:
if (new) {
- list_del(&e->next);
+ llist_del(&e->next, &dht.db.kv.ll);
dht_entry_destroy(e);
- --dht.db.kv.len;
}
fail:
pthread_rwlock_unlock(&dht.db.lock);
@@ -2858,7 +2807,7 @@ static void do_dht_kv_store(const dht_store_msg_t * store)
key = store->key.data;
exp = store->exp;
- if (dht_kv_store(store->key.data, val, store->exp) < 0) {
+ if (dht_kv_store(key, val, store->exp) < 0) {
log_err(KV_FMT " Failed to store.", KV_VAL(key, val));
return;
}
@@ -2902,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;
@@ -3004,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;
@@ -3189,7 +3140,7 @@ static void * dht_handle_packet(void * o)
}
#ifndef __DHT_TEST__
static void dht_post_packet(void * comp,
- struct shm_du_buff * sdb)
+ struct ssm_pk_buff * spb)
{
struct cmd * cmd;
@@ -3201,17 +3152,17 @@ static void dht_post_packet(void * comp,
goto fail_cmd;
}
- cmd->cbuf.data = malloc(shm_du_buff_len(sdb));
+ cmd->cbuf.data = malloc(ssm_pk_buff_len(spb));
if (cmd->cbuf.data == NULL) {
log_err("Command buffer malloc failed.");
goto fail_buf;
}
- cmd->cbuf.len = shm_du_buff_len(sdb);
+ cmd->cbuf.len = ssm_pk_buff_len(spb);
- memcpy(cmd->cbuf.data, shm_du_buff_head(sdb), cmd->cbuf.len);
+ memcpy(cmd->cbuf.data, ssm_pk_buff_head(spb), cmd->cbuf.len);
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
pthread_mutex_lock(&dht.cmds.mtx);
@@ -3226,7 +3177,7 @@ static void dht_post_packet(void * comp,
fail_buf:
free(cmd);
fail_cmd:
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
return;
}
#endif
@@ -3365,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;
@@ -3376,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;
@@ -3391,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:
@@ -3449,15 +3440,14 @@ static void dht_kv_remove_expired_reqs(void)
pthread_mutex_lock(&dht.reqs.mtx);
- list_for_each_safe(p, h, &dht.reqs.list) {
+ llist_for_each_safe(p, h, &dht.reqs.ll) {
struct dht_req * e;
e = list_entry(p, struct dht_req, next);
if (IS_EXPIRED(e, &now)) {
log_dbg(KEY_FMT " Removing expired request.",
KEY_VAL(e->key));
- list_del(&e->next);
+ llist_del(&e->next, &dht.reqs.ll);
dht_req_destroy(e);
- --dht.reqs.len;
}
}
@@ -3481,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,
@@ -3489,7 +3481,7 @@ static void dht_entry_get_repl_lists(const struct dht_entry * e,
struct list_head * p;
struct val_entry * n;
- list_for_each(p, &e->vals.list) {
+ llist_for_each(p, &e->vals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
if (MUST_REPLICATE(v, now) && !IS_EXPIRED(v, now)) {
n = val_entry_create(v->val, v->t_exp);
@@ -3500,11 +3492,11 @@ static void dht_entry_get_repl_lists(const struct dht_entry * e,
}
}
- list_for_each(p, &e->lvals.list) {
+ 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_expire);
+ n = val_entry_create(v->val, now->tv_sec + dht.t_exp);
if (n == NULL)
continue;
@@ -3533,10 +3525,10 @@ static int dht_kv_next_values(uint8_t * key,
pthread_rwlock_rdlock(&dht.db.lock);
- if (dht.db.kv.len == 0)
+ if (llist_is_empty(&dht.db.kv.ll))
goto no_entries;
- list_for_each_safe(p, h, &dht.db.kv.list) {
+ llist_for_each_safe(p, h, &dht.db.kv.ll) {
e = list_entry(p, struct dht_entry, next);
if (IS_CLOSER(e->key, key))
continue; /* Already processed */
@@ -3578,7 +3570,7 @@ static void dht_kv_republish_value(const uint8_t * key,
assert(MUST_REPLICATE(v, now));
if (MUST_REPUBLISH(v, now))
- assert(v->t_exp >= now->tv_sec + dht.t_expire);
+ assert(v->t_exp >= now->tv_sec + dht.t_exp);
if (dht_kv_store_remote(key, v->val, v->t_exp) == 0) {
log_dbg(KV_FMT " Republished.", KV_VAL(key, v->val));
@@ -3786,12 +3778,15 @@ static void * work(void * o)
nanosleep(&now, NULL);
}
- intv = gcd(dht.t_expire, (dht.t_expire - DHT_N_REPUB * dht.t_repl));
- intv = gcd(intv, gcd(dht.t_repl, dht.t_refresh)) / 2;
+ intv = gcd(dht.t_exp, (dht.t_exp - DHT_N_REPUB * dht.t_repl));
+ intv = gcd(intv, gcd(dht.t_repl, dht.t_refr)) / 2;
intv = MAX(1, intv / n);
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) {
@@ -3868,13 +3863,13 @@ int dht_init(struct dir_dht_config * conf)
dht.id.len = DHT_TEST_KEY_LEN;
dht.addr = DHT_TEST_ADDR;
#endif
- dht.t0 = now.tv_sec;
- dht.alpha = conf->params.alpha;
- dht.k = conf->params.k;
- dht.t_expire = conf->params.t_expire;
- dht.t_refresh = conf->params.t_refresh;
- dht.t_repl = conf->params.t_replicate;
- dht.peer = conf->peer;
+ dht.t0 = now.tv_sec;
+ dht.alpha = conf->params.alpha;
+ dht.k = conf->params.k;
+ dht.t_exp = conf->params.t_expire;
+ dht.t_refr = conf->params.t_refresh;
+ dht.t_repl = conf->params.t_replicate;
+ dht.peer = conf->peer;
dht.magic = generate_cookie();
@@ -3899,8 +3894,7 @@ int dht_init(struct dir_dht_config * conf)
goto fail_cmds_cond;
}
- list_head_init(&dht.reqs.list);
- dht.reqs.len = 0;
+ llist_init(&dht.reqs.ll);
if (pthread_mutex_init(&dht.reqs.mtx, NULL)) {
log_err("Failed to initialize request mutex.");
@@ -3922,8 +3916,7 @@ int dht_init(struct dir_dht_config * conf)
goto fail_reqs_cond;
}
- list_head_init(&dht.db.kv.list);
- dht.db.kv.len = 0;
+ llist_init(&dht.db.kv.ll);
dht.db.kv.vals = 0;
dht.db.kv.lvals = 0;
@@ -3960,9 +3953,9 @@ int dht_init(struct dir_dht_config * conf)
log_dbg(" address: " ADDR_FMT32 ".", ADDR_VAL32(&dht.addr));
log_dbg(" peer: " ADDR_FMT32 ".", ADDR_VAL32(&dht.peer));
log_dbg(" magic cookie: " HASH_FMT64 ".", HASH_VAL64(&dht.magic));
- log_info(" parameters: alpha=%u, k=%zu, t_expire=%ld, "
- "t_refresh=%ld, t_replicate=%ld.",
- dht.alpha, dht.k, dht.t_expire, dht.t_refresh, dht.t_repl);
+ log_info(" parameters: alpha=%u, k=%zu, t_exp=%ld, "
+ "t_refr=%ld, t_replicate=%ld.",
+ dht.alpha, dht.k, dht.t_exp, dht.t_refr, dht.t_repl);
#endif
dht.state = DHT_INIT;
@@ -4015,11 +4008,10 @@ void dht_fini(void)
pthread_mutex_lock(&dht.reqs.mtx);
- list_for_each_safe(p, h, &dht.reqs.list) {
+ llist_for_each_safe(p, h, &dht.reqs.ll) {
struct dht_req * r = list_entry(p, struct dht_req, next);
- list_del(&r->next);
+ llist_del(&r->next, &dht.reqs.ll);
dht_req_destroy(r);
- dht.reqs.len--;
}
pthread_mutex_unlock(&dht.reqs.mtx);
@@ -4029,11 +4021,10 @@ void dht_fini(void)
pthread_rwlock_wrlock(&dht.db.lock);
- list_for_each_safe(p, h, &dht.db.kv.list) {
+ llist_for_each_safe(p, h, &dht.db.kv.ll) {
struct dht_entry * e = list_entry(p, struct dht_entry, next);
- list_del(&e->next);
+ llist_del(&e->next, &dht.db.kv.ll);
dht_entry_destroy(e);
- dht.db.kv.len--;
}
if (dht.db.contacts.root != NULL)
@@ -4043,10 +4034,10 @@ void dht_fini(void)
pthread_rwlock_destroy(&dht.db.lock);
- assert(dht.db.kv.len == 0);
+ assert(llist_is_empty(&dht.db.kv.ll));
assert(dht.db.kv.vals == 0);
assert(dht.db.kv.lvals == 0);
- assert(dht.reqs.len == 0);
+ assert(llist_is_empty(&dht.reqs.ll));
freebuf(dht.id);
}
diff --git a/src/ipcpd/unicast/dir/dht.h b/src/ipcpd/unicast/dir/dht.h
index 852a5130..ed3bb9f0 100644
--- a/src/ipcpd/unicast/dir/dht.h
+++ b/src/ipcpd/unicast/dir/dht.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Distributed Hash Table based on Kademlia
*
diff --git a/src/ipcpd/unicast/dir/dht.proto b/src/ipcpd/unicast/dir/dht.proto
index ea74805f..02b6b341 100644
--- a/src/ipcpd/unicast/dir/dht.proto
+++ b/src/ipcpd/unicast/dir/dht.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* DHT protocol, based on Kademlia
*
diff --git a/src/ipcpd/unicast/dir/ops.h b/src/ipcpd/unicast/dir/ops.h
index 8c6e5eb5..6c336ee0 100644
--- a/src/ipcpd/unicast/dir/ops.h
+++ b/src/ipcpd/unicast/dir/ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Directory policy ops
*
diff --git a/src/ipcpd/unicast/dir/pol.h b/src/ipcpd/unicast/dir/pol.h
index eae4b2e7..8ccf4f95 100644
--- a/src/ipcpd/unicast/dir/pol.h
+++ b/src/ipcpd/unicast/dir/pol.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Directory policies
*
diff --git a/src/ipcpd/unicast/dir/tests/CMakeLists.txt b/src/ipcpd/unicast/dir/tests/CMakeLists.txt
index 897f1ec2..eded823f 100644
--- a/src/ipcpd/unicast/dir/tests/CMakeLists.txt
+++ b/src/ipcpd/unicast/dir/tests/CMakeLists.txt
@@ -3,38 +3,36 @@ get_filename_component(CURRENT_SOURCE_PARENT_DIR
get_filename_component(CURRENT_BINARY_PARENT_DIR
${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
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
dht_test.c
- )
+)
-protobuf_generate_c(DHT_PROTO_SRCS KAD_PROTO_HDRS ../dht.proto)
+protobuf_generate_c(DHT_PROTO_SRCS KAD_PROTO_HDRS ${CURRENT_SOURCE_PARENT_DIR}/dht.proto)
add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests}
${DHT_PROTO_SRCS})
-target_link_libraries(${PARENT_DIR}_test ouroboros-common)
-add_dependencies(check ${PARENT_DIR}_test)
+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
+ ${CMAKE_SOURCE_DIR}/src/ipcpd/unicast
+ ${CMAKE_BINARY_DIR}/src/ipcpd/unicast
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
-set(tests_to_run ${${PARENT_DIR}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
+target_link_libraries(${PARENT_DIR}_test ouroboros-common)
+add_dependencies(build_tests ${PARENT_DIR}_test)
-foreach (test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(${test_name} ${C_TEST_PATH}/${PARENT_DIR}_test ${test_name})
-endforeach (test)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/ipcpd/unicast/dir/tests/dht_test.c b/src/ipcpd/unicast/dir/tests/dht_test.c
index cb6b0f9f..1f7026b3 100644
--- a/src/ipcpd/unicast/dir/tests/dht_test.c
+++ b/src/ipcpd/unicast/dir/tests/dht_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unit tests of the DHT
*
@@ -27,7 +27,7 @@
#define _POSIX_C_SOURCE 200112L
#endif
-#include <ouroboros/test.h>
+#include <test/test.h>
#include <ouroboros/list.h>
#include <ouroboros/utils.h>
@@ -46,10 +46,9 @@
/* forward declare for use in the dht code */
/* Packet sink for DHT tests */
struct {
- bool enabled;
+ bool enabled;
- struct list_head list;
- size_t len;
+ struct llist msgs;
} sink;
struct message {
@@ -66,8 +65,6 @@ static int sink_send_msg(buffer_t * pkt,
assert(pkt != NULL);
assert(addr != 0);
- assert(!list_is_empty(&sink.list) || sink.len == 0);
-
if (!sink.enabled)
goto finish;
@@ -83,9 +80,8 @@ static int sink_send_msg(buffer_t * pkt,
m->dst = addr;
- list_add_tail(&m->next, &sink.list);
+ llist_add_tail(&m->next, &sink.msgs);
- ++sink.len;
finish:
freebuf(*pkt);
@@ -103,8 +99,7 @@ static int sink_send_msg(buffer_t * pkt,
static void sink_init(void)
{
- list_head_init(&sink.list);
- sink.len = 0;
+ llist_init(&sink.msgs);
sink.enabled = true;
}
@@ -113,22 +108,20 @@ static void sink_clear(void)
struct list_head * p;
struct list_head * h;
- list_for_each_safe(p, h, &sink.list) {
+ llist_for_each_safe(p, h, &sink.msgs) {
struct message * m = list_entry(p, struct message, next);
- list_del(&m->next);
+ llist_del(&m->next, &sink.msgs);
dht_msg__free_unpacked((dht_msg_t *) m->msg, NULL);
free(m);
- --sink.len;
}
- assert(list_is_empty(&sink.list));
+ assert(llist_is_empty(&sink.msgs));
}
static void sink_fini(void)
{
sink_clear();
-
- assert(list_is_empty(&sink.list) || sink.len != 0);
+ sink.enabled = false;
}
static dht_msg_t * sink_read(void)
@@ -136,16 +129,12 @@ static dht_msg_t * sink_read(void)
struct message * m;
dht_msg_t * msg;
- assert(!list_is_empty(&sink.list) || sink.len == 0);
-
- if (list_is_empty(&sink.list))
+ if (llist_is_empty(&sink.msgs))
return NULL;
- m = list_first_entry(&sink.list, struct message, next);
-
- --sink.len;
+ m = llist_first_entry(&sink.msgs, struct message, next);
- list_del(&m->next);
+ llist_del(&m->next, &sink.msgs);
msg = m->msg;
@@ -978,7 +967,7 @@ static int test_dht_kv_find_node_rsp_msg_contacts(void)
}
if ((size_t) n < dht.k) {
- printf("Failed to get enough contacts (%zu < %zu).\n", n, dht.k);
+ printf("Failed to get all contacts (%zu < %zu).\n", n, dht.k);
goto fail_fill;
}
@@ -1204,7 +1193,7 @@ static int test_dht_kv_find_value_rsp_msg_contacts(void)
}
if ((size_t) n < dht.k) {
- printf("Failed to get enough contacts (%zu < %zu).\n", n, dht.k);
+ printf("Failed to get all contacts (%zu < %zu).\n", n, dht.k);
goto fail_fill;
}
@@ -1591,7 +1580,7 @@ static int test_dht_reg_unreg(void)
goto fail_reg;
}
- if (sink.len != 0) {
+ if (!llist_is_empty(&sink.msgs)) {
printf("Packet sent without contacts!");
goto fail_msg;
}
@@ -1642,7 +1631,7 @@ static int test_dht_reg_unreg_contacts(void)
goto fail_reg;
}
- if (sink.len != dht.alpha) {
+ if (sink.msgs.len != dht.alpha) {
printf("Packet sent to too few contacts!\n");
goto fail_msg;
}
@@ -1784,7 +1773,7 @@ static int test_dht_query(void)
goto fail_get;
}
- if (sink.len != 0) {
+ if (!llist_is_empty(&sink.msgs)) {
printf("Packet sent without contacts!");
goto fail_test;
}
diff --git a/src/ipcpd/unicast/dt.c b/src/ipcpd/unicast/dt.c
index e2679ffe..381ed815 100644
--- a/src/ipcpd/unicast/dt.c
+++ b/src/ipcpd/unicast/dt.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Data Transfer Component
*
@@ -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"
@@ -68,7 +71,7 @@
#endif
struct comp_info {
- void (* post_packet)(void * comp, struct shm_du_buff * sdb);
+ void (* post_packet)(void * comp, struct ssm_pk_buff * spb);
void * comp;
char * name;
};
@@ -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,14 +139,15 @@ 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);
}
-static void dt_pci_shrink(struct shm_du_buff * sdb)
+static void dt_pci_shrink(struct ssm_pk_buff * spb)
{
- assert(sdb);
+ assert(spb);
- shm_du_buff_head_release(sdb, dt_pci_info.head_size);
+ ssm_pk_buff_pop(spb, dt_pci_info.head_size);
}
struct {
@@ -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,23 +176,29 @@ 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[PROG_MAX_FLOWS];
+ } stat[PROC_MAX_FLOWS];
size_t n_flows;
#endif
struct bmp * res_fds;
- struct comp_info comps[PROG_RES_FDS];
+ struct comp_info comps[PROC_RES_FDS];
pthread_rwlock_t lock;
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,22 +222,21 @@ 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 >= PROG_RES_FDS) {
+ if (fd >= PROC_RES_FDS) {
fccntl(fd, FLOWGRXQLEN, &rxqlen);
fccntl(fd, FLOWGTXQLEN, &txqlen);
}
@@ -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 < PROG_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,29 +362,49 @@ static struct rib_ops r_ops = {
.getattr = dt_rib_getattr
};
-#ifdef IPCP_FLOW_STATS
static void stat_used(int fd,
uint64_t addr)
{
struct timespec now;
+ int i;
clock_gettime(CLOCK_REALTIME_COARSE, &now);
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- memset(&dt.stat[fd], 0, sizeof(dt.stat[fd]));
+ pthread_rwlock_wrlock(&dt.lock);
- dt.stat[fd].stamp = (addr != INVALID_ADDR) ? now.tv_sec : 0;
- dt.stat[fd].addr = addr;
+ STORE_RELEASE(&dt.stat[fd].stamp, 0);
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ /* 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);
+ }
- pthread_rwlock_wrlock(&dt.lock);
+ STORE_RELAXED(&dt.stat[fd].addr, addr);
- (addr != INVALID_ADDR) ? ++dt.n_flows : --dt.n_flows;
+ if (addr != INVALID_ADDR) {
+ STORE_RELEASE(&dt.stat[fd].stamp, now.tv_sec);
+ ++dt.n_flows;
+ } else {
+ --dt.n_flows;
+ }
pthread_rwlock_unlock(&dt.lock);
}
+#else
+#define dt_stat_inc(idx, name, qc, len) ((void) 0)
#endif
static void handle_event(void * self,
@@ -411,6 +424,7 @@ static void handle_event(void * self,
#ifdef IPCP_FLOW_STATS
stat_used(fd, c->conn_info.addr);
#endif
+ cap_reset(fd);
psched_add(dt.psched, fd);
log_dbg("Added fd %d to packet scheduler.", fd);
break;
@@ -427,46 +441,36 @@ static void handle_event(void * self,
}
}
-static void packet_handler(int fd,
- qoscube_t qc,
- struct shm_du_buff * sdb)
+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;
+ bool marks;
- len = shm_du_buff_len(sdb);
+ 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 = shm_du_buff_head(sdb);
+ head = ssm_pk_buff_head(spb);
dt_pci_des(head, &dt_pci);
if (dt_pci.dst_addr != dt.addr) {
if (dt_pci.ttl == 0) {
log_dbg("TTL was zero.");
- ipcp_sdb_release(sdb);
-#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;
+ ipcp_spb_release(spb);
+ dt_stat_inc(fd, r_drp, qc, len);
+ return 0;
}
/* FIXME: Use qoscube from PCI instead of incoming flow. */
@@ -474,75 +478,56 @@ static void packet_handler(int fd,
if (ofd < 0) {
log_dbg("No next hop for %" PRIu64 ".",
dt_pci.dst_addr);
- ipcp_sdb_release(sdb);
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
+ ipcp_spb_release(spb);
+ 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;
- pthread_mutex_unlock(&dt.stat[fd].lock);
-#endif
- return;
- }
+ (void) ca_calc_ecn(qlen, head + dt_pci_info.ecn_o, qc, len);
- (void) ca_calc_ecn(ofd, head + dt_pci_info.ecn_o, qc, len);
+ if (marks)
+ cap_stamp(head + dt_pci_info.cap_o, cap_get(ofd));
- ret = ipcp_flow_write(ofd, sdb);
+ ret = ipcp_flow_write(ofd, spb);
if (ret < 0) {
log_dbg("Failed to write packet to fd %d.", ofd);
if (ret == -EFLOWDOWN)
notifier_event(NOTIFY_DT_FLOW_DOWN, &ofd);
- ipcp_sdb_release(sdb);
-#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;
+ ipcp_spb_release(spb);
+ 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)
+ cap_update(ofd, qlen, len);
} else {
- dt_pci_shrink(sdb);
- if (dt_pci.eid >= PROG_RES_FDS) {
+ 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, sdb);
- 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_sdb_release(sdb);
- return;
+ ipcp_spb_release(spb);
+ return 0;
}
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- ++dt.stat[fd].lcl_r_pkt[qc];
- dt.stat[fd].lcl_r_bytes[qc] += len;
+ dt_stat_inc(fd, lcl_r, qc, len);
+ dt_stat_inc(dt_pci.eid, snd, 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,
- sdb);
+ spb);
}
+
+ return 0;
}
static void * dt_conn_handle(void * o)
@@ -569,7 +554,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;
@@ -599,9 +586,15 @@ 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 (cap_init() < 0) {
+ log_err("Failed to init capacity estimator.");
+ goto fail_cap;
+ }
+
if (connmgr_comp_init(COMPID_DT, &info)) {
log_err("Failed to register with connmgr.");
goto fail_connmgr_comp_init;
@@ -636,37 +629,27 @@ int dt_init(struct dt_config cfg)
goto fail_rwlock_init;
}
- dt.res_fds = bmp_create(PROG_RES_FDS, 0);
+ dt.res_fds = bmp_create(PROC_RES_FDS, 0);
if (dt.res_fds == NULL)
goto fail_res_fds;
#ifdef IPCP_FLOW_STATS
memset(dt.stat, 0, sizeof(dt.stat));
- for (i = 0; i < PROG_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 < PROG_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:
@@ -680,19 +663,21 @@ int dt_init(struct dt_config cfg)
fail_routing:
connmgr_comp_fini(COMPID_DT);
fail_connmgr_comp_init:
+ cap_fini();
+ fail_cap:
return -1;
}
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 < PROG_MAX_FLOWS; ++i)
- pthread_mutex_destroy(&dt.stat[i].lock);
#endif
bmp_destroy(dt.res_fds);
@@ -707,6 +692,8 @@ void dt_fini(void)
routing_fini();
connmgr_comp_fini(COMPID_DT);
+
+ cap_fini();
}
int dt_start(void)
@@ -758,7 +745,7 @@ void dt_stop(void)
}
int dt_reg_comp(void * comp,
- void (* func)(void * func, struct shm_du_buff *),
+ void (* func)(void * func, struct ssm_pk_buff *),
char * name)
{
int eid;
@@ -791,7 +778,7 @@ int dt_reg_comp(void * comp,
void dt_unreg_comp(int eid)
{
- assert(eid >= 0 && eid < PROG_RES_FDS);
+ assert(eid >= 0 && eid < PROC_RES_FDS);
pthread_rwlock_wrlock(&dt.lock);
@@ -809,64 +796,65 @@ void dt_unreg_comp(int eid)
int dt_write_packet(uint64_t dst_addr,
qoscube_t qc,
uint64_t eid,
- struct shm_du_buff * sdb)
+ 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;
+ bool marks;
- assert(sdb);
+ assert(spb);
assert(dst_addr != dt.addr);
- len = shm_du_buff_len(sdb);
-
#ifdef IPCP_FLOW_STATS
- if (eid < PROG_RES_FDS) {
- pthread_mutex_lock(&dt.stat[eid].lock);
+ len = ssm_pk_buff_len(spb);
- ++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 < PROG_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;
}
- head = shm_du_buff_head_alloc(sdb, dt_pci_info.head_size);
+ head = ssm_pk_buff_push(spb, dt_pci_info.head_size);
if (head == NULL) {
log_dbg("Failed to allocate DT header.");
goto fail_write;
}
- len = shm_du_buff_len(sdb);
+ len = ssm_pk_buff_len(spb);
dt_pci.dst_addr = 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;
+
+ (void) ca_calc_ecn(qlen, &dt_pci.ecn, qc, len);
+
+ if (marks)
+ dt_pci.cap = cap_get(fd);
- (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, sdb);
+ ret = ipcp_flow_write(fd, spb);
if (ret < 0) {
log_dbg("Failed to write packet to fd %d.", fd);
if (ret == -EFLOWDOWN)
@@ -874,31 +862,22 @@ int dt_write_packet(uint64_t dst_addr,
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 < PROG_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)
+ cap_update(fd, qlen, len);
+
return 0;
fail_write:
#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- if (eid < PROG_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 2c5b7978..a055efea 100644
--- a/src/ipcpd/unicast/dt.h
+++ b/src/ipcpd/unicast/dt.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Data Transfer component
*
@@ -25,7 +25,7 @@
#include <ouroboros/ipcp.h>
#include <ouroboros/qoscube.h>
-#include <ouroboros/shm_rdrbuff.h>
+#include <ouroboros/ssm_pool.h>
#define DT_COMP "Data Transfer"
#define DT_PROTO "dtp"
@@ -40,7 +40,7 @@ int dt_start(void);
void dt_stop(void);
int dt_reg_comp(void * comp,
- void (* func)(void * comp, struct shm_du_buff * sdb),
+ void (* func)(void * comp, struct ssm_pk_buff * spb),
char * name);
void dt_unreg_comp(int eid);
@@ -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 shm_du_buff * sdb);
+ 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 ac168bd9..ac2ecaea 100644
--- a/src/ipcpd/unicast/fa.c
+++ b/src/ipcpd/unicast/fa.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Flow allocator of the IPC Process
*
@@ -31,12 +31,15 @@
#define FA "flow-allocator"
#define OUROBOROS_PREFIX FA
+#include <ouroboros/atomics.h>
#include <ouroboros/endian.h>
#include <ouroboros/logs.h>
#include <ouroboros/fqueue.h>
#include <ouroboros/errno.h>
#include <ouroboros/dev.h>
#include <ouroboros/ipcp-dev.h>
+#include <ouroboros/np1_flow.h>
+#include <ouroboros/qoscube.h>
#include <ouroboros/rib.h>
#include <ouroboros/random.h>
#include <ouroboros/pthread.h>
@@ -48,6 +51,7 @@
#include "ipcp.h"
#include "dt.h"
#include "ca.h"
+#include "np1.h"
#include <inttypes.h>
#include <stdlib.h>
@@ -57,12 +61,13 @@
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
#endif
-#define TIMEOUT 10 * MILLION /* nanoseconds */
+#define TIMEOUT 10 * MILLION /* nanoseconds */
+#define MSGBUFSZ 32768
-#define FLOW_REQ 0
-#define FLOW_REPLY 1
-#define FLOW_UPDATE 2
-#define MSGBUFSZ 2048
+#define FLOW_REQ 0
+#define FLOW_REPLY 1
+#define FLOW_UPDATE 2
+#define FLOW_IRM_UPDATE 3
#define STAT_FILE_LEN 0
@@ -78,14 +83,15 @@ struct fa_msg {
uint32_t max_gap;
uint32_t timeout;
uint16_t ece;
+ uint8_t cap;
uint8_t code;
uint8_t availability;
- uint8_t in_order;
+ uint8_t service;
} __attribute__((packed));
struct cmd {
struct list_head next;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
};
struct fa_flow {
@@ -106,11 +112,13 @@ struct fa_flow {
uint64_t r_eid; /* Remote endpoint 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 {
pthread_rwlock_t flows_lock;
- struct fa_flow flows[PROG_MAX_FLOWS];
+ struct fa_flow flows[PROC_MAX_FLOWS];
#ifdef IPCP_FLOW_STATS
size_t n_flows;
#endif
@@ -124,11 +132,11 @@ struct {
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];
@@ -144,7 +152,7 @@ static int fa_rib_read(const char * path,
fd = atoi(entry);
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -1;
if (len < 1536)
@@ -198,17 +206,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;
@@ -224,7 +225,7 @@ static int fa_rib_readdir(char *** buf)
if (*buf == NULL)
goto fail_entries;
- for (i = 0; i < PROG_MAX_FLOWS; ++i) {
+ for (i = 0; i < PROC_MAX_FLOWS; ++i) {
struct fa_flow * flow;
flow = &fa.flows[i];
@@ -249,20 +250,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;
@@ -285,10 +282,7 @@ static int fa_rib_getattr(const char * path,
}
pthread_rwlock_unlock(&fa.flows_lock);
-#else
- (void) path;
- (void) attr;
-#endif
+
return 0;
}
@@ -297,6 +291,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)
{
@@ -305,7 +300,7 @@ static int eid_to_fd(uint64_t eid)
fd = eid & 0xFFFFFFFF;
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -1;
flow = &fa.flows[fd];
@@ -328,37 +323,40 @@ static uint64_t gen_eid(int fd)
return ((uint64_t) rnd << 32) + fd;
}
-static void packet_handler(int fd,
- qoscube_t qc,
- struct shm_du_buff * sdb)
+static time_t packet_handler(int fd,
+ qoscube_t qc,
+ struct ssm_pk_buff * spb)
{
struct fa_flow * flow;
uint64_t r_addr;
uint64_t r_eid;
- ca_wnd_t wnd;
+ time_t wait;
size_t len;
+ uint8_t ecn;
flow = &fa.flows[fd];
+ ecn = 0;
+
pthread_rwlock_wrlock(&fa.flows_lock);
- len = shm_du_buff_len(sdb);
+ len = ssm_pk_buff_len(spb);
#ifdef IPCP_FLOW_STATS
++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);
r_addr = flow->r_addr;
r_eid = flow->r_eid;
pthread_rwlock_unlock(&fa.flows_lock);
- ca_wnd_wait(wnd);
-
- if (dt_write_packet(r_addr, qc, r_eid, sdb)) {
- ipcp_sdb_release(sdb);
+ 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
pthread_rwlock_wrlock(&fa.flows_lock);
@@ -366,8 +364,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)
@@ -381,9 +383,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);
@@ -397,7 +397,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));
@@ -411,7 +412,7 @@ static void fa_flow_fini(struct fa_flow * flow)
}
static void fa_post_packet(void * comp,
- struct shm_du_buff * sdb)
+ struct ssm_pk_buff * spb)
{
struct cmd * cmd;
@@ -422,11 +423,11 @@ static void fa_post_packet(void * comp,
cmd = malloc(sizeof(*cmd));
if (cmd == NULL) {
log_err("Command failed. Out of memory.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
return;
}
- cmd->sdb = sdb;
+ cmd->spb = spb;
pthread_mutex_lock(&fa.mtx);
@@ -454,16 +455,16 @@ static size_t fa_wait_for_fa_msg(struct fa_msg * msg)
pthread_cleanup_pop(true);
- len = shm_du_buff_len(cmd->sdb);
+ len = ssm_pk_buff_len(cmd->spb);
if (len > MSGBUFSZ || len < sizeof(*msg)) {
log_warn("Invalid flow allocation message (len: %zd).", len);
free(cmd);
return 0; /* No valid message */
}
- memcpy(msg, shm_du_buff_head(cmd->sdb), len);
+ memcpy(msg, ssm_pk_buff_head(cmd->spb), len);
- ipcp_sdb_release(cmd->sdb);
+ ipcp_spb_release(cmd->spb);
free(cmd);
@@ -495,11 +496,12 @@ static int fa_handle_flow_req(struct fa_msg * msg,
qs.availability = msg->availability;
qs.loss = ntoh32(msg->loss);
qs.ber = ntoh32(msg->ber);
- qs.in_order = msg->in_order;
+ qs.service = msg->service;
qs.max_gap = ntoh32(msg->max_gap);
qs.timeout = ntoh32(msg->timeout);
- fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UNICAST_MPL, &data);
+ fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UNICAST_MPL,
+ IPCP_UNICAST_MTU, &data);
if (fd < 0)
return fd;
@@ -513,6 +515,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;
@@ -527,7 +536,8 @@ static int fa_handle_flow_reply(struct fa_msg * msg,
time_t mpl = IPCP_UNICAST_MPL;
int response;
- assert(len >= sizeof(*msg));
+ if (len < sizeof(*msg))
+ return -EINVAL;
data.data = (uint8_t *) msg + sizeof(*msg);
data.len = len - sizeof(*msg);
@@ -557,7 +567,8 @@ static int fa_handle_flow_reply(struct fa_msg * msg,
pthread_rwlock_unlock(&fa.flows_lock);
- if (ipcp_flow_alloc_reply(fd, response, mpl, &data) < 0) {
+ if (ipcp_flow_alloc_reply(fd, response, mpl,
+ IPCP_UNICAST_MTU, &data) < 0) {
log_err("Failed to reply for flow allocation on fd %d.", fd);
return -EIRMD;
}
@@ -571,8 +582,8 @@ static int fa_handle_flow_update(struct fa_msg * msg,
struct fa_flow * flow;
int fd;
- (void) len;
- assert(len >= sizeof(*msg));
+ if (len < sizeof(*msg))
+ return -EINVAL;
pthread_rwlock_wrlock(&fa.flows_lock);
@@ -588,13 +599,50 @@ 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;
}
+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;
@@ -623,6 +671,10 @@ 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;
default:
log_warn("Recieved unknown flow allocation message.");
break;
@@ -651,8 +703,10 @@ int fa_init(void)
if (pthread_cond_init(&fa.cond, &cattr))
goto fail_cond;
+#ifdef IPCP_FLOW_STATS
if (rib_reg(FA, &r_ops))
goto fail_rib_reg;
+#endif
fa.eid = dt_reg_comp(&fa, &fa_post_packet, FA);
if ((int) fa.eid < 0)
@@ -665,8 +719,10 @@ int fa_init(void)
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);
@@ -680,20 +736,29 @@ int fa_init(void)
void fa_fini(void)
{
+#ifdef IPCP_FLOW_STATS
rib_unreg(FA);
-
+#endif
pthread_cond_destroy(&fa.cond);;
pthread_mutex_destroy(&fa.mtx);
pthread_rwlock_destroy(&fa.flows_lock);
}
+static int np1_flow_read_fa(int fd,
+ struct ssm_pk_buff ** spb)
+{
+ return np1_flow_read(fd, spb, NP1_GET_POOL(fd));
+}
+
int fa_start(void)
{
+#ifndef BUILD_CONTAINER
struct sched_param par;
int pol;
int max;
+#endif
- fa.psched = psched_create(packet_handler, np1_flow_read);
+ fa.psched = psched_create(packet_handler, np1_flow_read_fa);
if (fa.psched == NULL) {
log_err("Failed to start packet scheduler.");
goto fail_psched;
@@ -704,6 +769,7 @@ int fa_start(void)
goto fail_thread;
}
+#ifndef BUILD_CONTAINER
if (pthread_getschedparam(fa.worker, &pol, &par)) {
log_err("Failed to get worker thread scheduling parameters.");
goto fail_sched;
@@ -721,12 +787,15 @@ int fa_start(void)
log_err("Failed to set scheduler priority to maximum.");
goto fail_sched;
}
+#endif
return 0;
+#ifndef BUILD_CONTAINER
fail_sched:
pthread_cancel(fa.worker);
pthread_join(fa.worker, NULL);
+#endif
fail_thread:
psched_destroy(fa.psched);
fail_psched:
@@ -747,7 +816,7 @@ int fa_alloc(int fd,
const buffer_t * data)
{
struct fa_msg * msg;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
struct fa_flow * flow;
uint64_t addr;
qoscube_t qc = QOS_CUBE_BE;
@@ -760,10 +829,10 @@ int fa_alloc(int fd,
len = sizeof(*msg) + ipcp_dir_hash_len();
- if (ipcp_sdb_reserve(&sdb, len + data->len))
+ if (ipcp_spb_reserve(&spb, len + data->len))
return -1;
- msg = (struct fa_msg *) shm_du_buff_head(sdb);
+ msg = (struct fa_msg *) ssm_pk_buff_head(spb);
memset(msg, 0, sizeof(*msg));
eid = gen_eid(fd);
@@ -776,17 +845,17 @@ int fa_alloc(int fd,
msg->availability = qs.availability;
msg->loss = hton32(qs.loss);
msg->ber = hton32(qs.ber);
- msg->in_order = qs.in_order;
+ 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(shm_du_buff_head(sdb) + len, data->data, data->len);
+ memcpy(ssm_pk_buff_head(spb) + len, data->data, data->len);
- if (dt_write_packet(addr, qc, fa.eid, sdb)) {
+ if (dt_write_packet(addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow allocation request packet.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
return -1;
}
@@ -798,6 +867,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;
@@ -808,7 +884,7 @@ int fa_alloc_resp(int fd,
const buffer_t * data)
{
struct fa_msg * msg;
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
struct fa_flow * flow;
qoscube_t qc = QOS_CUBE_BE;
@@ -819,13 +895,13 @@ int fa_alloc_resp(int fd,
goto fail_alloc_resp;
}
- if (ipcp_sdb_reserve(&sdb, sizeof(*msg) + data->len)) {
- log_err("Failed to reserve sdb (%zu bytes).",
+ if (ipcp_spb_reserve(&spb, sizeof(*msg) + data->len)) {
+ log_err("Failed to reserve spb (%zu bytes).",
sizeof(*msg) + data->len);
goto fail_reserve;
}
- msg = (struct fa_msg *) shm_du_buff_head(sdb);
+ msg = (struct fa_msg *) ssm_pk_buff_head(spb);
memset(msg, 0, sizeof(*msg));
msg->code = FLOW_REPLY;
@@ -840,7 +916,7 @@ int fa_alloc_resp(int fd,
pthread_rwlock_unlock(&fa.flows_lock);
- if (dt_write_packet(flow->r_addr, qc, fa.eid, sdb)) {
+ if (dt_write_packet(flow->r_addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow allocation response packet.");
goto fail_packet;
}
@@ -856,7 +932,7 @@ int fa_alloc_resp(int fd,
return 0;
fail_packet:
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
fail_reserve:
pthread_rwlock_wrlock(&fa.flows_lock);
fa_flow_fini(flow);
@@ -865,6 +941,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)
@@ -884,20 +998,21 @@ 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 shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
qoscube_t qc = QOS_CUBE_BE;
struct fa_flow * flow;
uint64_t r_addr;
- if (ipcp_sdb_reserve(&sdb, sizeof(*msg))) {
- log_err("Failed to reserve sdb (%zu bytes).", sizeof(*msg));
+ if (ipcp_spb_reserve(&spb, sizeof(*msg))) {
+ log_err("Failed to reserve spb (%zu bytes).", sizeof(*msg));
return -1;
}
- msg = (struct fa_msg *) shm_du_buff_head(sdb);
+ msg = (struct fa_msg *) ssm_pk_buff_head(spb);
memset(msg, 0, sizeof(*msg));
@@ -908,6 +1023,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
@@ -916,9 +1032,9 @@ static int fa_update_remote(int fd,
pthread_rwlock_unlock(&fa.flows_lock);
- if (dt_write_packet(r_addr, qc, fa.eid, sdb)) {
+ if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow update packet.");
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
return -1;
}
@@ -927,15 +1043,17 @@ static int fa_update_remote(int fd,
void fa_np1_rcv(uint64_t eid,
uint8_t ecn,
- struct shm_du_buff * sdb)
+ 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;
- len = shm_du_buff_len(sdb);
+ len = ssm_pk_buff_len(spb);
pthread_rwlock_wrlock(&fa.flows_lock);
@@ -943,7 +1061,7 @@ void fa_np1_rcv(uint64_t eid,
if (fd < 0) {
pthread_rwlock_unlock(&fa.flows_lock);
log_dbg("Received packet for unknown EID %" PRIu64 ".", eid);
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
return;
}
@@ -953,13 +1071,13 @@ 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);
- if (ipcp_flow_write(fd, sdb) < 0) {
+ if (np1_flow_write(fd, spb, NP1_GET_POOL(fd)) < 0) {
log_dbg("Failed to write to flow %d.", fd);
- ipcp_sdb_release(sdb);
+ ipcp_spb_release(spb);
#ifdef IPCP_FLOW_STATS
pthread_rwlock_wrlock(&fa.flows_lock);
++flow->p_rcv_f;
@@ -969,5 +1087,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 1e716966..1d0012b0 100644
--- a/src/ipcpd/unicast/fa.h
+++ b/src/ipcpd/unicast/fa.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Flow allocator of the IPC Process
*
@@ -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,
- struct shm_du_buff * sdb);
+ 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 7989d3e1..1155b88b 100644
--- a/src/ipcpd/unicast/main.c
+++ b/src/ipcpd/unicast/main.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unicast IPC Process
*
@@ -273,7 +273,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,
@@ -307,8 +308,8 @@ int main(int argc,
ipcp_sigwait();
if (ipcp_get_state() == IPCP_SHUTDOWN) {
- stop_components();
ipcp_stop();
+ stop_components();
finalize_components();
} else {
ipcp_stop();
diff --git a/src/ipcpd/unicast/pff.c b/src/ipcpd/unicast/pff.c
index 9b2aa2b4..c8c3126f 100644
--- a/src/ipcpd/unicast/pff.c
+++ b/src/ipcpd/unicast/pff.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* PDU Forwarding Function
*
diff --git a/src/ipcpd/unicast/pff.h b/src/ipcpd/unicast/pff.h
index f44e5531..2eb42ce6 100644
--- a/src/ipcpd/unicast/pff.h
+++ b/src/ipcpd/unicast/pff.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* PDU Forwarding Function
*
diff --git a/src/ipcpd/unicast/pff/alternate.c b/src/ipcpd/unicast/pff/alternate.c
index 85e85914..1c508c1b 100644
--- a/src/ipcpd/unicast/pff/alternate.c
+++ b/src/ipcpd/unicast/pff/alternate.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for PFF with alternate next hops
*
@@ -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/alternate.h b/src/ipcpd/unicast/pff/alternate.h
index 96207e74..ae3758dc 100644
--- a/src/ipcpd/unicast/pff/alternate.h
+++ b/src/ipcpd/unicast/pff/alternate.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for PFF with alternate next hops
*
diff --git a/src/ipcpd/unicast/pff/multipath.c b/src/ipcpd/unicast/pff/multipath.c
index cbab0f5f..9ba59592 100644
--- a/src/ipcpd/unicast/pff/multipath.c
+++ b/src/ipcpd/unicast/pff/multipath.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for PFF supporting multipath routing
*
@@ -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;
diff --git a/src/ipcpd/unicast/pff/multipath.h b/src/ipcpd/unicast/pff/multipath.h
index 0eb03476..5329f7fc 100644
--- a/src/ipcpd/unicast/pff/multipath.h
+++ b/src/ipcpd/unicast/pff/multipath.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Policy for PFF supporting multipath routing
*
diff --git a/src/ipcpd/unicast/pff/ops.h b/src/ipcpd/unicast/pff/ops.h
index 16a31273..e4cabd11 100644
--- a/src/ipcpd/unicast/pff/ops.h
+++ b/src/ipcpd/unicast/pff/ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Pff policy ops
*
diff --git a/src/ipcpd/unicast/pff/pft.c b/src/ipcpd/unicast/pff/pft.c
index 8c436113..d0e562d6 100644
--- a/src/ipcpd/unicast/pff/pft.c
+++ b/src/ipcpd/unicast/pff/pft.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Packet forwarding table (PFT) with chaining on collisions
*
@@ -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));
@@ -113,22 +110,10 @@ void pft_flush(struct pft * pft)
}
}
-static uint64_t hash(uint64_t key)
-{
- uint64_t res[2];
-
- mem_hash(HASH_MD5, res, (uint8_t *) &key, sizeof(key));
-
- return res[0];
-}
-
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 711dabcb..15bbe451 100644
--- a/src/ipcpd/unicast/pff/pft.h
+++ b/src/ipcpd/unicast/pff/pft.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Packet forwarding table (PFT) with chaining on collisions
*
@@ -24,14 +24,12 @@
#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);
diff --git a/src/ipcpd/unicast/pff/pol.h b/src/ipcpd/unicast/pff/pol.h
index 245b03c4..e31c2794 100644
--- a/src/ipcpd/unicast/pff/pol.h
+++ b/src/ipcpd/unicast/pff/pol.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* PDU Forwarding Function policies
*
diff --git a/src/ipcpd/unicast/pff/simple.c b/src/ipcpd/unicast/pff/simple.c
index 5f95e3ce..7befa42f 100644
--- a/src/ipcpd/unicast/pff/simple.c
+++ b/src/ipcpd/unicast/pff/simple.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Simple PDU Forwarding Function
*
@@ -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);
diff --git a/src/ipcpd/unicast/pff/simple.h b/src/ipcpd/unicast/pff/simple.h
index 0966a186..1046e4c4 100644
--- a/src/ipcpd/unicast/pff/simple.h
+++ b/src/ipcpd/unicast/pff/simple.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Simple policy for PFF
*
diff --git a/src/ipcpd/unicast/pff/tests/CMakeLists.txt b/src/ipcpd/unicast/pff/tests/CMakeLists.txt
index 99c32e7a..8c0e3d51 100644
--- a/src/ipcpd/unicast/pff/tests/CMakeLists.txt
+++ b/src/ipcpd/unicast/pff/tests/CMakeLists.txt
@@ -3,36 +3,32 @@ get_filename_component(CURRENT_SOURCE_PARENT_DIR
get_filename_component(CURRENT_BINARY_PARENT_DIR
${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
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
pft_test.c
)
add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
-target_link_libraries(${PARENT_DIR}_test ouroboros-common)
-add_dependencies(check ${PARENT_DIR}_test)
+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)
-set(tests_to_run ${${PARENT_DIR}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
+add_dependencies(build_tests ${PARENT_DIR}_test)
-foreach (test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(${test_name} ${C_TEST_PATH}/${PARENT_DIR}_test ${test_name})
-endforeach (test)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/ipcpd/unicast/pff/tests/pft_test.c b/src/ipcpd/unicast/pff/tests/pft_test.c
index 18287fb8..20e73a94 100644
--- a/src/ipcpd/unicast/pff/tests/pft_test.c
+++ b/src/ipcpd/unicast/pff/tests/pft_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the hash table
*
@@ -38,15 +38,7 @@ int pft_test(int argc,
(void) argc;
(void) argv;
- pft = pft_create(TBL_SIZE, true);
- if (pft == NULL) {
- printf("Failed to create.\n");
- return -1;
- }
-
- pft_destroy(pft);
-
- pft = pft_create(TBL_SIZE, false);
+ pft = pft_create(TBL_SIZE);
if (pft == NULL) {
printf("Failed to create.\n");
return -1;
diff --git a/src/ipcpd/unicast/psched.c b/src/ipcpd/unicast/psched.c
index 7e12148b..2b535d67 100644
--- a/src/ipcpd/unicast/psched.c
+++ b/src/ipcpd/unicast/psched.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Packet scheduler component
*
@@ -30,6 +30,7 @@
#include <ouroboros/errno.h>
#include <ouroboros/notifier.h>
+#include <ouroboros/time.h>
#include "common/connmgr.h"
#include "ipcp.h"
@@ -41,14 +42,16 @@
#include <stdlib.h>
#include <string.h>
+#ifndef BUILD_CONTAINER
static int qos_prio [] = {
QOS_PRIO_BE,
QOS_PRIO_VIDEO,
QOS_PRIO_VOICE,
};
+#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];
@@ -57,23 +60,139 @@ 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;
+}
+
+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;
+ time_t wait;
+
+ for (i = 0; i < d->n; ) {
+ fd = d->active[i];
+
+ if (d->deadline[fd] > now) {
+ if (dmin == 0 || d->deadline[fd] < dmin)
+ dmin = d->deadline[fd];
+ ++i;
+ continue;
+ }
+
+ if (sched->read(fd, &spb) < 0) {
+ dsched_untrack(d, fd);
+ continue;
+ }
+
+ wait = sched->callback(fd, qc, spb);
+ if (wait == 0)
+ continue;
+
+ d->deadline[fd] = now + (uint64_t) wait;
+ if (dmin == 0 || d->deadline[fd] < dmin)
+ dmin = d->deadline[fd];
+ ++i;
+ }
+
+ 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 shm_du_buff * sdb;
- 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();
@@ -83,37 +202,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, &sdb) < 0)
- continue;
-
- sched->callback(fd, qc, sdb);
- break;
- default:
- break;
- }
- }
+ dsched_events(d, fq, now);
}
pthread_cleanup_pop(true);
+ pthread_cleanup_pop(true);
return (void *) 0;
}
@@ -135,7 +268,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)
@@ -153,6 +286,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) {
@@ -168,6 +302,7 @@ struct psched * psched_create(next_packet_fn_t callback,
}
}
+#ifndef BUILD_CONTAINER
for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) {
struct sched_param par;
int pol = SCHED_RR;
@@ -185,16 +320,20 @@ struct psched * psched_create(next_packet_fn_t callback,
if (pthread_setschedparam(psched->readers[i], pol, &par))
goto fail_sched;
}
+#endif
return psched;
+#ifndef BUILD_CONTAINER
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);
@@ -213,7 +352,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);
@@ -227,7 +366,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,
@@ -238,5 +377,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 831f8084..8c2914b3 100644
--- a/src/ipcpd/unicast/psched.h
+++ b/src/ipcpd/unicast/psched.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Packet scheduler component
*
@@ -26,12 +26,12 @@
#include <ouroboros/ipcp-dev.h>
#include <ouroboros/fqueue.h>
-typedef void (* next_packet_fn_t)(int fd,
- qoscube_t qc,
- struct shm_du_buff * sdb);
+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 shm_du_buff ** sdb);
+ struct ssm_pk_buff ** spb);
struct psched * psched_create(next_packet_fn_t callback,
read_fn_t read);
diff --git a/src/ipcpd/unicast/routing.c b/src/ipcpd/unicast/routing.c
index 2ad7b234..1a4e4372 100644
--- a/src/ipcpd/unicast/routing.c
+++ b/src/ipcpd/unicast/routing.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Routing component of the IPCP
*
diff --git a/src/ipcpd/unicast/routing.h b/src/ipcpd/unicast/routing.h
index e14960b5..8d721095 100644
--- a/src/ipcpd/unicast/routing.h
+++ b/src/ipcpd/unicast/routing.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Routing component of the IPCP
*
diff --git a/src/ipcpd/unicast/routing/graph.c b/src/ipcpd/unicast/routing/graph.c
index 32442dad..c168eb7d 100644
--- a/src/ipcpd/unicast/routing/graph.c
+++ b/src/ipcpd/unicast/routing/graph.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Undirected graph structure
*
@@ -57,10 +57,7 @@ struct edge {
};
struct graph {
- struct {
- struct list_head list;
- size_t len;
- } vertices;
+ struct llist vertices;
pthread_mutex_t lock;
};
@@ -88,7 +85,7 @@ static struct vertex * find_vertex_by_addr(struct graph * graph,
assert(graph);
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
struct vertex * e = list_entry(p, struct vertex, next);
if (e->addr == addr)
return e;
@@ -142,7 +139,7 @@ static struct vertex * add_vertex(struct graph * graph,
vertex->addr = addr;
/* Keep them ordered on address. */
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
struct vertex * v = list_entry(p, struct vertex, next);
if (v->addr > addr)
break;
@@ -151,7 +148,7 @@ static struct vertex * add_vertex(struct graph * graph,
vertex->index = i;
- list_add_tail(&vertex->next, p);
+ llist_add_tail_at(&vertex->next, p, &graph->vertices);
/* Increase the index of the vertices to the right. */
list_for_each(p, &vertex->next) {
@@ -160,37 +157,41 @@ static struct vertex * add_vertex(struct graph * graph,
v->index++;
}
- ++graph->vertices.len;
-
return vertex;
}
+static void free_edges(struct list_head * edges)
+{
+ struct list_head * p;
+ struct list_head * h;
+
+ list_for_each_safe(p, h, edges) {
+ struct edge * e = list_entry(p, struct edge, next);
+ list_del(&e->next);
+ free(e);
+ }
+}
+
static void del_vertex(struct graph * graph,
struct vertex * vertex)
{
struct list_head * p;
- struct list_head * h;
assert(graph != NULL);
assert(vertex != NULL);
- list_del(&vertex->next);
+ llist_del(&vertex->next, &graph->vertices);
/* Decrease the index of the vertices to the right. */
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
struct vertex * v = list_entry(p, struct vertex, next);
if (v->addr > vertex->addr)
v->index--;
}
- list_for_each_safe(p, h, &vertex->edges) {
- struct edge * e = list_entry(p, struct edge, next);
- del_edge(e);
- }
+ free_edges(&vertex->edges);
free(vertex);
-
- --graph->vertices.len;
}
struct graph * graph_create(void)
@@ -206,8 +207,7 @@ struct graph * graph_create(void)
return NULL;
}
- graph->vertices.len = 0;
- list_head_init(&graph->vertices.list);
+ llist_init(&graph->vertices);
return graph;
}
@@ -221,7 +221,7 @@ void graph_destroy(struct graph * graph)
pthread_mutex_lock(&graph->lock);
- list_for_each_safe(p, n, &graph->vertices.list) {
+ llist_for_each_safe(p, n, &graph->vertices) {
struct vertex * e = list_entry(p, struct vertex, next);
del_vertex(graph, e);
}
@@ -230,7 +230,7 @@ void graph_destroy(struct graph * graph)
pthread_mutex_destroy(&graph->lock);
- assert(graph->vertices.len == 0);
+ assert(llist_is_empty(&graph->vertices));
free(graph);
}
@@ -371,7 +371,7 @@ static int get_min_vertex(struct graph * graph,
*v = NULL;
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
if (!used[i] && dist[i] < min) {
min = dist[i];
index = i;
@@ -420,7 +420,7 @@ static int dijkstra(struct graph * graph,
memset(*nhops, 0, sizeof(**nhops) * graph->vertices.len);
memset(*dist, 0, sizeof(**dist) * graph->vertices.len);
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
v = list_entry(p, struct vertex, next);
(*dist)[i++] = (v->addr == src) ? 0 : INT_MAX;
}
@@ -526,7 +526,7 @@ static int graph_routing_table_simple(struct graph * graph,
list_head_init(table);
/* Now construct the routing table from the nhops. */
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
v = list_entry(p, struct vertex, next);
/* This is the src */
@@ -603,9 +603,9 @@ static int graph_routing_table_lfa(struct graph * graph,
struct list_head * table,
int ** dist)
{
- int * n_dist[PROG_MAX_FLOWS];
- uint64_t addrs[PROG_MAX_FLOWS];
- int n_index[PROG_MAX_FLOWS];
+ int * n_dist[PROC_MAX_FLOWS];
+ uint64_t addrs[PROC_MAX_FLOWS];
+ int n_index[PROC_MAX_FLOWS];
struct list_head * p;
struct list_head * q;
struct vertex * v;
@@ -618,13 +618,13 @@ static int graph_routing_table_lfa(struct graph * graph,
if (graph_routing_table_simple(graph, s_addr, table, dist))
goto fail_table;
- for (j = 0; j < PROG_MAX_FLOWS; j++) {
+ for (j = 0; j < PROC_MAX_FLOWS; j++) {
n_dist[j] = NULL;
n_index[j] = -1;
addrs[j] = -1;
}
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
v = list_entry(p, struct vertex, next);
if (v->addr != s_addr)
@@ -650,7 +650,7 @@ static int graph_routing_table_lfa(struct graph * graph,
}
/* Loop though all nodes to see if we have a LFA for them. */
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
v = list_entry(p, struct vertex, next);
if (v->addr == s_addr)
@@ -695,7 +695,6 @@ static int graph_routing_table_ecmp(struct graph * graph,
{
struct vertex ** nhops;
struct list_head * p;
- struct list_head * h;
size_t i;
struct vertex * v;
struct vertex * src_v;
@@ -735,16 +734,15 @@ static int graph_routing_table_ecmp(struct graph * graph,
free(nhops);
- list_for_each(h, &graph->vertices.list) {
- v = list_entry(h, struct vertex, next);
- if (tmp_dist[v->index] + 1 == (*dist)[v->index]) {
+ for (i = 0; i < graph->vertices.len; ++i) {
+ if (tmp_dist[i] + 1 == (*dist)[i]) {
n = malloc(sizeof(*n));
if (n == NULL) {
free(tmp_dist);
goto fail_src_v;
}
n->nhop = e->nb->addr;
- list_add_tail(&n->next, &forwarding[v->index]);
+ list_add_tail(&n->next, &forwarding[i]);
}
}
@@ -753,38 +751,34 @@ static int graph_routing_table_ecmp(struct graph * graph,
list_head_init(table);
i = 0;
- list_for_each(p, &graph->vertices.list) {
+ llist_for_each(p, &graph->vertices) {
v = list_entry(p, struct vertex, next);
- if (v->addr == s_addr) {
+ if (v->addr == s_addr || list_is_empty(&forwarding[i])) {
++i;
continue;
}
t = malloc(sizeof(*t));
if (t == NULL)
- goto fail_t;
+ goto fail_malloc;
t->dst = v->addr;
list_head_init(&t->nhops);
- if (&forwarding[i] != forwarding[i].nxt) {
- t->nhops.nxt = forwarding[i].nxt;
- t->nhops.prv = forwarding[i].prv;
- forwarding[i].prv->nxt = &t->nhops;
- forwarding[i].nxt->prv = &t->nhops;
- }
+ t->nhops.nxt = forwarding[i].nxt;
+ t->nhops.prv = forwarding[i].prv;
+ forwarding[i].prv->nxt = &t->nhops;
+ forwarding[i].nxt->prv = &t->nhops;
list_add(&t->next, table);
++i;
}
- free(*dist);
- *dist = NULL;
free(forwarding);
return 0;
- fail_t:
+ fail_malloc:
free_routing_table(table);
fail_src_v:
free(*dist);
diff --git a/src/ipcpd/unicast/routing/graph.h b/src/ipcpd/unicast/routing/graph.h
index 8190cc6c..f3766771 100644
--- a/src/ipcpd/unicast/routing/graph.h
+++ b/src/ipcpd/unicast/routing/graph.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Undirected graph structure
*
diff --git a/src/ipcpd/unicast/routing/link-state.c b/src/ipcpd/unicast/routing/link-state.c
index e5edf539..c4ea9e1c 100644
--- a/src/ipcpd/unicast/routing/link-state.c
+++ b/src/ipcpd/unicast/routing/link-state.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Link state routing policy
*
@@ -56,7 +56,7 @@
#include <string.h>
#define LS_ENTRY_SIZE 104
-#define LSDB "lsdb"
+#define lsdb "lsdb"
#ifndef CLOCK_REALTIME_COARSE
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
@@ -121,16 +121,8 @@ struct {
struct graph * graph;
struct {
- struct {
- struct list_head list;
- size_t len;
- } nbs;
-
- struct {
- struct list_head list;
- size_t len;
- } db;
-
+ struct llist nbs;
+ struct llist db;
pthread_rwlock_t lock;
};
@@ -189,7 +181,7 @@ static struct adjacency * get_adj(const char * path)
assert(path);
- list_for_each(p, &ls.db.list) {
+ llist_for_each(p, &ls.db) {
struct adjacency * a = list_entry(p, struct adjacency, next);
sprintf(entry, LINK_FMT, LINK_VAL(a->src, a->dst));
if (strcmp(entry, path) == 0)
@@ -245,7 +237,7 @@ static int lsdb_rib_read(const char * path,
pthread_rwlock_rdlock(&ls.lock);
- if (ls.db.len + ls.nbs.len == 0)
+ if (llist_is_empty(&ls.db) && llist_is_empty(&ls.nbs))
goto fail;
a = get_adj(entry);
@@ -274,7 +266,7 @@ static int lsdb_rib_readdir(char *** buf)
pthread_rwlock_rdlock(&ls.lock);
- if (ls.db.len + ls.nbs.len == 0) {
+ if (llist_is_empty(&ls.db) && llist_is_empty(&ls.nbs)) {
*buf = NULL;
goto no_entries;
}
@@ -284,7 +276,7 @@ static int lsdb_rib_readdir(char *** buf)
if (*buf == NULL)
goto fail_entries;
- list_for_each(p, &ls.nbs.list) {
+ llist_for_each(p, &ls.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
char * str = (nb->type == NB_DT ? ".dt " : ".mgmt ");
sprintf(entry, "%s" ADDR_FMT32 , str, ADDR_VAL32(&nb->addr));
@@ -295,7 +287,7 @@ static int lsdb_rib_readdir(char *** buf)
strcpy((*buf)[idx++], entry);
}
- list_for_each(p, &ls.db.list) {
+ llist_for_each(p, &ls.db) {
struct adjacency * a = list_entry(p, struct adjacency, next);
sprintf(entry, LINK_FMT, LINK_VAL(a->src, a->dst));
(*buf)[idx] = malloc(strlen(entry) + 1);
@@ -333,7 +325,7 @@ static int lsdb_add_nb(uint64_t addr,
pthread_rwlock_wrlock(&ls.lock);
- list_for_each(p, &ls.nbs.list) {
+ llist_for_each(p, &ls.nbs) {
struct nb * el = list_entry(p, struct nb, next);
if (addr > el->addr)
break;
@@ -360,9 +352,7 @@ static int lsdb_add_nb(uint64_t addr,
nb->fd = fd;
nb->type = type;
- list_add_tail(&nb->next, p);
-
- ++ls.nbs.len;
+ llist_add_tail_at(&nb->next, p, &ls.nbs);
log_dbg("Type %s neighbor " ADDR_FMT32 " added.",
nb->type == NB_DT ? "dt" : "mgmt", ADDR_VAL32(&addr));
@@ -380,13 +370,12 @@ static int lsdb_del_nb(uint64_t addr,
pthread_rwlock_wrlock(&ls.lock);
- list_for_each_safe(p, h, &ls.nbs.list) {
+ llist_for_each_safe(p, h, &ls.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->addr != addr || nb->fd != fd)
continue;
- list_del(&nb->next);
- --ls.nbs.len;
+ llist_del(&nb->next, &ls.nbs);
pthread_rwlock_unlock(&ls.lock);
log_dbg("Type %s neighbor " ADDR_FMT32 " deleted.",
nb->type == NB_DT ? "dt" : "mgmt", ADDR_VAL32(&addr));
@@ -406,7 +395,7 @@ static int nbr_to_fd(uint64_t addr)
pthread_rwlock_rdlock(&ls.lock);
- list_for_each(p, &ls.nbs.list) {
+ llist_for_each(p, &ls.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->addr == addr && nb->type == NB_DT) {
fd = nb->fd;
@@ -426,7 +415,7 @@ static void calculate_pff(struct routing_i * instance)
struct list_head table;
struct list_head * p;
struct list_head * q;
- int fds[PROG_MAX_FLOWS];
+ int fds[PROC_MAX_FLOWS];
assert(instance);
@@ -494,7 +483,7 @@ static int lsdb_add_link(uint64_t src,
pthread_rwlock_wrlock(&ls.lock);
- list_for_each(p, &ls.db.list) {
+ llist_for_each(p, &ls.db) {
struct adjacency * a = list_entry(p, struct adjacency, next);
if (a->dst == dst && a->src == src) {
if (a->seqno < seqno) {
@@ -521,9 +510,7 @@ static int lsdb_add_link(uint64_t src,
adj->seqno = seqno;
adj->stamp = now.tv_sec;
- list_add_tail(&adj->next, p);
-
- ls.db.len++;
+ llist_add_tail_at(&adj->next, p, &ls.db);
if (graph_update_edge(ls.graph, src, dst, *qs))
log_warn("Failed to add edge to graph.");
@@ -543,15 +530,13 @@ static int lsdb_del_link(uint64_t src,
pthread_rwlock_wrlock(&ls.lock);
- list_for_each_safe(p, h, &ls.db.list) {
+ llist_for_each_safe(p, h, &ls.db) {
struct adjacency * a = list_entry(p, struct adjacency, next);
if (a->dst == dst && a->src == src) {
- list_del(&a->next);
+ llist_del(&a->next, &ls.db);
if (graph_del_edge(ls.graph, src, dst))
log_warn("Failed to delete edge from graph.");
- ls.db.len--;
-
pthread_rwlock_unlock(&ls.lock);
set_pff_modified(false);
free(a);
@@ -599,7 +584,7 @@ static void send_lsm(uint64_t src,
lsm.s_addr = hton64(src);
lsm.seqno = hton64(seqno);
- list_for_each(p, &ls.nbs.list) {
+ llist_for_each(p, &ls.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->type != NB_MGMT)
continue;
@@ -628,7 +613,7 @@ static void lsdb_replicate(int fd)
/* Lock the lsdb, copy the lsms and send outside of lock. */
pthread_rwlock_rdlock(&ls.lock);
- list_for_each(p, &ls.db.list) {
+ llist_for_each(p, &ls.db) {
struct adjacency * adj;
struct adjacency * cpy;
adj = list_entry(p, struct adjacency, next);
@@ -675,11 +660,11 @@ static void * lsupdate(void * o)
pthread_cleanup_push(__cleanup_rwlock_unlock, &ls.lock);
- list_for_each_safe(p, h, &ls.db.list) {
+ llist_for_each_safe(p, h, &ls.db) {
struct adjacency * adj;
adj = list_entry(p, struct adjacency, next);
if (now.tv_sec > adj->stamp + ls.conf.t_timeo) {
- list_del(&adj->next);
+ llist_del(&adj->next, &ls.db);
log_dbg(LINK_FMT " timed out.",
LINK_VAL(adj->src, adj->dst));
if (graph_del_edge(ls.graph, adj->src,
@@ -746,7 +731,7 @@ static void forward_lsm(uint8_t * buf,
pthread_cleanup_push(__cleanup_rwlock_unlock, &ls.lock);
- list_for_each(p, &ls.nbs.list) {
+ llist_for_each(p, &ls.nbs) {
struct nb * nb = list_entry(p, struct nb, next);
if (nb->type != NB_MGMT || nb->fd == in_fd)
continue;
@@ -874,19 +859,19 @@ static void handle_event(void * self,
pthread_cleanup_pop(true);
if (lsdb_add_nb(c->conn_info.addr, c->flow_info.fd, NB_DT))
- log_dbg("Failed to add neighbor to LSDB.");
+ log_dbg("Failed to add neighbor to lsdb.");
if (lsdb_add_link(ls.addr, c->conn_info.addr, 0, &qs))
- log_dbg("Failed to add new adjacency to LSDB.");
+ log_dbg("Failed to add new adjacency to lsdb.");
break;
case NOTIFY_DT_CONN_DEL:
flow_event(c->flow_info.fd, false);
if (lsdb_del_nb(c->conn_info.addr, c->flow_info.fd))
- log_dbg("Failed to delete neighbor from LSDB.");
+ log_dbg("Failed to delete neighbor from lsdb.");
if (lsdb_del_link(ls.addr, c->conn_info.addr))
- log_dbg("Local link was not in LSDB.");
+ log_dbg("Local link was not in lsdb.");
break;
case NOTIFY_DT_CONN_QOS:
log_dbg("QoS changes currently unsupported.");
@@ -902,14 +887,14 @@ static void handle_event(void * self,
fccntl(c->flow_info.fd, FLOWSFLAGS, flags | FLOWFRNOPART);
fset_add(ls.mgmt_set, c->flow_info.fd);
if (lsdb_add_nb(c->conn_info.addr, c->flow_info.fd, NB_MGMT))
- log_warn("Failed to add mgmt neighbor to LSDB.");
+ log_warn("Failed to add mgmt neighbor to lsdb.");
/* replicate the entire lsdb */
lsdb_replicate(c->flow_info.fd);
break;
case NOTIFY_MGMT_CONN_DEL:
fset_del(ls.mgmt_set, c->flow_info.fd);
if (lsdb_del_nb(c->conn_info.addr, c->flow_info.fd))
- log_warn("Failed to delete mgmt neighbor from LSDB.");
+ log_warn("Failed to delete mgmt neighbor from lsdb.");
break;
default:
break;
@@ -1090,16 +1075,13 @@ int link_state_init(struct ls_config * conf,
goto fail_fset_create;
}
- list_head_init(&ls.db.list);
- list_head_init(&ls.nbs.list);
+ llist_init(&ls.db);
+ llist_init(&ls.nbs);
list_head_init(&ls.instances.list);
- if (rib_reg(LSDB, &r_ops))
+ if (rib_reg(lsdb, &r_ops))
goto fail_rib_reg;
- ls.db.len = 0;
- ls.nbs.len = 0;
-
return 0;
fail_rib_reg:
@@ -1121,7 +1103,7 @@ void link_state_fini(void)
struct list_head * p;
struct list_head * h;
- rib_unreg(LSDB);
+ rib_unreg(lsdb);
fset_destroy(ls.mgmt_set);
@@ -1131,9 +1113,9 @@ void link_state_fini(void)
pthread_rwlock_wrlock(&ls.lock);
- list_for_each_safe(p, h, &ls.db.list) {
+ llist_for_each_safe(p, h, &ls.db) {
struct adjacency * a = list_entry(p, struct adjacency, next);
- list_del(&a->next);
+ llist_del(&a->next, &ls.db);
free(a);
}
diff --git a/src/ipcpd/unicast/routing/link-state.h b/src/ipcpd/unicast/routing/link-state.h
index 69eb6781..38e19065 100644
--- a/src/ipcpd/unicast/routing/link-state.h
+++ b/src/ipcpd/unicast/routing/link-state.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Link state routing policy
*
diff --git a/src/ipcpd/unicast/routing/ops.h b/src/ipcpd/unicast/routing/ops.h
index 4bf75c80..b19c5176 100644
--- a/src/ipcpd/unicast/routing/ops.h
+++ b/src/ipcpd/unicast/routing/ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Routing policy ops
*
diff --git a/src/ipcpd/unicast/routing/pol.h b/src/ipcpd/unicast/routing/pol.h
index b6a6f150..545f5df2 100644
--- a/src/ipcpd/unicast/routing/pol.h
+++ b/src/ipcpd/unicast/routing/pol.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Routing policies
*
diff --git a/src/ipcpd/unicast/routing/tests/CMakeLists.txt b/src/ipcpd/unicast/routing/tests/CMakeLists.txt
index b5011474..be2de72c 100644
--- a/src/ipcpd/unicast/routing/tests/CMakeLists.txt
+++ b/src/ipcpd/unicast/routing/tests/CMakeLists.txt
@@ -3,36 +3,32 @@ get_filename_component(CURRENT_SOURCE_PARENT_DIR
get_filename_component(CURRENT_BINARY_PARENT_DIR
${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-include_directories(${CURRENT_BINARY_PARENT_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
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
graph_test.c
)
add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
-target_link_libraries(${PARENT_DIR}_test ouroboros-common)
-add_dependencies(check ${PARENT_DIR}_test)
+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)
-set(tests_to_run ${${PARENT_DIR}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
+add_dependencies(build_tests ${PARENT_DIR}_test)
-foreach (test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(${test_name} ${C_TEST_PATH}/${PARENT_DIR}_test ${test_name})
-endforeach (test)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/ipcpd/unicast/routing/tests/graph_test.c b/src/ipcpd/unicast/routing/tests/graph_test.c
index d805640c..40a744ff 100644
--- a/src/ipcpd/unicast/routing/tests/graph_test.c
+++ b/src/ipcpd/unicast/routing/tests/graph_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the graph structure
*
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..7867b490
--- /dev/null
+++ b/src/ipcpd/unicast/tests/cap_test.c
@@ -0,0 +1,593 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests for link capacity estimation
+ *
+ * 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>
+
+#define TICK (50 * 1000ULL) /* 50 us between packets */
+#define LEN 1000ULL /* default packet size (B) */
+#define QLEN 8 /* steady ring backlog */
+#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))
+
+static int test_cap_init_fini(void)
+{
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ if (cap_get(0) != 0 || cap_get(PROC_MAX_FLOWS - 1) != 0) {
+ printf("Fresh estimator not unknown.\n");
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* 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;
+}
+
+static int test_cap_est_busy_window(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ /* 1000 B every 50 us, ring steady at 8: drain = 20 MB/s. */
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, QLEN, LEN, i * TICK);
+
+ if (cap_get(0) != cap_enc(RATE)) {
+ printf("Estimated code: exp %u, got %u.\n",
+ cap_enc(RATE), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_idle_tolerated(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, i == 21 ? 0 : QLEN, LEN, i * TICK);
+
+ if (cap_get(0) != cap_enc(RATE)) {
+ printf("Grazed window: exp %u, got %u.\n",
+ cap_enc(RATE), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_mostly_idle_rejects(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 100; i++)
+ cap_update_at(0, 0, LEN, i * TICK);
+
+ if (cap_get(0) != 0) {
+ printf("Idle ring estimated %u.\n", cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_slow_link_extends(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ /* 1000 B every 100 us: 10 slots/ms closes on a 2 ms window. */
+ for (i = 1; i <= 30; i++)
+ cap_update_at(0, QLEN, LEN, i * 2 * TICK);
+
+ if (cap_get(0) != cap_enc(RATE / 2)) {
+ printf("Slow link: exp %u, got %u.\n",
+ cap_enc(RATE / 2), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_shaped_link(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ /* 1250 B every ms; one empty observation per 20 packets. */
+ for (i = 1; i <= 100; i++)
+ cap_update_at(0, i % SHP_STEP == 0 ? 0 : 6, SHP_LEN,
+ i * SHP_STEP * TICK);
+
+ if (cap_get(0) != cap_enc(SHP_RATE)) {
+ printf("Shaped link: exp %u, got %u.\n",
+ cap_enc(SHP_RATE), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_stale_discard(void)
+{
+ uint64_t t;
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ /* Open a window, trickle 4 slots, then ~200 ms of silence. */
+ for (i = 1; i <= 5; i++)
+ cap_update_at(0, QLEN, LEN, i * CAP_T_MIN);
+
+ t = 205 * CAP_T_MIN;
+
+ cap_update_at(0, QLEN, LEN, t);
+
+ if (cap_get(0) != 0) {
+ printf("Gap window estimated %u.\n", cap_get(0));
+ goto fail_init;
+ }
+
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, QLEN, LEN, t + i * TICK);
+
+ if (cap_get(0) != cap_enc(RATE)) {
+ printf("Post-gap: exp %u, got %u.\n",
+ cap_enc(RATE), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_est_empty_start_no_raise(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ cap_update_at(0, 0, LEN, CAP_T_MIN);
+
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, QLEN, LEN, CAP_T_MIN + i * TICK);
+
+ if (cap_get(0) != 0) {
+ printf("Empty-start window raised to %u.\n",
+ cap_get(0));
+ goto fail_init;
+ }
+
+ for (i = 41; i <= 60; i++)
+ cap_update_at(0, QLEN, LEN, CAP_T_MIN + i * TICK);
+
+ if (cap_get(0) != cap_enc(RATE)) {
+ printf("Backlogged window: exp %u, got %u.\n",
+ cap_enc(RATE), cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Max filter: fast attack on a high sample, slow release on lower. */
+static int test_cap_est_max_filter(void)
+{
+ uint8_t high;
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, QLEN, LEN, i * TICK);
+
+ high = cap_get(0);
+ if (high != cap_enc(RATE)) {
+ printf("Attack missed: exp %u, got %u.\n", cap_enc(RATE),
+ high);
+ goto fail_init;
+ }
+
+ /* Halved packet size: valid samples at 10 MB/s. */
+ for (i = 41; i <= 80; i++)
+ cap_update_at(0, QLEN, LEN / 2, i * TICK);
+
+ if (cap_get(0) >= high) {
+ printf("Release did not decay: %u.\n", cap_get(0));
+ goto fail_init;
+ }
+
+ if (cap_get(0) <= cap_enc(RATE / 2)) {
+ printf("Release collapsed to %u.\n", cap_get(0));
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* No fold within CAP_T_MIN of the previous one. */
+static int test_cap_est_gate(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ cap_update_at(0, QLEN, LEN, CAP_T_MIN);
+
+ for (i = 0; i < 5; i++)
+ cap_update_at(0, QLEN, LEN, CAP_T_MIN + CAP_T_MIN / 2);
+
+ if (cap.est[0].t_gate != CAP_T_MIN) {
+ printf("Fold ran inside the gate.\n");
+ goto fail_init;
+ }
+
+ if (LOAD_RELAXED(&cap.est[0].c_pkt) != 6) {
+ printf("Gated packets not counted.\n");
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_reset(void)
+{
+ size_t i;
+
+ TEST_START();
+
+ if (cap_init() < 0) {
+ printf("Failed to init cap.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 40; i++)
+ cap_update_at(0, QLEN, LEN, i * TICK);
+
+ if (cap_get(0) == 0) {
+ printf("No estimate to reset.\n");
+ goto fail_init;
+ }
+
+ cap_reset(0);
+
+ if (cap_get(0) != 0) {
+ printf("Reset did not clear the estimate.\n");
+ goto fail_init;
+ }
+
+ cap_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ cap_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int cap_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_cap_init_fini();
+ ret |= test_cap_codec_roundtrip();
+ ret |= test_cap_codec_bounds();
+ ret |= test_cap_min();
+ ret |= test_cap_stamp();
+ 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_reset();
+
+ return ret;
+}
diff --git a/src/irmd/CMakeLists.txt b/src/irmd/CMakeLists.txt
index 32904d59..5aa457ff 100644
--- a/src/irmd/CMakeLists.txt
+++ b/src/irmd/CMakeLists.txt
@@ -1,114 +1,67 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
+# IRMd (IPC Resource Manager daemon) build configuration
+# Configuration options are in cmake/config/global.cmake and cmake/config/irmd.cmake
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(OUROBOROS_CONFIG_DIR /etc/ouroboros CACHE STRING
- "Configuration directory")
-
-find_library(LIBTOML_LIBRARIES toml QUIET)
-if (LIBTOML_LIBRARIES)
- set(DISABLE_CONFIGFILE FALSE CACHE BOOL
- "Disable configuration file support")
- if (NOT DISABLE_CONFIGFILE)
- set(OUROBOROS_CONFIG_FILE irmd.conf CACHE STRING
- "Name of the IRMd configuration file")
- set(HAVE_TOML TRUE)
- message(STATUS "Found TOML C99 library: " ${LIBTOML_LIBRARIES})
- message(STATUS "Configuration file support enabled")
- message(STATUS "Configuration directory: ${OUROBOROS_CONFIG_DIR}")
- set(INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}")
- configure_file("${CMAKE_SOURCE_DIR}/irmd.conf.in"
- "${CMAKE_BINARY_DIR}/irmd.conf.example" @ONLY)
- install(FILES "${CMAKE_BINARY_DIR}/irmd.conf.example"
- DESTINATION "${OUROBOROS_CONFIG_DIR}")
- unset(INSTALL_DIR)
- find_path(LIBTOML_INCLUDE toml.h)
- mark_as_advanced(LIBTOML_LIBRARIES LIBTOML_INCLUDE)
- else ()
- message(STATUS "Configuration file support disabled by user")
- unset(OUROBOROS_CONFIG_FILE CACHE)
- set(HAVE_TOML FALSE)
- endif ()
-else ()
- message(STATUS "Install tomlc99 for config file support")
- message(STATUS " https://github.com/cktan/tomlc99")
- set(LIBTOML_LIBRARIES "")
- unset(DISABLE_CONFIGFILE CACHE)
- unset(HAVE_TOML)
-endif ()
-
-set(OUROBOROS_SECURITY_DIR "${OUROBOROS_CONFIG_DIR}/security" CACHE STRING
- "Security directory holding authentication information")
-set(OUROBOROS_CA_CRT_DIR "${OUROBOROS_SECURITY_DIR}/cacert" CACHE STRING
- "Directory holding trusted CA certificates")
-set(OUROBOROS_SRV_CRT_DIR "${OUROBOROS_SECURITY_DIR}/server" CACHE STRING
- "Directory holding trusted CA certificates")
-set(OUROBOROS_CLI_CRT_DIR "${OUROBOROS_SECURITY_DIR}/client" CACHE STRING
- "Directory holding trusted CA certificates")
-set(OUROBOROS_UNTRUSTED_DIR "${OUROBOROS_SECURITY_DIR}/untrusted" CACHE STRING
- "Directory holding untrusted intermediate certificates")
-
-set(IRMD_REQ_ARR_TIMEOUT 1000 CACHE STRING
- "Timeout for an application to respond to a new flow (ms)")
-set(BOOTSTRAP_TIMEOUT 5000 CACHE STRING
- "Timeout for an IPCP to bootstrap (ms)")
-set(ENROLL_TIMEOUT 20000 CACHE STRING
- "Timeout for an IPCP to enroll (ms)")
-set(REG_TIMEOUT 20000 CACHE STRING
- "Timeout for registering a name (ms)")
-set(QUERY_TIMEOUT 200 CACHE STRING
- "Timeout to query a name with an IPCP (ms)")
-set(CONNECT_TIMEOUT 20000 CACHE STRING
- "Timeout to connect an IPCP to another IPCP (ms)")
-set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
- "Timeout for a flow allocation response (ms)")
-set(IRMD_MIN_THREADS 8 CACHE STRING
- "Minimum number of worker threads in the IRMd")
-set(IRMD_ADD_THREADS 8 CACHE STRING
- "Number of extra threads to start when the IRMD faces thread starvation")
-set(IRMD_PKILL_TIMEOUT 30 CACHE STRING
- "Number of seconds to wait before sending SIGKILL to subprocesses on exit")
-set(IRMD_KILL_ALL_PROCESSES TRUE CACHE BOOL
- "Kill all processes on exit")
-set(DEBUG_PROTO_OAP FALSE CACHE BOOL
- "Add Flow allocation protocol message output to IRMd debug logging")
+# Generate and install configuration files if TOML support available
+# HAVE_TOML is set in cmake/dependencies/irmd/libtoml.cmake
+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}/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}/sec.conf.example"
+ DESTINATION "${OUROBOROS_CONFIG_DIR}")
+ install(CODE "
+ if(NOT EXISTS \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\")
+ file(WRITE \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\" \"\")
+ endif()
+ ")
+ unset(INSTALL_DIR)
+endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
-set(SOURCE_FILES
- # Add source files here
+set(IRMD_SOURCES
ipcp.c
configfile.c
main.c
- oap.c
+ oap/io.c
+ oap/hdr.c
+ oap/auth.c
+ oap/srv.c
+ oap/cli.c
reg/flow.c
reg/ipcp.c
+ reg/pool.c
reg/proc.c
reg/prog.c
reg/name.c
reg/reg.c
- )
+)
+
+add_executable(irmd ${IRMD_SOURCES})
-add_executable (irmd ${SOURCE_FILES})
+add_dependencies(irmd version_header)
-target_link_libraries (irmd LINK_PUBLIC ouroboros-common
- ${LIBTOML_LIBRARIES})
+target_include_directories(irmd PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include)
-if (HAVE_TOML)
- target_include_directories(irmd PUBLIC ${LIBTOML_INCLUDE})
-endif ()
+target_link_libraries(irmd PRIVATE ouroboros-common)
+if(HAVE_TOML)
+ target_link_libraries(irmd PRIVATE toml::toml)
+endif()
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(irmd -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+ouroboros_target_debug_definitions(irmd)
install(TARGETS irmd RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
-add_subdirectory(reg)
if(BUILD_TESTS)
- add_subdirectory(tests)
-endif ()
+ add_subdirectory(oap/tests)
+ add_subdirectory(reg/tests)
+endif()
diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in
index 527694c0..53f80ca4 100644
--- a/src/irmd/config.h.in
+++ b/src/irmd/config.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Configuration for the IPC Resource Manager
*
@@ -41,6 +41,11 @@
#define FLOW_ALLOC_TIMEOUT @FLOW_ALLOC_TIMEOUT@
#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@
#define REG_TIMEOUT @REG_TIMEOUT@
@@ -51,6 +56,8 @@
#define IRMD_MIN_THREADS @IRMD_MIN_THREADS@
#define IRMD_ADD_THREADS @IRMD_ADD_THREADS@
+#define SSM_PID_GSPP 0
+
#cmakedefine HAVE_FUSE
#ifdef HAVE_FUSE
#define FUSE_PREFIX "@FUSE_PREFIX@"
@@ -70,9 +77,14 @@
#define IRMD_PKILL_TIMEOUT @IRMD_PKILL_TIMEOUT@
+#cmakedefine DISABLE_DIRECT_IPC
#cmakedefine IRMD_KILL_ALL_PROCESSES
#cmakedefine HAVE_LIBGCRYPT
#cmakedefine HAVE_OPENSSL
+#ifdef HAVE_OPENSSL
+#cmakedefine HAVE_ML
+#endif
+#define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@
#ifdef CONFIG_OUROBOROS_DEBUG
#cmakedefine DEBUG_PROTO_OAP
#endif
diff --git a/src/irmd/configfile.c b/src/irmd/configfile.c
index ce9fc8fc..5967e490 100644
--- a/src/irmd/configfile.c
+++ b/src/irmd/configfile.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager / Configuration from file
*
@@ -457,7 +457,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;
@@ -922,10 +922,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 +957,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 +969,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");
diff --git a/src/irmd/configfile.h b/src/irmd/configfile.h
index 3ccf53fd..62f522a4 100644
--- a/src/irmd/configfile.h
+++ b/src/irmd/configfile.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager / Configuration from file
*
diff --git a/src/irmd/ipcp.c b/src/irmd/ipcp.c
index 6226aeda..7eccfc80 100644
--- a/src/irmd/ipcp.c
+++ b/src/irmd/ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API to instruct IPCPs
*
@@ -421,6 +421,8 @@ int ipcp_flow_join(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.has_hash = true;
msg.hash.data = (uint8_t *) dst.data;
msg.hash.len = dst.len;
@@ -442,6 +444,38 @@ 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;
+
+ 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)
@@ -455,6 +489,8 @@ int ipcp_flow_alloc(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.qosspec = qos_spec_s_to_msg(&flow->qs);
msg.has_hash = true;
msg.hash.data = (uint8_t *) dst.data;
@@ -495,6 +531,8 @@ int ipcp_flow_alloc_resp(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.has_response = true;
msg.response = response;
msg.has_pk = response == 0;
diff --git a/src/irmd/ipcp.h b/src/irmd/ipcp.h
index b7413cd2..8d06623c 100644
--- a/src/irmd/ipcp.h
+++ b/src/irmd/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API for the IRM to instruct IPCPs
*
@@ -68,4 +68,7 @@ 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 3e54904a..f88378ad 100644
--- a/src/irmd/irmd.h
+++ b/src/irmd/irmd.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager
*
diff --git a/src/irmd/main.c b/src/irmd/main.c
index daaf4129..19be4ab9 100644
--- a/src/irmd/main.c
+++ b/src/irmd/main.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager
*
@@ -22,8 +22,10 @@
#if defined(__linux__) || defined(__CYGWIN__)
#define _DEFAULT_SOURCE
+#define _GNU_SOURCE
#else
-#define _POSIX_C_SOURCE 200809L
+#define _DEFAULT_SOURCE
+#define _BSD_SOURCE
#endif
#include "config.h"
@@ -34,15 +36,17 @@
#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>
#include <ouroboros/lockfile.h>
#include <ouroboros/logs.h>
+#include <ouroboros/protobuf.h>
#include <ouroboros/pthread.h>
#include <ouroboros/random.h>
#include <ouroboros/rib.h>
-#include <ouroboros/shm_rdrbuff.h>
+#include <ouroboros/ssm_pool.h>
#include <ouroboros/sockets.h>
#include <ouroboros/time.h>
#include <ouroboros/tpm.h>
@@ -56,15 +60,21 @@
#include "configfile.h"
#include <dirent.h>
-#include <sys/socket.h>
-#include <sys/un.h>
+#include <grp.h>
+#include <pwd.h>
#include <signal.h>
+#include <spawn.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
+#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
-#include <spawn.h>
+#include <sys/un.h>
+#ifdef __APPLE__
+#include <sys/types.h>
+#include <unistd.h>
+#endif
#ifdef HAVE_LIBGCRYPT
#include <gcrypt.h>
@@ -77,6 +87,11 @@
#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
enum irm_state {
IRMD_NULL = 0,
@@ -85,13 +100,6 @@ enum irm_state {
IRMD_SHUTDOWN
};
-struct oaph {
- struct list_head next;
-
- uint64_t stamp;
- uint8_t id[OAP_ID_SIZE];
-};
-
struct cmd {
struct list_head next;
@@ -100,19 +108,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 {
- struct auth_ctx * ctx; /* default authentication ctx */
- struct list_head list; /* OAP headers seen before */
- pthread_mutex_t mtx; /* mutex for OAP headers */
- } auth;
-
struct lockfile * lf; /* single irmd per system */
- struct shm_rdrbuff * rdrb; /* rdrbuff for packets */
+ struct ssm_pool * gspp; /* pool for packets */
int sockfd; /* UNIX socket */
@@ -127,6 +154,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)
@@ -455,8 +489,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.cfg", 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);
@@ -464,8 +498,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.cfg", 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);
@@ -613,18 +647,21 @@ static int unbind_program(const char * prog,
if (name == NULL) {
if (reg_destroy_prog(prog) < 0) {
log_err("Failed to unbind %s.", prog);
- return -1;
+ goto fail;
}
log_info("Program %s unbound.", prog);
} else {
if (reg_unbind_prog(name, prog) < 0) {
log_err("Failed to unbind %s from %s", prog, name);
- return -1;
+ goto fail;
}
log_info("Name %s unbound for %s.", name, prog);
}
return 0;
+
+ fail:
+ return -1;
}
static int unbind_process(pid_t pid,
@@ -633,18 +670,21 @@ static int unbind_process(pid_t pid,
if (name == NULL) {
if (reg_destroy_proc(pid) < 0) {
log_err("Failed to unbind %d.", pid);
- return -1;
+ goto fail;
}
log_info("Process %d unbound.", pid);
} else {
if (reg_unbind_proc(name, pid) < 0) {
log_err("Failed to unbind %d from %s", pid, name);
- return -1;
+ goto fail;
}
log_info("Name %s unbound for process %d.", name, pid);
}
return 0;
+
+ fail:
+ return -1;
}
static int list_ipcps(ipcp_list_msg_t *** ipcps,
@@ -716,8 +756,7 @@ int name_reg(const char * name,
if (ipcp_reg(pid, hash)) {
log_err("Could not register " HASH_FMT32 " with IPCP %d.",
HASH_VAL32(hash.data), pid);
- freebuf(hash);
- return -1;
+ goto fail_hash;
}
log_info("Registered %s with IPCP %d as " HASH_FMT32 ".",
@@ -726,6 +765,10 @@ int name_reg(const char * name,
freebuf(hash);
return 0;
+
+ fail_hash:
+ freebuf(hash);
+ return -1;
}
static int name_unreg(const char * name,
@@ -760,8 +803,7 @@ static int name_unreg(const char * name,
if (ipcp_unreg(pid, hash)) {
log_err("Could not unregister %s with IPCP %d.", name, pid);
- freebuf(hash);
- return -1;
+ goto fail_hash;
}
log_info("Unregistered %s from %d.", name, pid);
@@ -769,293 +811,93 @@ static int name_unreg(const char * name,
freebuf(hash);
return 0;
-}
-
-static int proc_announce(const struct proc_info * info)
-{
- if (reg_create_proc(info) < 0) {
- log_err("Failed to add process %d.", info->pid);
- goto fail_proc;
- }
-
- log_info("Process added: %d (%s).", info->pid, info->prog);
-
- return 0;
- fail_proc:
+ fail_hash:
+ freebuf(hash);
return -1;
}
-static int proc_exit(pid_t pid)
+static int get_peer_ids(int fd,
+ uid_t * uid,
+ gid_t * gid,
+ pid_t * pid)
{
- if (reg_destroy_proc(pid) < 0)
- log_err("Failed to remove process %d.", pid);
+#if defined(__linux__)
+ struct ucred ucred;
+ socklen_t len;
- log_info("Process removed: %d.", pid);
+ len = sizeof(ucred);
- return 0;
-}
-
-static void __cleanup_pkp(void * pkp)
-{
- if (pkp != NULL)
- crypt_dh_pkp_destroy(pkp);
-}
-
-static void __cleanup_flow(void * flow)
-{
- reg_destroy_flow(((struct flow_info *) flow)->id);
-}
-
-static bool file_exists(const char * path)
-{
- struct stat s;
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0)
+ goto fail;
- if (stat(path, &s) < 0 && errno == ENOENT) {
- log_dbg("File %s does not exist.", path);
- return false;
- }
+ *uid = ucred.uid;
+ *gid = ucred.gid;
+ if (pid != NULL)
+ *pid = ucred.pid;
+#else
+ if (getpeereid(fd, uid, gid) < 0)
+ goto fail;
- return true;
+ if (pid != NULL)
+ *pid = -1; /* no portable SO_PEERCRED.pid equivalent */
+#endif
+ return 0;
+ fail:
+ return -1;
}
-static int load_credentials(const char * name,
- const struct name_sec_paths * paths,
- void ** pkp,
- void ** crt,
- bool * crypt)
+static int proc_announce(const struct proc_info * info)
{
- assert(paths != NULL);
- assert(pkp != NULL);
- assert(crt != NULL);
-
- *pkp = NULL;
- *crt = NULL;
-
- /* TODO: Allow configuration. For now, encrypt if path exists */
- *crypt = file_exists(paths->enc);
- if (*crypt)
- log_info("Encryption enabled for %s.", name);
-
- if (!file_exists(paths->crt) || !file_exists(paths->key)) {
- log_info("No security info for %s.", name);
- return 0;
- }
-
- if (crypt_load_crt_file(paths->crt, crt) < 0) {
- log_err("Failed to load %s for %s.", paths->crt, name);
- goto fail_crt;
+ if (reg_prepare_pool(info->uid, info->gid) < 0) {
+ log_err("Failed to prepare pool for uid %d.", info->uid);
+ goto fail;
}
- if (crypt_load_privkey_file(paths->key, pkp) < 0) {
- log_err("Failed to load %s for %s.", paths->key, name);
- goto fail_key;
+ if (reg_create_proc(info) < 0) {
+ log_err("Failed to add process %d.", info->pid);
+ goto fail;
}
- log_info("Loaded security keys for %s.", name);
+ log_info("Process added: %d (%s).", info->pid, info->prog);
return 0;
- fail_key:
- crypt_free_crt(*crt);
- *crt = NULL;
- fail_crt:
- return -EAUTH;
-}
-
-static int load_srv_credentials(const char * name,
- void ** pkp,
- void ** crt,
- bool * crypt)
-{
- struct name_info info;
-
- assert(name != NULL);
- assert(pkp != NULL);
- assert(crt != NULL);
-
- if (reg_get_name_info(name, &info) < 0) {
- log_err("Failed to get name info for %s.", name);
- return -ENAME;
- }
-
- return load_credentials(name, &info.s, pkp, crt, crypt);
-}
-
-static int load_cli_credentials(const char * name,
- void ** pkp,
- void ** crt,
- bool * crypt)
-{
- struct name_info info;
-
- assert(name != NULL);
- assert(pkp != NULL);
- assert(crt != NULL);
-
- if (reg_get_name_info(name, &info) < 0) {
- log_err("Failed to get name info for %s.", name);
- return -ENAME;
- }
-
- return load_credentials(name, &info.c, pkp, crt, crypt);
+ fail:
+ return -1;
}
-#define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0)
-static int irm_check_oap_hdr(const struct oap_hdr * oap_hdr,
- time_t mpl)
+static int proc_exit(pid_t pid)
{
- struct list_head * p;
- struct list_head * h;
- struct timespec now;
- struct oaph * new;
- uint64_t stamp;
- uint64_t cur;
- uint8_t * id;
- ssize_t delta;
-
- assert(oap_hdr != NULL);
-
- stamp = oap_hdr->timestamp;
- id = oap_hdr->id.data;
-
- clock_gettime(CLOCK_REALTIME, &now);
-
- cur = TS_TO_UINT64(now);
-
- delta = (ssize_t)(cur - stamp) / MILLION;
- if (delta > mpl)
- log_warn("Transit time exceeds MPL by %zd ms.", delta);
- if (delta < -TIMESYNC_SLACK)
- log_warn("OAP header sent %zd ms from the future.", -delta);
-
- new = malloc(sizeof(*new));
- if (new == NULL) {
- log_err("Failed to allocate memory for OAP element.");
- return -ENOMEM;
- }
-
- pthread_mutex_lock(&irmd.auth.mtx);
-
- list_for_each_safe(p, h, &irmd.auth.list) {
- struct oaph * oaph = list_entry(p, struct oaph, next);
- if (cur > oaph->stamp + OAP_SEEN_TIMER * BILLION) {
- list_del(&oaph->next);
- free(oaph);
- continue;
- }
-
- if (oaph->stamp == stamp && ID_IS_EQUAL(oaph->id, id)) {
- log_warn("OAP header already known: " HASH_FMT64 ".",
- HASH_VAL64(id));
- goto fail_replay;
- }
- }
-
- memcpy(new->id, id, OAP_ID_SIZE);
- new->stamp = stamp;
-
- list_add_tail(&new->next, &irmd.auth.list);
+ if (reg_destroy_proc(pid) < 0)
+ log_err("Failed to remove process %d.", pid);
- pthread_mutex_unlock(&irmd.auth.mtx);
+ log_info("Process removed: %d.", pid);
return 0;
-
- fail_replay:
- pthread_mutex_unlock(&irmd.auth.mtx);
- free(new);
- return -EAUTH;
}
-static int irm_auth_peer(const char * name,
- const struct oap_hdr * oap_hdr,
- const struct oap_hdr * r_oap_hdr)
+static void __cleanup_flow(void * flow)
{
- void * crt;
- void * pk;
- buffer_t sign;
- const char * n = name == NULL ? "<client>" : name;
-
- if (memcmp(r_oap_hdr->id.data, oap_hdr->id.data, OAP_ID_SIZE) != 0) {
- log_err("OAP ID mismatch in flow allocation.");
- goto fail_check;
- }
-
- if (r_oap_hdr->crt.len == 0) {
- log_info("No certificate provided by %s.", n);
- return 0;
- }
-
- if (crypt_load_crt_der(r_oap_hdr->crt, &crt) < 0) {
- log_err("Failed to load certificate from %s.", n);
- goto fail_check;
- }
-
- log_dbg("Loaded peer certificate for %s.", n);
-
- if (name != NULL) {
- if (crypt_check_crt_name(crt, n) < 0) {
- log_err("Certificate does not match %s.", n);
- goto fail_crt;
- }
- log_dbg("Certificate matches name %s.", n);
- }
-
- if (crypt_get_pubkey_crt(crt, &pk) < 0) {
- log_err("Failed to get pubkey from certificate for %s.", n);
- goto fail_crt;
- }
-
- log_dbg("Got public key from certificate for %s.", n);
-
- if (auth_verify_crt(irmd.auth.ctx, crt) < 0) {
- log_err("Failed to verify peer %s with CA store.", n);
- goto fail_crt;
- }
-
- log_info("Successfully verified peer certificate for %s.", n);
-
- sign = r_oap_hdr->hdr;
- sign.len -= (r_oap_hdr->sig.len + sizeof(uint16_t));
-
- if (auth_verify_sig(pk, sign, r_oap_hdr->sig) < 0) {
- log_err("Failed to verify signature for peer %s.", n);
- goto fail_check_sig;
- }
-
- crypt_free_key(pk);
- crypt_free_crt(crt);
-
- log_info("Successfully authenticated %s.", n);
-
- return 0;
-
- fail_check_sig:
- crypt_free_key(pk);
- fail_crt:
- crypt_free_crt(crt);
- fail_check:
- return -1;
+ reg_destroy_flow(((struct flow_info *) flow)->id);
}
static int flow_accept(struct flow_info * flow,
- buffer_t * symmkey,
buffer_t * data,
- struct timespec * abstime)
-{
- struct oap_hdr oap_hdr; /* incoming request */
- struct oap_hdr r_oap_hdr; /* outgoing response */
- uint8_t buf[MSGBUFSZ]; /* buffer for local ephkey */
- buffer_t lpk = BUF_INIT; /* local ephemeral pubkey */
- char name[NAME_SIZE + 1]; /* name for flow */
- void * pkp = NULL; /* signing private key */
- void * crt = NULL; /* signing certificate */
- int err;
- bool crypt;
+ struct timespec * abstime,
+ struct crypt_sk * sk)
+{
+ 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;
- /* piggyback of user data not yet implemented */
assert(data != NULL && BUF_IS_EMPTY(data));
- assert(symmkey != NULL && BUF_IS_EMPTY(symmkey));
+
+ clrbuf(req_hdr);
+ clrbuf(resp_hdr);
if (!reg_has_proc(flow->n_pid)) {
log_err("Unknown process %d calling accept.", flow->n_pid);
@@ -1077,7 +919,7 @@ static int flow_accept(struct flow_info * flow,
pthread_cleanup_push(__cleanup_flow, flow);
- err = reg_wait_flow_accepted(flow, &oap_hdr.hdr, abstime);
+ err = reg_wait_flow_accepted(flow, &req_hdr, abstime);
pthread_cleanup_pop(false);
@@ -1097,119 +939,63 @@ static int flow_accept(struct flow_info * flow,
if (reg_get_name_for_flow_id(name, flow->id) < 0) {
log_err("Failed to get name for flow %d.", flow->id);
err = -EIPCP;
- goto fail_cred;
+ goto fail_oap;
+ }
+
+ if (reg_get_name_info(name, &info) < 0) {
+ log_err("Failed to get name info for %s.", name);
+ err = -ENAME;
+ goto fail_oap;
}
log_dbg("IPCP %d accepting flow %d for %s.",
flow->n_pid, flow->id, name);
- if (load_srv_credentials(name, &pkp, &crt, &crypt) < 0) {
- log_err("Failed to load security keys for %s.", name);
- err = -EAUTH;
- goto fail_cred;
- }
+ flow->uid = reg_get_proc_uid(flow->n_pid);
- if (oap_hdr_decode(oap_hdr.hdr, &oap_hdr) < 0) {
- log_err("Failed to decode OAP header from %s.", name);
- err = -EIPCP;
- goto fail_oap_hdr;
+ 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;
}
-#ifdef DEBUG_PROTO_OAP
- debug_oap_hdr_rcv(&oap_hdr);
-#endif
- if (irm_check_oap_hdr(&oap_hdr, flow->mpl) < 0) {
- log_err("OAP header failed replay check.");
- goto fail_oap_hdr;
- }
-
- if (crypt && oap_hdr.eph.len == 0) {
- log_warn("Encryption required but no key provided.");
- err = -ECRYPT;
- goto fail_oap_hdr;
+ if (err < 0) {
+ log_err("OAP processing failed for %s.", name);
+ goto fail_oap;
}
- if (oap_hdr.eph.len > 0) { /* crypto requested */
- uint8_t * s; /* symmetric encryption key */
- ssize_t key_len; /* length of local pubkey */
- void * pkp = NULL; /* ephemeral private key pair */
-
- s = malloc(SYMMKEYSZ);
- if (s == NULL) {
- log_err("Failed to malloc symmkey.");
- err = -ENOMEM;
- goto fail_keys;
- }
-
- key_len = crypt_dh_pkp_create(&pkp, buf);
- if (key_len < 0) {
- free(s);
- log_err("Failed to generate key pair.");
- err = -ECRYPT;
- goto fail_keys;
- }
-
- lpk.data = buf;
- lpk.len = (size_t) key_len;
-
- log_dbg("Generated ephemeral keys for %d.", flow->n_pid);
-
- if (crypt_dh_derive(pkp, oap_hdr.eph, s) < 0) {
- log_err("Failed to derive secret for %d.", flow->id);
- crypt_dh_pkp_destroy(pkp);
- free(s);
- err = -ECRYPT;
- goto fail_derive;
+ if (reg_flow_is_direct(flow->id)) {
+ if (reg_respond_flow_direct(flow->id, &resp_hdr) < 0) {
+ log_err("Failed to respond to direct flow.");
+ goto fail_resp;
}
-
- symmkey->data = s;
- symmkey->len = SYMMKEYSZ;
-
- crypt_dh_pkp_destroy(pkp);
- }
-
- if (oap_hdr_init(oap_hdr.id, pkp, crt, lpk, *data, &r_oap_hdr) < 0) {
- log_err("Failed to create OAP header.");
- err = -ENOMEM;
- goto fail_r_oap_hdr;
- }
-
- if (irm_auth_peer(NULL, &r_oap_hdr, &oap_hdr) < 0) {
- log_err("Failed to auth %s client, flow %d.", name, flow->id);
- err = -EAUTH;
- goto fail_r_oap_hdr;
- }
-
- crypt_free_crt(crt);
- crypt_free_key(pkp);
-
-#ifdef DEBUG_PROTO_OAP
- debug_oap_hdr_snd(&r_oap_hdr);
-#endif
- if (ipcp_flow_alloc_resp(flow, 0, r_oap_hdr.hdr) < 0) {
+ 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);
}
- log_info("Flow %d accepted by %d for %s.",
- flow->id, flow->n_pid, name);
-
- oap_hdr_fini(&oap_hdr);
- oap_hdr_fini(&r_oap_hdr);
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
return 0;
- fail_r_oap_hdr:
- freebuf(*symmkey);
- fail_derive:
- clrbuf(lpk);
- fail_keys:
- oap_hdr_fini(&oap_hdr);
- fail_oap_hdr:
- crypt_free_crt(crt);
- crypt_free_key(pkp);
- fail_cred:
- assert(lpk.data == NULL && lpk.len == 0);
- ipcp_flow_alloc_resp(flow, err, lpk);
+ fail_oap:
+ 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:
reg_destroy_flow(flow->id);
fail_flow:
@@ -1217,10 +1003,9 @@ static int flow_accept(struct flow_info * flow,
fail_resp:
flow->state = FLOW_NULL;
- oap_hdr_fini(&r_oap_hdr);
- freebuf(*symmkey);
- clrbuf(lpk);
- oap_hdr_fini(&oap_hdr);
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
reg_destroy_flow(flow->id);
return -EIPCP;
}
@@ -1235,14 +1020,17 @@ static int flow_join(struct flow_info * flow,
buffer_t pbuf = BUF_INIT; /* nothing to piggyback */
int err;
- log_info("Allocating flow for %d to %s.", flow->n_pid, dst);
-
if (reg_create_flow(flow) < 0) {
log_err("Failed to create flow.");
err = -EBADF;
goto fail_flow;
}
+ flow->uid = reg_get_proc_uid(flow->n_pid);
+
+ log_info("Allocating flow for %d to %s (uid %d).",
+ flow->n_pid, dst, flow->uid);
+
strcpy(layer.name, dst);
if (reg_get_ipcp_by_layer(&ipcp, &layer) < 0) {
log_err("Failed to get IPCP for layer %s.", dst);
@@ -1308,7 +1096,7 @@ static int get_ipcp_by_dst(const char * dst,
pid_t * pid,
buffer_t * hash)
{
- ipcp_list_msg_t ** ipcps;
+ ipcp_list_msg_t ** ipcps = NULL;
int n;
int i;
int err = -EIPCP;
@@ -1361,196 +1149,6 @@ static int get_ipcp_by_dst(const char * dst,
return err;
}
-static int flow_alloc(struct flow_info * flow,
- const char * dst,
- buffer_t * symmkey,
- buffer_t * data,
- struct timespec * abstime)
-{
- struct oap_hdr oap_hdr; /* outgoing request */
- struct oap_hdr r_oap_hdr; /* incoming response */
- uint8_t buf[MSGBUFSZ]; /* buffer for local ephkey */
- buffer_t lpk = BUF_INIT; /* local ephemeral pubkey */
- void * pkp = NULL; /* ephemeral private key pair */
- uint8_t * s = NULL; /* symmetric key */
- void * cpkp = NULL; /* signing private key */
- void * ccrt = NULL; /* signing certificate */
- buffer_t hash;
- uint8_t idbuf[OAP_ID_SIZE];
- buffer_t id;
- int err;
- bool crypt;
-
- /* piggyback of user data not yet implemented */
- assert(data != NULL && BUF_IS_EMPTY(data));
- assert(symmkey != NULL && BUF_IS_EMPTY(symmkey));
-
- log_info("Allocating flow for %d to %s.", flow->n_pid, dst);
-
- if (random_buffer(idbuf, OAP_ID_SIZE) < 0) {
- log_err("Failed to generate ID.");
- err = -EIRMD;
- goto fail_id;
- }
-
- id.data = idbuf;
- id.len = OAP_ID_SIZE;
-
- if (load_cli_credentials(dst, &cpkp, &ccrt, &crypt) < 0) {
- log_err("Failed to load security keys for %s.", dst);
- err = -EAUTH;
- goto fail_cred;
- }
-
- if (crypt > 0) {
- ssize_t key_len;
-
- s = malloc(SYMMKEYSZ);
- if (s == NULL) {
- log_err("Failed to malloc symmetric key");
- err = -ENOMEM;
- goto fail_malloc;
- }
-
- key_len = crypt_dh_pkp_create(&pkp, buf);
- if (key_len < 0) {
- log_err("Failed to generate key pair.");
- err = -ECRYPT;
- goto fail_pkp;
- }
-
- lpk.data = buf;
- lpk.len = (size_t) key_len;
-
- log_dbg("Generated ephemeral keys for %d.", flow->n_pid);
- }
-
- if (oap_hdr_init(id, cpkp, ccrt, lpk, *data, &oap_hdr) < 0) {
- log_err("Failed to create OAP header.");
- err = -ENOMEM;
- goto fail_oap_hdr;
- }
-#ifdef DEBUG_PROTO_OAP
- debug_oap_hdr_snd(&oap_hdr);
-#endif
- if (reg_create_flow(flow) < 0) {
- log_err("Failed to create flow.");
- err = -EBADF;
- goto fail_flow;
- }
-
- if (get_ipcp_by_dst(dst, &flow->n_1_pid, &hash) < 0) {
- log_err("Failed to find IPCP for %s.", dst);
- err = -EIPCP;
- goto fail_ipcp;
- }
-
- reg_prepare_flow_alloc(flow);
-
- if (ipcp_flow_alloc(flow, hash, oap_hdr.hdr)) {
- log_err("Flow allocation %d failed.", flow->id);
- err = -ENOTALLOC;
- goto fail_alloc;
- }
-
- pthread_cleanup_push(__cleanup_flow, flow);
- pthread_cleanup_push(__cleanup_pkp, pkp);
- pthread_cleanup_push(free, hash.data);
- pthread_cleanup_push(free, s);
-
- err = reg_wait_flow_allocated(flow, &r_oap_hdr.hdr, abstime);
-
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
-
- if (err == -ETIMEDOUT) {
- log_err("Flow allocation timed out.");
- goto fail_alloc;
- }
-
- if (err == -1) {
- log_dbg("Flow allocation terminated.");
- err = -EIPCP;
- goto fail_alloc;
- }
-
- log_dbg("Response received for flow %d to %s.", flow->id, dst);
-
- if (err < 0) {
- log_warn("Flow allocation rejected for %s: %d.", dst, err);
- goto fail_alloc;
- }
-
- if (oap_hdr_decode(r_oap_hdr.hdr, &r_oap_hdr) < 0) {
- log_err("Failed to decode OAP header.");
- err = -EIPCP;
- goto fail_r_oap_hdr;
- }
-#ifdef DEBUG_PROTO_OAP
- debug_oap_hdr_rcv(&r_oap_hdr);
-#endif
- if (irm_check_oap_hdr(&r_oap_hdr, flow->mpl) < 0) {
- log_err("OAP header failed replay check.");
- err = -EAUTH;
- goto fail_r_oap_hdr;
- }
-
- if (irm_auth_peer(dst, &oap_hdr, &r_oap_hdr) < 0) {
- log_err("Failed to authenticate %s (flow %d).", dst, flow->id);
- err = -EAUTH;
- goto fail_r_oap_hdr;
- }
-
- if (lpk.len > 0) { /* crypto requested */
- if (crypt_dh_derive(pkp, r_oap_hdr.eph, s) < 0) {
- log_err("Failed to derive secret for %d.", flow->id);
- err = -ECRYPT;
- goto fail_r_oap_hdr;
- }
- crypt_dh_pkp_destroy(pkp);
-
- symmkey->data = s;
- symmkey->len = SYMMKEYSZ;
- s = NULL;
- }
-
- oap_hdr_fini(&r_oap_hdr);
- oap_hdr_fini(&oap_hdr);
-
- crypt_free_crt(ccrt);
- crypt_free_key(cpkp);
-
- /* TODO: piggyback user data if needed */
-
- freebuf(hash);
- free(s);
-
- return 0;
-
- fail_r_oap_hdr:
- flow->state = FLOW_DEALLOCATED;
- oap_hdr_fini(&r_oap_hdr);
- fail_alloc:
- freebuf(hash);
- fail_ipcp:
- reg_destroy_flow(flow->id);
- fail_flow:
- oap_hdr_fini(&oap_hdr);
- fail_oap_hdr:
- crypt_dh_pkp_destroy(pkp);
- fail_pkp:
- free(s);
- fail_malloc:
- crypt_free_crt(ccrt);
- crypt_free_key(cpkp);
- fail_cred:
- clrbuf(id);
- fail_id:
- return err;
-}
-
static int wait_for_accept(const char * name)
{
struct timespec timeo = TIMESPEC_INIT_MS(IRMD_REQ_ARR_TIMEOUT);
@@ -1643,6 +1241,209 @@ static int flow_req_arr(struct flow_info * flow,
return ret;
}
+#ifndef DISABLE_DIRECT_IPC
+static int flow_alloc_direct(const char * dst,
+ struct flow_info * flow,
+ buffer_t * data,
+ struct timespec * abstime,
+ struct crypt_sk * sk,
+ struct name_info * info)
+{
+ 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;
+
+ acc.id = wait_for_accept(dst);
+ if (acc.id < 0) {
+ log_dbg("No accepting process for %s.", dst);
+ return -EAGAIN;
+ }
+
+ if (oap_cli_prepare(&ctx, info, &req_hdr, *data, false) < 0) {
+ log_err("Failed to prepare OAP for %s.", dst);
+ return -EBADF;
+ }
+
+ acc.n_1_pid = flow->n_pid;
+ acc.mpl = DIRECT_MPL;
+ acc.mtu = DIRECT_MTU;
+ acc.qs = flow->qs;
+ acc.state = FLOW_ALLOCATED;
+
+ err = reg_prepare_flow_direct(&acc, &req_hdr, flow->uid);
+ if (err == -EPERM) {
+ log_dbg("UID mismatch, falling back.");
+ oap_ctx_free(ctx);
+ freebuf(req_hdr);
+ return -EPERM;
+ }
+
+ if (err < 0) {
+ log_err("Failed to prepare direct flow.");
+ oap_ctx_free(ctx);
+ freebuf(req_hdr);
+ return -EBADF;
+ }
+
+ err = reg_wait_flow_direct(acc.id, &resp_hdr, abstime);
+ if (err < 0) {
+ log_err("Timeout waiting for OAP response.");
+ oap_ctx_free(ctx);
+ return -ETIMEDOUT;
+ }
+
+ err = oap_cli_complete(ctx, info, resp_hdr, data, sk, NULL, NULL);
+ if (err < 0) {
+ log_err("OAP completion failed for %s.", dst);
+ freebuf(resp_hdr);
+ return err;
+ }
+
+ flow->id = acc.id;
+ flow->n_1_pid = acc.n_pid;
+ flow->mpl = DIRECT_MPL;
+ 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);
+
+ freebuf(resp_hdr);
+
+ return 0;
+}
+#endif /* DISABLE_DIRECT_IPC */
+
+static int flow_alloc(const char * dst,
+ struct flow_info * flow,
+ buffer_t * data,
+ struct timespec * abstime,
+ struct crypt_sk * sk)
+{
+ 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;
+
+ /* piggyback of user data not yet implemented */
+ assert(data != NULL && BUF_IS_EMPTY(data));
+
+ /* Look up name_info for dst */
+ if (reg_get_name_info(dst, &info) < 0) {
+ log_err("Failed to get name info for %s.", dst);
+ err = -ENAME;
+ goto fail_flow;
+ }
+
+ flow->uid = reg_get_proc_uid(flow->n_pid);
+
+ log_info("Allocating flow for %d to %s (uid %d).",
+ flow->n_pid, dst, flow->uid);
+
+#ifndef DISABLE_DIRECT_IPC
+ err = flow_alloc_direct(dst, flow, data, abstime, sk, &info);
+ if (err == 0)
+ return 0;
+
+ if (err != -EPERM && err != -EAGAIN)
+ goto fail_flow;
+#endif
+ if (reg_create_flow(flow) < 0) {
+ log_err("Failed to create flow.");
+ err = -EBADF;
+ 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;
+ goto fail_ipcp;
+ }
+
+ if (reg_prepare_flow_alloc(flow) < 0) {
+ log_err("Failed to prepare flow allocation.");
+ err = -EBADF;
+ goto fail_prepare;
+ }
+
+ if (oap_cli_prepare(&ctx, &info, &req_hdr, *data, false) < 0) {
+ log_err("Failed to prepare OAP request for %s.", dst);
+ err = -EBADF;
+ goto fail_prepare;
+ }
+
+ if (ipcp_flow_alloc(flow, hash, req_hdr)) {
+ log_err("Flow allocation %d failed.", flow->id);
+ err = -EIPCP;
+ goto fail_alloc;
+ }
+
+ pthread_cleanup_push(__cleanup_flow, flow);
+ pthread_cleanup_push(free, hash.data);
+
+ err = reg_wait_flow_allocated(flow, &resp_hdr, abstime);
+
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+
+ if (err == -ETIMEDOUT) {
+ log_err("Flow allocation timed out.");
+ goto fail_wait;
+ }
+
+ log_dbg("Response for flow %d to %s.", flow->id, dst);
+
+ if (err < 0) {
+ log_warn("Allocation rejected: %s (%d).", dst, err);
+ goto fail_peer;
+ }
+
+ err = oap_cli_complete(ctx, &info, 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);
+
+ return 0;
+
+ fail_complete:
+ freebuf(peer_crt);
+ ctx = NULL; /* free'd on complete */
+ fail_peer:
+ flow->state = FLOW_DEALLOCATED;
+ fail_wait:
+ freebuf(resp_hdr);
+ fail_alloc:
+ freebuf(req_hdr);
+ oap_ctx_free(ctx);
+ fail_prepare:
+ freebuf(hash);
+ fail_ipcp:
+ reg_destroy_flow(flow->id);
+ fail_flow:
+ return err;
+}
+
static int flow_alloc_reply(struct flow_info * flow,
int response,
buffer_t * data)
@@ -1666,6 +1467,12 @@ static int flow_dealloc(struct flow_info * flow,
reg_dealloc_flow(flow);
+ if (reg_flow_is_direct(flow->id)) {
+ if (flow->state == FLOW_DEALLOCATED)
+ reg_destroy_flow(flow->id);
+ return 0;
+ }
+
if (ipcp_flow_dealloc(flow->n_1_pid, flow->id, ts->tv_sec) < 0) {
log_err("Failed to request dealloc from %d.", flow->n_1_pid);
return -EIPCP;
@@ -1688,6 +1495,741 @@ 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;
+
+ 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, &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, &info, 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, &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;
@@ -1742,21 +2284,29 @@ static void __cleanup_irm_msg(void * o)
irm_msg__free_unpacked((irm_msg_t *) o, NULL);
}
-static irm_msg_t * do_command_msg(irm_msg_t * msg)
+static irm_msg_t * do_command_msg(irm_msg_t * msg,
+ int fd)
{
- struct ipcp_config conf;
- struct ipcp_info ipcp;
- struct flow_info flow;
- struct proc_info proc;
- struct name_info name;
- struct timespec * abstime;
- struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT);
- struct timespec now;
- struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */
- int res;
- irm_msg_t * ret_msg;
- buffer_t data;
- buffer_t symmkey = BUF_INIT;;
+ struct ipcp_config conf;
+ struct ipcp_info ipcp;
+ struct flow_info flow;
+ struct proc_info proc;
+ struct name_info name;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ]; /* stack buffer for OAP */
+ uint8_t * hbuf = NULL; /* heap copy for response */
+ struct timespec * abstime;
+ struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT);
+ 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;
memset(&flow, 0, sizeof(flow));
@@ -1821,7 +2371,11 @@ 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 = proc_announce(&proc);
+ 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
+ res = proc_announce(&proc);
break;
case IRM_MSG_CODE__IRM_PROC_EXIT:
res = proc_exit(msg->pid);
@@ -1858,16 +2412,31 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
msg->has_pk = false;
assert(data.len > 0 ? data.data != NULL : data.data == NULL);
flow = flow_info_msg_to_s(msg->flow_info);
- res = flow_accept(&flow, &symmkey, &data, abstime);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_symmkey = symmkey.len != 0;
- ret_msg->symmkey.data = symmkey.data;
- ret_msg->symmkey.len = symmkey.len;
- ret_msg->has_pk = data.len != 0;
- ret_msg->pk.data = data.data;
- ret_msg->pk.len = data.len;
+ sk.key = kbuf;
+ res = flow_accept(&flow, &data, abstime, &sk);
+ 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;
@@ -1876,16 +2445,31 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
assert(data.len > 0 ? data.data != NULL : data.data == NULL);
flow = flow_info_msg_to_s(msg->flow_info);
abstime = abstime == NULL ? &max : abstime;
- res = flow_alloc(&flow, msg->dst, &symmkey, &data, abstime);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_symmkey = symmkey.len != 0;
- ret_msg->symmkey.data = symmkey.data;
- ret_msg->symmkey.len = symmkey.len;
- ret_msg->has_pk = data.len != 0;
- ret_msg->pk.data = data.data;
- ret_msg->pk.len = data.len;
+ sk.key = kbuf;
+ res = flow_alloc(msg->dst, &flow, &data, abstime, &sk);
+ 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);
@@ -1924,6 +2508,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;
@@ -1938,20 +2567,30 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
else
ret_msg->result = res;
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+
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;
- irm_msg_t * msg;
- buffer_t buffer;
+ int sfd;
+ irm_msg_t * msg;
+ buffer_t buffer;
(void) o;
while (true) {
irm_msg_t * ret_msg;
struct cmd * cmd;
+ bool had_key;
pthread_mutex_lock(&irmd.cmd_lock);
@@ -1981,7 +2620,7 @@ static void * mainloop(void * o)
pthread_cleanup_push(__cleanup_close_ptr, &sfd);
pthread_cleanup_push(__cleanup_irm_msg, msg);
- ret_msg = do_command_msg(msg);
+ ret_msg = do_command_msg(msg, sfd);
pthread_cleanup_pop(true);
pthread_cleanup_pop(false);
@@ -2015,6 +2654,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);
@@ -2029,6 +2671,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);
@@ -2036,6 +2681,7 @@ static void * mainloop(void * o)
continue;
fail:
+ clear_msg_key(ret_msg);
irm_msg__free_unpacked(ret_msg, NULL);
fail_msg:
close(sfd);
@@ -2074,7 +2720,7 @@ static void destroy_mount(char * mnt)
static int ouroboros_reset(void)
{
- shm_rdrbuff_purge();
+ ssm_pool_gspp_purge();
lockfile_destroy(irmd.lf);
return 0;
@@ -2095,10 +2741,8 @@ static void cleanup_pid(pid_t pid)
}
destroy_mount(mnt);
-
-#else
- (void) pid;
#endif
+ ssm_pool_reclaim_orphans(irmd.gspp, pid);
}
void * irm_sanitize(void * o)
@@ -2121,12 +2765,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);
@@ -2170,7 +2816,9 @@ static int irm_load_store(char * dpath)
goto fail_file;
}
- if (auth_add_crt_to_store(irmd.auth.ctx, 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;
@@ -2198,6 +2846,8 @@ static int irm_load_store(char * dpath)
static int irm_init(void)
{
struct stat st;
+ struct group * grp;
+ gid_t gid;
pthread_condattr_t cattr;
#ifdef HAVE_FUSE
mode_t mask;
@@ -2265,6 +2915,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.");
@@ -2283,11 +2956,23 @@ static int irm_init(void)
goto fail_sock_path;
}
- if ((irmd.rdrb = shm_rdrbuff_create()) == NULL) {
- log_err("Failed to create rdrbuff.");
- goto fail_rdrbuff;
+ grp = getgrnam("ouroboros");
+ if (grp == NULL) {
+ log_warn("ouroboros group not found, using gid %d.", getgid());
+ gid = getgid();
+ } else {
+ gid = grp->gr_gid;
+ }
+
+ irmd.gspp = ssm_pool_create(getuid(), gid);
+ if (irmd.gspp == NULL) {
+ log_err("Failed to create GSPP.");
+ goto fail_pool;
}
+ if (ssm_pool_mlock(irmd.gspp) < 0)
+ log_warn("Failed to mlock pool.");
+
irmd.tpm = tpm_create(IRMD_MIN_THREADS, IRMD_ADD_THREADS,
mainloop, NULL);
if (irmd.tpm == NULL) {
@@ -2295,27 +2980,19 @@ static int irm_init(void)
goto fail_tpm_create;
}
- if (pthread_mutex_init(&irmd.auth.mtx, NULL) < 0) {
- log_err("Failed to initialize auth mutex.");
- goto fail_auth_mtx;
+ if (oap_auth_init() < 0) {
+ log_err("Failed to initialize OAP module.");
+ goto fail_oap;
}
- irmd.auth.ctx = auth_create_ctx();
- if (irmd.auth.ctx == NULL) {
- log_err("Failed to create auth store context.");
- goto fail_auth_ctx;
- }
-
- list_head_init(&irmd.auth.list);
-
- 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_auth_ctx;
+ 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_auth_ctx;
+ goto fail_load_store;
}
#ifdef HAVE_FUSE
@@ -2352,19 +3029,22 @@ static int irm_init(void)
#ifdef HAVE_FUSE
rmdir(FUSE_PREFIX);
#endif
- auth_destroy_ctx(irmd.auth.ctx);
#endif
- fail_auth_ctx:
- pthread_mutex_destroy(&irmd.auth.mtx);
- fail_auth_mtx:
+ fail_load_store:
+ oap_auth_fini();
+ fail_oap:
tpm_destroy(irmd.tpm);
fail_tpm_create:
- shm_rdrbuff_destroy(irmd.rdrb);
- fail_rdrbuff:
+ ssm_pool_destroy(irmd.gspp);
+ fail_pool:
close(irmd.sockfd);
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);
@@ -2388,18 +3068,7 @@ static void irm_fini(void)
if (irmd_get_state() != IRMD_INIT)
log_warn("Unsafe destroy.");
- pthread_mutex_lock(&irmd.auth.mtx);
-
- list_for_each_safe(p, h, &irmd.auth.list) {
- struct oaph * oaph = list_entry(p, struct oaph, next);
- list_del(&oaph->next);
- free(oaph);
- }
-
- pthread_mutex_unlock(&irmd.auth.mtx);
- pthread_mutex_destroy(&irmd.auth.mtx);
-
- auth_destroy_ctx(irmd.auth.ctx);
+ oap_auth_fini();
tpm_destroy(irmd.tpm);
@@ -2408,8 +3077,7 @@ static void irm_fini(void)
if (unlink(IRM_SOCK_PATH))
log_dbg("Failed to unlink %s.", IRM_SOCK_PATH);
- if (irmd.rdrb != NULL)
- shm_rdrbuff_destroy(irmd.rdrb);
+ ssm_pool_destroy(irmd.gspp);
if (irmd.lf != NULL)
lockfile_destroy(irmd.lf);
@@ -2425,13 +3093,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
@@ -2464,10 +3147,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);
@@ -2507,6 +3198,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);
@@ -2532,10 +3228,8 @@ static void irm_argparse(int argc,
argc--;
argv++;
} else if (strcmp(*argv, "--version") == 0) {
- printf("Ouroboros version %d.%d.%d\n",
- OUROBOROS_VERSION_MAJOR,
- OUROBOROS_VERSION_MINOR,
- OUROBOROS_VERSION_PATCH);
+ printf("Ouroboros version %s\n",
+ OUROBOROS_VERSION_STRING);
exit(EXIT_SUCCESS);
#ifdef HAVE_TOML
} else if (strcmp (*argv, "--config") == 0) {
@@ -2629,8 +3323,15 @@ int main(int argc,
goto fail_irm_init;
}
- if (irm_init() < 0)
+ if (crypt_secure_malloc_init(IRMD_SECMEM_MAX) < 0) {
+ log_err("Failed to initialize secure memory allocation.");
+ goto fail_secmem;
+ }
+
+ if (irm_init() < 0) {
+ log_err("Failed to initialize IRMd.");
goto fail_irm_init;
+ }
if (reg_init() < 0) {
log_err("Failed to initialize registry.");
@@ -2639,11 +3340,14 @@ int main(int argc,
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
- if (irm_start() < 0)
+ if (irm_start() < 0) {
+ log_err("Failed to start IRMd.");
goto fail_irm_start;
+ }
#ifdef HAVE_TOML
if (irm_configure(irmd.cfg_file) < 0) {
+ log_err("Failed to load IRMd configuration.");
irmd_set_state(IRMD_SHUTDOWN);
ret = EXIT_FAILURE;
}
@@ -2662,6 +3366,10 @@ int main(int argc,
irm_fini();
+ crypt_secure_malloc_fini();
+
+ crypt_cleanup();
+
log_info("Ouroboros IPC Resource Manager daemon exited. Bye.");
log_fini();
@@ -2673,5 +3381,8 @@ int main(int argc,
fail_reg:
irm_fini();
fail_irm_init:
+ crypt_secure_malloc_fini();
+ crypt_cleanup();
+ fail_secmem:
exit(EXIT_FAILURE);
}
diff --git a/src/irmd/oap.c b/src/irmd/oap.c
deleted file mode 100644
index 500da6f1..00000000
--- a/src/irmd/oap.c
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Ouroboros flow allocation protocol header
- *
- * 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
-
-#define OUROBOROS_PREFIX "irmd/oap"
-
-#include <ouroboros/crypt.h>
-#include <ouroboros/endian.h>
-#include <ouroboros/logs.h>
-#include <ouroboros/rib.h>
-#include <ouroboros/time.h>
-
-#include "config.h"
-
-#include "oap.h"
-
-#include <assert.h>
-
-int oap_hdr_init(buffer_t id,
- void * pkp,
- void * pubcrt,
- buffer_t ephkey,
- buffer_t data,
- struct oap_hdr * oap_hdr)
-{
- struct timespec now;
- uint64_t stamp;
- buffer_t hdr;
- buffer_t der = BUF_INIT;
- buffer_t sig = BUF_INIT;
- buffer_t sign;
- uint16_t len;
- off_t offset;
-
- assert(id.data != NULL && id.len == OAP_ID_SIZE);
- assert(oap_hdr != NULL);
- memset(oap_hdr, 0, sizeof(*oap_hdr));
-
- clock_gettime(CLOCK_REALTIME, &now);
- stamp = hton64(TS_TO_UINT64(now));
-
- if (pubcrt != NULL && crypt_crt_der(pubcrt, &der) < 0)
- goto fail_der;
-
- hdr.len = id.len +
- sizeof(stamp) +
- sizeof(len) + der.len +
- sizeof(len) + ephkey.len +
- sizeof(len) + data.len +
- sizeof(len); /* sig len */
-
- hdr.data = malloc(hdr.len);
- if (hdr.data == NULL)
- goto fail_hdr;
-
- offset = 0;
-
- memcpy(hdr.data, id.data, id.len);
- offset += id.len;
-
- memcpy(hdr.data + offset, &stamp, sizeof(stamp));
- offset += sizeof(stamp);
-
- /* pubcrt */
- len = hton16((uint16_t) der.len);
- memcpy(hdr.data + offset, &len, sizeof(len));
- offset += sizeof(len);
- if (der.len != 0)
- memcpy(hdr.data + offset, der.data, der.len);
- offset += der.len;
-
- /* ephkey */
- len = hton16((uint16_t) ephkey.len);
- memcpy(hdr.data + offset, &len, sizeof(len));
- offset += sizeof(len);
- if (ephkey.len != 0)
- memcpy(hdr.data + offset, ephkey.data, ephkey.len);
- offset += ephkey.len;
-
- /* data */
- len = hton16((uint16_t) data.len);
- memcpy(hdr.data + offset, &len, sizeof(len));
- offset += sizeof(len);
- if (data.len != 0)
- memcpy(hdr.data + offset, data.data, data.len);
- offset += data.len;
-
- sign.data = hdr.data;
- sign.len = hdr.len - sizeof(len);
-
- if (pkp != NULL && auth_sign(pkp, sign, &sig) < 0)
- goto fail_sig;
-
- len = hton16((uint16_t) sig.len);
- memcpy(hdr.data + offset, &len, sizeof(len));
- offset += sizeof(len);
-
- oap_hdr->hdr = hdr;
-
- assert((size_t) offset == hdr.len);
-
- if (sig.len > 0) {
- oap_hdr->hdr.len += sig.len;
- oap_hdr->hdr.data = realloc(hdr.data, oap_hdr->hdr.len);
- if (oap_hdr->hdr.data == NULL)
- goto fail_oap_hdr;
-
- memcpy(oap_hdr->hdr.data + offset, sig.data, sig.len);
- clrbuf(hdr);
- }
-
- if (oap_hdr_decode(oap_hdr->hdr, oap_hdr) < 0)
- goto fail_decode;
-
- freebuf(der);
- freebuf(sig);
-
- return 0;
-
- fail_decode:
- oap_hdr_fini(oap_hdr);
- fail_oap_hdr:
- freebuf(sig);
- fail_sig:
- freebuf(hdr);
- fail_hdr:
- freebuf(der);
- fail_der:
- memset(oap_hdr, 0, sizeof(*oap_hdr));
- return -1;
-}
-
-void oap_hdr_fini(struct oap_hdr * oap_hdr)
-{
- assert(oap_hdr != NULL);
-
- freebuf(oap_hdr->hdr);
- memset(oap_hdr, 0, sizeof(*oap_hdr));
-}
-
-int oap_hdr_decode(buffer_t hdr,
- struct oap_hdr * oap_hdr)
-{
- off_t offset;
-
- assert(oap_hdr != NULL);
- memset(oap_hdr, 0, sizeof(*oap_hdr));
-
- if (hdr.len < OAP_HDR_MIN_SIZE)
- goto fail_decode;
-
- oap_hdr->id.data = hdr.data;
- oap_hdr->id.len = OAP_ID_SIZE;
-
- offset = OAP_ID_SIZE;
-
- oap_hdr->timestamp = ntoh64(*(uint64_t *)(hdr.data + offset));
-
- offset += sizeof(uint64_t);
-
- oap_hdr->crt.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
- oap_hdr->crt.data = hdr.data + offset + sizeof(uint16_t);
-
- offset += sizeof(uint16_t) + oap_hdr->crt.len;
-
- if ((size_t) offset + sizeof(uint16_t) >= hdr.len)
- goto fail_decode;
-
- oap_hdr->eph.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
- oap_hdr->eph.data = hdr.data + offset + sizeof(uint16_t);
-
- offset += sizeof(uint16_t) + oap_hdr->eph.len;
-
- if ((size_t) offset + sizeof(uint16_t) >= hdr.len)
- goto fail_decode;
-
- oap_hdr->data.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
- oap_hdr->data.data = hdr.data + offset + sizeof(uint16_t);
-
- offset += sizeof(uint16_t) + oap_hdr->data.len;
-
- if ((size_t) offset + sizeof(uint16_t) > hdr.len)
- goto fail_decode;
-
- oap_hdr->sig.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
- oap_hdr->sig.data = hdr.data + offset + sizeof(uint16_t);
-
- offset += sizeof(uint16_t) + oap_hdr->sig.len;
-
- if ((size_t) offset != hdr.len)
- goto fail_decode;
-
- oap_hdr->hdr = hdr;
-
- return 0;
-
- fail_decode:
- memset(oap_hdr, 0, sizeof(*oap_hdr));
- return -1;
-}
-
-#ifdef DEBUG_PROTO_OAP
-static void debug_oap_hdr(const struct oap_hdr * hdr)
-{
- assert(hdr);
-
- if (hdr->crt.len > 0)
- log_proto(" Certificate: [%zu bytes]", hdr->crt.len);
- else
- log_proto(" Certificate: <none>");
-
- if (hdr->eph.len > 0)
- log_proto(" Ephemeral Public Key: [%zu bytes]", hdr->eph.len);
- else
- log_proto(" Ephemeral Public Key: <none>");
- if (hdr->data.len > 0)
- log_proto(" Data: [%zu bytes]", hdr->data.len);
- else
- log_proto(" Data: <none>");
- if (hdr->sig.len > 0)
- log_proto(" Signature: [%zu bytes]", hdr->sig.len);
- else
- log_proto(" Signature: <none>");
-}
-
-void debug_oap_hdr_rcv(const struct oap_hdr * hdr)
-{
- struct tm * tm;
- char tmstr[RIB_TM_STRLEN];
- time_t stamp;
-
- assert(hdr);
-
- stamp = (time_t) hdr->timestamp / BILLION;
-
- tm = gmtime(&stamp);
- strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
-
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--",
- HASH_VAL64(hdr->id.data), tmstr);
-
- debug_oap_hdr(hdr);
-}
-
-void debug_oap_hdr_snd(const struct oap_hdr * hdr)
-{
- struct tm * tm;
- char tmstr[RIB_TM_STRLEN];
- time_t stamp;
-
- assert(hdr);
-
- stamp = (time_t) hdr->timestamp / BILLION;
-
- tm = gmtime(&stamp);
- strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
-
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->",
- HASH_VAL64(hdr->id.data), tmstr);
-
- debug_oap_hdr(hdr);
-}
-#endif
-
diff --git a/src/irmd/oap.h b/src/irmd/oap.h
index ccdfa804..86f11e21 100644
--- a/src/irmd/oap.h
+++ b/src/irmd/oap.h
@@ -1,7 +1,7 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
- * Ouroboros flow allocation protocol header
+ * Ouroboros Allocation Protocol (OAP) Component
*
* Dimitri Staessens <dimitri@ouroboros.rocks>
* Sander Vrijders <sander@ouroboros.rocks>
@@ -23,72 +23,62 @@
#ifndef OUROBOROS_IRMD_OAP_H
#define OUROBOROS_IRMD_OAP_H
+#include <ouroboros/crypt.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
#include <ouroboros/utils.h>
-#define OAP_ID_SIZE (16)
-#define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 4 * sizeof(uint16_t))
+#include <stdbool.h>
+/* OAP authentication state (in oap/auth.c) */
+int oap_auth_init(void);
-/*
- * 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
- * +---------------------------------------------------------------+
- * | |
- * | id (128 bits) |
- * | |
- * | |
- * +---------------------------------------------------------------+
- * | timestamp (64 bits) |
- * | |
- * +---------------------------------------------------------------+
- * | crt_len (16 bits) | |
- * +-----------+-----------------+ |
- * | certificate |
- * | |
- * +---------------------------------------------------------------+
- * | eph_len (16 bits) | |
- * +-----------+-----------------+ |
- * | public key for ECDHE |
- * | |
- * +---------------------------------------------------------------+
- * | data_len (16 bits) | |
- * +-----------+-----------------+ |
- * | piggy backed application data |
- * | |
- * +---------------------------------------------------------------+
- * | sig_len (16 bits) | |
- * +-----------+-----------------+ |
- * | signature |
- * | |
- * +---------------------------------------------------------------+
- */
+void oap_auth_fini(void);
-struct oap_hdr {
- uint64_t timestamp;
- buffer_t id;
- buffer_t crt;
- buffer_t eph;
- buffer_t data;
- buffer_t sig;
- buffer_t hdr;
-};
+int oap_auth_add_ca_crt(void * crt);
-int oap_hdr_init(buffer_t id,
- void * pkp,
- void * pubcrt,
- buffer_t ephkey,
- buffer_t data,
- struct oap_hdr * oap_hdr);
+int oap_auth_add_chain_crt(void * crt);
-void oap_hdr_fini(struct oap_hdr * oap_hdr);
+/*
+* Prepare OAP request header for server, returns context
+* Passes client data for srv, returns srv data for client
+* rekey forces ephemeral server-encap KEX (no client-encap; preserves FS/PCS)
+*/
+int oap_cli_prepare(void ** ctx,
+ const struct name_info * info,
+ buffer_t * req_buf,
+ buffer_t data,
+ bool rekey);
-int oap_hdr_decode(buffer_t hdr,
- struct oap_hdr * oap_hdr);
+/*
+ * Server processes header, creates response header, returns secret key.
+ * data is in/out: input=srv data to send, output=cli data received.
+ * rekey drops the cert and verifies against cached_crt; peer_crt (or NULL)
+ * receives 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,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
-#ifdef DEBUG_PROTO_OAP
-void debug_oap_hdr_snd(const struct oap_hdr * hdr);
+/*
+ * 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,
+ const struct name_info * info,
+ buffer_t rsp_buf,
+ buffer_t * data,
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
-void debug_oap_hdr_rcv(const struct oap_hdr * hdr);
-#endif /* DEBUG_PROTO_OAP */
+/* Free OAP state (on failure before complete) */
+void oap_ctx_free(void * ctx);
#endif /* OUROBOROS_IRMD_OAP_H */
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c
new file mode 100644
index 00000000..f70f9df1
--- /dev/null
+++ b/src/irmd/oap/auth.c
@@ -0,0 +1,558 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Authentication, replay detection, and validation
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/time.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * 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 {
+ 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;
+ }
+
+ 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_bucket;
+ }
+
+ return 0;
+
+ 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;
+}
+
+void oap_auth_fini(void)
+{
+ int i;
+
+ pthread_mutex_lock(&oap_auth.replay.mtx);
+
+ 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);
+ pthread_mutex_destroy(&oap_auth.replay.mtx);
+
+ auth_destroy_ctx(oap_auth.ca_ctx);
+}
+
+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 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);
+
+ stamp = hdr->timestamp;
+ id = hdr->id.data;
+
+ clock_gettime(CLOCK_REALTIME, &now);
+
+ cur = TS_TO_UINT64(now);
+
+ delta = (ssize_t)(cur - stamp) / MILLION;
+ if (delta < -TIMESYNC_SLACK) {
+ log_err_id(id, "OAP header from %zd ms into future.", -delta);
+ return -EAUTH;
+ }
+
+ if (delta > OAP_REPLAY_TIMER * 1000) {
+ log_err_id(id, "OAP header too old (%zd ms).", delta);
+ return -EAUTH;
+ }
+
+ gen = stamp / ((uint64_t) OAP_REPLAY_TIMER * BILLION);
+
+ pthread_mutex_lock(&oap_auth.replay.mtx);
+
+ b = &oap_auth.replay.bucket[gen % OAP_REPLAY_GENS];
+
+ /* Rotate a stale bucket in O(1): its old-gen slots become free. */
+ if (b->gen != gen) {
+ b->gen = gen;
+ b->count = 0;
+ }
+
+ slots = b->slots;
+
+ 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;
+ }
+
+ h = (h + 1) & oap_auth.replay.mask;
+ }
+
+ /* 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;
+ }
+
+ 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);
+
+ return ret;
+}
+
+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;
+ 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);
+
+ if (memcmp(peer_hdr->id.data, local_hdr->id.data, OAP_ID_SIZE) != 0) {
+ log_err_id(id, "OAP ID mismatch in flow allocation.");
+ goto fail_check;
+ }
+
+ /* 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(crt_der, &crt) < 0) {
+ log_err_id(id, "Failed to load crt.");
+ goto fail_check;
+ }
+
+ log_dbg_id(id, "Loaded peer crt.");
+
+ if (crypt_get_pubkey_crt(crt, &pk) < 0) {
+ log_err_id(id, "Failed to get pubkey from crt.");
+ goto fail_crt;
+ }
+
+ log_dbg_id(id, "Got public key from crt.");
+
+ 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_pin;
+ }
+
+ log_dbg_id(id, "Successfully verified peer crt.");
+
+ /* 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_pin;
+ }
+
+ 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);
+
+ log_dbg_id(id, "Successfully authenticated peer.");
+
+ return 0;
+
+ fail_pin:
+ if (pin != NULL)
+ crypt_free_crt(pin);
+ fail_crt:
+ crypt_free_key(pk);
+ crypt_free_crt(crt);
+ fail_check:
+ return -EAUTH;
+}
diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h
new file mode 100644
index 00000000..72938b53
--- /dev/null
+++ b/src/irmd/oap/auth.h
@@ -0,0 +1,68 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Authentication functions
+ *
+ * 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_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 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
new file mode 100644
index 00000000..ebfcd71f
--- /dev/null
+++ b/src/irmd/oap/cli.c
@@ -0,0 +1,682 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Client-side processing
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/random.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+#include "io.h"
+#include "../oap.h"
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Client context between oap_cli_prepare and oap_cli_complete */
+struct oap_cli_ctx {
+ uint8_t __id[OAP_ID_SIZE];
+ buffer_t id;
+ uint8_t kex_buf[CRYPT_KEY_BUFSZ];
+ uint8_t req_hash[MAX_HASH_SIZE];
+ size_t req_hash_len;
+ int req_md_nid;
+ struct sec_config scfg;
+ struct oap_hdr local_hdr;
+ void * pkp; /* Ephemeral keypair */
+ uint8_t * key; /* For client-encap KEM */
+};
+
+#define OAP_CLI_CTX_INIT(s) \
+ do { s->id.len = OAP_ID_SIZE; s->id.data = s->__id; } while (0)
+
+/* Client-side credential loading, mocked in tests */
+
+#ifdef OAP_TEST_MODE
+extern int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+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 * buf);
+#else
+
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ assert(info != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ return load_credentials(info->name, &info->c, pkp, crt);
+}
+
+int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ assert(info != NULL);
+ assert(cfg != NULL);
+
+ 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,
+ struct sec_config * cfg,
+ buffer_t * pk)
+{
+ char path[PATH_MAX];
+ const char * ext;
+
+ assert(name != NULL);
+ assert(cfg != NULL);
+ assert(pk != NULL);
+
+ ext = IS_HYBRID_KEM(cfg->x.str) ? "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) {
+ log_err("Failed to load %s pubkey from %s.", ext, path);
+ return -1;
+ }
+ } else {
+ if (crypt_load_pubkey_file_to_der(path, pk) < 0) {
+ log_err("Failed to load %s pubkey from %s.", ext, path);
+ return -1;
+ }
+ }
+
+ log_dbg("Loaded %s pubkey from %s (%zu bytes).", ext, path, pk->len);
+
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
+
+static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s)
+{
+ 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(scfg, &s->pkp, kex->data);
+ if (len < 0) {
+ log_err_id(id, "Failed to generate DHE keypair.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Generated ephemeral %s keys (%zd bytes).",
+ scfg->x.str, len);
+
+ return 0;
+}
+
+static int do_client_kex_prepare_kem_encap(const char * server_name,
+ struct oap_cli_ctx * s)
+{
+ 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, scfg, &server_pk) < 0) {
+ log_err_id(id, "Failed to load server KEM pk.");
+ return -ECRYPT;
+ }
+
+ if (IS_HYBRID_KEM(scfg->x.str))
+ len = kex_kem_encap_raw(server_pk, kex->data,
+ scfg->k.nid, key_buf);
+ else
+ len = kex_kem_encap(server_pk, kex->data,
+ scfg->k.nid, key_buf);
+
+ freebuf(server_pk);
+
+ if (len < 0) {
+ log_err_id(id, "Failed to encapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Client encaps: CT len=%zd.", len);
+
+ /* Store derived key */
+ s->key = crypt_secure_malloc(SYMMKEYSZ);
+ if (s->key == NULL) {
+ log_err_id(id, "Failed to allocate secure key.");
+ return -ENOMEM;
+ }
+ memcpy(s->key, key_buf, SYMMKEYSZ);
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ return 0;
+}
+
+static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s)
+{
+ 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(scfg, &s->pkp, kex->data);
+ if (len < 0) {
+ log_err_id(id, "Failed to generate KEM keypair.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Client PK for server encaps (%zd bytes).", len);
+
+ return 0;
+}
+
+static int do_client_kex_prepare(const char * server_name,
+ struct oap_cli_ctx * s)
+{
+ struct sec_config * scfg = &s->scfg;
+
+ if (!IS_KEX_ALGO_SET(scfg))
+ return 0;
+
+ 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);
+ }
+
+ return do_client_kex_prepare_dhe(s);
+}
+
+int oap_cli_prepare(void ** ctx,
+ const struct name_info * info,
+ buffer_t * req_buf,
+ 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);
+ assert(req_buf != NULL);
+
+ clrbuf(*req_buf);
+ *ctx = NULL;
+
+ /* Allocate ctx to carry between prepare and complete */
+ s = malloc(sizeof(*s));
+ if (s == NULL) {
+ log_err("Failed to allocate OAP client ctx.");
+ return -ENOMEM;
+ }
+
+ memset(s, 0, sizeof(*s));
+ OAP_CLI_CTX_INIT(s);
+
+ /* 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);
+
+ /* Load client credentials */
+ if (load_cli_credentials(info, &pkp, &crt) < 0) {
+ log_err_id(s->id.data, "Failed to load credentials for %s.",
+ info->name);
+ goto fail_id;
+ }
+
+ /* 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;
+ }
+
+ /* 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) {
+ log_err_id(s->id.data, "Failed to prepare client KEX.");
+ goto fail_kex;
+ }
+
+ 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;
+ }
+
+ debug_oap_hdr_snd(&s->local_hdr);
+
+ /* Compute and store hash of request for verification in complete */
+ 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.");
+ goto fail_hash;
+ }
+ s->req_hash_len = (size_t) ret;
+
+ /* Transfer ownership of request buffer */
+ *req_buf = s->local_hdr.hdr;
+ clrbuf(s->local_hdr.hdr);
+
+ /* oap_hdr_encode repoints id into hdr; restore to __id */
+ s->local_hdr.id = s->id;
+
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+
+ *ctx = s;
+
+ log_dbg_id(s->id.data, "OAP request prepared for %s.", info->name);
+
+ return 0;
+
+ fail_hash:
+ oap_hdr_fini(&s->local_hdr);
+ fail_hdr:
+ crypt_secure_free(s->key, SYMMKEYSZ);
+ crypt_free_key(s->pkp);
+ fail_kex:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ fail_id:
+ free(s);
+ return -ECRYPT;
+}
+
+void oap_ctx_free(void * ctx)
+{
+ struct oap_cli_ctx * s = ctx;
+
+ if (s == NULL)
+ return;
+
+ oap_hdr_fini(&s->local_hdr);
+
+ if (s->pkp != NULL)
+ crypt_free_key(s->pkp);
+
+ if (s->key != NULL)
+ crypt_secure_free(s->key, SYMMKEYSZ);
+
+ memset(s, 0, sizeof(*s));
+ free(s);
+}
+
+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 * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ uint8_t key_buf[SYMMKEYSZ];
+ 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;
+ }
+
+ /* 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;
+
+ 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 = scfg->c.nid;
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
+
+ return 0;
+}
+
+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 * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ uint8_t key_buf[SYMMKEYSZ];
+
+ /* DHE: derive from server's public key */
+ if (peer_hdr->kex.len == 0) {
+ log_err_id(id, "Server did not send DHE public key.");
+ return -ECRYPT;
+ }
+
+ if (kex_dhe_derive(scfg, s->pkp, peer_hdr->kex, key_buf) < 0) {
+ log_err_id(id, "Failed to derive DHE secret.");
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "DHE: derived shared secret.");
+
+ memcpy(sk->key, key_buf, SYMMKEYSZ);
+ sk->nid = scfg->c.nid;
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
+
+ return 0;
+}
+
+
+static int do_client_kex_complete(struct oap_cli_ctx * s,
+ const struct oap_hdr * peer_hdr,
+ struct crypt_sk * sk)
+{
+ struct sec_config * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ int cipher_nid;
+ int kdf_nid;
+
+ if (!IS_KEX_ALGO_SET(scfg))
+ return 0;
+
+ /* Save client's configured minimums */
+ cipher_nid = scfg->c.nid;
+ kdf_nid = scfg->k.nid;
+
+ /* Accept server's cipher choice */
+ if (peer_hdr->cipher_str == NULL) {
+ log_err_id(id, "Server did not provide cipher.");
+ return -ECRYPT;
+ }
+
+ SET_KEX_CIPHER(scfg, peer_hdr->cipher_str);
+ if (crypt_validate_nid(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(scfg->c.nid) < crypt_cipher_rank(cipher_nid)) {
+ log_err_id(id, "Server cipher %s too weak.",
+ peer_hdr->cipher_str);
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "Accepted server cipher %s.",
+ peer_hdr->cipher_str);
+
+ /* Accept server's KDF for non-client-encap modes */
+ 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(scfg, peer_hdr->kdf_nid);
+ log_dbg_id(id, "Accepted server KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ }
+
+ /* Derive shared secret */
+ 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,
+ 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);
+
+ sk->nid = NID_undef;
+
+ clrbuf(*data);
+
+ memset(&peer_hdr, 0, sizeof(peer_hdr));
+
+ id = s->id.data;
+
+ log_dbg_id(id, "Completing OAP for %s.", info->name);
+
+ /* Decode response header using client's md_nid for hash length */
+ 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;
+ }
+
+ debug_oap_hdr_rcv(&peer_hdr);
+
+ /* Verify response ID matches request */
+ if (memcmp(peer_hdr.id.data, id, OAP_ID_SIZE) != 0) {
+ log_err_id(id, "OAP response ID mismatch.");
+ goto fail_oap;
+ }
+
+ /* 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;
+ }
+
+ 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;
+ }
+
+ /* 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) {
+ log_err_id(id, "Peer crt for '%s' does not match '%s'.",
+ peer, info->name);
+ 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 */
+ if (oap_hdr_copy_data(&peer_hdr, data) < 0) {
+ log_err_id(id, "Failed to copy server data.");
+ goto fail_oap;
+ }
+
+ log_info_id(id, "OAP completed for %s.", info->name);
+
+ 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
new file mode 100644
index 00000000..0cff345c
--- /dev/null
+++ b/src/irmd/oap/hdr.c
@@ -0,0 +1,761 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Header encoding, decoding, and debugging
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/hash.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/rib.h>
+#include <ouroboros/time.h>
+
+#include "config.h"
+
+#include "hdr.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <stdlib.h>
+#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,
+ bool rekey)
+{
+ off_t offset;
+ uint16_t kex_len;
+ uint16_t ciph_nid;
+ size_t crt_len;
+ size_t data_len;
+ size_t hash_len;
+ size_t sig_len;
+
+ assert(oap_hdr != NULL);
+ memset(oap_hdr, 0, sizeof(*oap_hdr));
+
+ if (hdr.len < OAP_HDR_MIN_SIZE)
+ goto fail_decode;
+
+ /* Parse fixed header (36 bytes) */
+ oap_hdr->id.data = hdr.data;
+ oap_hdr->id.len = OAP_ID_SIZE;
+
+ offset = OAP_ID_SIZE;
+
+ oap_hdr->timestamp = ntoh64(*(uint64_t *)(hdr.data + offset));
+ offset += sizeof(uint64_t);
+
+ /* cipher NID */
+ ciph_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->nid = ciph_nid;
+ oap_hdr->cipher_str = crypt_nid_to_str(ciph_nid);
+ offset += sizeof(uint16_t);
+
+ /* kdf NID */
+ oap_hdr->kdf_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->kdf_str = md_nid_to_str(oap_hdr->kdf_nid);
+ offset += sizeof(uint16_t);
+
+ /* md NID (signature hash) */
+ oap_hdr->md_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ 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.
+ * 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;
+ if (oap_hdr->md_nid != NID_undef &&
+ md_validate_nid(oap_hdr->md_nid) < 0)
+ goto fail_decode;
+
+ /* crt_len */
+ crt_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
+ offset += sizeof(uint16_t);
+
+ /* kex_len + flags */
+ kex_len = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->kex.len = (size_t) (kex_len & OAP_KEX_LEN_MASK);
+ oap_hdr->kex_flags.fmt = (kex_len & OAP_KEX_FMT_BIT) ? 1 : 0;
+ oap_hdr->kex_flags.role = (kex_len & OAP_KEX_ROLE_BIT) ? 1 : 0;
+ offset += sizeof(uint16_t);
+
+ /* data_len */
+ data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
+ offset += sizeof(uint16_t);
+
+ 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)
+ goto fail_decode;
+
+ /* Derive sig_len from remaining bytes */
+ sig_len = hdr.len - offset - crt_len - oap_hdr->kex.len -
+ data_len - hash_len;
+
+ /*
+ * 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 */
+ oap_hdr->crt.data = hdr.data + offset;
+ oap_hdr->crt.len = crt_len;
+ offset += crt_len;
+
+ oap_hdr->kex.data = hdr.data + offset;
+ offset += oap_hdr->kex.len;
+
+ oap_hdr->data.data = hdr.data + offset;
+ oap_hdr->data.len = data_len;
+ offset += data_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;
+ oap_hdr->sig.len = sig_len;
+
+ oap_hdr->hdr = hdr;
+
+ return 0;
+
+ fail_decode:
+ memset(oap_hdr, 0, sizeof(*oap_hdr));
+ return -1;
+}
+
+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));
+}
+
+int oap_hdr_copy_data(const struct oap_hdr * hdr,
+ buffer_t * out)
+{
+ assert(hdr != NULL);
+ assert(out != NULL);
+
+ if (hdr->data.len == 0) {
+ clrbuf(*out);
+ return 0;
+ }
+
+ out->data = malloc(hdr->data.len);
+ if (out->data == NULL)
+ return -ENOMEM;
+
+ memcpy(out->data, hdr->data.data, hdr->data.len);
+ out->len = hdr->data.len;
+
+ return 0;
+}
+
+void oap_hdr_init(struct oap_hdr * hdr,
+ buffer_t id,
+ uint8_t * kex_buf,
+ buffer_t data,
+ uint16_t nid)
+{
+ assert(hdr != NULL);
+ assert(id.data != NULL && id.len == OAP_ID_SIZE);
+
+ memset(hdr, 0, sizeof(*hdr));
+
+ hdr->id = id;
+ hdr->kex.data = kex_buf;
+ hdr->kex.len = 0;
+ hdr->data = data;
+ 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(scfg->x.str))
+ 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 * scfg,
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key,
+ int flags)
+{
+ struct timespec now;
+ uint64_t stamp;
+ buffer_t out;
+ buffer_t der = BUF_INIT;
+ buffer_t sig = BUF_INIT;
+ buffer_t sign;
+ off_t offset;
+
+ assert(hdr != NULL);
+ assert(hdr->id.data != NULL && hdr->id.len == OAP_ID_SIZE);
+ 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));
+
+ if (crt != NULL && crypt_crt_der(crt, &der) < 0)
+ goto fail_der;
+
+ /* Fixed header (36 bytes) + variable fields + rsp_tag (rsp only) */
+ out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len +
+ rsp_tag.len;
+
+ out.data = malloc(out.len);
+ if (out.data == NULL)
+ goto fail_out;
+
+ 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;
+
+ /* 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);
+
+ /* Sign the entire header (fixed + variable, excluding signature) */
+ sign.data = out.data;
+ sign.len = out.len;
+
+ if (pkp != NULL && auth_sign(pkp, scfg->d.nid, sign, &sig) < 0)
+ goto fail_sig;
+
+ hdr->hdr = out;
+
+ /* Append signature */
+ if (sig.len > 0) {
+ hdr->hdr.len += sig.len;
+ hdr->hdr.data = realloc(out.data, hdr->hdr.len);
+ if (hdr->hdr.data == NULL)
+ goto fail_realloc;
+
+ memcpy(hdr->hdr.data + offset, sig.data, sig.len);
+ }
+
+ /* 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);
+ freebuf(sig);
+
+ return 0;
+
+ fail_decode:
+ oap_hdr_fini(hdr);
+ fail_realloc:
+ freebuf(sig);
+ fail_sig:
+ freebuf(out);
+ fail_out:
+ freebuf(der);
+ fail_der:
+ 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]",
+ hdr->kex.len,
+ hdr->kex_flags.role ?
+ "Client encaps" : "Server encaps");
+ else
+ log_proto(" Key Exchange Data: [%zu bytes]",
+ hdr->kex.len);
+ } else
+ log_proto(" Key Exchange Data: <none>");
+
+ if (hdr->cipher_str != NULL)
+ log_proto(" Cipher: %s", hdr->cipher_str);
+ else
+ log_proto(" Cipher: <none>");
+
+ if (hdr->kdf_str != NULL)
+ log_proto(" KDF: HKDF-%s", hdr->kdf_str);
+ else
+ log_proto(" KDF: <none>");
+
+ if (hdr->md_str != NULL)
+ log_proto(" Digest: %s", hdr->md_str);
+ else
+ log_proto(" Digest: <none>");
+
+ 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->rsp_tag.len > 0)
+ log_proto(" Rsp Tag: [%zu bytes]", hdr->rsp_tag.len);
+ else
+ 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>");
+}
+#endif
+
+void debug_oap_hdr_rcv(const struct oap_hdr * hdr)
+{
+#ifdef DEBUG_PROTO_OAP
+ struct tm * tm;
+ char tmstr[RIB_TM_STRLEN];
+ time_t stamp;
+
+ assert(hdr);
+
+ stamp = (time_t) hdr->timestamp / BILLION;
+
+ tm = gmtime(&stamp);
+ strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
+
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s <--",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
+
+ debug_oap_hdr(hdr);
+#else
+ (void) hdr;
+#endif
+}
+
+void debug_oap_hdr_snd(const struct oap_hdr * hdr)
+{
+#ifdef DEBUG_PROTO_OAP
+ struct tm * tm;
+ char tmstr[RIB_TM_STRLEN];
+ time_t stamp;
+
+ assert(hdr);
+
+ stamp = (time_t) hdr->timestamp / BILLION;
+
+ tm = gmtime(&stamp);
+ strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
+
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s -->",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
+
+ debug_oap_hdr(hdr);
+#else
+ (void) hdr;
+#endif
+}
diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h
new file mode 100644
index 00000000..66fbac9a
--- /dev/null
+++ b/src/irmd/oap/hdr.h
@@ -0,0 +1,199 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Header definitions and functions
+ *
+ * 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_IRMD_OAP_HDR_H
+#define OUROBOROS_IRMD_OAP_HDR_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/utils.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#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_ROLE_BIT 0x4000 /* bit 14: 0=Server encaps, 1=Client encaps */
+#define OAP_KEX_LEN_MASK 0x3FFF /* bits 0-13: Length (0-16383 bytes) */
+
+#define OAP_KEX_ROLE(hdr) (hdr->kex_flags.role)
+#define OAP_KEX_FMT(hdr) (hdr->kex_flags.fmt)
+
+#define OAP_KEX_IS_X509_FMT(hdr) (((hdr)->kex_flags.fmt) == 0)
+#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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | | |
+ * + + |
+ * | | |
+ * + id (128 bits) + |
+ * | Unique flow allocation ID | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + timestamp (64 bits) + |
+ * | UTC nanoseconds since epoch | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | cipher_nid (16 bits) | kdf_nid (16 bits) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | md_nid (16 bits) | crt_len (16 bits) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * |F|R| kex_len (14 bits) | data_len (16 bits) | | Signed
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Region
+ * | | |
+ * + certificate (variable) + |
+ * | X.509 certificate, DER encoded | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + kex_data (variable) + |
+ * | public key (DER/raw) or ciphertext (KEM) | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + data (variable) + |
+ * | Piggybacked application data | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + rsp_tag (variable, response only) + |
+ * | key-confirm tag (enc), else H(request) | |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | |
+ * + signature (variable) +
+ * | 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)
+ *
+ * kex_len field bit layout:
+ * F (bit 15): Format - 0 = X.509 DER, 1 = Raw/Hybrid
+ * R (bit 14): Role - 0 = Server encaps, 1 = Client encaps
+ * (R is ignored for non-KEM algorithms)
+ * Bits 0-13: Length (0-16383 bytes)
+ *
+ * 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 */
+struct oap_hdr {
+ const char * cipher_str;
+ const char * kdf_str;
+ const char * md_str;
+ uint64_t timestamp;
+ uint16_t nid;
+ uint16_t kdf_nid;
+ uint16_t md_nid;
+ struct {
+ bool fmt; /* Format */
+ bool role; /* Role */
+ } kex_flags;
+
+ buffer_t id;
+ buffer_t crt;
+ buffer_t kex;
+ buffer_t data;
+ 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_fini(struct oap_hdr * oap_hdr);
+
+/* oap_hdr_encode option flags */
+#define OAP_ENC_REKEY (1U << 0) /* signed, cert-less re-key packet */
+
+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_decode(struct oap_hdr * hdr,
+ buffer_t buf,
+ int req_md_nid,
+ bool rekey);
+
+/* 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_snd(const struct oap_hdr * hdr);
+
+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
new file mode 100644
index 00000000..4a156723
--- /dev/null
+++ b/src/irmd/oap/internal.h
@@ -0,0 +1,119 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP internal definitions
+ *
+ * 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_IRMD_OAP_INTERNAL_H
+#define OUROBOROS_IRMD_OAP_INTERNAL_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/list.h>
+#include <ouroboros/name.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/utils.h>
+
+#include "hdr.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+int oap_check_hdr(const struct oap_hdr * 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 * scfg);
+
+#ifndef OAP_TEST_MODE
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt);
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg);
+#endif
+
+#ifndef OAP_TEST_MODE
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+
+int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+
+int load_server_kem_keypair(const char * name,
+ struct sec_config * cfg,
+ void ** pkp);
+#else
+extern int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+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,
+ void ** pkp);
+#endif
+
+int do_server_kex(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk);
+
+#ifndef OAP_TEST_MODE
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+
+int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+
+int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk);
+#else
+extern int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+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 * 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 * scfg,
+ void * pkp,
+ uint8_t * key);
+
+#endif /* OUROBOROS_IRMD_OAP_INTERNAL_H */
diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c
new file mode 100644
index 00000000..845723fa
--- /dev/null
+++ b/src/irmd/oap/io.c
@@ -0,0 +1,158 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - File I/O for credentials and configuration
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+
+#include "config.h"
+
+#include "io.h"
+
+#include <assert.h>
+#include <string.h>
+#include <sys/stat.h>
+
+/*
+ * Shared credential and configuration loading helpers
+ */
+
+#ifndef OAP_TEST_MODE
+
+static bool file_exists(const char * path)
+{
+ struct stat s;
+
+ 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;
+}
+
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt)
+{
+ assert(paths != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!file_exists(paths->crt) || !file_exists(paths->key)) {
+ log_info("No authentication certificates for %s.", name);
+ return 0;
+ }
+
+ if (crypt_load_crt_file(paths->crt, crt) < 0) {
+ log_err("Failed to load %s for %s.", paths->crt, name);
+ goto fail_crt;
+ }
+
+ if (crypt_load_privkey_file(paths->key, pkp) < 0) {
+ log_err("Failed to load %s for %s.", paths->key, name);
+ goto fail_key;
+ }
+
+ log_info("Loaded authentication certificates for %s.", name);
+
+ return 0;
+
+ fail_key:
+ crypt_free_crt(*crt);
+ *crt = NULL;
+ fail_crt:
+ return -EAUTH;
+}
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg)
+{
+ void * pin;
+
+ assert(name != NULL);
+ assert(cfg != NULL);
+
+ /* Load security config */
+ if (!file_exists(path))
+ log_dbg("No encryption %s for %s.", path, name);
+
+ if (load_sec_config_file(cfg, path) < 0) {
+ log_warn("Failed to load %s for %s.", path, 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_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 (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;
+ }
+
+ log_info("Encryption enabled for %s.", name);
+
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
diff --git a/src/irmd/oap/io.h b/src/irmd/oap/io.h
new file mode 100644
index 00000000..953e3898
--- /dev/null
+++ b/src/irmd/oap/io.h
@@ -0,0 +1,40 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Credential and configuration file I/O
+ *
+ * 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_IRMD_OAP_IO_H
+#define OUROBOROS_IRMD_OAP_IO_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/name.h>
+
+#ifndef OAP_TEST_MODE
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt);
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg);
+#endif
+
+#endif /* OUROBOROS_IRMD_OAP_IO_H */
diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c
new file mode 100644
index 00000000..d78fc8d4
--- /dev/null
+++ b/src/irmd/oap/srv.c
@@ -0,0 +1,592 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Server-side processing
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+#include "io.h"
+#include "oap.h"
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef OAP_TEST_MODE
+extern int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+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);
+#else
+
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ assert(info != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ return load_credentials(info->name, &info->s, pkp, crt);
+}
+
+int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ assert(info != NULL);
+ assert(cfg != NULL);
+
+ 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)
+{
+ char path[PATH_MAX];
+ const char * ext;
+
+ assert(name != NULL);
+ assert(pkp != NULL);
+
+ 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) {
+ log_err("Failed to load %s keypair from %s.",
+ ext, path);
+ return -ECRYPT;
+ }
+ } else {
+ if (crypt_load_privkey_file(path, pkp) < 0) {
+ log_err("Failed to load %s keypair from %s.",
+ ext, path);
+ return -ECRYPT;
+ }
+ }
+
+ log_dbg("Loaded server KEM keypair from %s.", path);
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
+
+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;
+
+ 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.");
+ return -ECRYPT;
+ }
+ } else {
+ ret = kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf);
+ if (ret < 0) {
+ log_err_id(id, "Failed to get algo from DER key.");
+ return -ECRYPT;
+ }
+ }
+
+ return 0;
+}
+
+static int negotiate_cipher(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg)
+{
+ uint8_t * id = peer_hdr->id.data;
+ int cli_nid;
+ int cli_rank;
+ int srv_rank;
+
+ /* Cipher: select the strongest of client and server */
+ 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) {
+ 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(scfg->c.nid);
+
+ if (cli_rank > srv_rank) {
+ 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.", scfg->c.str);
+ } else {
+ log_err_id(id, "Encryption requested, no cipher.");
+ return -ECRYPT;
+ }
+
+ /* KDF: select the strongest of client and server */
+ if (peer_hdr->kdf_nid != NID_undef
+ && crypt_kdf_rank(peer_hdr->kdf_nid) < 0) {
+ log_err_id(id, "Unsupported KDF NID %d.",
+ peer_hdr->kdf_nid);
+ return -ENOTSUP;
+ }
+
+ cli_rank = crypt_kdf_rank(peer_hdr->kdf_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) {
+ if (srv_rank > cli_rank) {
+ log_err_id(id, "Client KDF too weak.");
+ return -ECRYPT;
+ }
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
+ } else if (cli_rank > srv_rank) {
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
+ log_dbg_id(id, "Selected client KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ } else if (srv_rank > 0) {
+ log_dbg_id(id, "Selected server KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ }
+
+ if (IS_KEX_ALGO_SET(scfg))
+ log_info_id(id, "Negotiated %s + %s.",
+ scfg->x.str, scfg->c.str);
+ else
+ log_info_id(id, "No key exchange.");
+
+ return 0;
+}
+
+static int do_server_kem_decap(const struct name_info * info,
+ const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ struct crypt_sk * sk)
+{
+ buffer_t ct;
+ void * server_pkp = NULL;
+ 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, scfg->k.nid, sk->key);
+
+ crypt_free_key(server_pkp);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to decapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "Client encaps: decapsulated CT.");
+
+ return 0;
+}
+
+static int do_server_kem_encap(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ buffer_t client_pk;
+ ssize_t ct_len;
+ uint8_t * id = peer_hdr->id.data;
+
+ client_pk.data = peer_hdr->kex.data;
+ client_pk.len = peer_hdr->kex.len;
+
+ if (IS_HYBRID_KEM(scfg->x.str))
+ ct_len = kex_kem_encap_raw(client_pk, kex->data,
+ scfg->k.nid, sk->key);
+ else
+ ct_len = kex_kem_encap(client_pk, kex->data,
+ scfg->k.nid, sk->key);
+
+ if (ct_len < 0) {
+ log_err_id(id, "Failed to encapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) ct_len;
+
+ log_dbg_id(id, "Server encaps: generated CT, len=%zd.", ct_len);
+
+ return 0;
+}
+
+static int do_server_kex_kem(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ int ret;
+
+ scfg->x.mode = peer_hdr->kex_flags.role;
+
+ 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, scfg, kex, sk);
+ }
+
+ return ret;
+}
+
+static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ ssize_t key_len;
+ void * epkp;
+ int ret;
+ uint8_t * id = peer_hdr->id.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;
+ }
+
+ kex->len = (size_t) key_len;
+
+ log_dbg_id(id, "Generated %s ephemeral keys.", scfg->x.str);
+
+ 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);
+ return -ECRYPT;
+ }
+
+ kex_pkp_destroy(epkp);
+
+ return 0;
+}
+
+int do_server_kex(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ char algo_buf[KEX_ALGO_BUFSZ];
+ int srv_kex_nid;
+ uint8_t * id;
+
+ id = peer_hdr->id.data;
+
+ /* No KEX data from client */
+ if (peer_hdr->kex.len == 0) {
+ if (IS_KEX_ALGO_SET(scfg)) {
+ log_warn_id(id, "KEX requested without info.");
+ return -ECRYPT;
+ }
+ return 0;
+ }
+
+ if (negotiate_cipher(peer_hdr, scfg) < 0)
+ return -ECRYPT;
+
+ /* Save server's configured KEX before overwriting */
+ 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(scfg, algo_buf);
+
+ /* Reject if client KEX is weaker than server's */
+ if (crypt_kex_rank(scfg->x.nid)
+ < crypt_kex_rank(srv_kex_nid)) {
+ log_err_id(id, "Client KEX %s too weak.",
+ scfg->x.str);
+ return -ECRYPT;
+ }
+ }
+
+ /* Dispatch based on algorithm type */
+ 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, 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,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
+{
+ struct oap_hdr peer_hdr;
+ struct oap_hdr local_hdr;
+ struct sec_config scfg;
+ uint8_t kex_buf[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);
+ assert(data != NULL);
+ assert(sk != NULL);
+
+ sk->nid = NID_undef;
+
+ memset(&peer_hdr, 0, sizeof(peer_hdr));
+ memset(&local_hdr, 0, sizeof(local_hdr));
+ clrbuf(*rsp_buf);
+
+ log_dbg("Processing OAP request for %s.", info->name);
+
+ if (load_srv_credentials(info, &pkp, &crt) < 0) {
+ log_err("Failed to load security keys for %s.", info->name);
+ goto fail_cred;
+ }
+
+ /* 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, rekey) < 0) {
+ log_err("Failed to decode OAP header.");
+ goto fail_auth;
+ }
+
+ debug_oap_hdr_rcv(&peer_hdr);
+
+ id = peer_hdr.id.data; /* Logging */
+
+ ret = oap_check_hdr(&peer_hdr);
+ if (ret == -EREPLAY) {
+ log_warn_id(id, "OAP header failed replay check.");
+ goto fail_replay;
+ }
+ if (ret < 0) {
+ log_err_id(id, "OAP header check failed.");
+ goto fail_auth;
+ }
+
+ oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef);
+
+ 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;
+ }
+
+ /* 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 = scfg.c.nid;
+
+ /* Build response header with hash of client request */
+ local_hdr.nid = sk->nid;
+
+ /* Use client's md_nid, defaulting to SHA-384 for PQC */
+ req_md_nid = peer_hdr.md_nid != NID_undef ?
+ peer_hdr.md_nid : NID_sha384;
+
+ /* Compute request hash using client's md_nid */
+ hash_ret = md_digest(req_md_nid, req_buf, hash_buf);
+ if (hash_ret < 0) {
+ log_err_id(id, "Failed to hash request.");
+ goto fail_auth;
+ }
+ req_hash.data = hash_buf;
+ 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_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;
+ }
+
+ debug_oap_hdr_snd(&local_hdr);
+
+ if (oap_hdr_copy_data(&peer_hdr, data) < 0) {
+ log_err_id(id, "Failed to copy client data.");
+ goto fail_data;
+ }
+
+ /* Transfer ownership of response buffer */
+ *rsp_buf = local_hdr.hdr;
+
+ log_info_id(id, "OAP request processed for %s.", info->name);
+
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+
+ return 0;
+
+ 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:
+ return -EAUTH;
+
+ fail_replay:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ return -EREPLAY;
+
+ fail_kex:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ return -ECRYPT;
+}
diff --git a/src/irmd/oap/tests/CMakeLists.txt b/src/irmd/oap/tests/CMakeLists.txt
new file mode 100644
index 00000000..b534cb72
--- /dev/null
+++ b/src/irmd/oap/tests/CMakeLists.txt
@@ -0,0 +1,64 @@
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
+
+get_filename_component(OAP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY)
+get_filename_component(OAP_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" DIRECTORY)
+get_filename_component(IRMD_SOURCE_DIR "${OAP_SOURCE_DIR}" DIRECTORY)
+get_filename_component(IRMD_BINARY_DIR "${OAP_BINARY_DIR}" DIRECTORY)
+
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
+ # Add new tests here
+ oap_test.c
+)
+
+create_test_sourcelist(${PARENT_DIR}_ml_dsa_tests test_suite_ml_dsa.c
+ # ML-DSA-specific tests
+ oap_test_ml_dsa.c
+)
+
+# OAP test needs io.c compiled with OAP_TEST_MODE
+set(OAP_TEST_SOURCES
+ ${OAP_SOURCE_DIR}/io.c
+ ${OAP_SOURCE_DIR}/hdr.c
+ ${OAP_SOURCE_DIR}/auth.c
+ ${OAP_SOURCE_DIR}/srv.c
+ ${OAP_SOURCE_DIR}/cli.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/common.c
+)
+
+# Regular test executable (ECDSA)
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests} ${OAP_TEST_SOURCES})
+set_source_files_properties(${OAP_TEST_SOURCES}
+ PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE"
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test ouroboros-irm)
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${IRMD_SOURCE_DIR}
+ ${IRMD_BINARY_DIR}
+)
+
+# ML-DSA test executable
+add_executable(${PARENT_DIR}_ml_dsa_test ${${PARENT_DIR}_ml_dsa_tests} ${OAP_TEST_SOURCES})
+set_source_files_properties(${OAP_TEST_SOURCES}
+ TARGET_DIRECTORY ${PARENT_DIR}_ml_dsa_test
+ PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE"
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_ml_dsa_test)
+target_link_libraries(${PARENT_DIR}_ml_dsa_test ouroboros-irm)
+target_include_directories(${PARENT_DIR}_ml_dsa_test PRIVATE
+ ${IRMD_SOURCE_DIR}
+ ${IRMD_BINARY_DIR}
+)
+
+add_dependencies(build_tests ${PARENT_DIR}_test ${PARENT_DIR}_ml_dsa_test)
+
+# Regular tests
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
+
+# ML-DSA tests
+ouroboros_register_tests(TARGET ${PARENT_DIR}_ml_dsa_test TESTS ${${PARENT_DIR}_ml_dsa_tests})
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c
new file mode 100644
index 00000000..16d52c63
--- /dev/null
+++ b/src/irmd/oap/tests/common.c
@@ -0,0 +1,717 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Common test helper functions for OAP 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/.
+ */
+
+#include "common.h"
+
+#include <ouroboros/crypt.h>
+
+#include "oap.h"
+
+#include <string.h>
+#include <stdio.h>
+
+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_KEM_MODE(cfg, test_cfg.srv.kem_mode);
+
+ return 0;
+}
+
+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_KEM_MODE(cfg, test_cfg.cli.kem_mode);
+
+ return 0;
+}
+
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ (void) info;
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!test_cfg.srv.auth)
+ return 0;
+
+ return mock_load_credentials(pkp, crt);
+}
+
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ (void) info;
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!test_cfg.cli.auth)
+ return 0;
+
+ return mock_load_credentials(pkp, crt);
+}
+
+int oap_test_setup(struct oap_test_ctx * ctx,
+ const char * root_ca_str,
+ const char * im_ca_str)
+{
+ memset(ctx, 0, sizeof(*ctx));
+
+ strcpy(ctx->srv.info.name, "test-1.unittest.o7s");
+ strcpy(ctx->cli.info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_crt_str(root_ca_str, &ctx->root_ca) < 0) {
+ printf("Failed to load root CA cert.\n");
+ goto fail_root_ca;
+ }
+
+ if (crypt_load_crt_str(im_ca_str, &ctx->im_ca) < 0) {
+ printf("Failed to load intermediate CA cert.\n");
+ goto fail_im_ca;
+ }
+
+ if (oap_auth_add_ca_crt(ctx->root_ca) < 0) {
+ printf("Failed to add root CA cert to store.\n");
+ goto fail_add_ca;
+ }
+
+ if (oap_auth_add_ca_crt(ctx->im_ca) < 0) {
+ printf("Failed to add intermediate CA cert to store.\n");
+ goto fail_add_ca;
+ }
+
+ return 0;
+
+ fail_add_ca:
+ crypt_free_crt(ctx->im_ca);
+ fail_im_ca:
+ crypt_free_crt(ctx->root_ca);
+ fail_root_ca:
+ oap_auth_fini();
+ fail_init:
+ memset(ctx, 0, sizeof(*ctx));
+ return -1;
+}
+
+void oap_test_teardown(struct oap_test_ctx * ctx)
+{
+ struct crypt_sk res;
+ buffer_t dummy = BUF_INIT;
+
+ if (ctx->cli.state != NULL) {
+ res.key = ctx->cli.key;
+ oap_cli_complete(ctx->cli.state, &ctx->cli.info, 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);
+
+ oap_auth_fini();
+ memset(ctx, 0, sizeof(*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, ctx->rekey);
+}
+
+int oap_srv_process_ctx(struct oap_test_ctx * ctx)
+{
+ struct crypt_sk res = { .nid = NID_undef, .key = ctx->srv.key };
+ int ret;
+
+ ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr,
+ &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;
+
+ return ret;
+}
+
+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,
+ ctx->rekey ? &ctx->cli_crt : NULL,
+ ctx->rekey ? NULL : &ctx->cli_crt);
+ ctx->cli.state = NULL;
+
+ if (ret == 0)
+ ctx->cli.nid = res.nid;
+
+ return ret;
+}
+
+int roundtrip_auth_only(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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 (ctx.cli.nid != NID_undef || ctx.srv.nid != NID_undef) {
+ printf("Cipher should not be set for auth-only.\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 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;
+ struct name_info srv_info;
+ struct crypt_sk res;
+ uint8_t cli_key[SYMMKEYSZ];
+ uint8_t srv_key[SYMMKEYSZ];
+ int cli_nid;
+ int srv_nid;
+ buffer_t req_hdr = BUF_INIT;
+ buffer_t resp_hdr = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ void * cli_state = NULL;
+
+ TEST_START();
+
+ memset(&cli_info, 0, sizeof(cli_info));
+ memset(&srv_info, 0, sizeof(srv_info));
+
+ strcpy(cli_info.name, "test-1.unittest.o7s");
+ strcpy(srv_info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ if (oap_cli_prepare(&cli_state, &cli_info, &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,
+ false, NULL, NULL) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ srv_nid = res.nid;
+
+ res.key = cli_key;
+
+ if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res,
+ NULL, NULL) < 0) {
+ printf("Client complete failed.\n");
+ cli_state = NULL;
+ goto fail_cleanup;
+ }
+
+ cli_nid = res.nid;
+ cli_state = NULL;
+
+ if (memcmp(cli_key, srv_key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ if (cli_nid == NID_undef || srv_nid == NID_undef) {
+ printf("Cipher should be set for kex-only.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(resp_hdr);
+ freebuf(req_hdr);
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ if (cli_state != NULL) {
+ res.key = cli_key;
+ oap_cli_complete(cli_state, &cli_info, resp_hdr, &data,
+ &res, NULL, NULL);
+ }
+ freebuf(resp_hdr);
+ freebuf(req_hdr);
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int corrupted_request(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the request */
+ if (ctx.req_hdr.len > 100) {
+ ctx.req_hdr.data[50] ^= 0xFF;
+ ctx.req_hdr.data[51] ^= 0xAA;
+ ctx.req_hdr.data[52] ^= 0x55;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject corrupted request.\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;
+}
+
+int corrupted_response(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+ struct crypt_sk res;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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;
+ }
+
+ /* Corrupt the response */
+ if (ctx.resp_hdr.len > 100) {
+ ctx.resp_hdr.data[50] ^= 0xFF;
+ ctx.resp_hdr.data[51] ^= 0xAA;
+ ctx.resp_hdr.data[52] ^= 0x55;
+ }
+
+ res.key = ctx.cli.key;
+
+ if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr,
+ &ctx.data, &res, NULL, NULL) == 0) {
+ printf("Client should reject corrupted response.\n");
+ ctx.cli.state = NULL;
+ goto fail_cleanup;
+ }
+
+ ctx.cli.state = NULL;
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int truncated_request(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+ size_t orig_len;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Truncate the request buffer */
+ orig_len = ctx.req_hdr.len;
+ ctx.req_hdr.len = orig_len / 2;
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject truncated request.\n");
+ ctx.req_hdr.len = orig_len;
+ goto fail_cleanup;
+ }
+
+ ctx.req_hdr.len = orig_len;
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h
new file mode 100644
index 00000000..7aead07a
--- /dev/null
+++ b/src/irmd/oap/tests/common.h
@@ -0,0 +1,122 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Common test helper functions for OAP 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/.
+ */
+
+#ifndef IRMD_TESTS_COMMON_H
+#define IRMD_TESTS_COMMON_H
+
+#include <ouroboros/utils.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <test/test.h>
+
+#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) */
+ bool req_auth; /* Require peer authentication */
+};
+
+/* Test configuration - set by each test before running roundtrip */
+extern struct test_cfg {
+ struct test_sec_cfg srv;
+ struct test_sec_cfg cli;
+} test_cfg;
+
+/* Each test file defines this with its own certificates */
+extern int mock_load_credentials(void ** pkp,
+ void ** crt);
+
+/* Per-side test context */
+struct oap_test_side {
+ struct name_info info;
+ struct flow_info flow;
+ uint8_t key[SYMMKEYSZ];
+ int nid;
+ void * state;
+};
+
+/* Test context - holds all common state for OAP tests */
+struct oap_test_ctx {
+ struct oap_test_side srv;
+ struct oap_test_side cli;
+
+ buffer_t req_hdr;
+ buffer_t resp_hdr;
+ 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,
+ const char * root_ca_str,
+ const char * im_ca_str);
+
+void oap_test_teardown(struct oap_test_ctx * ctx);
+
+int oap_cli_prepare_ctx(struct oap_test_ctx * ctx);
+
+int oap_srv_process_ctx(struct oap_test_ctx * ctx);
+
+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,
+ const char * im_ca_str);
+
+int corrupted_response(const char * root_ca,
+ const char * im_ca_str);
+
+int truncated_request(const char * root_ca,
+ const char * im_ca_str);
+
+#endif /* IRMD_TESTS_COMMON_H */
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
new file mode 100644
index 00000000..b24bb786
--- /dev/null
+++ b/src/irmd/oap/tests/oap_test.c
@@ -0,0 +1,2109 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests of Ouroboros Allocation Protocol (OAP)
+ *
+ * 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__)
+ #ifndef _DEFAULT_SOURCE
+ #define _DEFAULT_SOURCE
+ #endif
+#else
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include "config.h"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <ouroboros/random.h>
+#include <ouroboros/time.h>
+
+#include <test/test.h>
+#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
+
+extern const uint16_t kex_supported_nids[];
+extern const uint16_t md_supported_nids[];
+
+struct test_cfg test_cfg;
+
+/* Mock load - called by load_*_credentials in common.c */
+int mock_load_credentials(void ** pkp,
+ void ** crt)
+{
+ *crt = NULL;
+
+ if (crypt_load_privkey_str(server_pkp_ec, pkp) < 0)
+ goto fail_privkey;
+
+ if (crypt_load_crt_str(signed_server_crt_ec, crt) < 0)
+ goto fail_crt;
+
+ return 0;
+
+ fail_crt:
+ crypt_free_key(*pkp);
+ fail_privkey:
+ *pkp = NULL;
+ return -1;
+}
+
+/* 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)
+{
+ return -1;
+}
+
+int load_server_kem_pk(__attribute__((unused)) const char * name,
+ __attribute__((unused)) struct sec_config * cfg,
+ __attribute__((unused)) buffer_t * pk)
+{
+ return -1;
+}
+
+static void test_default_cfg(void)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: X25519, AES-256-GCM, SHA-256, with 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 = AUTH;
+
+ /* Client: same KEX/cipher/kdf/md, 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;
+}
+
+/* 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();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ TEST_START("(%s)", kex_str);
+
+ test_default_cfg();
+ test_cfg.srv.kex = kex;
+ test_cfg.cli.kex = kex;
+
+ 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;
+ }
+
+ if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip_auth_only(void)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth only, no encryption */
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: no auth, no encryption */
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_rekey(bool srv_auth,
+ bool cli_auth)
+{
+ test_default_cfg();
+ test_cfg.srv.auth = srv_auth;
+ test_cfg.cli.auth = cli_auth;
+
+ return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec,
+ 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_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();
+}
+
+static int test_oap_piggyback_data(void)
+{
+ struct oap_test_ctx ctx;
+ const char * cli_data_str = "client_data";
+ const char * srv_data_str = "server_data";
+ buffer_t srv_data = BUF_INIT;
+
+ TEST_START();
+
+ test_default_cfg();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ /* Client prepares request with piggybacked data */
+ ctx.data.len = strlen(cli_data_str);
+ 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)
+ goto fail_cleanup;
+
+ /* Set server's response data (ctx.data will take cli data) */
+ srv_data.len = strlen(srv_data_str);
+ srv_data.data = (uint8_t *) srv_data_str;
+
+ freebuf(ctx.data);
+ ctx.data.data = srv_data.data;
+ ctx.data.len = srv_data.len;
+ srv_data.data = NULL;
+ srv_data.len = 0;
+
+ if (oap_srv_process_ctx(&ctx) < 0)
+ goto fail_cleanup;
+
+ /* Verify server received client's piggybacked data */
+ if (ctx.data.len != strlen(cli_data_str) ||
+ memcmp(ctx.data.data, cli_data_str, ctx.data.len) != 0) {
+ printf("Server did not receive correct client data.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(ctx.data);
+
+ if (oap_cli_complete_ctx(&ctx) < 0)
+ goto fail_cleanup;
+
+ /* Verify client received server's piggybacked data */
+ if (ctx.data.len != strlen(srv_data_str) ||
+ memcmp(ctx.data.data, srv_data_str, ctx.data.len) != 0) {
+ printf("Client did not receive correct server data.\n");
+ goto fail_cleanup;
+ }
+
+ /* Free the copied data */
+ free(ctx.data.data);
+ ctx.data.data = NULL;
+ ctx.data.len = 0;
+
+ 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:
+ freebuf(srv_data);
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_corrupted_request(void)
+{
+ test_default_cfg();
+ test_cfg.cli.auth = AUTH;
+
+ return corrupted_request(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_corrupted_response(void)
+{
+ test_default_cfg();
+
+ return corrupted_response(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_truncated_request(void)
+{
+ test_default_cfg();
+
+ return truncated_request(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+/* After ID (16), timestamp (8), cipher_nid (2), kdf_nid (2), md (2) */
+#define OAP_CERT_LEN_OFFSET 30
+static int test_oap_inflated_length_field(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t fake;
+
+ 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_CERT_LEN_OFFSET + 2) {
+ printf("Request too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Set cert length to claim more bytes than packet contains */
+ fake = hton16(60000);
+ memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject inflated length field.\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;
+}
+
+/* Attacker claims cert is smaller - causes misparse of subsequent fields */
+static int test_oap_deflated_length_field(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t fake;
+
+ 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_CERT_LEN_OFFSET + 2) {
+ printf("Request too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Set cert length to claim fewer bytes - will misparse rest */
+ fake = hton16(1);
+ memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject deflated length field.\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;
+}
+
+/* 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)
+{
+ struct oap_test_ctx ctx;
+ uint16_t cipher_nid;
+ uint16_t zero = 0;
+
+ 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;
+ }
+
+ /* Tamper: keep cipher_nid but set kex_len=0, truncate KEX data */
+ cipher_nid = hton16(NID_aes_256_gcm);
+ memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &cipher_nid,
+ sizeof(cipher_nid));
+ memcpy(ctx.req_hdr.data + OAP_KEX_LEN_OFFSET, &zero, sizeof(zero));
+ ctx.req_hdr.len = 36; /* Fixed header only, no KEX data */
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject cipher NID without KEX data.\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;
+}
+
+/* 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_enc_noauth_cfg();
+
+ TEST_START("(%s)", label);
+
+ 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;
+ }
+
+ 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 %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:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip_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]);
+
+ /* Skip KEM algorithms - tested in oap_test_ml_dsa */
+ if (IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_roundtrip(kex_supported_nids[i]);
+ }
+
+ return ret;
+}
+
+/* Cipher negotiation - strongest cipher and KDF are selected */
+static int test_oap_cipher_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: AES-128-GCM, SHA-256 */
+ test_cfg.srv.kex = NID_X25519;
+ test_cfg.srv.cipher = NID_aes_128_gcm;
+ test_cfg.srv.kdf = NID_sha256;
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: AES-256-GCM, SHA-512 */
+ test_cfg.cli.kex = NID_X25519;
+ test_cfg.cli.cipher = NID_aes_256_gcm;
+ test_cfg.cli.kdf = NID_sha512;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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;
+ }
+
+ /* Verify: both should have the strongest cipher */
+ if (ctx.srv.nid != NID_aes_256_gcm) {
+ printf("Server cipher mismatch: expected %s, got %s\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.srv.nid));
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid != NID_aes_256_gcm) {
+ printf("Client cipher mismatch: expected %s, got %s\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.cli.nid));
+ goto fail_cleanup;
+ }
+
+ /* Parse response header to check negotiated KDF */
+ if (ctx.resp_hdr.len > 26) {
+ uint16_t resp_kdf_nid;
+ /* KDF NID at offset 26: ID(16) + ts(8) + cipher(2) */
+ resp_kdf_nid = ntoh16(*(uint16_t *)(ctx.resp_hdr.data + 26));
+
+ if (resp_kdf_nid != NID_sha512) {
+ printf("Response KDF mismatch: expected %s, got %s\n",
+ md_nid_to_str(NID_sha512),
+ md_nid_to_str(resp_kdf_nid));
+ 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 encryption, client none: server rejects (no KEX data) */
+static int test_oap_srv_enc_cli_none(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: encryption configured */
+ 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;
+
+ /* Client: no encryption */
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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;
+ }
+
+ /* Server should reject: KEX required but client sent none */
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should have rejected.\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 encryption, server none: use client settings */
+static int test_oap_cli_enc_srv_none(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: no encryption configured */
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: encryption configured */
+ 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;
+
+ 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("Key mismatch.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid != NID_aes_256_gcm) {
+ printf("Expected %s, got %s.\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.cli.nid));
+ goto fail_cleanup;
+ }
+
+ if (ctx.srv.nid != NID_aes_256_gcm) {
+ printf("Expected %s, got %s.\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.srv.nid));
+ 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;
+}
+
+/* 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_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;
+ }
+
+ /* Tamper: replace cipher NID with weaker one */
+ weak = hton16(NID_aes_128_gcm);
+ memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
+ &weak, sizeof(weak));
+
+ /* Client should reject the downgraded cipher */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted downgrade.\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;
+}
+
+/*
+ * 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:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server rejects client with weaker KEX algorithm */
+static int test_oap_srv_rejects_weak_kex(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: secp521r1 (strong) */
+ test_cfg.srv.kex = NID_secp521r1;
+ 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;
+
+ /* Client: ffdhe2048 (weakest) */
+ test_cfg.cli.kex = NID_ffdhe2048;
+ 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;
+
+ 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;
+ }
+
+ /* Server should reject: client KEX too weak */
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject weak 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;
+}
+
+/* Test roundtrip with different signature digest algorithms */
+static int test_oap_roundtrip_md(int md)
+{
+ struct oap_test_ctx ctx;
+ const char * md_str = md_nid_to_str(md);
+
+ TEST_START("(%s)", md_str ? md_str : "default");
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth + KEX with specified md */
+ test_cfg.srv.kex = NID_X25519;
+ test_cfg.srv.cipher = NID_aes_256_gcm;
+ test_cfg.srv.kdf = NID_sha256;
+ test_cfg.srv.md = md;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: 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 = md;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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("(%s)", md_str ? md_str : "default");
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", md_str ? md_str : "default");
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip_md_all(void)
+{
+ int ret = 0;
+ int i;
+
+ /* Test with default */
+ ret |= test_oap_roundtrip_md(NID_undef);
+
+ /* Test with all supported digest NIDs */
+ for (i = 0; md_supported_nids[i] != NID_undef; i++)
+ ret |= test_oap_roundtrip_md(md_supported_nids[i]);
+
+ return ret;
+}
+
+/* Timestamp is at offset 16 (after the 16-byte ID) */
+#define OAP_TIMESTAMP_OFFSET 16
+/* Test that packets with outdated timestamps are rejected */
+/* 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 ts;
+ uint64_t stamp;
+
+ test_default_cfg();
+
+ TEST_START("(%s)", label);
+
+ 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;
+ }
+
+ 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 %s packet.\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_ts_reject_all(void)
+{
+ int ret = 0;
+
+ /* 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");
+
+ return ret;
+}
+
+/* Test that replayed packets (same ID + timestamp) are rejected */
+static int test_oap_replay_packet(void)
+{
+ struct oap_test_ctx ctx;
+ buffer_t saved_req;
+
+ 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;
+ }
+
+ /* Save the request for replay */
+ saved_req.len = ctx.req_hdr.len;
+ saved_req.data = malloc(saved_req.len);
+ if (saved_req.data == NULL) {
+ printf("Failed to allocate saved request.\n");
+ goto fail_cleanup;
+ }
+ memcpy(saved_req.data, ctx.req_hdr.data, saved_req.len);
+
+ /* First request should succeed */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("First request should succeed.\n");
+ free(saved_req.data);
+ goto fail_cleanup;
+ }
+
+ /* Free response from first request before replay */
+ freebuf(ctx.resp_hdr);
+
+ /* Restore the saved request for replay */
+ freebuf(ctx.req_hdr);
+ ctx.req_hdr = saved_req;
+
+ /* Replay must return -EREPLAY so callers can drop silently. */
+ if (oap_srv_process_ctx(&ctx) != -EREPLAY) {
+ printf("Replayed packet rejection != -EREPLAY.\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;
+}
+
+/* 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)
+{
+ struct oap_test_ctx ctx;
+ void * im_ca = NULL;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ memset(&ctx, 0, sizeof(ctx));
+
+ strcpy(ctx.srv.info.name, "test-1.unittest.o7s");
+ strcpy(ctx.cli.info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ /* Load intermediate CA but intentionally omit the root CA */
+ if (crypt_load_crt_str(im_ca_crt_ec, &im_ca) < 0) {
+ printf("Failed to load intermediate CA cert.\n");
+ goto fail_fini;
+ }
+
+ ctx.im_ca = im_ca;
+
+ if (oap_auth_add_ca_crt(im_ca) < 0) {
+ printf("Failed to add intermediate CA cert to store.\n");
+ goto fail_fini;
+ }
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_fini;
+ }
+
+ /* Server processes and signs response - succeeds without root CA */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_teardown;
+ }
+
+ /* Client verifies server certificate against trust store:
+ * should reject because root CA is not in the store */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject without root CA.\n");
+ goto fail_teardown;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_teardown:
+ oap_test_teardown(&ctx);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+ fail_fini:
+ crypt_free_crt(im_ca);
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Test that client rejects server with wrong certificate name */
+static int test_oap_server_name_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ /* Set client's expected name to something different from cert name */
+ strcpy(ctx.cli.info.name, "wrong.server.name");
+
+ 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;
+ }
+
+ /* Client should reject due to name mismatch */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject server with wrong cert name.\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 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:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int oap_test(int argc,
+ char **argv)
+{
+ int ret = 0;
+
+ (void) 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_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_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_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
+ crypt_cleanup();
+
+ return ret;
+}
diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c
new file mode 100644
index 00000000..36712830
--- /dev/null
+++ b/src/irmd/oap/tests/oap_test_ml_dsa.c
@@ -0,0 +1,777 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests of OAP ML-KEM/ML-DSA key exchange
+ *
+ * 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 "config.h"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <ouroboros/random.h>
+#include <test/test.h>
+
+#include <test/certs/ml_dsa.h>
+
+#include "oap.h"
+#include "common.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+#ifdef HAVE_OPENSSL
+#include <openssl/evp.h>
+#endif
+
+#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[];
+
+static int get_random_kdf(void)
+{
+ static int idx = 0;
+ int count;
+
+ if (md_supported_nids[0] == NID_undef)
+ return NID_undef;
+
+ for (count = 0; md_supported_nids[count] != NID_undef; count++)
+ ;
+
+ return md_supported_nids[(idx++) % count];
+}
+
+struct test_cfg test_cfg;
+
+/* KEM keypair storage for tests (server-side keypair for KEM modes) */
+static void * test_kem_pkp = NULL; /* Private key pair */
+static uint8_t test_kem_pk[4096]; /* Public key buffer */
+static size_t test_kem_pk_len = 0;
+
+/* Mock load - called by load_*_credentials in common.c */
+int mock_load_credentials(void ** pkp,
+ void ** crt)
+{
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (crypt_load_privkey_str(server_pkp_ml, pkp) < 0)
+ return -1;
+
+ if (crypt_load_crt_str(signed_server_crt_ml, crt) < 0) {
+ crypt_free_key(*pkp);
+ *pkp = NULL;
+ return -1;
+ }
+
+ return 0;
+}
+
+int load_server_kem_keypair(const char * name,
+ bool raw_fmt,
+ void ** pkp)
+{
+#ifdef HAVE_OPENSSL
+ struct sec_config local_cfg;
+ ssize_t pk_len;
+
+ (void) name;
+ (void) raw_fmt;
+
+ /*
+ * Uses reference counting. The caller will call
+ * EVP_PKEY_free which decrements the count.
+ */
+ if (test_kem_pkp != NULL) {
+ if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1)
+ return -1;
+
+ *pkp = test_kem_pkp;
+ return 0;
+ }
+
+ /*
+ * Generate a new KEM keypair from test_cfg.srv.kex.
+ */
+ memset(&local_cfg, 0, sizeof(local_cfg));
+ if (test_cfg.srv.kex == NID_undef)
+ goto fail;
+
+ SET_KEX_ALGO_NID(&local_cfg, test_cfg.srv.kex);
+
+ pk_len = kex_pkp_create(&local_cfg, &test_kem_pkp, test_kem_pk);
+ if (pk_len < 0)
+ goto fail;
+
+ test_kem_pk_len = (size_t) pk_len;
+
+ if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1)
+ goto fail_ref;
+
+ *pkp = test_kem_pkp;
+
+ return 0;
+ fail_ref:
+ kex_pkp_destroy(test_kem_pkp);
+ test_kem_pkp = NULL;
+ test_kem_pk_len = 0;
+ fail:
+ return -1;
+
+#else
+ (void) name;
+ (void) raw_fmt;
+ (void) pkp;
+ return -1;
+#endif
+}
+
+int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk)
+{
+ ssize_t len;
+
+ (void) name;
+
+ if (test_kem_pk_len > 0) {
+ 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;
+ return 0;
+ }
+
+ /* Generate keypair on demand if not already done */
+ len = kex_pkp_create(cfg, &test_kem_pkp, test_kem_pk);
+ if (len < 0)
+ return -1;
+
+ test_kem_pk_len = (size_t) len;
+ 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;
+
+ return 0;
+}
+
+static void reset_kem_state(void)
+{
+ if (test_kem_pkp != NULL) {
+ kex_pkp_destroy(test_kem_pkp);
+ test_kem_pkp = NULL;
+ }
+ test_kem_pk_len = 0;
+}
+
+static void test_cfg_init(int kex,
+ int cipher,
+ int kdf,
+ int kem_mode,
+ bool cli_auth)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server config */
+ test_cfg.srv.kex = kex;
+ test_cfg.srv.cipher = cipher;
+ test_cfg.srv.kdf = kdf;
+ test_cfg.srv.kem_mode = kem_mode;
+ test_cfg.srv.auth = true;
+
+ /* Client config */
+ test_cfg.cli.kex = kex;
+ test_cfg.cli.cipher = cipher;
+ test_cfg.cli.kdf = kdf;
+ test_cfg.cli.kem_mode = kem_mode;
+ test_cfg.cli.auth = cli_auth;
+}
+
+static int oap_test_setup_kem(struct oap_test_ctx * ctx,
+ const char * root_ca,
+ const char * im_ca)
+{
+ reset_kem_state();
+ return oap_test_setup(ctx, root_ca, im_ca);
+}
+
+static void oap_test_teardown_kem(struct oap_test_ctx * ctx)
+{
+ oap_test_teardown(ctx);
+}
+
+static int test_oap_roundtrip_auth_only(void)
+{
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, false);
+
+ 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, AUTH);
+
+ return corrupted_request(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_corrupted_response(void)
+{
+ test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
+ SRV_ENCAP, NO_AUTH);
+
+ return corrupted_response(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_truncated_request(void)
+{
+ test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
+ SRV_ENCAP, NO_AUTH);
+
+ return truncated_request(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_roundtrip_kem(int kex,
+ int kem_mode)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(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_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("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;
+ }
+
+ if (ctx.cli.nid == NID_undef ||
+ ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\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_roundtrip_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_roundtrip_kem(kex_supported_nids[i], SRV_ENCAP);
+ ret |= test_oap_roundtrip_kem(kex_supported_nids[i], CLI_ENCAP);
+ }
+
+ 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;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth only, no KEX configured */
+ test_cfg.srv.auth = true;
+
+ /* Client: requests KEM with server-side encapsulation */
+ test_cfg.cli.kex = kex;
+ test_cfg.cli.cipher = NID_aes_256_gcm;
+ test_cfg.cli.kdf = get_random_kdf();
+ test_cfg.cli.kem_mode = SRV_ENCAP;
+ test_cfg.cli.auth = false;
+
+ 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 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;
+ }
+
+ if (ctx.cli.nid == NID_undef ||
+ ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\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_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ const char * algo;
+
+ algo = kex_nid_to_str(kex_supported_nids[i]);
+
+ if (!IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_kem_srv_uncfg(kex_supported_nids[i]);
+ }
+
+ return ret;
+}
+
+int oap_test_ml_dsa(int argc,
+ char **argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#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_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_corrupted_request;
+ (void) test_oap_corrupted_response;
+ (void) test_oap_truncated_request;
+
+ ret = TEST_RC_SKIP;
+#endif
+ crypt_cleanup();
+
+ return ret;
+}
diff --git a/src/irmd/reg/CMakeLists.txt b/src/irmd/reg/CMakeLists.txt
deleted file mode 100644
index d3844908..00000000
--- a/src/irmd/reg/CMakeLists.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-if(BUILD_TESTS)
- add_subdirectory(tests)
-endif ()
diff --git a/src/irmd/reg/flow.c b/src/irmd/reg/flow.c
index 4d091b23..8be2dfc7 100644
--- a/src/irmd/reg/flow.c
+++ b/src/irmd/reg/flow.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows
*
@@ -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)
{
@@ -42,6 +44,7 @@ struct reg_flow * reg_flow_create(const struct flow_info * info)
assert(info->n_pid != 0);
assert(info->n_1_pid == 0);
assert(info->mpl == 0);
+ assert(info->mtu == 0);
assert(info->state == FLOW_INIT);
flow = malloc(sizeof(*flow));
@@ -66,11 +69,13 @@ struct reg_flow * reg_flow_create(const struct flow_info * info)
static void destroy_rbuffs(struct reg_flow * flow)
{
if (flow->n_rb != NULL)
- shm_rbuff_destroy(flow->n_rb);
+ ssm_rbuff_destroy(flow->n_rb);
+
flow->n_rb = NULL;
if (flow->n_1_rb != NULL)
- shm_rbuff_destroy(flow->n_1_rb);
+ ssm_rbuff_destroy(flow->n_1_rb);
+
flow->n_1_rb = NULL;
}
@@ -78,9 +83,14 @@ 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->data);
+ clrbuf(flow->req_data);
/* FALLTHRU */
default:
destroy_rbuffs(flow);
@@ -89,8 +99,10 @@ void reg_flow_destroy(struct reg_flow * flow)
assert(flow->n_rb == NULL);
assert(flow->n_1_rb == NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
assert(list_is_empty(&flow->next));
@@ -103,22 +115,28 @@ static int create_rbuffs(struct reg_flow * flow,
assert(flow != NULL);
assert(info != NULL);
- flow->n_rb = shm_rbuff_create(info->n_pid, info->id);
+ flow->n_rb = ssm_rbuff_create(info->n_pid, info->id);
if (flow->n_rb == NULL)
goto fail_n_rb;
+ if (ssm_rbuff_mlock(flow->n_rb) < 0)
+ log_warn("Failed to mlock n_rb for flow %d.", info->id);
+
assert(flow->info.n_1_pid == 0);
assert(flow->n_1_rb == NULL);
flow->info.n_1_pid = info->n_1_pid;
- flow->n_1_rb = shm_rbuff_create(info->n_1_pid, info->id);
+ flow->n_1_rb = ssm_rbuff_create(info->n_1_pid, info->id);
if (flow->n_1_rb == NULL)
goto fail_n_1_rb;
+ if (ssm_rbuff_mlock(flow->n_1_rb) < 0)
+ log_warn("Failed to mlock n_1_rb for flow %d.", info->id);
+
return 0;
fail_n_1_rb:
- shm_rbuff_destroy(flow->n_rb);
+ ssm_rbuff_destroy(flow->n_rb);
fail_n_rb:
return -ENOMEM;
}
@@ -152,6 +170,7 @@ int reg_flow_update(struct reg_flow * flow,
assert(info->mpl != 0);
flow->info.mpl = info->mpl;
+ flow->info.mtu = info->mtu;
if (flow->info.state == FLOW_ALLOC_PENDING)
break;
@@ -172,6 +191,7 @@ int reg_flow_update(struct reg_flow * flow,
}
flow->info.state = info->state;
+ flow->info.uid = info->uid;
*info = flow->info;
@@ -179,30 +199,3 @@ int reg_flow_update(struct reg_flow * flow,
fail:
return -ENOMEM;
}
-
-void reg_flow_set_data(struct reg_flow * flow,
- const buffer_t * buf)
-{
- assert(flow != NULL);
- assert(buf != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
-
- flow->data = *buf;
-}
-
-void reg_flow_get_data(struct reg_flow * flow,
- buffer_t * buf)
-{
- assert(flow != NULL);
- assert(buf != NULL);
-
- *buf = flow->data;
-
- clrbuf(flow->data);
-}
-
-void reg_flow_free_data(struct reg_flow * flow)
-{
- freebuf(flow->data);
-}
diff --git a/src/irmd/reg/flow.h b/src/irmd/reg/flow.h
index d1e4811c..166bed61 100644
--- a/src/irmd/reg/flow.h
+++ b/src/irmd/reg/flow.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows
*
@@ -28,9 +28,10 @@
#include <ouroboros/name.h>
#include <ouroboros/pthread.h>
#include <ouroboros/qos.h>
-#include <ouroboros/shm_rbuff.h>
+#include <ouroboros/ssm_rbuff.h>
#include <ouroboros/utils.h>
+#include <stdbool.h>
#include <sys/types.h>
#include <time.h>
@@ -40,13 +41,32 @@ struct reg_flow {
struct flow_info info;
int response;
- buffer_t data;
+ buffer_t req_data;
+ buffer_t rsp_data;
struct timespec t0;
char name[NAME_SIZE + 1];
- struct shm_rbuff * n_rb;
- struct shm_rbuff * n_1_rb;
+ bool direct;
+
+ /* 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;
};
struct reg_flow * reg_flow_create(const struct flow_info * info);
@@ -56,12 +76,4 @@ void reg_flow_destroy(struct reg_flow * flow);
int reg_flow_update(struct reg_flow * flow,
struct flow_info * info);
-void reg_flow_set_data(struct reg_flow * flow,
- const buffer_t * buf);
-
-void reg_flow_get_data(struct reg_flow * flow,
- buffer_t * buf);
-
-void reg_flow_free_data(struct reg_flow * flow);
-
#endif /* OUROBOROS_IRMD_REG_FLOW_H */
diff --git a/src/irmd/reg/ipcp.c b/src/irmd/reg/ipcp.c
index 474527a4..b193e28f 100644
--- a/src/irmd/reg/ipcp.c
+++ b/src/irmd/reg/ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs
*
@@ -77,7 +77,6 @@ void reg_ipcp_update(struct reg_ipcp * ipcp,
const struct ipcp_info * info)
{
assert(ipcp != NULL);
- assert(info->state != IPCP_NULL);
ipcp->info = *info;
}
diff --git a/src/irmd/reg/ipcp.h b/src/irmd/reg/ipcp.h
index 375973a7..fe9d3bf1 100644
--- a/src/irmd/reg/ipcp.h
+++ b/src/irmd/reg/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs
*
diff --git a/src/irmd/reg/name.c b/src/irmd/reg/name.c
index 4e609711..a3621fc3 100644
--- a/src/irmd/reg/name.c
+++ b/src/irmd/reg/name.c
@@ -1,6 +1,6 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names
*
@@ -69,9 +69,9 @@ struct reg_name * reg_name_create(const struct name_info * info)
memset(name, 0, sizeof(*name));
list_head_init(&name->next);
- list_head_init(&name->progs.list);
- list_head_init(&name->procs.list);
- list_head_init(&name->active.list);
+ llist_init(&name->progs);
+ llist_init(&name->procs);
+ llist_init(&name->active);
name->info = *info;
@@ -87,13 +87,9 @@ void reg_name_destroy(struct reg_name * name)
assert(list_is_empty(&name->next));
- assert(name->progs.len == 0);
- assert(name->procs.len == 0);
- assert(name->active.len == 0);
-
- assert(list_is_empty(&name->progs.list));
- assert(list_is_empty(&name->procs.list));
- assert(list_is_empty(&name->active.list));
+ assert(llist_is_empty(&name->progs));
+ assert(llist_is_empty(&name->procs));
+ assert(llist_is_empty(&name->active));
free(name);
}
@@ -106,7 +102,7 @@ static struct proc_entry * __reg_name_get_active(const struct reg_name * name,
assert(name != NULL);
assert(pid > 0);
- list_for_each(p, &name->active.list) {
+ llist_for_each(p, &name->active) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid)
@@ -122,13 +118,12 @@ static void __reg_name_del_all_active(struct reg_name * name,
struct list_head * p;
struct list_head * h;
- list_for_each_safe(p, h, &name->active.list) {
+ llist_for_each_safe(p, h, &name->active) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid) {
- list_del(&entry->next);
+ llist_del(&entry->next, &name->active);
free(entry);
- --name->active.len;
}
}
}
@@ -141,7 +136,7 @@ static struct proc_entry * __reg_name_get_proc(const struct reg_name * name,
assert(name != NULL);
assert(pid > 0);
- list_for_each(p, &name->procs.list) {
+ llist_for_each(p, &name->procs) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid)
@@ -159,9 +154,11 @@ static struct prog_entry * __reg_name_get_prog(const struct reg_name * name,
assert(name != NULL);
assert(prog != NULL);
- list_for_each(p, &name->progs.list) {
+ 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;
}
@@ -194,17 +191,15 @@ int reg_name_add_active(struct reg_name * name,
switch (name->info.pol_lb) {
case LB_RR: /* Round robin policy. */
- list_add_tail(&entry->next, &name->active.list);
+ llist_add_tail(&entry->next, &name->active);
break;
case LB_SPILL: /* Keep accepting flows on the current process */
- list_add(&entry->next, &name->active.list);
+ llist_add(&entry->next, &name->active);
break;
default:
goto fail_unreachable;
}
- ++name->active.len;
-
return 0;
fail_unreachable:
@@ -223,9 +218,7 @@ void reg_name_del_active(struct reg_name * name,
if (entry == NULL)
return;
- list_del(&entry->next);
-
- --name->active.len;
+ llist_del(&entry->next, &name->active);
free(entry);
}
@@ -236,10 +229,10 @@ pid_t reg_name_get_active(struct reg_name * name)
assert(name != NULL);
- if (list_is_empty(&name->active.list))
+ if (llist_is_empty(&name->active))
return -1;
- e = list_first_entry(&name->active.list, struct proc_entry, next);
+ e = llist_first_entry(&name->active, struct proc_entry, next);
return e->pid;
}
@@ -262,9 +255,7 @@ int reg_name_add_proc(struct reg_name * name,
entry->pid = pid;
- list_add(&entry->next, &name->procs.list);
-
- ++name->procs.len;
+ llist_add(&entry->next, &name->procs);
return 0;
@@ -286,12 +277,10 @@ void reg_name_del_proc(struct reg_name * name,
__reg_name_del_all_active(name, pid);
- list_del(&entry->next);
+ llist_del(&entry->next, &name->procs);
free(entry);
- --name->procs.len;
-
assert(__reg_name_get_proc(name, pid) == NULL);
}
@@ -324,12 +313,10 @@ int reg_name_add_prog(struct reg_name * name,
goto fail_exec;
}
- list_add(&entry->next, &name->progs.list);
+ llist_add(&entry->next, &name->progs);
log_dbg("Add prog %s to name %s.", exec[0], name->info.name);
- ++name->progs.len;
-
return 0;
fail_exec:
@@ -350,12 +337,10 @@ void reg_name_del_prog(struct reg_name * name,
if (entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &name->progs);
__free_prog_entry(entry);
- --name->progs.len;
-
assert(__reg_name_get_prog(name, prog) == NULL);
}
@@ -372,10 +357,10 @@ char ** reg_name_get_exec(const struct reg_name * name)
{
struct prog_entry * e;
- if (list_is_empty(&name->progs.list))
+ if (llist_is_empty(&name->progs))
return NULL;
- e = list_first_entry(&name->progs.list, struct prog_entry, next);
+ e = llist_first_entry(&name->progs, struct prog_entry, next);
return e->exec;
}
diff --git a/src/irmd/reg/name.h b/src/irmd/reg/name.h
index 30a64e1c..59d6d9bd 100644
--- a/src/irmd/reg/name.h
+++ b/src/irmd/reg/name.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names
*
@@ -38,20 +38,11 @@ struct reg_name {
void * crt;
} cache;
- struct {
- struct list_head list;
- size_t len;
- } progs; /* autostart programs for this name */
+ struct llist progs; /* autostart programs for this name */
- struct {
- struct list_head list;
- size_t len;
- } procs; /* processes bound to this name */
+ struct llist procs; /* processes bound to this name */
- struct {
- struct list_head list;
- size_t len;
- } active; /* processes actively calling accept */
+ struct llist active; /* processes actively calling accept */
};
struct reg_name * reg_name_create(const struct name_info * info);
diff --git a/src/irmd/reg/pool.c b/src/irmd/reg/pool.c
new file mode 100644
index 00000000..4b1486bb
--- /dev/null
+++ b/src/irmd/reg/pool.c
@@ -0,0 +1,97 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * The IPC Resource Manager - Registry - Per-User Pools
+ *
+ * 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
+
+#define OUROBOROS_PREFIX "reg/pool"
+
+#include <ouroboros/logs.h>
+#include <ouroboros/ssm_pool.h>
+
+#include "pool.h"
+
+#include <assert.h>
+#include <stdlib.h>
+
+struct reg_pool * reg_pool_create(uid_t uid,
+ gid_t gid)
+{
+ struct reg_pool * pool;
+
+ pool = malloc(sizeof(*pool));
+ if (pool == NULL) {
+ log_err("Failed to malloc pool.");
+ goto fail_malloc;
+ }
+
+ pool->ssm = ssm_pool_create(uid, gid);
+ if (pool->ssm == NULL) {
+ log_err("Failed to create PUP for uid %d.", uid);
+ goto fail_ssm;
+ }
+
+ list_head_init(&pool->next);
+ pool->uid = uid;
+ pool->gid = gid;
+ pool->refcount = 1;
+
+ log_dbg("Created PUP for uid %d gid %d.", uid, gid);
+
+ return pool;
+
+ fail_ssm:
+ free(pool);
+ fail_malloc:
+ return NULL;
+}
+
+void reg_pool_destroy(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount == 0);
+
+ log_dbg("Destroying PUP for uid %d.", pool->uid);
+
+ ssm_pool_destroy(pool->ssm);
+
+ assert(list_is_empty(&pool->next));
+
+ free(pool);
+}
+
+void reg_pool_ref(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount > 0);
+
+ pool->refcount++;
+}
+
+int reg_pool_unref(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount > 0);
+
+ pool->refcount--;
+
+ return pool->refcount == 0 ? 0 : 1;
+}
diff --git a/src/irmd/reg/pool.h b/src/irmd/reg/pool.h
new file mode 100644
index 00000000..576f491c
--- /dev/null
+++ b/src/irmd/reg/pool.h
@@ -0,0 +1,48 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * The IPC Resource Manager - Registry - Per-User Pools
+ *
+ * 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_IRMD_REG_POOL_H
+#define OUROBOROS_IRMD_REG_POOL_H
+
+#include <ouroboros/list.h>
+#include <ouroboros/ssm_pool.h>
+
+#include <sys/types.h>
+
+struct reg_pool {
+ struct list_head next;
+ uid_t uid;
+ gid_t gid;
+ size_t refcount;
+ struct ssm_pool * ssm;
+};
+
+struct reg_pool * reg_pool_create(uid_t uid,
+ gid_t gid);
+
+void reg_pool_destroy(struct reg_pool * pool);
+
+void reg_pool_ref(struct reg_pool * pool);
+
+int reg_pool_unref(struct reg_pool * pool);
+
+#endif /* OUROBOROS_IRMD_REG_POOL_H */
diff --git a/src/irmd/reg/proc.c b/src/irmd/reg/proc.c
index 9bbdf0eb..94ac6b8f 100644
--- a/src/irmd/reg/proc.c
+++ b/src/irmd/reg/proc.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes
*
@@ -25,6 +25,7 @@
#define OUROBOROS_PREFIX "reg/proc"
#include <ouroboros/logs.h>
+#include <ouroboros/utils.h>
#include "proc.h"
@@ -54,12 +55,11 @@ static void __reg_proc_clear_names(struct reg_proc * proc)
assert(proc != NULL);
- list_for_each_safe(p, h, &proc->names) {
+ llist_for_each_safe(p, h, &proc->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &proc->names);
__free_name_entry(entry);
- proc->n_names--;
}
}
@@ -75,17 +75,18 @@ struct reg_proc * reg_proc_create(const struct proc_info * info)
goto fail_malloc;
}
- proc->set = shm_flow_set_create(info->pid);
+ memset(proc, 0, sizeof(*proc));
+
+ proc->set = ssm_flow_set_create(info->pid);
if (proc->set == NULL) {
log_err("Failed to create flow set for %d.", info->pid);
goto fail_set;
}
list_head_init(&proc->next);
- list_head_init(&proc->names);
+ llist_init(&proc->names);
proc->info = *info;
- proc->n_names = 0;
return proc;
@@ -99,15 +100,13 @@ void reg_proc_destroy(struct reg_proc * proc)
{
assert(proc != NULL);
- shm_flow_set_destroy(proc->set);
+ ssm_flow_set_destroy(proc->set);
__reg_proc_clear_names(proc);
assert(list_is_empty(&proc->next));
- assert(proc->n_names == 0);
-
- assert(list_is_empty(&proc->names));
+ assert(llist_is_empty(&proc->names));
free(proc);
}
@@ -117,9 +116,10 @@ static struct name_entry * __reg_proc_get_name(const struct reg_proc * proc,
{
struct list_head * p;
- list_for_each(p, &proc->names) {
+ 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;
}
@@ -141,14 +141,12 @@ 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;
}
- list_add(&entry->next, &proc->names);
-
- proc->n_names++;
+ llist_add(&entry->next, &proc->names);
return 0;
@@ -167,12 +165,10 @@ void reg_proc_del_name(struct reg_proc * proc,
if(entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &proc->names);
__free_name_entry(entry);
- proc->n_names--;
-
assert(__reg_proc_get_name(proc, name) == NULL);
}
@@ -181,3 +177,10 @@ bool reg_proc_has_name(const struct reg_proc * proc,
{
return __reg_proc_get_name(proc, name) != NULL;
}
+
+bool reg_proc_is_privileged(const struct reg_proc * proc)
+{
+ assert(proc != NULL);
+
+ return is_ouroboros_member_uid(proc->info.uid);
+}
diff --git a/src/irmd/reg/proc.h b/src/irmd/reg/proc.h
index 499ecc72..18cc2803 100644
--- a/src/irmd/reg/proc.h
+++ b/src/irmd/reg/proc.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes
*
@@ -25,17 +25,16 @@
#include <ouroboros/list.h>
#include <ouroboros/proc.h>
-#include <ouroboros/shm_flow_set.h>
+#include <ouroboros/ssm_flow_set.h>
struct reg_proc {
struct list_head next;
struct proc_info info;
- struct list_head names; /* process accepts flows for names */
- size_t n_names; /* number of names */
+ struct llist names; /* process accepts flows for names */
- struct shm_flow_set * set;
+ struct ssm_flow_set * set;
};
struct reg_proc * reg_proc_create(const struct proc_info * info);
@@ -53,4 +52,6 @@ void reg_proc_del_name(struct reg_proc * proc,
bool reg_proc_has_name(const struct reg_proc * proc,
const char * name);
+bool reg_proc_is_privileged(const struct reg_proc * proc);
+
#endif /* OUROBOROS_IRMD_REG_PROC_H */
diff --git a/src/irmd/reg/prog.c b/src/irmd/reg/prog.c
index 9b9e7510..1e977c89 100644
--- a/src/irmd/reg/prog.c
+++ b/src/irmd/reg/prog.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs
*
@@ -55,12 +55,11 @@ static void __reg_prog_clear_names(struct reg_prog * prog)
assert(prog != NULL);
- list_for_each_safe(p, h, &prog->names) {
+ llist_for_each_safe(p, h, &prog->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &prog->names);
__free_name_entry(entry);
- prog->n_names--;
}
}
@@ -77,10 +76,9 @@ struct reg_prog * reg_prog_create(const struct prog_info * info)
}
list_head_init(&p->next);
- list_head_init(&p->names);
+ llist_init(&p->names);
p->info = *info;
- p->n_names = 0;
return p;
@@ -96,9 +94,7 @@ void reg_prog_destroy(struct reg_prog * prog)
assert(list_is_empty(&prog->next));
- assert(prog->n_names == 0);
-
- assert(list_is_empty(&prog->names));
+ assert(llist_is_empty(&prog->names));
free(prog);
}
@@ -108,9 +104,10 @@ static struct name_entry * __reg_prog_get_name(const struct reg_prog * prog,
{
struct list_head * p;
- list_for_each(p, &prog->names) {
+ 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;
}
@@ -132,14 +129,12 @@ 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;
}
- list_add(&entry->next, &prog->names);
-
- prog->n_names++;
+ llist_add(&entry->next, &prog->names);
return 0;
@@ -158,12 +153,10 @@ void reg_prog_del_name(struct reg_prog * prog,
if (entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &prog->names);
__free_name_entry(entry);
- prog->n_names--;
-
assert(__reg_prog_get_name(prog, name) == NULL);
}
diff --git a/src/irmd/reg/prog.h b/src/irmd/reg/prog.h
index a98fc6a1..e52b8e15 100644
--- a/src/irmd/reg/prog.h
+++ b/src/irmd/reg/prog.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs
*
@@ -33,8 +33,7 @@ struct reg_prog {
struct prog_info info;
- struct list_head names; /* names to listen for */
- size_t n_names; /* number of names in list */
+ struct llist names; /* names to listen for */
};
struct reg_prog * reg_prog_create(const struct prog_info * info);
diff --git a/src/irmd/reg/reg.c b/src/irmd/reg/reg.c
index a24a9d1d..ebf3959d 100644
--- a/src/irmd/reg/reg.c
+++ b/src/irmd/reg/reg.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
The IPC Resource Manager - Registry
*
@@ -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>
@@ -35,6 +36,7 @@ The IPC Resource Manager - Registry
#include "flow.h"
#include "ipcp.h"
#include "name.h"
+#include "pool.h"
#include "proc.h"
#include "prog.h"
@@ -46,28 +48,18 @@ The IPC Resource Manager - Registry
#define ID_OFFT 1 /* reserve some flow_ids */
struct {
- struct bmp * flow_ids; /* flow_ids for flows */
-
- struct list_head flows; /* flow information */
- size_t n_flows; /* number of flows */
-
- struct list_head ipcps; /* list of ipcps in system */
- size_t n_ipcps; /* number of ipcps */
-
- struct list_head names; /* registered names known */
- size_t n_names; /* number of names */
-
- struct list_head procs; /* processes */
- size_t n_procs; /* number of processes */
-
- struct list_head progs; /* programs known */
- size_t n_progs; /* number of programs */
-
- struct list_head spawned; /* child processes */
- size_t n_spawned; /* number of child processes */
-
- pthread_mutex_t mtx; /* registry lock */
- pthread_cond_t cond; /* condvar for reg changes */
+ struct bmp * ids; /* flow bitmap */
+
+ struct llist flows; /* list of flows */
+ struct llist ipcps; /* list of ipcps in system */
+ struct llist names; /* registered names known */
+ struct llist pools; /* per-user pools */
+ struct llist procs; /* processes known */
+ struct llist progs; /* programs known */
+ struct llist spawned; /* child processes */
+
+ pthread_mutex_t mtx; /* registry lock */
+ pthread_cond_t cond; /* condvar for reg changes */
} reg;
struct pid_entry {
@@ -81,7 +73,7 @@ static struct reg_flow * __reg_get_flow(int flow_id)
assert(flow_id >= ID_OFFT);
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.id == flow_id)
@@ -95,7 +87,7 @@ static struct reg_flow * __reg_get_accept_flow(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.state != FLOW_ACCEPT_PENDING)
@@ -113,7 +105,7 @@ static struct list_head * __reg_after_flow(int flow_id)
assert(flow_id >= ID_OFFT);
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.id > flow_id)
@@ -129,7 +121,7 @@ static struct reg_ipcp * __reg_get_ipcp(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (entry->info.pid == pid)
@@ -143,7 +135,7 @@ static struct reg_ipcp * __reg_get_ipcp_by_layer(const char * layer)
{
struct list_head * p;
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (strcmp(entry->layer.name, layer) == 0)
@@ -160,7 +152,7 @@ static struct list_head * __reg_after_ipcp(const struct ipcp_info * info)
assert(info != NULL);
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (entry->info.type < info->type)
@@ -182,7 +174,7 @@ static struct reg_name * __reg_get_name(const char * name)
assert(name != NULL);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
if (strcmp(entry->info.name, name) == 0)
@@ -225,7 +217,7 @@ static struct list_head * __reg_after_name(const char * name)
assert(name != NULL);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
if (strcmp(entry->info.name, name) > 0)
@@ -235,11 +227,25 @@ static struct list_head * __reg_after_name(const char * name)
return p;
}
+static struct reg_pool * __reg_get_pool(uid_t uid)
+{
+ struct list_head * p;
+
+ llist_for_each(p, &reg.pools) {
+ struct reg_pool * entry;
+ entry = list_entry(p, struct reg_pool, next);
+ if (entry->uid == uid)
+ return entry;
+ }
+
+ return NULL;
+}
+
static struct reg_proc * __reg_get_proc(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (entry->info.pid == pid)
@@ -253,7 +259,7 @@ static struct list_head * __reg_after_proc(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (entry->info.pid > pid)
@@ -267,7 +273,7 @@ static void __reg_kill_all_proc(int signal)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
kill(entry->info.pid, signal);
@@ -278,7 +284,7 @@ static pid_t __reg_get_dead_proc(void)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (kill(entry->info.pid, 0) < 0)
@@ -293,7 +299,7 @@ static void __reg_cancel_flows_for_proc(pid_t pid)
struct list_head * p;
bool changed = false;
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.n_pid != pid)
@@ -319,7 +325,7 @@ static struct pid_entry * __reg_get_spawned(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
if (entry->pid == pid)
@@ -333,7 +339,7 @@ static struct list_head * __reg_after_spawned(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
if (entry->pid > pid)
@@ -347,7 +353,7 @@ static void __reg_kill_all_spawned(int signal)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
kill(entry->pid, signal);
@@ -356,17 +362,17 @@ static void __reg_kill_all_spawned(int signal)
static pid_t __reg_first_spawned(void)
{
- if (list_is_empty(&reg.spawned))
+ if (llist_is_empty(&reg.spawned))
return -1;
- return list_first_entry(&reg.spawned, struct pid_entry, next)->pid;
+ return llist_first_entry(&reg.spawned, struct pid_entry, next)->pid;
}
static struct reg_prog * __reg_get_prog(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
if (strcmp(entry->info.name, name) == 0)
@@ -380,7 +386,7 @@ static char ** __reg_get_exec(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
if (strcmp(entry->info.name, name) == 0)
@@ -394,7 +400,7 @@ static struct list_head * __reg_after_prog(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
if (strcmp(entry->info.name, name) > 0)
@@ -408,7 +414,7 @@ static void __reg_del_name_from_procs(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * proc;
proc = list_entry(p, struct reg_proc, next);
reg_proc_del_name(proc, name);
@@ -419,7 +425,7 @@ static void __reg_del_name_from_progs(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * prog;
prog = list_entry(p, struct reg_prog, next);
reg_prog_del_name(prog, name);
@@ -431,13 +437,13 @@ static void __reg_proc_update_names(struct reg_proc * proc)
struct list_head * p;
struct reg_prog * prog;
- assert(list_is_empty(&proc->names));
+ assert(llist_is_empty(&proc->names));
prog = __reg_get_prog(proc->info.prog);
if (prog == NULL)
return;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
assert(!reg_name_has_proc(name, proc->info.pid));
@@ -452,7 +458,7 @@ static void __reg_del_proc_from_names(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_proc(name, pid);
@@ -463,7 +469,7 @@ static void __reg_del_prog_from_names(const char * prog)
{
struct list_head * p;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_prog(name, prog);
@@ -478,7 +484,7 @@ static int __reg_add_active_proc(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
if (reg_name_has_proc(name, pid)) {
@@ -500,7 +506,7 @@ static void __reg_del_active_proc(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_active(name, pid);
@@ -529,20 +535,21 @@ int reg_init(void)
goto fail_cond;
}
- reg.flow_ids = bmp_create(SYS_MAX_FLOWS -ID_OFFT, ID_OFFT);
- if (reg.flow_ids == NULL) {
+ reg.ids = bmp_create(SYS_MAX_FLOWS - ID_OFFT, ID_OFFT);
+ if (reg.ids == NULL) {
log_err("Failed to create flow_ids bitmap.");
goto fail_flow_ids;
}
pthread_condattr_destroy(&cattr);
- list_head_init(&reg.flows);
- list_head_init(&reg.ipcps);
- list_head_init(&reg.names);
- list_head_init(&reg.procs);
- list_head_init(&reg.progs);
- list_head_init(&reg.spawned);
+ llist_init(&reg.flows);
+ llist_init(&reg.ipcps);
+ llist_init(&reg.names);
+ llist_init(&reg.pools);
+ llist_init(&reg.procs);
+ llist_init(&reg.progs);
+ llist_init(&reg.spawned);
return 0;
@@ -563,54 +570,56 @@ void reg_clear(void)
pthread_mutex_lock(&reg.mtx);
- list_for_each_safe(p, h, &reg.spawned) {
+ llist_for_each_safe(p, h, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.spawned);
free(entry);
- reg.n_spawned--;
}
- list_for_each_safe(p, h, &reg.progs) {
+ llist_for_each_safe(p, h, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.progs);
__reg_del_prog_from_names(entry->info.path);
reg_prog_destroy(entry);
- reg.n_progs--;
}
- list_for_each_safe(p, h, &reg.procs) {
+ llist_for_each_safe(p, h, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.procs);
__reg_del_proc_from_names(entry->info.pid);
reg_proc_destroy(entry);
- reg.n_procs--;
}
- list_for_each_safe(p, h, &reg.flows) {
+ llist_for_each_safe(p, h, &reg.pools) {
+ struct reg_pool * entry;
+ entry = list_entry(p, struct reg_pool, next);
+ llist_del(&entry->next, &reg.pools);
+ entry->refcount = 0; /* Force destroy during cleanup */
+ reg_pool_destroy(entry);
+ }
+
+ llist_for_each_safe(p, h, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.flows);
reg_flow_destroy(entry);
- reg.n_flows--;
}
- list_for_each_safe(p, h, &reg.names) {
+ llist_for_each_safe(p, h, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.names);
reg_name_destroy(entry);
- reg.n_names--;
}
- list_for_each_safe(p, h, &reg.ipcps) {
+ llist_for_each_safe(p, h, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.ipcps);
reg_ipcp_destroy(entry);
- reg.n_ipcps--;
}
pthread_mutex_unlock(&reg.mtx);
@@ -618,21 +627,15 @@ void reg_clear(void)
void reg_fini(void)
{
- assert(list_is_empty(&reg.spawned));
- assert(list_is_empty(&reg.progs));
- assert(list_is_empty(&reg.procs));
- assert(list_is_empty(&reg.names));
- assert(list_is_empty(&reg.ipcps));
- assert(list_is_empty(&reg.flows));
-
- assert(reg.n_spawned == 0);
- assert(reg.n_progs == 0);
- assert(reg.n_procs == 0);
- assert(reg.n_names == 0);
- assert(reg.n_ipcps == 0);
- assert(reg.n_flows == 0);
+ assert(llist_is_empty(&reg.spawned));
+ assert(llist_is_empty(&reg.progs));
+ assert(llist_is_empty(&reg.procs));
+ assert(llist_is_empty(&reg.pools));
+ assert(llist_is_empty(&reg.names));
+ assert(llist_is_empty(&reg.ipcps));
+ assert(llist_is_empty(&reg.flows));
- bmp_destroy(reg.flow_ids);
+ bmp_destroy(reg.ids);
if (pthread_cond_destroy(&reg.cond) != 0)
log_warn("Failed to destroy condvar.");
@@ -652,8 +655,8 @@ int reg_create_flow(struct flow_info * info)
pthread_mutex_lock(&reg.mtx);
- info->id = bmp_allocate(reg.flow_ids);
- if (!bmp_is_id_valid(reg.flow_ids, info->id)) {
+ info->id = bmp_allocate(reg.ids);
+ if (!bmp_is_id_valid(reg.ids, info->id)) {
log_err("Failed to allocate flow id.");
goto fail_id;
}
@@ -664,16 +667,14 @@ int reg_create_flow(struct flow_info * info)
goto fail_flow;
}
- list_add(&f->next, __reg_after_flow(info->id));
-
- reg.n_flows++;
+ llist_add_at(&f->next, __reg_after_flow(info->id), &reg.flows);
pthread_mutex_unlock(&reg.mtx);
return 0;
fail_flow:
- bmp_release(reg.flow_ids, info->id);
+ bmp_release(reg.ids, info->id);
info->id = 0;
fail_id:
pthread_mutex_unlock(&reg.mtx);
@@ -692,11 +693,9 @@ int reg_destroy_flow(int flow_id)
goto no_flow;
}
- list_del(&f->next);
-
- reg.n_flows--;
+ llist_del(&f->next, &reg.flows);
- bmp_release(reg.flow_ids, flow_id);
+ bmp_release(reg.ids, flow_id);
pthread_mutex_unlock(&reg.mtx);
@@ -755,11 +754,10 @@ int reg_create_ipcp(const struct ipcp_info * info)
entry->pid = info->pid;
- list_add_tail(&ipcp->next, __reg_after_ipcp(info));
- list_add(&entry->next, __reg_after_spawned(info->pid));
-
- reg.n_ipcps++;
- reg.n_spawned++;
+ llist_add_tail_at(&ipcp->next, __reg_after_ipcp(info), &reg.ipcps);
+ llist_add_at(&entry->next,
+ __reg_after_spawned(info->pid),
+ &reg.spawned);
pthread_mutex_unlock(&reg.mtx);
@@ -849,16 +847,16 @@ int reg_list_ipcps(ipcp_list_msg_t *** ipcps)
pthread_mutex_lock(&reg.mtx);
- if (reg.n_ipcps == 0)
+ if (llist_is_empty(&reg.ipcps))
goto finish;
- *ipcps = malloc(reg.n_ipcps * sizeof(**ipcps));
+ *ipcps = malloc(reg.ipcps.len * sizeof(**ipcps));
if (*ipcps == NULL) {
log_err("Failed to malloc ipcps.");
goto fail_malloc;
}
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (__get_ipcp_info(&(*ipcps)[i], entry) < 0)
@@ -874,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);
@@ -900,9 +899,7 @@ int reg_create_name(const struct name_info * info)
goto fail_name;
}
- list_add(&n->next, __reg_after_name(info->name));
-
- reg.n_names++;
+ llist_add_at(&n->next, __reg_after_name(info->name), &reg.names);
pthread_mutex_unlock(&reg.mtx);
return 0;
@@ -931,9 +928,7 @@ int reg_destroy_name(const char * name)
__reg_del_name_from_procs(name);
__reg_del_name_from_progs(name);
- list_del(&n->next);
-
- reg.n_names--;
+ llist_del(&n->next, &reg.names);
pthread_mutex_unlock(&reg.mtx);
@@ -1004,7 +999,7 @@ int reg_get_name_for_hash(char * buf,
pthread_mutex_lock(&reg.mtx);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * n = list_entry(p, struct reg_name, next);
str_hash(algo, thash, n->info.name);
if (memcmp(thash, hash, len) == 0) {
@@ -1039,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;
@@ -1046,16 +1055,16 @@ int reg_list_names(name_info_msg_t *** names)
pthread_mutex_lock(&reg.mtx);
- if (reg.n_names == 0)
+ if (llist_is_empty(&reg.names))
goto finish;
- *names = malloc(reg.n_names * sizeof(**names));
+ *names = malloc(reg.names.len * sizeof(**names));
if (*names == NULL) {
log_err("Failed to malloc names.");
goto fail_malloc;
}
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
(*names)[i] = name_info_s_to_msg(&entry->info);
@@ -1083,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);
@@ -1090,6 +1100,34 @@ int reg_list_names(name_info_msg_t *** names)
return -ENOMEM;
}
+int reg_prepare_pool(uid_t uid,
+ gid_t gid)
+{
+ struct reg_pool * pool;
+
+ if (is_ouroboros_member_uid(uid))
+ return 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ pool = __reg_get_pool(uid);
+ if (pool == NULL) {
+ pool = reg_pool_create(uid, gid);
+ if (pool == NULL) {
+ log_err("Failed to create pool for uid %d.", uid);
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+ }
+ llist_add(&pool->next, &reg.pools);
+ }
+
+ reg_pool_ref(pool);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+}
+
int reg_create_proc(const struct proc_info * info)
{
struct reg_proc * proc;
@@ -1100,20 +1138,18 @@ int reg_create_proc(const struct proc_info * info)
if (__reg_get_proc(info->pid) != NULL) {
log_err("Process %d already exists.", info->pid);
- goto fail_proc;
+ goto fail;
}
proc = reg_proc_create(info);
if (proc == NULL) {
log_err("Failed to create process %d.", info->pid);
- goto fail_proc;
+ goto fail;
}
__reg_proc_update_names(proc);
- list_add(&proc->next, __reg_after_proc(info->pid));
-
- reg.n_procs++;
+ llist_add_at(&proc->next, __reg_after_proc(info->pid), &reg.procs);
pthread_cond_broadcast(&reg.cond);
@@ -1121,7 +1157,7 @@ int reg_create_proc(const struct proc_info * info)
return 0;
- fail_proc:
+ fail:
pthread_mutex_unlock(&reg.mtx);
return -1;
}
@@ -1129,6 +1165,7 @@ int reg_create_proc(const struct proc_info * info)
int reg_destroy_proc(pid_t pid)
{
struct reg_proc * proc;
+ struct reg_pool * pool = NULL;
struct pid_entry * spawn;
struct reg_ipcp * ipcp;
@@ -1136,24 +1173,27 @@ int reg_destroy_proc(pid_t pid)
proc = __reg_get_proc(pid);
if (proc != NULL) {
- list_del(&proc->next);
- reg.n_procs--;
+ if (!is_ouroboros_member_uid(proc->info.uid))
+ pool = __reg_get_pool(proc->info.uid);
+ llist_del(&proc->next, &reg.procs);
reg_proc_destroy(proc);
__reg_del_proc_from_names(pid);
__reg_cancel_flows_for_proc(pid);
+ if (pool != NULL && reg_pool_unref(pool) == 0) {
+ llist_del(&pool->next, &reg.pools);
+ reg_pool_destroy(pool);
+ }
}
spawn = __reg_get_spawned(pid);
if (spawn != NULL) {
- list_del(&spawn->next);
- reg.n_spawned--;
+ llist_del(&spawn->next, &reg.spawned);
free(spawn);
}
ipcp = __reg_get_ipcp(pid);
if (ipcp != NULL) {
- list_del(&ipcp->next);
- reg.n_ipcps--;
+ llist_del(&ipcp->next, &reg.ipcps);
reg_ipcp_destroy(ipcp);
}
@@ -1175,6 +1215,38 @@ bool reg_has_proc(pid_t pid)
return ret;
}
+bool reg_is_proc_privileged(pid_t pid)
+{
+ struct reg_proc * proc;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL)
+ ret = reg_proc_is_privileged(proc);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+uid_t reg_get_proc_uid(pid_t pid)
+{
+ struct reg_proc * proc;
+ uid_t ret = 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL && !is_ouroboros_member_uid(proc->info.uid))
+ ret = proc->info.uid;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
void reg_kill_all_proc(int signal)
{
pthread_mutex_lock(&reg.mtx);
@@ -1216,9 +1288,7 @@ int reg_create_spawned(pid_t pid)
entry->pid = pid;
- list_add(&entry->next, __reg_after_spawned(pid));
-
- reg.n_spawned++;
+ llist_add_at(&entry->next, __reg_after_spawned(pid), &reg.spawned);
pthread_mutex_unlock(&reg.mtx);
@@ -1388,9 +1458,7 @@ int reg_create_prog(const struct prog_info * info)
goto fail_prog;
}
- list_add(&prog->next, __reg_after_prog(info->name));
-
- reg.n_progs++;
+ llist_add_at(&prog->next, __reg_after_prog(info->name), &reg.progs);
exists:
pthread_mutex_unlock(&reg.mtx);
@@ -1418,9 +1486,7 @@ int reg_destroy_prog(const char * name)
__reg_del_prog_from_names(prog->info.path);
- list_del(&prog->next);
-
- reg.n_progs--;
+ llist_del(&prog->next, &reg.progs);
pthread_mutex_unlock(&reg.mtx);
@@ -1736,7 +1802,8 @@ int reg_wait_flow_allocated(struct flow_info * info,
}
if (flow != NULL) {
- reg_flow_get_data(flow, pbuf);
+ *pbuf = flow->rsp_data;
+ clrbuf(flow->rsp_data);
*info = flow->info;
}
@@ -1770,9 +1837,13 @@ int reg_respond_alloc(struct flow_info * info,
goto fail_flow;
}
- assert(flow->info.state == FLOW_ALLOC_PENDING);
- assert(flow->data.len == 0);
- assert(flow->data.data == NULL);
+ if (flow->info.state != FLOW_ALLOC_PENDING) {
+ log_warn("Flow %d already responded.", info->id);
+ goto fail_flow;
+ }
+
+ assert(flow->rsp_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
info->n_pid = flow->info.n_pid;
info->n_1_pid = flow->info.n_pid;
@@ -1784,8 +1855,10 @@ int reg_respond_alloc(struct flow_info * info,
flow->response = response;
- if (info->state == FLOW_ALLOCATED)
- reg_flow_set_data(flow, pbuf);
+ if (info->state == FLOW_ALLOCATED) {
+ flow->rsp_data = *pbuf;
+ clrbuf(*pbuf);
+ }
pthread_cond_broadcast(&reg.cond);
@@ -1816,6 +1889,8 @@ int reg_prepare_flow_accept(struct flow_info * info)
ret = reg_flow_update(flow, info);
+ pthread_cond_broadcast(&reg.cond);
+
pthread_mutex_unlock(&reg.mtx);
return ret;
@@ -1893,7 +1968,8 @@ int reg_wait_flow_accepted(struct flow_info * info,
pthread_cleanup_pop(true); /* __cleanup_wait_accept */
if (flow != NULL) {
- reg_flow_get_data(flow, pbuf);
+ *pbuf = flow->req_data;
+ clrbuf(flow->req_data);
*info = flow->info;
}
@@ -1953,8 +2029,8 @@ int reg_respond_accept(struct flow_info * info,
info->n_pid = flow->info.n_pid;
- reg_flow_set_data(flow, pbuf);
- clrbuf(pbuf);
+ flow->req_data = *pbuf;
+ clrbuf(*pbuf);
if (reg_flow_update(flow, info) < 0) {
log_err("Failed to create flow structs.");
@@ -1972,6 +2048,669 @@ int reg_respond_accept(struct flow_info * info,
return -1;
}
+int reg_prepare_flow_direct(struct flow_info * info,
+ buffer_t * pbuf,
+ uid_t alloc_uid)
+{
+ struct reg_flow * flow;
+ struct reg_proc * proc;
+ uid_t accept_uid = 0;
+
+ assert(info != NULL);
+ assert(info->state == FLOW_ALLOCATED);
+ assert(info->n_1_pid != 0);
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(info->id);
+ if (flow == NULL) {
+ log_err("Flow not found: %d.", info->id);
+ goto fail_flow;
+ }
+
+ assert(flow->info.state == FLOW_ACCEPT_PENDING);
+
+ info->n_pid = flow->info.n_pid;
+
+ proc = __reg_get_proc(info->n_pid);
+ if (proc != NULL && !is_ouroboros_member_uid(proc->info.uid))
+ accept_uid = proc->info.uid;
+
+ if (alloc_uid != accept_uid) {
+ pthread_mutex_unlock(&reg.mtx);
+ return -EPERM;
+ }
+
+ flow->direct = true;
+
+ flow->req_data = *pbuf;
+ clrbuf(*pbuf);
+
+ if (reg_flow_update(flow, info) < 0) {
+ log_err("Failed to create flow structs.");
+ goto fail_flow;
+ }
+
+ pthread_cond_broadcast(&reg.cond);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+
+ fail_flow:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+bool reg_flow_is_direct(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+
+ ret = flow != NULL && flow->direct;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+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_bits(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_bits(flow->n_rb, RB_REKEY);
+
+ if (flow->n_1_rb != NULL && !(flow->rk.pulled & RK_N_1_PID))
+ ssm_rbuff_set_bits(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_bits(flow->n_1_rb, RB_REKEY);
+ } else {
+ flow->rk.pulled |= RK_N_PID;
+ if (flow->n_rb != NULL)
+ ssm_rbuff_clr_bits(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_bits(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 endpoints 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)
+{
+ struct reg_flow * flow;
+
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL) {
+ log_err("Flow %d not found.", flow_id);
+ goto fail;
+ }
+
+ assert(flow->direct);
+ assert(flow->rsp_data.data == NULL);
+
+ flow->rsp_data = *pbuf;
+ clrbuf(*pbuf);
+
+ pthread_cond_broadcast(&reg.cond);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+ fail:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+int reg_wait_flow_direct(int flow_id,
+ buffer_t * pbuf,
+ const struct timespec * abstime)
+{
+ struct reg_flow * flow;
+ int ret = -1;
+
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL)
+ goto fail;
+
+ assert(flow->direct);
+
+ pthread_cleanup_push(__cleanup_mutex_unlock, &reg.mtx);
+
+ while (flow != NULL && flow->rsp_data.data == NULL) {
+ ret = -__timedwait(&reg.cond, &reg.mtx, abstime);
+ if (ret == -ETIMEDOUT)
+ break;
+ flow = __reg_get_flow(flow_id);
+ }
+
+ if (flow != NULL && flow->rsp_data.data != NULL) {
+ *pbuf = flow->rsp_data;
+ clrbuf(flow->rsp_data);
+ ret = 0;
+ }
+
+ pthread_cleanup_pop(true);
+
+ return ret;
+ fail:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+static int direct_flow_dealloc(struct reg_flow * flow,
+ pid_t pid)
+{
+ if (!flow->direct)
+ return -1;
+
+ if (pid == flow->info.n_pid && flow->info.n_pid != -1)
+ flow->info.n_pid = -1;
+ else if (pid == flow->info.n_1_pid && flow->info.n_1_pid != -1)
+ flow->info.n_1_pid = -1;
+ else
+ return -1;
+
+ return 0;
+}
+
void reg_dealloc_flow(struct flow_info * info)
{
struct reg_flow * flow;
@@ -1985,13 +2724,32 @@ void reg_dealloc_flow(struct flow_info * info)
flow = __reg_get_flow(info->id);
assert(flow != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
+
+ info->n_1_pid = flow->info.n_1_pid;
+
+ if (flow->info.state == FLOW_DEALLOC_PENDING) {
+ if (direct_flow_dealloc(flow, info->n_pid) < 0) {
+ info->state = FLOW_DEALLOC_PENDING;
+ pthread_mutex_unlock(&reg.mtx);
+ return;
+ }
+ flow->info.state = FLOW_DEALLOCATED;
+ info->state = FLOW_DEALLOCATED;
+ reg_flow_update(flow, info);
+ pthread_mutex_unlock(&reg.mtx);
+ return;
+ }
+
assert(flow->info.state == FLOW_ALLOCATED);
flow->info.state = FLOW_DEALLOC_PENDING;
info->state = FLOW_DEALLOC_PENDING;
- info->n_1_pid = flow->info.n_1_pid;
+
+ direct_flow_dealloc(flow, info->n_pid);
memset(flow->name, 0, sizeof(flow->name));
@@ -2013,8 +2771,10 @@ void reg_dealloc_flow_resp(struct flow_info * info)
flow = __reg_get_flow(info->id);
assert(flow != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
assert(flow->info.state == FLOW_DEALLOC_PENDING);
flow->info.state = FLOW_DEALLOCATED;
diff --git a/src/irmd/reg/reg.h b/src/irmd/reg/reg.h
index 7728c80f..8a313d46 100644
--- a/src/irmd/reg/reg.h
+++ b/src/irmd/reg/reg.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry
*
@@ -31,6 +31,8 @@
#include <ouroboros/time.h>
#include <ouroboros/utils.h>
+#include "pool.h"
+
int reg_init(void);
void reg_clear(void);
@@ -50,6 +52,13 @@ int reg_destroy_proc(pid_t pid);
bool reg_has_proc(pid_t pid);
+bool reg_is_proc_privileged(pid_t pid);
+
+int reg_prepare_pool(uid_t uid,
+ gid_t gid);
+
+uid_t reg_get_proc_uid(pid_t pid);
+
void reg_kill_all_proc(int signal);
pid_t reg_get_dead_proc(void);
@@ -100,6 +109,9 @@ int reg_get_name_for_hash(char * buf,
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);
@@ -141,6 +153,83 @@ int reg_wait_flow_accepting(const char * name,
int reg_respond_accept(struct flow_info * info,
buffer_t * pbuf);
+int reg_prepare_flow_direct(struct flow_info * info,
+ buffer_t * pbuf,
+ uid_t alloc_uid);
+
+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);
+
+/* 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);
+
+bool reg_flow_rekey_arr_admit(int flow_id,
+ pid_t n_1_pid,
+ bool is_req);
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req);
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid);
+
+void reg_notify_flow(int flow_id,
+ int event);
+
+void reg_notify_flow_peers(int flow_id,
+ int event);
+
void reg_dealloc_flow(struct flow_info * info);
void reg_dealloc_flow_resp(struct flow_info * info);
diff --git a/src/irmd/reg/tests/CMakeLists.txt b/src/irmd/reg/tests/CMakeLists.txt
index 73df911b..e8521545 100644
--- a/src/irmd/reg/tests/CMakeLists.txt
+++ b/src/irmd/reg/tests/CMakeLists.txt
@@ -1,7 +1,9 @@
-get_filename_component(tmp ".." ABSOLUTE)
-get_filename_component(src_folder "${tmp}" NAME)
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
-create_test_sourcelist(${src_folder}_tests test_suite.c
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
# Add new tests here
flow_test.c
ipcp_test.c
@@ -11,23 +13,21 @@ create_test_sourcelist(${src_folder}_tests test_suite.c
reg_test.c
)
-add_executable(${src_folder}_test ${${src_folder}_tests})
-target_link_libraries(${src_folder}_test ouroboros-common)
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(${src_folder}_test -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+ ${CMAKE_SOURCE_DIR}/src/irmd
+ ${CMAKE_BINARY_DIR}/src/irmd
+)
-add_dependencies(check ${src_folder}_test)
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common)
+ouroboros_target_debug_definitions(${PARENT_DIR}_test)
-set(tests_to_run ${${src_folder}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
+add_dependencies(build_tests ${PARENT_DIR}_test)
-foreach(test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(irmd/reg/${test_name} ${C_TEST_PATH}/${src_folder}_test ${test_name})
-endforeach(test)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/irmd/reg/tests/flow_test.c b/src/irmd/reg/tests/flow_test.c
index 27fd61b0..18214078 100644
--- a/src/irmd/reg/tests/flow_test.c
+++ b/src/irmd/reg/tests/flow_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows - Unit Tests
*
@@ -22,11 +22,7 @@
#include "../flow.c"
-#include <ouroboros/test.h>
-
-#include <string.h>
-
-#define TEST_DATA "testpiggybackdata"
+#include <test/test.h>
static int test_reg_flow_create_destroy(void)
{
@@ -126,6 +122,21 @@ static int test_reg_flow_create_has_mpl(void) {
return TEST_RC_SUCCESS;
}
+static int test_reg_flow_create_has_mtu(void) {
+ struct flow_info info = {
+ .id = 1,
+ .n_pid = 1,
+ .n_1_pid = 0,
+ .mtu = 1400,
+ .qs = qos_raw,
+ .state = FLOW_ALLOC_PENDING
+ };
+
+ reg_flow_create(&info); /* assert fail */
+
+ return TEST_RC_SUCCESS;
+}
+
static int test_reg_flow_update(void)
{
struct reg_flow * f;
@@ -140,7 +151,7 @@ static int test_reg_flow_update(void)
struct flow_info upd = {
.id = 1,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -183,7 +194,7 @@ static int test_reg_flow_update_wrong_id(void)
struct flow_info upd = {
.id = 2,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -214,61 +225,12 @@ static int test_reg_flow_assert_fails(void)
ret |= test_assert_fail(test_reg_flow_create_has_n_1_pid);
ret |= test_assert_fail(test_reg_flow_create_wrong_state);
ret |= test_assert_fail(test_reg_flow_create_has_mpl);
+ ret |= test_assert_fail(test_reg_flow_create_has_mtu);
ret |= test_assert_fail(test_reg_flow_update_wrong_id);
return ret;
}
-static int test_flow_data(void)
-{
- struct reg_flow * f;
-
- struct flow_info info = {
- .id = 1,
- .n_pid = 1,
- .qs = qos_raw,
- .state = FLOW_INIT
- };
-
- char * data;
- buffer_t buf;
- buffer_t rcv = {0, NULL};
-
- TEST_START();
-
- data = strdup(TEST_DATA);
- if (data == NULL) {
- printf("Failed to strdup data.\n");
- goto fail;
- }
-
- buf.data = (uint8_t *) data;
- buf.len = strlen(data);
-
- f = reg_flow_create(&info);
- if (f == NULL) {
- printf("Failed to create flow.\n");
- goto fail;
- }
-
- reg_flow_set_data(f, &buf);
-
- reg_flow_get_data(f, &rcv);
-
- freebuf(buf);
- clrbuf(rcv);
-
- reg_flow_destroy(f);
-
- TEST_SUCCESS();
-
- return TEST_RC_SUCCESS;
- fail:
- free(data);
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
-
int flow_test(int argc,
char ** argv)
{
@@ -280,7 +242,6 @@ int flow_test(int argc,
ret |= test_reg_flow_create_destroy();
ret |= test_reg_flow_update();
ret |= test_reg_flow_assert_fails();
- ret |= test_flow_data();
return ret;
}
diff --git a/src/irmd/reg/tests/ipcp_test.c b/src/irmd/reg/tests/ipcp_test.c
index d7d8e524..5279283e 100644
--- a/src/irmd/reg/tests/ipcp_test.c
+++ b/src/irmd/reg/tests/ipcp_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs - Unit Tests
*
@@ -20,7 +20,7 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
-#include <ouroboros/test.h>
+#include <test/test.h>
#include "../ipcp.c"
diff --git a/src/irmd/reg/tests/name_test.c b/src/irmd/reg/tests/name_test.c
index 9071364b..403c8a6c 100644
--- a/src/irmd/reg/tests/name_test.c
+++ b/src/irmd/reg/tests/name_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names - Unit Tests
*
@@ -23,7 +23,7 @@
#include "../name.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
#define TEST_PID 65534
#define TEST_PROG "/usr/bin/testprog"
@@ -88,7 +88,7 @@ static int test_reg_name_add_proc(void)
reg_name_del_proc(n, TEST_PID);
- if (n->procs.len != 0) {
+ if (!llist_is_empty(&n->procs)) {
printf("Proc not removed from list.\n");
goto fail;
}
@@ -138,7 +138,7 @@ static int test_reg_name_add_prog(void)
reg_name_del_prog(n, TEST_PROG);
- if (n->progs.len != 0) {
+ if (!llist_is_empty(&n->progs)) {
printf("Prog not removed from list.\n");
goto fail;
}
@@ -263,12 +263,12 @@ static int test_reg_name_add_active(enum pol_balance lb)
reg_name_del_proc(n, TEST_PID);
- if (n->procs.len != 0) {
+ if (!llist_is_empty(&n->procs)) {
printf("Procs list not cleared.\n");
goto fail;
}
- if (n->active.len != 0) {
+ if (!llist_is_empty(&n->active)) {
printf("Active list not cleared.\n");
goto fail;
}
diff --git a/src/irmd/reg/tests/proc_test.c b/src/irmd/reg/tests/proc_test.c
index df0527fb..a85f4039 100644
--- a/src/irmd/reg/tests/proc_test.c
+++ b/src/irmd/reg/tests/proc_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes - Unit Tests
*
@@ -22,18 +22,22 @@
#include "../proc.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
#define TEST_PID 65534
#define TEST_PROG "usr/bin/testprog"
+#define TEST_PROC { \
+ .pid = TEST_PID, \
+ .prog = TEST_PROG, \
+ .uid = getuid(), \
+ .gid = getgid() \
+}
+
static int test_reg_proc_create_destroy(void)
{
struct reg_proc * proc;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC;
TEST_START();
@@ -56,10 +60,7 @@ static int test_reg_proc_create_destroy(void)
static int test_reg_proc_add_name(void)
{
struct reg_proc * proc;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC;
char * name = "testname";
@@ -76,7 +77,7 @@ static int test_reg_proc_add_name(void)
goto fail;
}
- if (proc->n_names != 1) {
+ if (proc->names.len != 1) {
printf("n_names not updated.\n");
goto fail;
}
@@ -88,7 +89,7 @@ static int test_reg_proc_add_name(void)
reg_proc_del_name(proc, name);
- if (proc->n_names != 0) {
+ if (!llist_is_empty(&proc->names)) {
printf("n_names not updated.\n");
goto fail;
}
diff --git a/src/irmd/reg/tests/prog_test.c b/src/irmd/reg/tests/prog_test.c
index c394c222..91264aba 100644
--- a/src/irmd/reg/tests/prog_test.c
+++ b/src/irmd/reg/tests/prog_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs - Unit Tests
*
@@ -22,7 +22,7 @@
#include "../prog.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
#define TEST_PROG "usr/bin/testprog"
@@ -73,7 +73,7 @@ static int test_reg_prog_add_name(void)
goto fail;
}
- if (prog->n_names != 1) {
+ if (prog->names.len != 1) {
printf("n_names not updated.\n");
goto fail;
}
@@ -85,7 +85,7 @@ static int test_reg_prog_add_name(void)
reg_prog_del_name(prog, name);
- if (prog->n_names != 0) {
+ if (!llist_is_empty(&prog->names)) {
printf("n_names not updated.\n");
goto fail;
}
diff --git a/src/irmd/reg/tests/reg_test.c b/src/irmd/reg/tests/reg_test.c
index 4699beab..a8c1b1fa 100644
--- a/src/irmd/reg/tests/reg_test.c
+++ b/src/irmd/reg/tests/reg_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Unit Tests
*
@@ -21,22 +21,31 @@
*/
+#include "../pool.c"
+#undef OUROBOROS_PREFIX
#include "../reg.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
#define TEST_PID 3666
#define TEST_N_1_PID 3999
#define TEST_FAKE_ID 9128349
#define TEST_MPL 5
+#define TEST_MTU 1400
#define TEST_PROG "reg_test" /* own binary for binary check */
#define TEST_IPCP "testipcp"
#define TEST_NAME "testname"
#define TEST_DATA "testpbufdata"
#define TEST_DATA2 "testpbufdata2"
#define TEST_LAYER "testlayer"
+#define TEST_PROC_INFO { \
+ .pid = TEST_PID, \
+ .prog = TEST_PROG, \
+ .uid = 0, \
+ .gid = 0 \
+}
#define REG_TEST_FAIL() \
- do { TEST_FAIL(); memset(&reg, 0, sizeof(reg)); abort();} while(0)
+ do { TEST_FAIL(); reg_clear(); return TEST_RC_FAIL;} while(0)
static int test_reg_init(void)
{
@@ -81,7 +90,7 @@ static int test_reg_create_flow(void)
goto fail;
}
- if (reg.n_flows != 1) {
+ if (reg.flows.len != 1) {
printf("n_flows was not updated.\n");
goto fail;
}
@@ -96,8 +105,8 @@ static int test_reg_create_flow(void)
goto fail;
}
- if (reg.n_flows != 0) {
- printf("n_flows was not updated.\n");
+ if (!llist_is_empty(&reg.flows)) {
+ printf("flows.len was not updated.\n");
goto fail;
}
@@ -155,7 +164,7 @@ static int test_reg_allocate_flow_timeout(void)
reg_destroy_flow(info.id);
- if (reg.n_flows != 0) {
+ if (!llist_is_empty(&reg.flows)) {
printf("Flow did not destroy.\n");
goto fail;
}
@@ -189,6 +198,8 @@ static void * test_flow_respond_alloc(void * o)
reg_respond_alloc(info, &pbuf, response);
+ freebuf(pbuf);
+
return (void *) 0;
fail:
return (void *) -1;
@@ -208,6 +219,8 @@ static void * test_flow_respond_accept(void * o)
reg_respond_accept(info, &pbuf);
+ freebuf(pbuf);
+
return (void *) 0;
fail:
return (void *) -1;
@@ -227,7 +240,7 @@ static int test_reg_accept_flow_success(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -254,11 +267,15 @@ static int test_reg_accept_flow_success(void)
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) {
printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -269,6 +286,11 @@ static int test_reg_accept_flow_success(void)
goto fail;
}
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
if (rbuf.data == NULL) {
printf("rbuf data not returned.\n");
goto fail;
@@ -321,7 +343,7 @@ static int test_reg_accept_flow_success_no_crypt(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -348,11 +370,15 @@ static int test_reg_accept_flow_success_no_crypt(void)
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0 ) {
printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -363,6 +389,11 @@ static int test_reg_accept_flow_success_no_crypt(void)
goto fail;
}
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
if (rbuf.data == NULL) {
printf("rbuf data was not returned.\n");
goto fail;
@@ -413,7 +444,7 @@ static int test_reg_allocate_flow_fail(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED /* RESPONSE FAIL */
};
@@ -446,6 +477,9 @@ static int test_reg_allocate_flow_fail(void)
if (reg_wait_flow_allocated(&info, &buf, &abstime) == 0 ) {
printf("Flow allocation succeeded.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -468,6 +502,436 @@ static int test_reg_allocate_flow_fail(void)
return TEST_RC_FAIL;
}
+static int test_reg_respond_alloc_duplicate(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t empty = BUF_INIT;
+ struct flow_info dup_info;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ struct flow_info n_1_info = {
+ .n_1_pid = TEST_N_1_PID,
+ .qs = qos_msg,
+ .state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
+ };
+
+ TEST_START();
+
+ 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;
+ }
+
+ info.n_1_pid = TEST_N_1_PID;
+
+ if (reg_prepare_flow_alloc(&info) < 0) {
+ printf("Failed to prepare flow for alloc.\n");
+ goto fail;
+ }
+
+ n_1_info.id = info.id;
+ n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
+
+ pthread_create(&thr, NULL, test_flow_respond_alloc, &n_1_info);
+
+ if (reg_wait_flow_allocated(&info, &rbuf, &abstime) < 0) {
+ printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
+ goto fail;
+ }
+
+ pthread_join(thr, NULL);
+ freebuf(rbuf);
+
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
+ /* Duplicate reply on an already-ALLOCATED flow must not assert. */
+ dup_info = n_1_info;
+ dup_info.state = FLOW_DEALLOCATED;
+
+ if (reg_respond_alloc(&dup_info, &empty, -EREPLAY) != -1) {
+ printf("Duplicate respond_alloc should return -1.\n");
+ goto fail;
+ }
+
+ reg_dealloc_flow(&info);
+ reg_dealloc_flow_resp(&info);
+ reg_destroy_flow(n_1_info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+struct direct_alloc_info {
+ struct flow_info info;
+ buffer_t rsp;
+ struct timespec abstime;
+};
+
+static void * test_flow_alloc_direct(void * o)
+{
+ struct direct_alloc_info * dai;
+ buffer_t req;
+
+ dai = (struct direct_alloc_info *) o;
+
+ req.data = (uint8_t *) strdup(TEST_DATA);
+ if (req.data == NULL) {
+ printf("Failed to strdup req data.\n");
+ goto fail;
+ }
+ req.len = strlen(TEST_DATA) + 1;
+
+ if (reg_prepare_flow_direct(&dai->info, &req, 0) < 0) {
+ printf("Failed to prepare direct flow.\n");
+ freebuf(req);
+ goto fail;
+ }
+
+ if (reg_wait_flow_direct(dai->info.id, &dai->rsp, &dai->abstime) < 0) {
+ printf("Failed to wait direct flow.\n");
+ goto fail;
+ }
+
+ return (void *) 0;
+ fail:
+ return (void *) -1;
+}
+
+static int test_reg_direct_flow_success(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t rsp;
+ struct direct_alloc_info dai;
+ void * thr_ret;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ TEST_START();
+
+ 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);
+ reg_destroy_flow(info.id);
+ reg_fini();
+ goto fail;
+ }
+
+ if (info.state != FLOW_ALLOCATED) {
+ printf("Flow not in allocated state.\n");
+ goto fail;
+ }
+
+ if (rbuf.data == NULL) {
+ printf("req_data not received.\n");
+ goto fail;
+ }
+
+ if (strcmp((char *) rbuf.data, TEST_DATA) != 0) {
+ printf("req_data content mismatch.\n");
+ goto fail;
+ }
+
+ freebuf(rbuf);
+
+ if (!reg_flow_is_direct(info.id)) {
+ printf("Flow not marked direct.\n");
+ goto fail;
+ }
+
+ rsp.data = (uint8_t *) strdup(TEST_DATA2);
+ if (rsp.data == NULL) {
+ printf("Failed to strdup rsp data.\n");
+ 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);
+ goto fail;
+ }
+
+ pthread_join(thr, &thr_ret);
+
+ if (thr_ret != (void *) 0) {
+ printf("Allocator thread failed.\n");
+ goto fail;
+ }
+
+ if (dai.rsp.data == NULL) {
+ printf("rsp_data not received.\n");
+ goto fail;
+ }
+
+ if (strcmp((char *) dai.rsp.data, TEST_DATA2) != 0) {
+ printf("rsp_data content mismatch.\n");
+ goto fail;
+ }
+
+ freebuf(dai.rsp);
+
+ reg_dealloc_flow(&info);
+
+ if (info.state != FLOW_DEALLOC_PENDING) {
+ printf("Flow not in dealloc pending.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_PID;
+
+ reg_dealloc_flow(&info);
+
+ if (info.state != FLOW_DEALLOC_PENDING) {
+ printf("Same endpoint dealloc changed state.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_N_1_PID;
+
+ reg_dealloc_flow(&info);
+
+ if (info.state != FLOW_DEALLOCATED) {
+ printf("Flow not deallocated.\n");
+ goto fail;
+ }
+
+ reg_destroy_flow(info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ 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;
@@ -476,6 +940,9 @@ static int test_reg_flow(void) {
rc |= test_reg_accept_flow_success();
rc |= test_reg_accept_flow_success_no_crypt();
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;
}
@@ -500,7 +967,7 @@ static int test_reg_create_ipcp(void)
goto fail;
}
- if (reg.n_ipcps != 1) {
+ if (reg.ipcps.len != 1) {
printf("n_ipcps was not updated.\n");
goto fail;
}
@@ -515,8 +982,8 @@ static int test_reg_create_ipcp(void)
goto fail;
}
- if (reg.n_ipcps != 0) {
- printf("n_ipcps was not updated.\n");
+ if (reg.ipcps.len != 0) {
+ printf("ipcps.len was not updated.\n");
goto fail;
}
@@ -570,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++)
@@ -636,6 +1104,7 @@ static int test_insert_ipcps(void)
while (len-- > 0)
ipcp_list_msg__free_unpacked(ipcps[len], NULL);
+
free(ipcps);
reg_clear();
@@ -744,7 +1213,7 @@ static int test_reg_create_name(void)
goto fail;
}
- if (reg.n_names != 1) {
+ if (reg.names.len != 1) {
printf("n_names was not updated.\n");
goto fail;
}
@@ -759,7 +1228,7 @@ static int test_reg_create_name(void)
goto fail;
}
- if (reg.n_names != 0) {
+ if (!llist_is_empty(&reg.names)) {
printf("n_names was not updated.\n");
goto fail;
}
@@ -813,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++) {
@@ -843,10 +1313,7 @@ static int test_reg_name(void)
static int test_reg_create_proc(void)
{
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC_INFO;
TEST_START();
@@ -860,7 +1327,7 @@ static int test_reg_create_proc(void)
goto fail;
}
- if (reg.n_procs != 1) {
+ if (reg.procs.len != 1) {
printf("n_procs was not updated.\n");
goto fail;
}
@@ -875,7 +1342,7 @@ static int test_reg_create_proc(void)
goto fail;
}
- if (reg.n_procs != 0) {
+ if (!llist_is_empty(&reg.procs)) {
printf("n_procs was not updated.\n");
goto fail;
}
@@ -913,7 +1380,7 @@ static int test_reg_spawned(void)
goto fail;
}
- if (reg.n_spawned != 1) {
+ if (reg.spawned.len != 1) {
printf("n_spawned was not updated.\n");
goto fail;
}
@@ -928,7 +1395,7 @@ static int test_reg_spawned(void)
goto fail;
}
- if (reg.n_spawned != 0) {
+ if (!llist_is_empty(&reg.spawned)) {
printf("n_spawned was not updated.\n");
goto fail;
}
@@ -961,7 +1428,7 @@ static int test_reg_create_prog(void)
goto fail;
}
- if (reg.n_progs != 1) {
+ if (reg.progs.len != 1) {
printf("n_progs was not updated.\n");
goto fail;
}
@@ -976,7 +1443,7 @@ static int test_reg_create_prog(void)
goto fail;
}
- if (reg.n_progs != 0) {
+ if (!llist_is_empty(&reg.progs)) {
printf("n_progs was not updated.\n");
goto fail;
}
@@ -1002,10 +1469,7 @@ static int test_reg_prog(void)
static int test_bind_proc(void)
{
- struct proc_info pinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info pinfo = TEST_PROC_INFO;
struct name_info ninfo = {
.name = TEST_NAME,
@@ -1158,10 +1622,7 @@ static int test_inherit_prog(void)
.name = TEST_PROG
};
- struct proc_info procinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info procinfo = TEST_PROC_INFO;
char * exec[] = { TEST_PROG, NULL};
@@ -1296,13 +1757,10 @@ static int test_wait_accepting_fail_name(void)
static void * test_call_flow_accept(void * o)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_MS(1);
+ struct timespec timeo = TIMESPEC_INIT_MS(30);
buffer_t pbuf = BUF_INIT;
- struct proc_info pinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info pinfo = TEST_PROC_INFO;
struct flow_info info = {
.n_pid = pinfo.pid,
@@ -1326,16 +1784,21 @@ static void * test_call_flow_accept(void * o)
info.state = FLOW_ACCEPT_PENDING;
+ reg_prepare_flow_accept(&info);
+
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- reg_prepare_flow_accept(&info);
-
if (reg_wait_flow_accepted(&info, &pbuf, &abstime) != -ETIMEDOUT) {
printf("Wait allocated did not timeout.\n");
goto fail;
}
+ if (reg_unbind_proc((char *) o, pinfo.pid) < 0) {
+ printf("Failed to unbind proc.\n");
+ goto fail;
+ }
+
reg_destroy_flow(info.id);
reg_destroy_proc(pinfo.pid);
@@ -1347,7 +1810,7 @@ static void * test_call_flow_accept(void * o)
static int test_wait_accepting_success(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
int flow_id;
struct name_info ninfo = {
@@ -1375,7 +1838,10 @@ static int test_wait_accepting_success(void)
flow_id = reg_wait_flow_accepting(ninfo.name, &abstime);
if (flow_id < 0) {
- printf("Wait accept did not return a flow id: %d.", flow_id);
+ printf("Wait accept did not return a flow id: %d.\n", flow_id);
+ pthread_join(thr, NULL);
+ reg_destroy_name(TEST_NAME);
+ reg_fini();
goto fail;
}
@@ -1461,7 +1927,7 @@ static void * test_ipcp_respond(void * o)
static int test_wait_ipcp_boot_fail(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
struct ipcp_info info = {
.name = TEST_IPCP,
@@ -1471,7 +1937,7 @@ static int test_wait_ipcp_boot_fail(void)
struct ipcp_info resp_info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_INIT
+ .state = IPCP_NULL
};
TEST_START();
@@ -1495,6 +1961,9 @@ static int test_wait_ipcp_boot_fail(void)
if (reg_wait_ipcp_boot(&info, &abstime) == 0) {
printf("IPCP boot reported success.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1505,8 +1974,8 @@ static int test_wait_ipcp_boot_fail(void)
goto fail;
}
- if (reg.n_ipcps != 0) {
- printf("n_ipcps was not updated.\n");
+ if (!llist_is_empty(&reg.ipcps)) {
+ printf("ipcps.len was not updated.\n");
goto fail;
}
@@ -1524,7 +1993,7 @@ static int test_wait_ipcp_boot_success(void)
{
pthread_t thr;
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
@@ -1557,6 +2026,9 @@ static int test_wait_ipcp_boot_success(void)
if (reg_wait_ipcp_boot(&info, &abstime) < 0) {
printf("IPCP boot failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1564,6 +2036,8 @@ static int test_wait_ipcp_boot_success(void)
if (info.state != IPCP_OPERATIONAL) {
printf("IPCP boot succeeded in non-operational state.\n");
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1636,12 +2110,9 @@ static void * test_proc(void * o)
static int test_wait_proc_success(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC_INFO;
TEST_START();
@@ -1657,6 +2128,9 @@ static int test_wait_proc_success(void)
if (reg_wait_proc(info.pid, &abstime) < 0) {
printf("Waiting for proc failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
diff --git a/src/irmd/tests/CMakeLists.txt b/src/irmd/tests/CMakeLists.txt
deleted file mode 100644
index 4b62261c..00000000
--- a/src/irmd/tests/CMakeLists.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-get_filename_component(tmp ".." ABSOLUTE)
-get_filename_component(src_folder "${tmp}" NAME)
-
-create_test_sourcelist(${src_folder}_tests test_suite.c
- # Add new tests here
- irm_test.c
- oap_test.c
-)
-
-add_executable(${src_folder}_test ${${src_folder}_tests})
-target_link_libraries(${src_folder}_test ouroboros-common)
-
-add_dependencies(check ${src_folder}_test)
-
-set(tests_to_run ${${src_folder}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
-
-foreach(test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(irmd/${test_name} ${C_TEST_PATH}/${src_folder}_test ${test_name})
-endforeach(test)
-
-set_property(TEST irmd/oap_test PROPERTY SKIP_RETURN_CODE 1)
diff --git a/src/irmd/tests/oap_test.c b/src/irmd/tests/oap_test.c
deleted file mode 100644
index 4e7fb2d1..00000000
--- a/src/irmd/tests/oap_test.c
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Unit tests of Ouroboros flow allocation protocol
- * 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 "oap.c"
-
-#include <ouroboros/random.h>
-#include <ouroboros/test.h>
-
-static const char * pkp_str = \
-"-----BEGIN EC PRIVATE KEY-----\n"
-"MHcCAQEEIC13y+5jdKe80HBJD7WITpQamcn3rrkTX1r0v+JwSk4NoAoGCCqGSM49\n"
-"AwEHoUQDQgAEcC0yLAfUtufH8cdLybrdWPc6U+xRuhDhqqrEcBO5+eob2xyqEaNk\n"
-"nIV/86724zPptGRahWz0rzW2PvNppJdNBg==\n"
-"-----END EC PRIVATE KEY-----\n";
-
-/* Valid signed server certificate for server-2.unittest.o7s */
-static const char * crt_str = \
-"-----BEGIN CERTIFICATE-----\n"
-"MIIDgjCCAyigAwIBAgICEAIwCgYIKoZIzj0EAwIwWzELMAkGA1UEBhMCQkUxDDAK\n"
-"BgNVBAgMA09WTDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n"
-"GTAXBgNVBAMMEGltMi51bml0dGVzdC5vN3MwHhcNMjUwNzA0MTMxODI5WhcNMzUw\n"
-"NzAyMTMxODI5WjBwMQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYDVQQH\n"
-"DAVHaGVudDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3MxHjAc\n"
-"BgNVBAMMFXNlcnZlci0yLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49\n"
-"AwEHA0IABHAtMiwH1Lbnx/HHS8m63Vj3OlPsUboQ4aqqxHATufnqG9scqhGjZJyF\n"
-"f/Ou9uMz6bRkWoVs9K81tj7zaaSXTQajggHFMIIBwTAJBgNVHRMEAjAAMBEGCWCG\n"
-"SAGG+EIBAQQEAwIGQDA6BglghkgBhvhCAQ0ELRYrR3JpbGxlZCBDaGVlc2UgR2Vu\n"
-"ZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUTt3xHTwE9amoglxh\n"
-"cEMqWv+PpDMwgb8GA1UdIwSBtzCBtIAUFfeZRx8QWWKQr7Aw8zjDu2shvcShgZek\n"
-"gZQwgZExCzAJBgNVBAYTAkJFMQwwCgYDVQQIDANPVkwxDjAMBgNVBAcMBUdoZW50\n"
-"MQwwCgYDVQQKDANvN3MxFTATBgNVBAsMDHVuaXR0ZXN0Lm83czEZMBcGA1UEAwwQ\n"
-"Y2EyLnVuaXR0ZXN0Lm83czEkMCIGCSqGSIb3DQEJARYVZHVtbXlAb3Vyb2Jvcm9z\n"
-"LnJvY2tzggIQAjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw\n"
-"EQYDVR0fBAowCDAGoASgAoYAMCoGCCsGAQUFBwEBBB4wHDAMBggrBgEFBQcwAoYA\n"
-"MAwGCCsGAQUFBzABhgAwIAYDVR0RBBkwF4IVc2VydmVyLTEudW5pdHRlc3Qubzdz\n"
-"MAoGCCqGSM49BAMCA0gAMEUCIQDHuDb62w/Uah4nKwUFoJVkr4rgdNGh2Rn3SWaK\n"
-"0FV/gAIgOLKorTwSgrTFdyOUkuPOhRs8BEMpah+dp8UTO8AnLvY=\n"
-"-----END CERTIFICATE-----\n";
-
-static int test_oap_hdr_init_fini(void)
-{
- struct oap_hdr oap_hdr;
- struct timespec now;
- uint64_t stamp;
- buffer_t ephkey = BUF_INIT;
- buffer_t data = BUF_INIT;
- uint8_t buf[OAP_ID_SIZE];
- buffer_t id;
- void * pkp = NULL;
- void * pubcrt = NULL;
-
- TEST_START();
-
- random_buffer(buf, OAP_ID_SIZE);
- id.data = buf;
- id.len = OAP_ID_SIZE;
-
- clock_gettime(CLOCK_REALTIME, &now);
- stamp = TS_TO_UINT64(now);
-
- if (oap_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) {
- printf("Failed to init OAP request header.\n");
- goto fail_req_hdr;
- }
-
- if (oap_hdr.hdr.len != OAP_HDR_MIN_SIZE) {
- printf("OAP request header wrong: %zu < %zu.\n",
- oap_hdr.hdr.len, OAP_HDR_MIN_SIZE);
- goto fail_req_hdr_chk;
- }
-
- if (oap_hdr.id.len != OAP_ID_SIZE) {
- printf("OAP request header ID wrong size: %zu != %zu.\n",
- oap_hdr.id.len, (size_t) OAP_ID_SIZE);
- goto fail_req_hdr_chk;
- }
-
- if (memcmp(oap_hdr.id.data, id.data, OAP_ID_SIZE) != 0) {
- printf("OAP request header ID mismatch.\n");
- goto fail_req_hdr_chk;
- }
-
- if (oap_hdr.timestamp < stamp) {
- printf("OAP request header timestamp is too old.\n");
- goto fail_req_hdr_chk;
- }
-
- if (oap_hdr.timestamp > stamp + 1 * BILLION) {
- printf("OAP request header timestamp is too new.\n");
- goto fail_req_hdr_chk;
- }
-
- oap_hdr_fini(&oap_hdr);
-
- TEST_SUCCESS();
-
- return TEST_RC_SUCCESS;
-
- fail_req_hdr_chk:
- oap_hdr_fini(&oap_hdr);
- fail_req_hdr:
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
-
-static int test_oap_hdr_init_fini_data(void)
-
-{
- struct oap_hdr oap_hdr;
- buffer_t data;
- buffer_t ephkey = BUF_INIT;
- uint8_t buf[OAP_ID_SIZE];
- buffer_t id;
- void * pkp = NULL;
- void * pubcrt = NULL;
-
- TEST_START();
-
- random_buffer(buf, OAP_ID_SIZE);
- id.data = buf;
- id.len = OAP_ID_SIZE;
-
- data.len = 100;
- data.data = malloc(data.len);
- if (data.data == NULL) {
- printf("Failed to allocate data buffer.\n");
- goto fail_data;
- }
-
- random_buffer(data.data, data.len);
-
- if (oap_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) {
- printf("Failed to create OAP request header.\n");
- goto fail_req_hdr;
- }
-
- if (oap_hdr.hdr.len != OAP_HDR_MIN_SIZE + data.len) {
- printf("OAP request header wrong: %zu < %zu.\n",
- oap_hdr.hdr.len, OAP_HDR_MIN_SIZE + data.len);
- goto fail_req_hdr_sz;
- }
-
- freebuf(data);
- oap_hdr_fini(&oap_hdr);
-
- TEST_SUCCESS();
-
- return TEST_RC_SUCCESS;
-
- fail_req_hdr_sz:
- oap_hdr_fini(&oap_hdr);
- fail_req_hdr:
- freebuf(data);
- fail_data:
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
-
-static int test_oap_hdr_init_fini_signed(void)
-{
- struct oap_hdr oap_hdr;
- buffer_t ephkey = BUF_INIT;
- buffer_t data = BUF_INIT;
- buffer_t sign;
- buffer_t id;
- uint8_t buf[OAP_ID_SIZE];
- void * pkp;
- void * pk;
- void * pubcrt;
- void * pubcrt2;
-
- TEST_START();
-
- random_buffer(buf, OAP_ID_SIZE);
- id.data = buf;
- id.len = OAP_ID_SIZE;
-
- if (crypt_load_privkey_str(pkp_str, &pkp) < 0) {
- printf("Failed to load private key.\n");
- goto fail_pkp;
- }
-
- if (crypt_load_crt_str(crt_str, &pubcrt) < 0) {
- printf("Failed to load public certificate.\n");
- goto fail_pubcrt;
- }
-
- if (oap_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) {
- printf("Failed to create OAP request header.\n");
- goto fail_req_hdr;
- }
-
- if (oap_hdr.crt.len == 0) {
- printf("OAP request header has no public certificate.\n");
- goto fail_req_hdr;
- }
-
- if (oap_hdr.sig.len == 0) {
- printf("OAP request header no signature.\n");
- goto fail_req_hdr;
- }
-
- if (crypt_load_crt_der(oap_hdr.crt, &pubcrt2) < 0) {
- printf("Failed to load public certificate from DER.\n");
- goto fail_crt_der;
- }
-
- if (crypt_get_pubkey_crt(pubcrt2, &pk) < 0) {
- printf("Failed to get public key from certificate.\n");
- goto fail_crt_pk;
- }
-
- sign = oap_hdr.hdr;
- sign.len -= (oap_hdr.sig.len + sizeof(uint16_t));
-
- if (auth_verify_sig(pk, sign, oap_hdr.sig) < 0) {
- printf("Failed to verify OAP request header signature.\n");
- goto fail_check_sig;
- }
-
- oap_hdr_fini(&oap_hdr);
-
- crypt_free_crt(pubcrt2);
- crypt_free_crt(pubcrt);
- crypt_free_key(pk);
- crypt_free_key(pkp);
-
- TEST_SUCCESS();
-
- return TEST_RC_SUCCESS;
-
- fail_check_sig:
- crypt_free_key(pk);
- fail_crt_pk:
- crypt_free_crt(pubcrt2);
- fail_crt_der:
- oap_hdr_fini(&oap_hdr);
- fail_req_hdr:
- crypt_free_crt(pubcrt);
- fail_pubcrt:
- crypt_free_key(pkp);
- fail_pkp:
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
-
-int oap_test(int argc,
- char **argv)
-{
- int ret = 0;
-
- (void) argc;
- (void) argv;
-
- ret |= test_oap_hdr_init_fini();
- ret |= test_oap_hdr_init_fini_data();
-#ifdef HAVE_OPENSSL
- ret |= test_oap_hdr_init_fini_signed();
-#else
- (void) test_oap_hdr_init_fini_signed;
-
- ret = TEST_RC_SKIP;
-#endif
- return ret;
-}
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index 14e89976..3abf39d0 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -1,271 +1,29 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
+# Ouroboros libraries build configuration
+# Configuration options are in cmake/config/lib.cmake
protobuf_generate_c(MODEL_PROTO_SRCS MODEL_PROTO_HDRS
- pb/model.proto)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/model.proto")
protobuf_generate_c(IPCP_CONFIG_PROTO_SRCS IPCP_CONFIG_PROTO_HDRS
- pb/ipcp_config.proto)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/ipcp_config.proto")
protobuf_generate_c(ENROLL_PROTO_SRCS ENROLL_PROTO_HDRS
- pb/enroll.proto)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/enroll.proto")
protobuf_generate_c(CEP_PROTO_SRCS CEP_PROTO_HDRS
- pb/cep.proto)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/cep.proto")
protobuf_generate_c(IRM_PROTO_SRCS IRM_PROTO_HDRS
- pb/irm.proto)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/irm.proto")
protobuf_generate_c(IPCP_PROTO_SRCS IPCP_PROTO_HDRS
- pb/ipcp.proto)
-
-if (NOT APPLE)
- find_library(LIBRT_LIBRARIES rt)
- if (NOT LIBRT_LIBRARIES)
- message(FATAL_ERROR "Could not find librt")
- endif ()
-else ()
- set(LIBRT_LIBRARIES "")
-endif ()
-
-find_library(LIBPTHREAD_LIBRARIES pthread)
-if (NOT LIBPTHREAD_LIBRARIES)
- message(FATAL_ERROR "Could not find libpthread")
-endif ()
-
-include(CheckSymbolExists)
-list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_POSIX_C_SOURCE=200809L)
-list(APPEND CMAKE_REQUIRED_DEFINITIONS -D__XSI_VISIBLE=500)
-list(APPEND CMAKE_REQUIRED_LIBRARIES pthread)
-check_symbol_exists(pthread_mutexattr_setrobust pthread.h HAVE_ROBUST_MUTEX)
-
-if (HAVE_ROBUST_MUTEX)
- set(DISABLE_ROBUST_MUTEXES FALSE CACHE BOOL "Disable robust mutex support")
- if (NOT DISABLE_ROBUST_MUTEXES)
- message(STATUS "Robust mutex support enabled")
- set(HAVE_ROBUST_MUTEX TRUE)
- else ()
- message(STATUS "Robust mutex support disabled by user")
- unset(HAVE_ROBUST_MUTEX)
- endif ()
-else()
- message(STATUS "Robust mutex support not available")
- unset(HAVE_ROBUST_MUTEX)
-endif ()
-
-find_library(FUSE_LIBRARIES fuse QUIET)
-if (FUSE_LIBRARIES)
- #FIXME: Check for version >= 2.6
- set(DISABLE_FUSE FALSE CACHE BOOL "Disable FUSE support")
- if (NOT DISABLE_FUSE)
- message(STATUS "FUSE support enabled")
- set(FUSE_PREFIX "/tmp/ouroboros" CACHE STRING
- "Mountpoint for RIB filesystem")
- set(HAVE_FUSE TRUE CACHE INTERNAL "")
- else ()
- message(STATUS "FUSE support disabled by user")
- unset(HAVE_FUSE CACHE)
- endif ()
-else ()
- message(STATUS "Install FUSE version > 2.6 to enable RIB access")
-endif ()
-
-if (NOT HAVE_FUSE)
- set(FUSE_LIBRARIES "")
- set(FUSE_INCLUDE_DIR "")
-endif ()
-
-mark_as_advanced(FUSE_LIBRARIES)
-
-find_library(LIBGCRYPT_LIBRARIES gcrypt QUIET)
-if (LIBGCRYPT_LIBRARIES)
- find_path(LIBGCRYPT_INCLUDE_DIR gcrypt.h
- HINTS /usr/include /usr/local/include)
- if (LIBGCRYPT_INCLUDE_DIR)
- file(STRINGS ${LIBGCRYPT_INCLUDE_DIR}/gcrypt.h GCSTR
- REGEX "^#define GCRYPT_VERSION ")
- string(REGEX REPLACE "^#define GCRYPT_VERSION \"(.*)\".*$" "\\1"
- GCVER "${GCSTR}")
- if (NOT GCVER VERSION_LESS "1.7.0")
- set(DISABLE_LIBGCRYPT FALSE CACHE BOOL "Disable libgcrypt support")
- if (NOT DISABLE_LIBGCRYPT)
- message(STATUS "libgcrypt support enabled")
- set(HAVE_LIBGCRYPT TRUE CACHE INTERNAL "")
- else ()
- message(STATUS "libgcrypt support disabled by user")
- unset(HAVE_LIBGCRYPT CACHE)
- endif()
- else ()
- message(STATUS "Install version >= \"1.7.0\" to enable libgcrypt support "
- "(found version \"${GCVER}\")")
- endif()
- endif ()
-endif ()
-
-if (NOT HAVE_LIBGCRYPT)
- set(LIBGCRYPT_LIBRARIES "")
- set(LIBGCRYPT_INCLUDE_DIR "")
-endif ()
-
-find_package(OpenSSL QUIET)
-if (OPENSSL_FOUND)
- set(HAVE_OPENSSL_RNG TRUE)
- if (OPENSSL_VERSION VERSION_LESS "1.1.0")
- message(STATUS "Install version >= \"1.1.0\" to enable OpenSSL support "
- "(found version \"${OPENSSL_VERSION}\")")
- else ()
- set(DISABLE_OPENSSL FALSE CACHE BOOL "Disable OpenSSL support")
- if (NOT DISABLE_OPENSSL)
- message(STATUS "OpenSSL support enabled")
- set(HAVE_OPENSSL TRUE CACHE INTERNAL "")
- else()
- message(STATUS "OpenSSL support disabled")
- unset(HAVE_OPENSSL)
- endif()
- endif ()
- set(OPENSSL_SOURCES crypt/openssl.c)
-else()
- message(STATUS "Install openSSL version >= \"1.1.0\" to enable OpenSSL support")
- unset(HAVE_OPENSSL_RNG)
- unset(HAVE_OPENSSL)
- set(OPENSSL_INCLUDE_DIR "")
- set(OPENSSL_LIBRARIES "")
- set(OPENSSL_CRYPTO_LIBRARY "")
- set(OPENSSL_SOURCES "")
-endif ()
-
-if (APPLE OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
- set(SYS_RND_HDR "")
-else ()
- find_path(SYS_RND_HDR sys/random.h PATH /usr/include/ /usr/local/include/)
- if (SYS_RND_HDR)
- message(STATUS "Found sys/random.h in ${SYS_RND_HDR}")
- set(HAVE_SYS_RANDOM TRUE)
- else ()
- set(SYS_RND_HDR "")
- unset(HAVE_SYS_RANDOM)
- endif ()
-endif()
-
-if (NOT ((CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") OR APPLE OR
- HAVE_SYS_RANDOM OR HAVE_OPENSSL_RNG OR HAVE_LIBGCRYPT))
- message(FATAL_ERROR "No secure random generator found, "
- "please install libgcrypt (> 1.7.0) or OpenSSL")
-endif ()
-
-mark_as_advanced(LIBRT_LIBRARIES LIBPTHREAD_LIBRARIES
- LIBGCRYPT_LIBRARIES OPENSSL_LIBRARIES OPENSSL_CRYPTO_LIBRARY
- SYS_RND_INCLUDE_DIR LIBGCRYPT_INCLUDE_DIR SYS_RND_HDR)
-
-set(SHM_BUFFER_SIZE 16384 CACHE STRING
- "Number of blocks in packet buffer, must be a power of 2")
-set(SHM_RBUFF_SIZE 1024 CACHE STRING
- "Number of blocks in rbuff buffer, must be a power of 2")
-set(SYS_MAX_FLOWS 10240 CACHE STRING
- "Maximum number of total flows for this system")
-set(PROG_MAX_FLOWS 4096 CACHE STRING
- "Maximum number of flows in an application")
-set(PROG_RES_FDS 64 CACHE STRING
- "Number of reserved flow descriptors per application")
-set(PROG_MAX_FQUEUES 32 CACHE STRING
- "Maximum number of flow sets per application")
-set(DU_BUFF_HEADSPACE 256 CACHE STRING
- "Bytes of headspace to reserve for future headers")
-set(DU_BUFF_TAILSPACE 32 CACHE STRING
- "Bytes of tailspace to reserve for future tails")
-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 ()
-set(SOCKET_TIMEOUT 500 CACHE STRING
- "Default timeout for responses from IPCPs (ms)")
-set(SHM_PREFIX "ouroboros" CACHE STRING
- "String to prepend to POSIX shared memory filenames")
-set(SHM_RBUFF_PREFIX "/${SHM_PREFIX}.rbuff." CACHE INTERNAL
- "Prefix for rbuff POSIX shared memory filenames")
-set(SHM_LOCKFILE_NAME "/${SHM_PREFIX}.lockfile" CACHE INTERNAL
- "Filename for the POSIX shared memory lockfile")
-set(SHM_FLOW_SET_PREFIX "/${SHM_PREFIX}.set." CACHE INTERNAL
- "Prefix for the POSIX shared memory flow set")
-set(SHM_RDRB_NAME "/${SHM_PREFIX}.rdrb" CACHE INTERNAL
- "Name for the main POSIX shared memory buffer")
-set(SHM_RDRB_BLOCK_SIZE "sysconf(_SC_PAGESIZE)" CACHE STRING
- "Packet buffer block size, multiple of pagesize for performance")
-set(SHM_RDRB_MULTI_BLOCK TRUE CACHE BOOL
- "Packet buffer multiblock packet support")
-set(SHM_RBUFF_LOCKLESS FALSE CACHE BOOL
- "Enable shared memory lockless rbuff support")
-set(QOS_DISABLE_CRC TRUE CACHE BOOL
- "Ignores ber setting on all QoS cubes")
-set(DELTA_T_MPL 60 CACHE STRING
- "Maximum packet lifetime (s)")
-set(DELTA_T_ACK 10 CACHE STRING
- "Maximum time to acknowledge a packet (s)")
-set(DELTA_T_RTX 120 CACHE STRING
- "Maximum time to retransmit a packet (s)")
-set(FRCT_REORDER_QUEUE_SIZE 256 CACHE STRING
- "Size of the reordering queue, must be a power of 2")
-set(FRCT_START_WINDOW 64 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
- "Minimum Retransmission Timeout (RTO) for FRCT (us)")
-set(FRCT_TICK_TIME 5000 CACHE STRING
- "Tick time for FRCT activity (retransmission, acknowledgments) (us)")
-set(RXM_BUFFER_ON_HEAP FALSE CACHE BOOL
- "Store packets for retransmission on the heap instead of in packet buffer")
-set(RXM_BLOCKING TRUE CACHE BOOL
- "Use blocking writes for retransmission")
-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")
-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")
-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")
-
-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 ()
-
-set(SOURCE_FILES_DEV
- # Add source files here
- cep.c
- dev.c
- )
-
-set(SOURCE_FILES_IRM
- irm.c
-)
+ "${CMAKE_CURRENT_SOURCE_DIR}/pb/ipcp.proto")
set(SOURCE_FILES_COMMON
bitmap.c
btree.c
- crc32.c
+ crc/crc8.c
+ crc/crc16.c
+ crc/crc32.c
+ crc/crc64.c
crypt.c
+ crypt/keyrot.c
hash.c
- list.c
lockfile.c
logs.c
md5.c
@@ -277,62 +35,138 @@ set(SOURCE_FILES_COMMON
serdes-irm.c
serdes-oep.c
sha3.c
- shm_flow_set.c
- shm_rbuff.c
- shm_rdrbuff.c
+ ssm/flow_set.c
+ ssm/rbuff.c
+ ssm/pool.c
sockets.c
tpm.c
+ tw.c
utils.c
)
-configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
- "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
-
-add_library(ouroboros-common SHARED ${SOURCE_FILES_COMMON} ${IRM_PROTO_SRCS}
- ${IPCP_PROTO_SRCS} ${IPCP_CONFIG_PROTO_SRCS} ${MODEL_PROTO_SRCS}
- ${ENROLL_PROTO_SRCS} ${OPENSSL_SOURCES})
-
-add_library(ouroboros-dev SHARED ${SOURCE_FILES_DEV} ${CEP_PROTO_SRCS})
+if(HAVE_OPENSSL)
+ list(APPEND SOURCE_FILES_COMMON crypt/openssl.c)
+endif()
-add_library(ouroboros-irm SHARED ${SOURCE_FILES_IRM})
+add_library(ouroboros-common SHARED
+ ${SOURCE_FILES_COMMON}
+ ${IRM_PROTO_SRCS}
+ ${IPCP_PROTO_SRCS}
+ ${IPCP_CONFIG_PROTO_SRCS}
+ ${MODEL_PROTO_SRCS}
+ ${ENROLL_PROTO_SRCS})
set_target_properties(ouroboros-common PROPERTIES
VERSION ${PACKAGE_VERSION}
SOVERSION ${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR})
+
+ouroboros_target_debug_definitions(ouroboros-common)
+
+target_include_directories(ouroboros-common
+ PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
+ $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+ PRIVATE
+ ${SYS_RND_HDR}
+ ${APPLE_INCLUDE_DIRS})
+
+target_link_libraries(ouroboros-common
+ PRIVATE
+ ${LIBRT_LIBRARIES}
+ Threads::Threads
+ PUBLIC
+ ProtobufC::ProtobufC)
+
+if(HAVE_OPENSSL)
+ target_link_libraries(ouroboros-common PUBLIC OpenSSL::Crypto)
+endif()
+
+if(HAVE_LIBGCRYPT)
+ target_link_libraries(ouroboros-common PUBLIC Gcrypt::Gcrypt)
+endif()
+
+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 this TU only.
+ set_source_files_properties(crypt/keyrot.c PROPERTIES
+ COMPILE_OPTIONS "-std=gnu99")
+endif()
+
+install(TARGETS ouroboros-common
+ EXPORT OuroborosTargets
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+
+set(SOURCE_FILES_DEV
+ cep.c
+ dev.c
+)
+
+add_library(ouroboros-dev SHARED
+ ${SOURCE_FILES_DEV}
+ ${CEP_PROTO_SRCS})
+
set_target_properties(ouroboros-dev PROPERTIES
VERSION ${PACKAGE_VERSION}
SOVERSION ${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR})
+
+ouroboros_target_debug_definitions(ouroboros-dev)
+
+target_include_directories(ouroboros-dev
+ PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}
+ ${SYS_RND_HDR})
+
+target_link_libraries(ouroboros-dev PUBLIC ouroboros-common)
+
+install(TARGETS ouroboros-dev
+ EXPORT OuroborosTargets
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+
+add_library(ouroboros-irm SHARED irm.c)
+
set_target_properties(ouroboros-irm PROPERTIES
VERSION ${PACKAGE_VERSION}
SOVERSION ${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR})
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(ouroboros-common -DCONFIG_OUROBOROS_DEBUG)
- add_compile_flags(ouroboros-dev -DCONFIG_OUROBOROS_DEBUG)
- add_compile_flags(ouroboros-irm -DCONFIG_OUROBOROS_DEBUG)
-endif ()
-
-target_link_libraries(ouroboros-common ${LIBRT_LIBRARIES}
- ${LIBPTHREAD_LIBRARIES} ${PROTOBUF_C_LIBRARY} ${OPENSSL_CRYPTO_LIBRARY}
- ${LIBGCRYPT_LIBRARIES} ${FUSE_LIBRARIES})
+ouroboros_target_debug_definitions(ouroboros-irm)
-target_link_libraries(ouroboros-dev ouroboros-common)
-target_link_libraries(ouroboros-irm ouroboros-common)
+target_include_directories(ouroboros-irm
+ PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}
+ ${SYS_RND_HDR})
-install(TARGETS ouroboros-common LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
-install(TARGETS ouroboros-dev LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
-install(TARGETS ouroboros-irm LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
+target_link_libraries(ouroboros-irm PUBLIC ouroboros-common)
-target_include_directories(ouroboros-common PUBLIC ${CMAKE_CURRENT_BINARY_DIR}
- ${SYS_RND_HDR} ${LIBGCRYPT_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR})
+install(TARGETS ouroboros-irm
+ EXPORT OuroborosTargets
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
-target_include_directories(ouroboros-dev PUBLIC ${CMAKE_CURRENT_BINARY_DIR}
- ${SYS_RND_HDR} ${LIBGCRYPT_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR})
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
-target_include_directories(ouroboros-irm PUBLIC ${CMAKE_CURRENT_BINARY_DIR}
- ${SYS_RND_HDR} ${LIBGCRYPT_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR})
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ssm/ssm.h.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/ssm.h" @ONLY)
if(BUILD_TESTS)
add_subdirectory(tests)
-endif ()
+ add_subdirectory(crc/tests)
+ add_subdirectory(ssm/tests)
+endif()
diff --git a/src/lib/bitmap.c b/src/lib/bitmap.c
index b0840c44..05cf4030 100644
--- a/src/lib/bitmap.c
+++ b/src/lib/bitmap.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bitmap implementation
*
diff --git a/src/lib/btree.c b/src/lib/btree.c
index 1af94b73..37ec5e52 100644
--- a/src/lib/btree.c
+++ b/src/lib/btree.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* B-trees
*
diff --git a/src/lib/cep.c b/src/lib/cep.c
index ba238023..e953e2d9 100644
--- a/src/lib/cep.c
+++ b/src/lib/cep.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The Ouroboros Connection Establishment Protocol
*
diff --git a/src/lib/config.h.in b/src/lib/config.h.in
index 8326a332..4ebaa194 100644
--- a/src/lib/config.h.in
+++ b/src/lib/config.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros library configuration
*
@@ -20,28 +20,34 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
+#ifndef MILLION
+#define MILLION 1000000LL
+#endif
+
+#ifndef BILLION
+#define BILLION 1000000000LL
+#endif
+
#cmakedefine HAVE_SYS_RANDOM
+#cmakedefine HAVE_EXPLICIT_BZERO
#cmakedefine HAVE_LIBGCRYPT
#cmakedefine HAVE_OPENSSL
-
#ifdef HAVE_OPENSSL
+#cmakedefine HAVE_ML
+#cmakedefine HAVE_SLH
#define HAVE_ENCRYPTION
+#define SECMEM_MINSIZE @SECMEM_MINSIZE@
#endif
+#define PROC_SECMEM_MAX @PROC_SECMEM_MAX@
#define SYS_MAX_FLOWS @SYS_MAX_FLOWS@
-#cmakedefine SHM_RBUFF_LOCKLESS
-#cmakedefine SHM_RDRB_MULTI_BLOCK
#cmakedefine QOS_DISABLE_CRC
#cmakedefine HAVE_OPENSSL_RNG
+#cmakedefine HAVE_PCLMUL
+#cmakedefine HAVE_PMULL
-#define SHM_RBUFF_PREFIX "@SHM_RBUFF_PREFIX@"
#define SHM_LOCKFILE_NAME "@SHM_LOCKFILE_NAME@"
-#define SHM_FLOW_SET_PREFIX "@SHM_FLOW_SET_PREFIX@"
-#define SHM_RDRB_NAME "@SHM_RDRB_NAME@"
-#define SHM_RDRB_BLOCK_SIZE @SHM_RDRB_BLOCK_SIZE@
-#define SHM_BUFFER_SIZE @SHM_BUFFER_SIZE@
-#define SHM_RBUFF_SIZE @SHM_RBUFF_SIZE@
#define FLOW_ALLOC_TIMEOUT @FLOW_ALLOC_TIMEOUT@
#define TPM_DEBUG_REPORT_INTERVAL @TPM_DEBUG_REPORT_INTERVAL@
@@ -63,19 +69,20 @@
#cmakedefine PROC_FLOW_STATS
#endif
-#define PTHREAD_COND_CLOCK @PTHREAD_COND_CLOCK@
+#cmakedefine HAVE_LIBURCU
+
+#cmakedefine FRCT_DEBUG_STDOUT
-#define PROG_MAX_FLOWS @PROG_MAX_FLOWS@
-#define PROG_RES_FDS @PROG_RES_FDS@
-#define PROG_MAX_FQUEUES @PROG_MAX_FQUEUES@
+#define PTHREAD_COND_CLOCK @PTHREAD_COND_CLOCK@
-#define DU_BUFF_HEADSPACE @DU_BUFF_HEADSPACE@
-#define DU_BUFF_TAILSPACE @DU_BUFF_TAILSPACE@
+#define PROC_MAX_FLOWS @PROC_MAX_FLOWS@
+#define PROC_RES_FDS @PROC_RES_FDS@
+#define PROC_MAX_FQUEUES @PROC_MAX_FQUEUES@
/* Default Delta-t parameters */
#cmakedefine FRCT_LINUX_RTT_ESTIMATOR
-#define DELT_A (@DELTA_T_ACK@) /* ns */
-#define DELT_R (@DELTA_T_RTX@) /* ns */
+#define DELT_A (@DELTA_T_ACK@) /* ms */
+#define DELT_R (@DELTA_T_RTX@) /* ms */
#define RQ_SIZE (@FRCT_REORDER_QUEUE_SIZE@)
#define START_WINDOW (@FRCT_START_WINDOW@)
@@ -86,9 +93,6 @@
#define TICTIME (@FRCT_TICK_TIME@ * 1000) /* ns */
/* Retransmission tuning */
-#cmakedefine RXM_BUFFER_ON_HEAP
-#cmakedefine RXM_BLOCKING
-
#define RXMQ_RES (@RXM_MIN_RESOLUTION@) /* 2^N ns */
#define RXMQ_BUMP (@RXM_WHEEL_MULTIPLIER@)
#define RXMQ_LVLS (@RXM_WHEEL_LEVELS@)
@@ -96,3 +100,10 @@
#define ACKQ_SLOTS (@ACK_WHEEL_SLOTS@)
#define ACKQ_RES (@ACK_WHEEL_RESOLUTION@) /* 2^N ns */
+
+#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/crc/crc16.c b/src/lib/crc/crc16.c
new file mode 100644
index 00000000..9dc59429
--- /dev/null
+++ b/src/lib/crc/crc16.c
@@ -0,0 +1,61 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 16-bit Cyclic Redundancy Check (CCITT-FALSE variant)
+ *
+ * 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/.
+ */
+
+/*
+ * CRC-16/CCITT-FALSE (reveng catalog, alias CRC-16/IBM-3740):
+ * poly = 0x1021
+ * init = 0xffff
+ * refin = false
+ * refout = false
+ * xorout = 0x0000
+ * check = crc16_ccitt_false("123456789") == 0x29b1
+ */
+
+#include "config.h"
+
+#include <ouroboros/crc16.h>
+
+/* Bit-by-bit MSB-first CRC. */
+void crc16_ccitt_false(uint16_t * crc,
+ const void * buf,
+ size_t len)
+{
+ const uint8_t * p;
+ uint16_t c;
+ size_t n;
+ int i;
+
+ p = (const uint8_t *) buf;
+ c = *crc ^ 0xffff;
+
+ for (n = 0; n < len; n++) {
+ c ^= ((uint16_t) p[n]) << 8;
+ for (i = 0; i < 8; i++) {
+ if (c & 0x8000)
+ c = (uint16_t) ((c << 1) ^ 0x1021);
+ else
+ c = (uint16_t) (c << 1);
+ }
+ }
+
+ *crc = c;
+}
diff --git a/src/lib/crc32.c b/src/lib/crc/crc32.c
index f369ad20..0fdb62b1 100644
--- a/src/lib/crc32.c
+++ b/src/lib/crc/crc32.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* 32-bit Cyclic Redundancy Check
*
diff --git a/src/lib/crc/crc64.c b/src/lib/crc/crc64.c
new file mode 100644
index 00000000..1b6fb5f6
--- /dev/null
+++ b/src/lib/crc/crc64.c
@@ -0,0 +1,363 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 64-bit Cyclic Redundancy Check (NVMe variant)
+ *
+ * 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/.
+ */
+
+/*
+ * CRC-64/NVMe (reveng catalog):
+ * poly = 0xad93d23594c93659
+ * init = 0xffffffffffffffff
+ * refin = true
+ * refout = true
+ * xorout = 0xffffffffffffffff
+ * check = crc64_nvme("123456789") == 0xae8b14860a799888
+ */
+
+#include "config.h"
+
+#include <ouroboros/crc64.h>
+
+/*
+ * Reflected CRC-64/NVMe table. Polynomial in reflected form:
+ * 0x9a6c9329ac4bc9b5 (bitrev of 0xad93d23594c93659).
+ */
+static const uint64_t crc64_nvme_tab[256] = {
+ 0x0000000000000000ULL, 0x7f6ef0c830358979ULL,
+ 0xfedde190606b12f2ULL, 0x81b31158505e9b8bULL,
+ 0xc962e5739841b68fULL, 0xb60c15bba8743ff6ULL,
+ 0x37bf04e3f82aa47dULL, 0x48d1f42bc81f2d04ULL,
+ 0xa61cecb46814fe75ULL, 0xd9721c7c5821770cULL,
+ 0x58c10d24087fec87ULL, 0x27affdec384a65feULL,
+ 0x6f7e09c7f05548faULL, 0x1010f90fc060c183ULL,
+ 0x91a3e857903e5a08ULL, 0xeecd189fa00bd371ULL,
+ 0x78e0ff3b88be6f81ULL, 0x078e0ff3b88be6f8ULL,
+ 0x863d1eabe8d57d73ULL, 0xf953ee63d8e0f40aULL,
+ 0xb1821a4810ffd90eULL, 0xceecea8020ca5077ULL,
+ 0x4f5ffbd87094cbfcULL, 0x30310b1040a14285ULL,
+ 0xdefc138fe0aa91f4ULL, 0xa192e347d09f188dULL,
+ 0x2021f21f80c18306ULL, 0x5f4f02d7b0f40a7fULL,
+ 0x179ef6fc78eb277bULL, 0x68f0063448deae02ULL,
+ 0xe943176c18803589ULL, 0x962de7a428b5bcf0ULL,
+ 0xf1c1fe77117cdf02ULL, 0x8eaf0ebf2149567bULL,
+ 0x0f1c1fe77117cdf0ULL, 0x7072ef2f41224489ULL,
+ 0x38a31b04893d698dULL, 0x47cdebccb908e0f4ULL,
+ 0xc67efa94e9567b7fULL, 0xb9100a5cd963f206ULL,
+ 0x57dd12c379682177ULL, 0x28b3e20b495da80eULL,
+ 0xa900f35319033385ULL, 0xd66e039b2936bafcULL,
+ 0x9ebff7b0e12997f8ULL, 0xe1d10778d11c1e81ULL,
+ 0x606216208142850aULL, 0x1f0ce6e8b1770c73ULL,
+ 0x8921014c99c2b083ULL, 0xf64ff184a9f739faULL,
+ 0x77fce0dcf9a9a271ULL, 0x08921014c99c2b08ULL,
+ 0x4043e43f0183060cULL, 0x3f2d14f731b68f75ULL,
+ 0xbe9e05af61e814feULL, 0xc1f0f56751dd9d87ULL,
+ 0x2f3dedf8f1d64ef6ULL, 0x50531d30c1e3c78fULL,
+ 0xd1e00c6891bd5c04ULL, 0xae8efca0a188d57dULL,
+ 0xe65f088b6997f879ULL, 0x9931f84359a27100ULL,
+ 0x1882e91b09fcea8bULL, 0x67ec19d339c963f2ULL,
+ 0xd75adabd7a6e2d6fULL, 0xa8342a754a5ba416ULL,
+ 0x29873b2d1a053f9dULL, 0x56e9cbe52a30b6e4ULL,
+ 0x1e383fcee22f9be0ULL, 0x6156cf06d21a1299ULL,
+ 0xe0e5de5e82448912ULL, 0x9f8b2e96b271006bULL,
+ 0x71463609127ad31aULL, 0x0e28c6c1224f5a63ULL,
+ 0x8f9bd7997211c1e8ULL, 0xf0f5275142244891ULL,
+ 0xb824d37a8a3b6595ULL, 0xc74a23b2ba0eececULL,
+ 0x46f932eaea507767ULL, 0x3997c222da65fe1eULL,
+ 0xafba2586f2d042eeULL, 0xd0d4d54ec2e5cb97ULL,
+ 0x5167c41692bb501cULL, 0x2e0934dea28ed965ULL,
+ 0x66d8c0f56a91f461ULL, 0x19b6303d5aa47d18ULL,
+ 0x980521650afae693ULL, 0xe76bd1ad3acf6feaULL,
+ 0x09a6c9329ac4bc9bULL, 0x76c839faaaf135e2ULL,
+ 0xf77b28a2faafae69ULL, 0x8815d86aca9a2710ULL,
+ 0xc0c42c4102850a14ULL, 0xbfaadc8932b0836dULL,
+ 0x3e19cdd162ee18e6ULL, 0x41773d1952db919fULL,
+ 0x269b24ca6b12f26dULL, 0x59f5d4025b277b14ULL,
+ 0xd846c55a0b79e09fULL, 0xa72835923b4c69e6ULL,
+ 0xeff9c1b9f35344e2ULL, 0x90973171c366cd9bULL,
+ 0x1124202993385610ULL, 0x6e4ad0e1a30ddf69ULL,
+ 0x8087c87e03060c18ULL, 0xffe938b633338561ULL,
+ 0x7e5a29ee636d1eeaULL, 0x0134d92653589793ULL,
+ 0x49e52d0d9b47ba97ULL, 0x368bddc5ab7233eeULL,
+ 0xb738cc9dfb2ca865ULL, 0xc8563c55cb19211cULL,
+ 0x5e7bdbf1e3ac9decULL, 0x21152b39d3991495ULL,
+ 0xa0a63a6183c78f1eULL, 0xdfc8caa9b3f20667ULL,
+ 0x97193e827bed2b63ULL, 0xe877ce4a4bd8a21aULL,
+ 0x69c4df121b863991ULL, 0x16aa2fda2bb3b0e8ULL,
+ 0xf86737458bb86399ULL, 0x8709c78dbb8deae0ULL,
+ 0x06bad6d5ebd3716bULL, 0x79d4261ddbe6f812ULL,
+ 0x3105d23613f9d516ULL, 0x4e6b22fe23cc5c6fULL,
+ 0xcfd833a67392c7e4ULL, 0xb0b6c36e43a74e9dULL,
+ 0x9a6c9329ac4bc9b5ULL, 0xe50263e19c7e40ccULL,
+ 0x64b172b9cc20db47ULL, 0x1bdf8271fc15523eULL,
+ 0x530e765a340a7f3aULL, 0x2c608692043ff643ULL,
+ 0xadd397ca54616dc8ULL, 0xd2bd67026454e4b1ULL,
+ 0x3c707f9dc45f37c0ULL, 0x431e8f55f46abeb9ULL,
+ 0xc2ad9e0da4342532ULL, 0xbdc36ec59401ac4bULL,
+ 0xf5129aee5c1e814fULL, 0x8a7c6a266c2b0836ULL,
+ 0x0bcf7b7e3c7593bdULL, 0x74a18bb60c401ac4ULL,
+ 0xe28c6c1224f5a634ULL, 0x9de29cda14c02f4dULL,
+ 0x1c518d82449eb4c6ULL, 0x633f7d4a74ab3dbfULL,
+ 0x2bee8961bcb410bbULL, 0x548079a98c8199c2ULL,
+ 0xd53368f1dcdf0249ULL, 0xaa5d9839ecea8b30ULL,
+ 0x449080a64ce15841ULL, 0x3bfe706e7cd4d138ULL,
+ 0xba4d61362c8a4ab3ULL, 0xc52391fe1cbfc3caULL,
+ 0x8df265d5d4a0eeceULL, 0xf29c951de49567b7ULL,
+ 0x732f8445b4cbfc3cULL, 0x0c41748d84fe7545ULL,
+ 0x6bad6d5ebd3716b7ULL, 0x14c39d968d029fceULL,
+ 0x95708ccedd5c0445ULL, 0xea1e7c06ed698d3cULL,
+ 0xa2cf882d2576a038ULL, 0xdda178e515432941ULL,
+ 0x5c1269bd451db2caULL, 0x237c997575283bb3ULL,
+ 0xcdb181ead523e8c2ULL, 0xb2df7122e51661bbULL,
+ 0x336c607ab548fa30ULL, 0x4c0290b2857d7349ULL,
+ 0x04d364994d625e4dULL, 0x7bbd94517d57d734ULL,
+ 0xfa0e85092d094cbfULL, 0x856075c11d3cc5c6ULL,
+ 0x134d926535897936ULL, 0x6c2362ad05bcf04fULL,
+ 0xed9073f555e26bc4ULL, 0x92fe833d65d7e2bdULL,
+ 0xda2f7716adc8cfb9ULL, 0xa54187de9dfd46c0ULL,
+ 0x24f29686cda3dd4bULL, 0x5b9c664efd965432ULL,
+ 0xb5517ed15d9d8743ULL, 0xca3f8e196da80e3aULL,
+ 0x4b8c9f413df695b1ULL, 0x34e26f890dc31cc8ULL,
+ 0x7c339ba2c5dc31ccULL, 0x035d6b6af5e9b8b5ULL,
+ 0x82ee7a32a5b7233eULL, 0xfd808afa9582aa47ULL,
+ 0x4d364994d625e4daULL, 0x3258b95ce6106da3ULL,
+ 0xb3eba804b64ef628ULL, 0xcc8558cc867b7f51ULL,
+ 0x8454ace74e645255ULL, 0xfb3a5c2f7e51db2cULL,
+ 0x7a894d772e0f40a7ULL, 0x05e7bdbf1e3ac9deULL,
+ 0xeb2aa520be311aafULL, 0x944455e88e0493d6ULL,
+ 0x15f744b0de5a085dULL, 0x6a99b478ee6f8124ULL,
+ 0x224840532670ac20ULL, 0x5d26b09b16452559ULL,
+ 0xdc95a1c3461bbed2ULL, 0xa3fb510b762e37abULL,
+ 0x35d6b6af5e9b8b5bULL, 0x4ab846676eae0222ULL,
+ 0xcb0b573f3ef099a9ULL, 0xb465a7f70ec510d0ULL,
+ 0xfcb453dcc6da3dd4ULL, 0x83daa314f6efb4adULL,
+ 0x0269b24ca6b12f26ULL, 0x7d0742849684a65fULL,
+ 0x93ca5a1b368f752eULL, 0xeca4aad306bafc57ULL,
+ 0x6d17bb8b56e467dcULL, 0x12794b4366d1eea5ULL,
+ 0x5aa8bf68aecec3a1ULL, 0x25c64fa09efb4ad8ULL,
+ 0xa4755ef8cea5d153ULL, 0xdb1bae30fe90582aULL,
+ 0xbcf7b7e3c7593bd8ULL, 0xc399472bf76cb2a1ULL,
+ 0x422a5673a732292aULL, 0x3d44a6bb9707a053ULL,
+ 0x759552905f188d57ULL, 0x0afba2586f2d042eULL,
+ 0x8b48b3003f739fa5ULL, 0xf42643c80f4616dcULL,
+ 0x1aeb5b57af4dc5adULL, 0x6585ab9f9f784cd4ULL,
+ 0xe436bac7cf26d75fULL, 0x9b584a0fff135e26ULL,
+ 0xd389be24370c7322ULL, 0xace74eec0739fa5bULL,
+ 0x2d545fb4576761d0ULL, 0x523aaf7c6752e8a9ULL,
+ 0xc41748d84fe75459ULL, 0xbb79b8107fd2dd20ULL,
+ 0x3acaa9482f8c46abULL, 0x45a459801fb9cfd2ULL,
+ 0x0d75adabd7a6e2d6ULL, 0x721b5d63e7936bafULL,
+ 0xf3a84c3bb7cdf024ULL, 0x8cc6bcf387f8795dULL,
+ 0x620ba46c27f3aa2cULL, 0x1d6554a417c62355ULL,
+ 0x9cd645fc4798b8deULL, 0xe3b8b53477ad31a7ULL,
+ 0xab69411fbfb21ca3ULL, 0xd407b1d78f8795daULL,
+ 0x55b4a08fdfd90e51ULL, 0x2ada5047efec8728ULL
+};
+
+static __inline__ uint64_t crc64_nvme_step(uint64_t c,
+ const uint8_t * p,
+ size_t len)
+{
+ size_t n;
+
+ for (n = 0; n < len; n++)
+ c = crc64_nvme_tab[(c ^ p[n]) & 0xff] ^ (c >> 8);
+
+ return c;
+}
+
+void crc64_nvme_table(uint64_t * crc,
+ const void * buf,
+ size_t len)
+{
+ uint64_t c;
+
+ c = crc64_nvme_step(*crc ^ UINT64_MAX,
+ (const uint8_t *) buf, len);
+
+ *crc = c ^ UINT64_MAX;
+}
+
+#ifdef HAVE_PCLMUL
+
+#include <smmintrin.h>
+#include <wmmintrin.h>
+
+/*
+ * Fold-by-16 constants for reflected CRC-64/NVMe. Properties of the
+ * polynomial; identical between the PCLMUL and PMULL backends.
+ * k3 = bitrev64(x^(128+64) mod P) << 1
+ * k4 = bitrev64(x^(128+0) mod P) << 1
+ */
+static const uint64_t k3_clmul = 0xeadc41fd2ba3d420ULL;
+static const uint64_t k4_clmul = 0x21e9761e252621acULL;
+
+__attribute__((target("pclmul,sse4.1")))
+static __m128i fold16(__m128i x,
+ __m128i k)
+{
+ __m128i lo;
+ __m128i hi;
+
+ lo = _mm_clmulepi64_si128(x, k, 0x00);
+ hi = _mm_clmulepi64_si128(x, k, 0x11);
+ return _mm_xor_si128(lo, hi);
+}
+
+/*
+ * Fold-by-16 over 16-byte chunks; the 128-bit folded state is then
+ * emitted as 16 little-endian bytes and run through the byte-table
+ * loop together with any tail (<=15 bytes). The 16-byte minimum on
+ * the bulk loop is why the short-input path uses the table directly.
+ */
+__attribute__((target("pclmul,sse4.1")))
+static void crc64_nvme_clmul(uint64_t * crc,
+ const void * buf,
+ size_t len)
+{
+ const uint8_t * p;
+ uint64_t seed;
+ uint64_t c;
+ size_t off;
+ __m128i x;
+ __m128i k;
+ uint8_t post[16];
+
+ p = (const uint8_t *) buf;
+ seed = *crc;
+
+ if (len < 16) {
+ c = crc64_nvme_step(seed ^ UINT64_MAX, p, len);
+ *crc = c ^ UINT64_MAX;
+ return;
+ }
+
+ x = _mm_loadu_si128((const __m128i *) p);
+ x = _mm_xor_si128(x, _mm_cvtsi64_si128((int64_t)
+ (seed ^ UINT64_MAX)));
+
+ k = _mm_set_epi64x((int64_t) k4_clmul, (int64_t) k3_clmul);
+
+ off = 16;
+ while (off + 16 <= len) {
+ __m128i d;
+
+ d = _mm_loadu_si128((const __m128i *) (p + off));
+ x = _mm_xor_si128(fold16(x, k), d);
+ off += 16;
+ }
+
+ _mm_storeu_si128((__m128i *) post, x);
+
+ c = crc64_nvme_step(0, post, 16);
+ c = crc64_nvme_step(c, p + off, len - off);
+
+ *crc = c ^ UINT64_MAX;
+}
+
+#endif /* HAVE_PCLMUL */
+
+#ifdef HAVE_PMULL
+
+#include <arm_neon.h>
+
+/* Same fold-by-16 constants as the PCLMUL path (poly properties). */
+static const uint64_t k3_pmull = 0xeadc41fd2ba3d420ULL;
+static const uint64_t k4_pmull = 0x21e9761e252621acULL;
+
+__attribute__((target("+crypto")))
+static uint64x2_t fold16_pmull(uint64x2_t x,
+ uint64x2_t k)
+{
+ poly64x2_t xp;
+ poly64x2_t kp;
+ uint64x2_t lo;
+ uint64x2_t hi;
+
+ xp = vreinterpretq_p64_u64(x);
+ kp = vreinterpretq_p64_u64(k);
+ lo = vreinterpretq_u64_p128(
+ vmull_p64((poly64_t) vgetq_lane_u64(x, 0),
+ (poly64_t) vgetq_lane_u64(k, 0)));
+ hi = vreinterpretq_u64_p128(vmull_high_p64(xp, kp));
+ return veorq_u64(lo, hi);
+}
+
+__attribute__((target("+crypto")))
+static void crc64_nvme_pmull(uint64_t * crc,
+ const void * buf,
+ size_t len)
+{
+ const uint8_t * p;
+ uint64_t seed;
+ uint64_t c;
+ size_t off;
+ uint64x2_t x;
+ uint64x2_t k;
+ uint64_t seed_lane[2];
+ uint64_t k_lanes[2];
+ uint8_t post[16];
+
+ p = (const uint8_t *) buf;
+ seed = *crc;
+
+ if (len < 16) {
+ c = crc64_nvme_step(seed ^ UINT64_MAX, p, len);
+ *crc = c ^ UINT64_MAX;
+ return;
+ }
+
+ x = vld1q_u64((const uint64_t *) p);
+ seed_lane[0] = seed ^ UINT64_MAX;
+ seed_lane[1] = 0;
+ x = veorq_u64(x, vld1q_u64(seed_lane));
+
+ k_lanes[0] = k3_pmull;
+ k_lanes[1] = k4_pmull;
+ k = vld1q_u64(k_lanes);
+
+ off = 16;
+ while (off + 16 <= len) {
+ uint64x2_t d;
+
+ d = vld1q_u64((const uint64_t *) (p + off));
+ x = veorq_u64(fold16_pmull(x, k), d);
+ off += 16;
+ }
+
+ vst1q_u8(post, vreinterpretq_u8_u64(x));
+
+ c = crc64_nvme_step(0, post, 16);
+ c = crc64_nvme_step(c, p + off, len - off);
+
+ *crc = c ^ UINT64_MAX;
+}
+#endif /* HAVE_PMULL */
+
+void crc64_nvme(uint64_t * crc,
+ const void * buf,
+ size_t len)
+{
+#ifdef HAVE_PCLMUL
+ crc64_nvme_clmul(crc, buf, len);
+#elif defined(HAVE_PMULL)
+ crc64_nvme_pmull(crc, buf, len);
+#else
+ crc64_nvme_table(crc, buf, len);
+#endif
+}
diff --git a/src/lib/crc/crc8.c b/src/lib/crc/crc8.c
new file mode 100644
index 00000000..20976b29
--- /dev/null
+++ b/src/lib/crc/crc8.c
@@ -0,0 +1,62 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * 8-bit Cyclic Redundancy Check (AUTOSAR variant)
+ *
+ * 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/.
+ */
+
+/*
+ * CRC-8/AUTOSAR (reveng catalog):
+ * poly = 0x2f
+ * init = 0xff
+ * refin = false
+ * refout = false
+ * xorout = 0xff
+ * check = crc8_autosar("123456789") == 0xdf
+ */
+
+#include "config.h"
+
+#include <ouroboros/crc8.h>
+
+
+ /* Bit-by-bit MSB-first CRC. */
+void crc8_autosar(uint8_t * crc,
+ const void * buf,
+ size_t len)
+{
+ const uint8_t * p;
+ uint8_t c;
+ size_t n;
+ int i;
+
+ p = (const uint8_t *) buf;
+ c = *crc ^ 0xff;
+
+ for (n = 0; n < len; n++) {
+ c ^= p[n];
+ for (i = 0; i < 8; i++) {
+ if (c & 0x80)
+ c = (uint8_t) ((c << 1) ^ 0x2f);
+ else
+ c = (uint8_t) (c << 1);
+ }
+ }
+
+ *crc = c ^ 0xff;
+}
diff --git a/src/lib/crc/tests/CMakeLists.txt b/src/lib/crc/tests/CMakeLists.txt
new file mode 100644
index 00000000..11daca5a
--- /dev/null
+++ b/src/lib/crc/tests/CMakeLists.txt
@@ -0,0 +1,21 @@
+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
+ crc8_test.c
+ crc16_test.c
+ crc32_test.c
+ crc64_test.c
+ )
+
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test ouroboros-common)
+
+add_dependencies(build_tests ${PARENT_DIR}_test)
+
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/lib/crc/tests/crc16_test.c b/src/lib/crc/tests/crc16_test.c
new file mode 100644
index 00000000..03a5b504
--- /dev/null
+++ b/src/lib/crc/tests/crc16_test.c
@@ -0,0 +1,67 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the CRC-16/CCITT-FALSE function
+ *
+ * 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/crc16.h>
+
+#include <test/test.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+/* reveng-catalog smoke vectors. */
+static int test_crc16_ccitt_false_basic(void)
+{
+ uint16_t crc;
+
+ TEST_START();
+
+ crc = 0;
+ crc16_ccitt_false(&crc, "", 0);
+ if (crc != 0xffff)
+ goto fail;
+
+ crc = 0;
+ crc16_ccitt_false(&crc, "123456789", 9);
+ if (crc != 0x29b1)
+ goto fail;
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int crc16_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_crc16_ccitt_false_basic();
+ return ret;
+}
diff --git a/src/lib/tests/crc32_test.c b/src/lib/crc/tests/crc32_test.c
index a26c8220..5a1ddd87 100644
--- a/src/lib/tests/crc32_test.c
+++ b/src/lib/crc/tests/crc32_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the CRC32 function
*
diff --git a/src/lib/crc/tests/crc64_test.c b/src/lib/crc/tests/crc64_test.c
new file mode 100644
index 00000000..cf3f5ca3
--- /dev/null
+++ b/src/lib/crc/tests/crc64_test.c
@@ -0,0 +1,126 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the CRC-64/NVMe function
+ *
+ * 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/crc64.h>
+#include <ouroboros/random.h>
+
+#include <test/test.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+/* Reference impl, internal to libouroboros-common. */
+extern void crc64_nvme_table(uint64_t * crc,
+ const void * buf,
+ size_t len);
+
+/* reveng-catalog smoke vectors plus a 16-byte fold-boundary check. */
+static int test_crc64_nvme_basic(void)
+{
+ uint64_t crc;
+
+ TEST_START();
+
+ crc = 0;
+ crc64_nvme(&crc, "", 0);
+ if (crc != 0x0000000000000000ULL)
+ goto fail;
+
+ crc = 0;
+ crc64_nvme(&crc, "123456789", 9);
+ if (crc != 0xae8b14860a799888ULL)
+ goto fail;
+
+ crc = 0;
+ crc64_nvme(&crc, "0123456789abcdef", 16);
+ if (crc != 0x091485ca7018730eULL)
+ goto fail;
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+#if defined(HAVE_PCLMUL) || defined(HAVE_PMULL)
+/* Cross-check the accelerated dispatcher path against the byte-table. */
+static int test_crc64_nvme_random(void)
+{
+ static const size_t lens[] = {
+ 0, 1, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128,
+ 129, 255, 256, 257, 1023, 1024, 1025, 4096
+ };
+ uint8_t buf[4096];
+ size_t i;
+ uint64_t ref;
+ uint64_t got;
+
+ TEST_START();
+
+ if (random_buffer(buf, sizeof(buf)) < 0) {
+ printf("Failed to generate random data.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < sizeof(lens) / sizeof(lens[0]); i++) {
+ ref = 0;
+ crc64_nvme_table(&ref, buf, lens[i]);
+
+ got = 0;
+ crc64_nvme(&got, buf, lens[i]);
+
+ if (ref == got)
+ continue;
+
+ printf("Mismatch at len=%zu: table=0x%016lx disp=0x%016lx\n",
+ lens[i],
+ (unsigned long) ref,
+ (unsigned long) got);
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+#endif
+}
+
+int crc64_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_crc64_nvme_basic();
+#if defined(HAVE_PCLMUL) || defined(HAVE_PMULL)
+ ret |= test_crc64_nvme_random();
+#endif
+ return ret;
+}
diff --git a/src/lib/crc/tests/crc8_test.c b/src/lib/crc/tests/crc8_test.c
new file mode 100644
index 00000000..f7bb33b8
--- /dev/null
+++ b/src/lib/crc/tests/crc8_test.c
@@ -0,0 +1,67 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the CRC-8/AUTOSAR function
+ *
+ * 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/crc8.h>
+
+#include <test/test.h>
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+/* reveng-catalog smoke vectors. */
+static int test_crc8_autosar_basic(void)
+{
+ uint8_t crc;
+
+ TEST_START();
+
+ crc = 0;
+ crc8_autosar(&crc, "", 0);
+ if (crc != 0x00)
+ goto fail;
+
+ crc = 0;
+ crc8_autosar(&crc, "123456789", 9);
+ if (crc != 0xdf)
+ goto fail;
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int crc8_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_crc8_autosar_basic();
+ return ret;
+}
diff --git a/src/lib/crypt.c b/src/lib/crypt.c
index 8b18140e..cbbb9bc2 100644
--- a/src/lib/crypt.c
+++ b/src/lib/crypt.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Cryptographic operations
*
@@ -20,34 +20,283 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
+#if defined(__linux__) || defined(__CYGWIN__)
+#define _DEFAULT_SOURCE
+#endif
+
#include <config.h>
-#include <ouroboros/crypt.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 "crypt/openssl.h"
-#endif /* HAVE_OPENSSL */
+#include <openssl/crypto.h>
+#include <openssl/evp.h>
+#include "crypt/openssl.h"
+#endif
#include <assert.h>
+#include <stdio.h>
#include <string.h>
+#include <sys/stat.h>
+
+struct nid_map {
+ uint16_t nid;
+ const char * name;
+};
+
+static const struct nid_map cipher_nid_map[] = {
+ {NID_aes_128_gcm, "aes-128-gcm"},
+ {NID_aes_192_gcm, "aes-192-gcm"},
+ {NID_aes_256_gcm, "aes-256-gcm"},
+ {NID_chacha20_poly1305, "chacha20-poly1305"},
+ {NID_undef, NULL}
+};
+
+/* Ordered in strength preference, lowest first */
+const uint16_t crypt_supported_nids[] = {
+#ifdef HAVE_OPENSSL
+ NID_aes_128_gcm,
+ NID_aes_192_gcm,
+ NID_aes_256_gcm,
+ NID_chacha20_poly1305,
+#endif
+ NID_undef
+};
+
+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}
+};
+
+/* Ordered in strength preference, lowest first (NIST SP 800-57 levels) */
+const uint16_t kex_supported_nids[] = {
+#ifdef HAVE_OPENSSL
+ 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_X25519MLKEM768, /* X25519 + ML-KEM-768, PQC L3 */
+ NID_X448MLKEM1024, /* X448 + ML-KEM-1024, PQC L5 */
+#endif
+#endif
+ NID_undef
+};
+
+static const struct nid_map md_nid_map[] = {
+ {NID_sha256, "sha256"},
+ {NID_sha384, "sha384"},
+ {NID_sha512, "sha512"},
+ {NID_sha3_256, "sha3-256"},
+ {NID_sha3_384, "sha3-384"},
+ {NID_sha3_512, "sha3-512"},
+ {NID_blake2b512, "blake2b512"},
+ {NID_blake2s256, "blake2s256"},
+ {NID_undef, NULL}
+};
+
+/* Ordered in strength preference, lowest first */
+const uint16_t md_supported_nids[] = {
+#ifdef HAVE_OPENSSL
+ NID_blake2s256,
+ NID_sha256,
+ NID_sha3_256,
+ NID_sha384,
+ NID_sha3_384,
+ NID_blake2b512,
+ NID_sha512,
+ NID_sha3_512,
+#endif
+ NID_undef
+};
struct crypt_ctx {
- void * ctx;
- uint8_t key[SYMMKEYSZ];
+ 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 */
};
-int crypt_dh_pkp_create(void ** pkp,
- uint8_t * pk)
+static int parse_kex_value(const char * value,
+ struct sec_config * cfg)
+{
+ SET_KEX_ALGO(cfg, value);
+ if (cfg->x.nid == NID_undef)
+ return -ENOTSUP;
+
+ return 0;
+}
+
+/* not in header, but non-static for unit testing */
+int parse_sec_config(struct sec_config * cfg,
+ FILE * fp)
+{
+ char line[256];
+ char * equals;
+ char * key;
+ char * value;
+ bool no_enc = false;
+
+ assert(cfg != NULL);
+ assert(fp != NULL);
+
+ /* Set defaults */
+ SET_KEX_ALGO_NID(cfg, NID_X9_62_prime256v1);
+ cfg->x.mode = KEM_MODE_SERVER_ENCAP;
+ 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;
+
+ /* Skip comments and empty lines */
+ if (line[0] == '#' || line[0] == '\n')
+ continue;
+
+ /* Bare 'none' keyword replaced by encryption=none */
+ trimmed = trim_whitespace(line);
+ if (strcmp(trimmed, "none") == 0)
+ return -EINVAL;
+
+ /* Find the = separator */
+ equals = strchr(line, '=');
+ if (equals == NULL)
+ continue;
+
+ /* Split into key and value */
+ *equals = '\0';
+ key = trim_whitespace(line);
+ value = trim_whitespace(equals + 1);
+
+ /* Parse key exchange field */
+ if (strcmp(key, "kex") == 0) {
+ if (parse_kex_value(value, cfg) < 0)
+ return -EINVAL;
+ } else if (strcmp(key, "cipher") == 0) {
+ SET_KEX_CIPHER(cfg, value);
+ if (cfg->c.nid == NID_undef)
+ return -EINVAL;
+ } else if (strcmp(key, "kdf") == 0) {
+ SET_KEX_KDF(cfg, value);
+ if (cfg->k.nid == NID_undef)
+ return -EINVAL;
+ } else if (strcmp(key, "digest") == 0) {
+ SET_KEX_DIGEST(cfg, value);
+ if (cfg->d.nid == NID_undef)
+ return -EINVAL;
+ } else if (strcmp(key, "kem_mode") == 0) {
+ if (strcmp(value, "server") == 0) {
+ cfg->x.mode = KEM_MODE_SERVER_ENCAP;
+ } else if (strcmp(value, "client") == 0) {
+ cfg->x.mode = KEM_MODE_CLIENT_ENCAP;
+ } 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;
+}
+
+/* Parse key exchange config from file */
+int load_sec_config_file(struct sec_config * cfg,
+ const char * path)
+{
+ FILE * fp;
+ int ret;
+
+ assert(cfg != NULL);
+ assert(path != NULL);
+
+ fp = fopen(path, "r");
+ if (fp == NULL) {
+ /* Absent config disables encryption; other errors fail */
+ if (errno == ENOENT) {
+ CLEAR_KEX_ALGO(cfg);
+ return 0;
+ }
+ return -errno;
+ }
+
+ pthread_cleanup_push(__cleanup_fclose, fp);
+ ret = parse_sec_config(cfg, fp);
+ pthread_cleanup_pop(0);
+
+ fclose(fp);
+
+ return ret;
+}
+
+int kex_pkp_create(struct sec_config * cfg,
+ void ** pkp,
+ uint8_t * pk)
{
#ifdef HAVE_OPENSSL
+ assert(cfg != NULL);
assert(pkp != NULL);
+
*pkp = NULL;
- return openssl_ecdh_pkp_create(pkp, pk);
+
+ if (cfg->x.str == NULL || kex_validate_nid(cfg->x.nid) < 0)
+ return -ENOTSUP;
+
+ return openssl_pkp_create(cfg->x.str, (EVP_PKEY **) pkp, pk);
#else
+ (void) cfg;
(void) pkp;
(void) pk;
@@ -57,12 +306,12 @@ int crypt_dh_pkp_create(void ** pkp,
#endif
}
-void crypt_dh_pkp_destroy(void * pkp)
+void kex_pkp_destroy(void * pkp)
{
if (pkp == NULL)
return;
#ifdef HAVE_OPENSSL
- openssl_ecdh_pkp_destroy(pkp);
+ openssl_pkp_destroy((EVP_PKEY *) pkp);
#else
(void) pkp;
@@ -70,12 +319,18 @@ void crypt_dh_pkp_destroy(void * pkp)
#endif
}
-int crypt_dh_derive(void * pkp,
- buffer_t pk,
- uint8_t * s)
+int kex_dhe_derive(struct sec_config * cfg,
+ void * pkp,
+ buffer_t pk,
+ uint8_t * s)
{
+ assert(cfg != NULL);
+
+ if (kex_validate_nid(cfg->x.nid) < 0)
+ return -ENOTSUP;
+
#ifdef HAVE_OPENSSL
- return openssl_ecdh_derive(pkp, pk, s);
+ return openssl_dhe_derive((EVP_PKEY *) pkp, pk, cfg->k.nid, s);
#else
(void) pkp;
(void) pk;
@@ -86,35 +341,352 @@ int crypt_dh_derive(void * pkp,
#endif
}
-int crypt_encrypt(struct crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+ssize_t kex_kem_encap(buffer_t pk,
+ uint8_t * ct,
+ int kdf,
+ uint8_t * s)
{
- assert(ctx != NULL);
- assert(ctx->ctx != NULL);
+#ifdef HAVE_OPENSSL
+ return openssl_kem_encap(pk, ct, kdf, s);
+#else
+ (void) pk;
+ (void) ct;
+ (void) kdf;
+ memset(s, 0, SYMMKEYSZ);
+
+ return -ECRYPT;
+#endif
+}
+
+ssize_t kex_kem_encap_raw(buffer_t pk,
+ uint8_t * ct,
+ int kdf,
+ uint8_t * s)
+{
#ifdef HAVE_OPENSSL
- return openssl_encrypt(ctx->ctx, ctx->key, in, out);
+ return openssl_kem_encap_raw(pk, ct, kdf, s);
#else
- (void) ctx;
+ (void) pk;
+ (void) ct;
+ (void) kdf;
+
+ memset(s, 0, SYMMKEYSZ);
+
+ return -ECRYPT;
+#endif
+}
+
+int kex_kem_decap(void * pkp,
+ buffer_t ct,
+ int kdf,
+ uint8_t * s)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_kem_decap((EVP_PKEY *) pkp, ct, kdf, s);
+#else
+ (void) pkp;
+ (void) ct;
+ (void) kdf;
+
+ memset(s, 0, SYMMKEYSZ);
+
+ return -ECRYPT;
+#endif
+}
+
+int kex_get_algo_from_pk_der(buffer_t pk,
+ char * algo)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_get_algo_from_pk_der(pk, algo);
+#else
+ (void) pk;
+ algo[0] = '\0';
+
+ return -ECRYPT;
+#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)
+ return -EINVAL;
+
+ /* Use NID validation instead of string array */
+ return kex_validate_nid(kex_str_to_nid(algo));
+}
+
+int crypt_validate_nid(int nid)
+{
+ const struct nid_map * p;
+
+ if (nid == NID_undef)
+ return -EINVAL;
+
+ for (p = cipher_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return 0;
+ }
+
+ return -ENOTSUP;
+}
+
+
+const char * crypt_nid_to_str(uint16_t nid)
+{
+ const struct nid_map * p;
+
+ for (p = cipher_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return p->name;
+ }
+
+ return NULL;
+}
+
+uint16_t crypt_str_to_nid(const char * cipher)
+{
+ const struct nid_map * p;
+
+ if (cipher == NULL)
+ return NID_undef;
+
+ /* fast, check if cipher pointer is in the map */
+ for (p = cipher_nid_map; p->name != NULL; p++) {
+ if (cipher == p->name)
+ return p->nid;
+ }
+
+ for (p = cipher_nid_map; p->name != NULL; p++) {
+ if (strcmp(p->name, cipher) == 0)
+ return p->nid;
+ }
+
+ return NID_undef;
+}
+
+const char * kex_nid_to_str(uint16_t nid)
+{
+ const struct nid_map * p;
+
+ for (p = kex_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return p->name;
+ }
+
+ return NULL;
+}
+
+uint16_t kex_str_to_nid(const char * algo)
+{
+ const struct nid_map * p;
+
+ if (algo == NULL)
+ return NID_undef;
+
+ /* Fast path: check if algo pointer is in the map */
+ for (p = kex_nid_map; p->name != NULL; p++) {
+ if (algo == p->name)
+ return p->nid;
+ }
+
+ /* Slow path: string comparison */
+ for (p = kex_nid_map; p->name != NULL; p++) {
+ if (strcmp(p->name, algo) == 0)
+ return p->nid;
+ }
+
+ return NID_undef;
+}
+
+int kex_validate_nid(int nid)
+{
+ const struct nid_map * p;
+
+ if (nid == NID_undef)
+ return -EINVAL;
+
+ for (p = kex_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return 0;
+ }
+
+ return -ENOTSUP;
+}
+
+const char * md_nid_to_str(uint16_t nid)
+{
+ const struct nid_map * p;
+
+ for (p = md_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return p->name;
+ }
+
+ return NULL;
+}
+
+uint16_t md_str_to_nid(const char * kdf)
+{
+ const struct nid_map * p;
+
+ if (kdf == NULL)
+ return NID_undef;
+
+ /* Fast path: check if kdf pointer is in the map */
+ for (p = md_nid_map; p->name != NULL; p++) {
+ if (kdf == p->name)
+ return p->nid;
+ }
+
+ /* Slow path: string comparison */
+ for (p = md_nid_map; p->name != NULL; p++) {
+ if (strcmp(p->name, kdf) == 0)
+ return p->nid;
+ }
+
+ return NID_undef;
+}
+
+int md_validate_nid(int nid)
+{
+ const struct nid_map * p;
+
+ if (nid == NID_undef)
+ return -EINVAL;
+
+ for (p = md_nid_map; p->name != NULL; p++) {
+ if (p->nid == nid)
+ return 0;
+ }
+
+ return -ENOTSUP;
+}
+
+int crypt_cipher_rank(int nid)
+{
+ int i;
+
+ if (nid == NID_undef)
+ return 0;
+
+ for (i = 0; crypt_supported_nids[i] != NID_undef; i++) {
+ if ((int) crypt_supported_nids[i] == nid)
+ return i + 1;
+ }
+
+ return -1;
+}
+
+int crypt_kdf_rank(int nid)
+{
+ int i;
+
+ if (nid == NID_undef)
+ return 0;
+
+ for (i = 0; md_supported_nids[i] != NID_undef; i++) {
+ if ((int) md_supported_nids[i] == nid)
+ return i + 1;
+ }
+
+ return -1;
+}
+
+int crypt_kex_rank(int nid)
+{
+ int i;
+
+ if (nid == NID_undef)
+ return 0;
+
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ if ((int) kex_supported_nids[i] == nid)
+ return i + 1;
+ }
+
+ return -1;
+}
+
+/* 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;
return -ECRYPT;
#endif
}
-int crypt_decrypt(struct crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+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_decrypt(ctx->ctx, ctx->key, 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;
@@ -122,28 +694,170 @@ int crypt_decrypt(struct crypt_ctx * ctx,
#endif
}
-struct crypt_ctx * crypt_create_ctx(const uint8_t * key)
+int crypt_oneshot_open(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_oneshot_open(nid, key, nonce, aad, in, out);
+#else
+ (void) nid;
+ (void) key;
+ (void) nonce;
+ (void) aad;
+ (void) in;
+ (void) out;
+
+ return -ECRYPT;
+#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)
{
struct crypt_ctx * crypt;
+ if (crypt_validate_nid(sk->nid) != 0)
+ return NULL;
+
crypt = malloc(sizeof(*crypt));
if (crypt == NULL)
goto fail_crypt;
memset(crypt, 0, sizeof(*crypt));
- if (key != NULL)
- memcpy(crypt->key, key, SYMMKEYSZ);
+ crypt->kr = keyrot_create(sk->key, sk->epoch, sk->role);
+ if (crypt->kr == NULL)
+ goto fail_kr;
+
#ifdef HAVE_OPENSSL
- crypt->ctx=openssl_crypt_create_ctx();
- if (crypt->ctx == NULL)
- goto fail_ctx;
+ crypt->cipher = openssl_crypt_create_ctx(sk);
+ if (crypt->cipher == NULL)
+ goto fail_cipher;
#endif
return crypt;
+
#ifdef HAVE_OPENSSL
- fail_ctx:
- free(crypt);
+ fail_cipher:
+ keyrot_destroy(crypt->kr);
#endif
+ fail_kr:
+ free(crypt);
fail_crypt:
return NULL;
}
@@ -153,15 +867,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_headsz(struct crypt_ctx * ctx)
+{
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ (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)
+{
+ assert(ctx != NULL);
+ assert(ctx->cipher != NULL);
+
+#ifdef HAVE_OPENSSL
+ return openssl_crypt_get_tagsz(ctx->cipher);
+#else
+ (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)
{
@@ -204,11 +979,91 @@ int crypt_load_pubkey_str(const char * str,
#endif
}
+int crypt_load_pubkey_file(const char * path,
+ void ** key)
+{
+ *key = NULL;
+
+#ifdef HAVE_OPENSSL
+ return openssl_load_pubkey_file(path, key);
+#else
+ (void) path;
+
+ return 0;
+#endif
+}
+
+int crypt_load_pubkey_file_to_der(const char * path,
+ buffer_t * buf)
+{
+ assert(buf != NULL);
+
+#ifdef HAVE_OPENSSL
+ return openssl_load_pubkey_file_to_der(path, buf);
+#else
+ (void) path;
+
+ buf->data = NULL;
+ buf->len = 0;
+ return 0;
+#endif
+}
+
+int crypt_load_pubkey_raw_file(const char * path,
+ buffer_t * buf)
+{
+ assert(buf != NULL);
+
+#ifdef HAVE_OPENSSL
+ return openssl_load_pubkey_raw_file(path, buf);
+#else
+ (void) path;
+
+ buf->data = NULL;
+ buf->len = 0;
+ return 0;
+#endif
+}
+
+int crypt_load_privkey_raw_file(const char * path,
+ void ** key)
+{
+ *key = NULL;
+
+#ifdef HAVE_OPENSSL
+ return openssl_load_privkey_raw_file(path, key);
+#else
+ (void) path;
+
+ 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)
{
#ifdef HAVE_OPENSSL
- return openssl_cmp_key(key1, key2);
+ return openssl_cmp_key((const EVP_PKEY *) key1,
+ (const EVP_PKEY *) key2);
#else
(void) key1;
(void) key2;
@@ -223,7 +1078,7 @@ void crypt_free_key(void * key)
return;
#ifdef HAVE_OPENSSL
- openssl_free_key(key);
+ openssl_free_key((EVP_PKEY *) key);
#endif
}
@@ -343,6 +1198,19 @@ int crypt_check_crt_name(void * crt,
#endif
}
+int crypt_get_crt_name(void * crt,
+ char * name)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_get_crt_name(crt, name);
+#else
+ (void) crt;
+ (void) name;
+
+ return 0;
+#endif
+}
+
struct auth_ctx * auth_create_ctx(void)
{
struct auth_ctx * ctx;
@@ -356,9 +1224,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
@@ -371,6 +1245,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);
@@ -392,27 +1267,71 @@ 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;
+#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 0;
#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
+}
+
int auth_sign(void * pkp,
+ int md_nid,
buffer_t msg,
buffer_t * sig)
{
#ifdef HAVE_OPENSSL
- return openssl_sign(pkp, msg, sig);
+ return openssl_sign((EVP_PKEY *) pkp, md_nid, msg, sig);
#else
(void) pkp;
+ (void) md_nid;
(void) msg;
(void) sig;
@@ -423,16 +1342,126 @@ int auth_sign(void * pkp,
}
int auth_verify_sig(void * pk,
+ int md_nid,
buffer_t msg,
buffer_t sig)
{
#ifdef HAVE_OPENSSL
- return openssl_verify_sig(pk, msg, sig);
+ return openssl_verify_sig((EVP_PKEY *) pk, md_nid, msg, sig);
#else
(void) pk;
+ (void) md_nid;
(void) msg;
(void) sig;
return 0;
#endif
}
+
+ssize_t md_digest(int md_nid,
+ buffer_t in,
+ uint8_t * out)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_md_digest(md_nid, in, out);
+#else
+ (void) md_nid;
+ (void) in;
+ (void) out;
+
+ return -1;
+#endif
+}
+
+ssize_t md_len(int md_nid)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_md_len(md_nid);
+#else
+ (void) md_nid;
+ return -1;
+#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_MINSIZE);
+#else
+ (void) max;
+ return 0;
+#endif
+}
+
+void crypt_secure_malloc_fini(void)
+{
+#ifdef HAVE_OPENSSL
+ openssl_secure_malloc_fini();
+#endif
+}
+
+void crypt_cleanup(void)
+{
+#ifdef HAVE_OPENSSL
+ openssl_cleanup();
+#endif
+}
+
+void * crypt_secure_malloc(size_t size)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_secure_malloc(size);
+#else
+ return malloc(size);
+#endif
+}
+
+void crypt_secure_free(void * ptr,
+ size_t size)
+{
+ if (ptr == NULL)
+ return;
+
+#ifdef HAVE_OPENSSL
+ openssl_secure_free(ptr, size);
+#else
+ memset(ptr, 0, size);
+ free(ptr);
+#endif
+}
+
+void crypt_secure_clear(void * ptr,
+ size_t size)
+{
+ volatile uint8_t * p;
+
+ if (ptr == NULL)
+ return;
+
+#ifdef HAVE_OPENSSL
+ (void) p;
+ openssl_secure_clear(ptr, size);
+#elif defined(HAVE_EXPLICIT_BZERO)
+ (void) p;
+ explicit_bzero(ptr, size);
+#else /* best effort to avoid optimizing out */
+ p = ptr;
+ while (size-- > 0)
+ *p++ = 0;
+#endif
+}
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 291a3418..d5d9adf5 100644
--- a/src/lib/crypt/openssl.c
+++ b/src/lib/crypt/openssl.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* OpenSSL based cryptographic operations
* Elliptic curve Diffie-Hellman key exchange
@@ -23,40 +23,397 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
+#define _POSIX_C_SOURCE 200809L
+
+#include <config.h>
+
#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/ec.h>
+#include <openssl/err.h>
+#include <openssl/kdf.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
+#include <openssl/provider.h>
#include <openssl/x509v3.h>
#include <openssl/x509_vfy.h>
#include <assert.h>
+#include <stdio.h>
+
+#define IS_EC_GROUP(str) (strcmp(str, "EC") == 0)
+#define IS_DH_GROUP(str) (strcmp(str, "DH") == 0)
+
+#define HKDF_INFO_DHE "o7s-ossl-dhe"
+#define HKDF_INFO_ENCAP "o7s-ossl-encap"
+#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 */
+
+struct ossl_crypt_ctx {
+ EVP_CIPHER_CTX * evp_ctx;
+ const EVP_CIPHER * cipher;
+ int tagsz;
+};
+
+struct kdf_info {
+ buffer_t secret;
+ int nid;
+ buffer_t salt;
+ buffer_t info;
+ buffer_t key;
+};
+
+/* Convert hash NID to OpenSSL digest name string for HKDF */
+static const char * hash_nid_to_digest_name(int nid)
+{
+ const EVP_MD * md;
+ const char * name;
+
+ md = EVP_get_digestbynid(nid);
+ if (md == NULL)
+ return NULL;
+
+ name = EVP_MD_get0_name(md);
+ if (name == NULL)
+ return NULL;
+
+ return name;
+}
+
+/* Extract public key bytes from a key pair for salt derivation */
+static int get_pk_bytes_from_key(EVP_PKEY * key,
+ buffer_t * pk)
+{
+ const char * name;
+ int ret;
+
+ assert(key != NULL);
+ assert(pk != NULL);
+
+ name = EVP_PKEY_get0_type_name(key);
+ if (name == NULL)
+ goto fail_name;
+
+ if (IS_HYBRID_KEM(name)) {
+ pk->len = EVP_PKEY_get1_encoded_public_key(key, &pk->data);
+ if (pk->len == 0)
+ goto fail_name;
+ } else {
+ /* Pure ML-KEM: use DER encoding to match encap */
+ pk->data = NULL;
+ ret = i2d_PUBKEY(key, &pk->data);
+ if (ret <= 0)
+ goto fail_name;
+ pk->len = (size_t) ret;
+ }
+
+ return 0;
+ fail_name:
+ return -ECRYPT;
+}
+
+/* Derive salt from public key bytes by hashing them */
+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.data != NULL);
+
+ if (EVP_Digest(pk.data, pk.len, hash, &hash_len,
+ EVP_sha256(), NULL) != 1)
+ goto fail_digest;
+
+ memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len);
+
+ return 0;
+ fail_digest:
+ return -ECRYPT;
+}
+
+/* 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,
+ buffer_t salt)
+{
+ uint8_t * concat;
+ size_t concat_len;
+ uint8_t hash[EVP_MAX_MD_SIZE];
+ unsigned hash_len;
+ size_t min_len;
+ int cmp;
+
+ assert(local.data != NULL);
+ assert(remote.data != NULL);
+ assert(salt.data != NULL);
+
+ concat_len = local.len + remote.len;
+ concat = OPENSSL_malloc(concat_len);
+ if (concat == NULL)
+ goto fail_malloc;
+
+ /* Canonical order: compare and concatenate smaller first */
+ min_len = local.len < remote.len ? local.len : remote.len;
+ cmp = memcmp(local.data, remote.data, min_len);
+ if (cmp < 0 || (cmp == 0 && local.len < remote.len)) {
+ memcpy(concat, local.data, local.len);
+ memcpy(concat + local.len, remote.data, remote.len);
+ } else {
+ memcpy(concat, remote.data, remote.len);
+ memcpy(concat + remote.len, local.data, local.len);
+ }
+
+ if (EVP_Digest(concat, concat_len, hash, &hash_len,
+ EVP_sha256(), NULL) != 1)
+ goto fail_digest;
+
+ OPENSSL_free(concat);
+
+ memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len);
+
+ return 0;
+ fail_digest:
+ OPENSSL_free(concat);
+ fail_malloc:
+ return -ECRYPT;
+}
+
+/* Derive key using HKDF */
+#define OPc_u_str OSSL_PARAM_construct_utf8_string
+#define OPc_o_str OSSL_PARAM_construct_octet_string
+static int derive_key_hkdf(struct kdf_info * ki)
+{
+ EVP_KDF * kdf;
+ EVP_KDF_CTX * kctx;
+ OSSL_PARAM params[5];
+ const char * digest;
+ 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)
+ goto fail_fetch;
+
+ kctx = EVP_KDF_CTX_new(kdf);
+ if (kctx == NULL)
+ goto fail_ctx;
+
+ idx = 0;
+ params[idx++] = OPc_u_str("digest", (char *) digest, 0);
+ params[idx++] = OPc_o_str("key", ki->secret.data, ki->secret.len);
+ params[idx++] = OPc_o_str("salt", ki->salt.data, ki->salt.len);
+ params[idx++] = OPc_o_str("info", ki->info.data, ki->info.len);
+
+ params[idx] = OSSL_PARAM_construct_end();
+
+ if (EVP_KDF_derive(kctx, ki->key.data, ki->key.len, params) != 1)
+ goto fail_derive;
+
+ EVP_KDF_CTX_free(kctx);
+ EVP_KDF_free(kdf);
+
+ return 0;
+
+ fail_derive:
+ EVP_KDF_CTX_free(kctx);
+ fail_ctx:
+ EVP_KDF_free(kdf);
+ fail_fetch:
+ return -ECRYPT;
+}
+
+int openssl_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out)
+{
+ 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;
+
+ 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 (EVP_KDF_derive(kctx, out.data, out.len, params) == 1)
+ ret = 0;
+
+ EVP_KDF_CTX_free(kctx);
+ fail_ctx:
+ EVP_KDF_free(kdf);
+ fail_fetch:
+ return ret;
+}
+
+/* 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)
+{
+ int out_sz;
+ int tmp_sz;
+
+ assert(ctx != NULL);
+ assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */
+
+ EVP_CIPHER_CTX_reset(ctx->evp_ctx);
+
+ if (EVP_EncryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL,
+ NULL, NULL) != 1)
+ return -1;
+
+ /* Pin the AEAD nonce to 96 bits (SP 800-38D deterministic IV). */
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_IVLEN,
+ AEAD_NONCE_LEN, NULL) != 1)
+ return -1;
+
+ if (EVP_EncryptInit_ex(ctx->evp_ctx, NULL, NULL,
+ key, nonce) != 1)
+ return -1;
+
+ if (EVP_EncryptUpdate(ctx->evp_ctx, NULL, &tmp_sz,
+ aad.data, (int) aad.len) != 1)
+ return -1;
+
+ if (EVP_EncryptUpdate(ctx->evp_ctx, out, &out_sz,
+ in.data, (int) in.len) != 1)
+ return -1;
+
+ if (EVP_EncryptFinal_ex(ctx->evp_ctx, out + out_sz, &tmp_sz) != 1)
+ return -1;
+
+ out_sz += tmp_sz;
+
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_GET_TAG,
+ ctx->tagsz, tag) != 1)
+ return -1;
+
+ return 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)
+{
+ int out_sz;
+ int tmp_sz;
+
+ assert(ctx != NULL);
+ assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */
+
+ EVP_CIPHER_CTX_reset(ctx->evp_ctx);
+
+ if (EVP_DecryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL,
+ NULL, NULL) != 1)
+ return -1;
+
+ /* Pin the AEAD nonce to 96 bits (SP 800-38D deterministic IV). */
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_IVLEN,
+ AEAD_NONCE_LEN, NULL) != 1)
+ return -1;
+
+ if (EVP_DecryptInit_ex(ctx->evp_ctx, NULL, NULL, key, nonce) != 1)
+ return -1;
+
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_TAG,
+ ctx->tagsz, (void *) tag) != 1)
+ return -1;
+
+ if (EVP_DecryptUpdate(ctx->evp_ctx, NULL, &tmp_sz,
+ aad.data, (int) aad.len) != 1)
+ return -1;
+
+ if (EVP_DecryptUpdate(ctx->evp_ctx, out->data, &out_sz,
+ in.data, (int) in.len) != 1)
+ return -1;
+
+ if (EVP_DecryptFinal_ex(ctx->evp_ctx, out->data + out_sz,
+ &tmp_sz) != 1)
+ return -1;
+
+ out_sz += tmp_sz;
+
+ out->len = (size_t) out_sz;
+
+ return out_sz;
+}
/*
* Derive the common secret from
- * - your public key pair (kp)
- * - the remote public key (pub).
+ * - your public key pair (pkp)
+ * - the remote public key bytes (remote_pk).
* Store it in a preallocated buffer (s).
*/
-static int __openssl_ecdh_derive_secret(EVP_PKEY * kp,
- EVP_PKEY * pub,
- uint8_t * s)
+static int __openssl_dhe_derive(EVP_PKEY * pkp,
+ EVP_PKEY * pub,
+ buffer_t remote_pk,
+ int kdf,
+ uint8_t * s)
{
- EVP_PKEY_CTX * ctx;
- int ret;
- uint8_t * secret;
- size_t secret_len;
-
- ctx = EVP_PKEY_CTX_new(kp, NULL);
+ EVP_PKEY_CTX * ctx;
+ struct kdf_info ki;
+ buffer_t local_pk;
+ int ret;
+ uint8_t * secret;
+ size_t secret_len;
+ uint8_t salt_buf[HKDF_SALT_LEN];
+
+ /* Extract local public key bytes */
+ local_pk.data = NULL;
+ 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, ki.salt) < 0)
+ goto fail_salt;
+
+ ctx = EVP_PKEY_CTX_new(pkp, NULL);
if (ctx == NULL)
- goto fail_new;
+ goto fail_salt;
ret = EVP_PKEY_derive_init(ctx);
if (ret != 1)
@@ -81,37 +438,76 @@ static int __openssl_ecdh_derive_secret(EVP_PKEY * kp,
if (ret != 1)
goto fail_derive;
- /* Hash the secret for use as AES key. */
- mem_hash(HASH_SHA3_256, s, secret, secret_len);
+ ki.nid = kdf;
+ ki.secret.len = secret_len;
+ ki.secret.data = secret;
+ ki.info.len = strlen(HKDF_INFO_DHE);
+ ki.info.data = (uint8_t *) HKDF_INFO_DHE;
+ ki.key.len = SYMMKEYSZ;
+ ki.key.data = s;
+
+ /* 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);
+
+ if (ret != 0)
+ return ret;
return 0;
fail_derive:
- OPENSSL_free(secret);
+ OPENSSL_clear_free(secret, secret_len);
fail_ctx:
EVP_PKEY_CTX_free(ctx);
- fail_new:
+ fail_salt:
+ OPENSSL_free(local_pk.data);
+ fail_local:
return -ECRYPT;
}
-static int __openssl_ecdh_gen_key(void ** kp)
+static int __openssl_dhe_gen_key(const char * algo,
+ EVP_PKEY ** kp)
{
EVP_PKEY_CTX * ctx = NULL;
EVP_PKEY_CTX * kctx = NULL;
EVP_PKEY * params = NULL;
+ int nid;
+ int type;
int ret;
- ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
+ assert(algo != NULL);
+ assert(kp != NULL);
+
+ nid = OBJ_txt2nid(algo);
+ if (nid == NID_undef)
+ return -ECRYPT;
+
+ /* X25519 and X448: direct keygen context */
+ if (nid == EVP_PKEY_X25519 || nid == EVP_PKEY_X448) {
+ kctx = EVP_PKEY_CTX_new_id(nid, NULL);
+ if (kctx == NULL)
+ goto fail_kctx;
+
+ goto keygen;
+ }
+ /* EC and FFDHE: parameter generation first */
+ type = (strncmp(algo, "ffdhe", 5) == 0) ? EVP_PKEY_DH : EVP_PKEY_EC;
+
+ ctx = EVP_PKEY_CTX_new_id(type, NULL);
if (ctx == NULL)
- goto fail_new_id;
+ goto fail_ctx;
ret = EVP_PKEY_paramgen_init(ctx);
if (ret != 1)
goto fail_paramgen;
- ret = EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_X9_62_prime256v1);
+ if (type == EVP_PKEY_EC)
+ ret = EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid);
+ else /* EVP_PKEY_DH */
+ ret = EVP_PKEY_CTX_set_dh_nid(ctx, nid);
+
if (ret != 1)
goto fail_paramgen;
@@ -121,214 +517,623 @@ static int __openssl_ecdh_gen_key(void ** kp)
kctx = EVP_PKEY_CTX_new(params, NULL);
if (kctx == NULL)
- goto fail_keygen_init;
+ goto fail_kctx;
+ EVP_PKEY_free(params);
+ EVP_PKEY_CTX_free(ctx);
+ keygen:
ret = EVP_PKEY_keygen_init(kctx);
if (ret != 1)
goto fail_keygen;
- ret = EVP_PKEY_keygen(kctx, (EVP_PKEY **) kp);
+ ret = EVP_PKEY_keygen(kctx, kp);
if (ret != 1)
goto fail_keygen;
- EVP_PKEY_free(params);
EVP_PKEY_CTX_free(kctx);
- EVP_PKEY_CTX_free(ctx);
return 0;
+
fail_keygen:
EVP_PKEY_CTX_free(kctx);
- fail_keygen_init:
- EVP_PKEY_free(params);
+ return -ECRYPT;
+ fail_kctx:
+ if (params != NULL)
+ EVP_PKEY_free(params);
fail_paramgen:
- EVP_PKEY_CTX_free(ctx);
- fail_new_id:
+ if (ctx != NULL)
+ EVP_PKEY_CTX_free(ctx);
+ fail_ctx:
return -ECRYPT;
}
-ssize_t openssl_ecdh_pkp_create(void ** pkp,
- uint8_t * pk)
+static int __openssl_kem_gen_key(const char * algo,
+ EVP_PKEY ** kp)
{
- uint8_t * pos;
- ssize_t len;
+ EVP_PKEY_CTX * kctx;
+ int ret;
+
+ assert(algo != NULL);
+ assert(kp != NULL);
+
+ /* PQC KEM (ML-KEM-512, ML-KEM-768, ML-KEM-1024) or hybrid */
+ kctx = EVP_PKEY_CTX_new_from_name(NULL, algo, NULL);
+ if (kctx == NULL)
+ goto fail_kctx;
+
+ ret = EVP_PKEY_keygen_init(kctx);
+ if (ret != 1)
+ goto fail_keygen;
+
+ ret = EVP_PKEY_keygen(kctx, kp);
+ if (ret != 1)
+ goto fail_keygen;
+
+ EVP_PKEY_CTX_free(kctx);
+
+ return 0;
+ fail_keygen:
+ EVP_PKEY_CTX_free(kctx);
+ fail_kctx:
+ 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)
+{
+ assert(algo != NULL);
+ assert(kp != NULL);
+
+ /* Dispatch based on algorithm name prefix */
+ if (IS_KEM_ALGORITHM(algo))
+ return __openssl_kem_gen_key(algo, kp);
+
+ return __openssl_dhe_gen_key(algo, kp);
+}
+
+ssize_t openssl_pkp_create(const char * algo,
+ EVP_PKEY ** pkp,
+ uint8_t * pk)
+{
+ uint8_t * pos;
+ buffer_t raw;
+ ssize_t len;
+
+ assert(algo != NULL);
assert(pkp != NULL);
assert(*pkp == NULL);
assert(pk != NULL);
- if (__openssl_ecdh_gen_key(pkp) < 0)
+ if (__openssl_kex_gen_key(algo, pkp) < 0)
goto fail_key;
- pos = pk; /* i2d_PUBKEY increments the pointer, don't use pk! */
- len = i2d_PUBKEY(*pkp, &pos);
- if (len < 0)
- goto fail_pubkey;
+ if (IS_HYBRID_KEM(algo)) { /* Raw encode hybrid KEM */
+ raw.len = EVP_PKEY_get1_encoded_public_key(*pkp, &raw.data);
+ if (raw.len == 0)
+ goto fail_pubkey;
- return len;
+ 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! */
+ if (i2d_PUBKEY(*pkp, &pos) < 0)
+ goto fail_pubkey;
+
+ return len;
+ }
fail_pubkey:
EVP_PKEY_free(*pkp);
fail_key:
return -ECRYPT;
}
-void openssl_ecdh_pkp_destroy(void * pkp)
+/* Common KEM encapsulation - pub key and salt already prepared */
+static ssize_t __openssl_kem_encap(EVP_PKEY * pub,
+ uint8_t * salt,
+ uint8_t * ct,
+ int kdf,
+ uint8_t * s)
{
- EVP_PKEY_free((EVP_PKEY *) pkp);
+ EVP_PKEY_CTX * ctx;
+ struct kdf_info ki;
+ uint8_t * secret;
+ size_t secret_len;
+ size_t ct_len;
+ int ret;
+
+ ctx = EVP_PKEY_CTX_new(pub, NULL);
+ if (ctx == NULL)
+ goto fail_ctx;
+
+ ret = EVP_PKEY_encapsulate_init(ctx, NULL);
+ if (ret != 1)
+ goto fail_encap;
+
+ /* Get required lengths */
+ ret = EVP_PKEY_encapsulate(ctx, NULL, &ct_len, NULL, &secret_len);
+ if (ret != 1 || ct_len > CRYPT_KEY_BUFSZ)
+ goto fail_encap;
+
+ /* Allocate buffer for secret */
+ secret = OPENSSL_malloc(secret_len);
+ if (secret == NULL)
+ goto fail_encap;
+
+ /* Perform encapsulation */
+ ret = EVP_PKEY_encapsulate(ctx, ct, &ct_len, secret, &secret_len);
+ if (ret != 1)
+ goto fail_secret;
+
+ ki.secret.len = secret_len;
+ ki.secret.data = secret;
+ ki.nid = kdf;
+ ki.info.len = strlen(HKDF_INFO_ENCAP);
+ ki.info.data = (uint8_t *) HKDF_INFO_ENCAP;
+ ki.key.len = SYMMKEYSZ;
+ ki.key.data = s;
+ ki.salt.len = HKDF_SALT_LEN;
+ ki.salt.data = salt;
+
+ /* Derive symmetric key from shared secret using HKDF */
+ ret = derive_key_hkdf(&ki);
+
+ OPENSSL_clear_free(secret, secret_len);
+ EVP_PKEY_CTX_free(ctx);
+
+ if (ret != 0)
+ return -ECRYPT;
+
+ return (ssize_t) ct_len;
+
+ fail_secret:
+ OPENSSL_clear_free(secret, secret_len);
+ fail_encap:
+ EVP_PKEY_CTX_free(ctx);
+ fail_ctx:
+ return -ECRYPT;
}
-int openssl_ecdh_derive(void * pkp,
- buffer_t pk,
- uint8_t * s)
+/* ML-KEM encapsulation - DER-encoded public key */
+ssize_t openssl_kem_encap(buffer_t pk,
+ uint8_t * ct,
+ int kdf,
+ uint8_t * s)
{
- uint8_t * pos;
EVP_PKEY * pub;
+ uint8_t * pos;
+ uint8_t salt[HKDF_SALT_LEN];
+ buffer_t salt_b;
+ ssize_t ret;
- pos = pk.data; /* d2i_PUBKEY increments pos, don't use key ptr! */
+ assert(pk.data != NULL);
+ assert(ct != NULL);
+ assert(s != NULL);
+
+ 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;
pub = d2i_PUBKEY(NULL, (const uint8_t **) &pos, (long) pk.len);
if (pub == NULL)
- goto fail_pubkey;
+ goto fail_salt;
- if (__openssl_ecdh_derive_secret(pkp, pub, s) < 0)
- goto fail_key;
+ ret = __openssl_kem_encap(pub, salt, ct, kdf, s);
EVP_PKEY_free(pub);
- return 0;
- fail_pubkey:
- EVP_PKEY_free(pub);
- fail_key:
+ return ret;
+ fail_salt:
return -ECRYPT;
}
-/*
- * AES encryption calls. If FRCT is disabled, we should generate a
- * 128-bit random IV and append it to the packet. If the flow is
- * reliable, we could initialize the context once, and consider the
- * stream a single encrypted message to avoid initializing the
- * encryption context for each packet.
- */
-
-int openssl_encrypt(void * ctx,
- uint8_t * key,
- buffer_t in,
- buffer_t * out)
+/* Hybrid KEM encapsulation: raw-encoded public key */
+ssize_t openssl_kem_encap_raw(buffer_t pk,
+ uint8_t * ct,
+ int kdf,
+ uint8_t * s)
{
- uint8_t * ptr;
- uint8_t * iv;
- int in_sz;
- int out_sz;
- int tmp_sz;
- int ret;
+ EVP_PKEY * pub;
+ const char * algo;
+ uint8_t salt[HKDF_SALT_LEN];
+ buffer_t salt_b;
+ ssize_t ret;
- in_sz = (int) in.len;
+ assert(pk.data != NULL);
+ assert(ct != NULL);
+ assert(s != NULL);
- out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + IVSZ);
- if (out->data == NULL)
- goto fail_malloc;
+ salt_b.len = HKDF_SALT_LEN;
+ salt_b.data = salt;
- iv = out->data;
- ptr = out->data + IVSZ;
+ if (derive_salt_from_pk_bytes(pk, salt_b) < 0)
+ goto fail_salt;
- if (random_buffer(iv, IVSZ) < 0)
- goto fail_iv;
+ algo = __openssl_hybrid_algo_from_len(pk.len);
+ if (algo == NULL)
+ goto fail_salt;
- EVP_CIPHER_CTX_reset(ctx);
+ pub = EVP_PKEY_new_raw_public_key_ex(NULL, algo, NULL,
+ pk.data, pk.len);
+ if (pub == NULL)
+ goto fail_salt;
- ret = EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);
+ ret = __openssl_kem_encap(pub, salt, ct, kdf, s);
+
+ EVP_PKEY_free(pub);
+
+ return ret;
+ fail_salt:
+ return -ECRYPT;
+}
+
+/* KEM decapsulation - used by party that generated the keypair */
+int openssl_kem_decap(EVP_PKEY * priv,
+ buffer_t ct,
+ int kdf,
+ uint8_t * s)
+{
+ EVP_PKEY_CTX * ctx;
+ struct kdf_info ki;
+ buffer_t pk;
+ uint8_t * secret;
+ 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;
+
+ 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);
+ if (ctx == NULL)
+ goto fail_salt;
+
+ ret = EVP_PKEY_decapsulate_init(ctx, NULL);
if (ret != 1)
- goto fail_iv;
+ goto fail_ctx;
- ret = EVP_EncryptUpdate(ctx, ptr, &tmp_sz, in.data, in_sz);
+ /* Get required secret length */
+ ret = EVP_PKEY_decapsulate(ctx, NULL, &secret_len, ct.data, ct.len);
if (ret != 1)
- goto fail_encrypt;
+ goto fail_ctx;
+
+ /* Allocate buffer for secret */
+ secret = OPENSSL_malloc(secret_len);
+ if (secret == NULL)
+ goto fail_ctx;
- out_sz = tmp_sz;
- ret = EVP_EncryptFinal_ex(ctx, ptr + tmp_sz, &tmp_sz);
+ /* Perform decapsulation */
+ ret = EVP_PKEY_decapsulate(ctx, secret, &secret_len, ct.data, ct.len);
if (ret != 1)
- goto fail_encrypt;
+ goto fail_secret;
+
+ ki.secret.len = secret_len;
+ ki.secret.data = secret;
+ ki.nid = kdf;
+ ki.info.len = strlen(HKDF_INFO_ENCAP);
+ ki.info.data = (uint8_t *) HKDF_INFO_ENCAP;
+ ki.key.len = SYMMKEYSZ;
+ ki.key.data = s;
+ ki.salt.len = HKDF_SALT_LEN;
+ ki.salt.data = salt;
+
+ /* Derive symmetric key from shared secret using HKDF */
+ ret = derive_key_hkdf(&ki);
+
+ OPENSSL_clear_free(secret, secret_len);
+ EVP_PKEY_CTX_free(ctx);
+ OPENSSL_free(pk.data);
- out_sz += tmp_sz;
+ if (ret != 0)
+ return ret;
+
+ return 0;
+
+ fail_secret:
+ OPENSSL_clear_free(secret, secret_len);
+ fail_ctx:
+ EVP_PKEY_CTX_free(ctx);
+ fail_salt:
+ OPENSSL_free(pk.data);
+ fail_pk:
+ return -ECRYPT;
+}
+
+void openssl_pkp_destroy(EVP_PKEY * pkp)
+{
+ EVP_PKEY_free(pkp);
+}
+
+static int openssl_get_curve(EVP_PKEY * pub,
+ char * algo)
+{
+ int ret;
+ size_t len = KEX_ALGO_BUFSZ;
- EVP_CIPHER_CTX_cleanup(ctx);
+ ret = EVP_PKEY_get_utf8_string_param(pub, "group", algo, len, &len);
- assert(out_sz >= in_sz);
+ return ret == 1 ? 0 : -ECRYPT;
+}
+
+int openssl_get_algo_from_pk_der(buffer_t pk,
+ char * algo)
+{
+ uint8_t * pos;
+ EVP_PKEY * pub;
+ char * type_str;
+
+ assert(pk.data != NULL);
+ assert(algo != NULL);
+
+ pos = pk.data;
+ pub = d2i_PUBKEY(NULL, (const uint8_t **) &pos, (long) pk.len);
+ if (pub == NULL)
+ goto fail_decode;
- out->len = (size_t) out_sz + IVSZ;
+ type_str = (char *) EVP_PKEY_get0_type_name(pub);
+ if (type_str == NULL)
+ goto fail_pub;
+ strcpy(algo, type_str);
+
+ 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;
- fail_encrypt:
- EVP_CIPHER_CTX_cleanup(ctx);
- fail_iv:
- free(out->data);
- fail_malloc:
- clrbuf(*out);
+
+ fail_pub:
+ EVP_PKEY_free(pub);
+ fail_decode:
return -ECRYPT;
}
-int openssl_decrypt(void * ctx,
- uint8_t * key,
- buffer_t in,
- buffer_t * out)
+int openssl_get_algo_from_pk_raw(buffer_t pk,
+ char * algo)
{
- uint8_t * ptr;
- uint8_t * iv;
- uint8_t * input;
- int ret;
- int out_sz;
- int in_sz;
- int tmp_sz;
+ const char * hybrid_algo;
- in_sz = (int) in.len - IVSZ;
- if (in_sz < 0)
+ assert(pk.data != NULL);
+ assert(algo != NULL);
+
+ hybrid_algo = __openssl_hybrid_algo_from_len(pk.len);
+ if (hybrid_algo == NULL)
return -ECRYPT;
- out->data = malloc(in_sz);
+ 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;
+
+ assert(pkp != NULL);
+ assert(pk.data != NULL);
+ assert(s != NULL);
+
+ /* X.509 DER decoding for DHE */
+ pos = pk.data; /* d2i_PUBKEY increments pos, don't use key ptr! */
+ pub = d2i_PUBKEY(NULL, (const uint8_t **) &pos, (long) pk.len);
+ if (pub == NULL)
+ goto fail_decode;
+
+ if (__openssl_dhe_derive(pkp, pub, pk, kdf, s) < 0)
+ goto fail_derive;
+
+ EVP_PKEY_free(pub);
+
+ return 0;
+ fail_derive:
+ EVP_PKEY_free(pub);
+ fail_decode:
+ return -ECRYPT;
+}
+
+/* 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)
+{
+ ctx->cipher = EVP_get_cipherbynid(nid);
+ if (ctx->cipher == NULL)
+ return -1;
+
+ /* IV must fit the NONCESZ nonce buffer. */
+ if (EVP_CIPHER_get_iv_length(ctx->cipher) > NONCESZ)
+ return -1;
+
+ /* Authenticated encryption is mandatory; reject non-AEAD ciphers. */
+ if ((EVP_CIPHER_flags(ctx->cipher) & EVP_CIPH_FLAG_AEAD_CIPHER) == 0)
+ return -1;
+
+ ctx->tagsz = AEAD_TAG_LEN;
+
+ ctx->evp_ctx = EVP_CIPHER_CTX_new();
+ if (ctx->evp_ctx == NULL)
+ return -1;
+
+ return 0;
+}
+
+/* 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;
+
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
+
+ memset(&ctx, 0, sizeof(ctx));
+
+ if (ossl_cipher_ctx_init(&ctx, nid) < 0)
+ goto fail_cipher;
+
+ out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + ctx.tagsz);
if (out->data == NULL)
- goto fail_malloc;
+ goto fail_ctx;
- iv = in.data;
- ptr = out->data;
- input = in.data + IVSZ;
+ out_sz = openssl_seal(&ctx, key, nonce, aad, in,
+ out->data, out->data + in.len);
+ if (out_sz < 0)
+ goto fail_seal;
- EVP_CIPHER_CTX_reset(ctx);
+ out->len = (size_t) out_sz + ctx.tagsz;
- ret = EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);
- if (ret != 1)
- goto fail_decrypt_init;
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
- ret = EVP_DecryptUpdate(ctx, ptr, &tmp_sz, input, in_sz);
- if (ret != 1)
- goto fail_decrypt;
+ return 0;
- out_sz = tmp_sz;
- ret = EVP_DecryptFinal_ex(ctx, ptr + tmp_sz, &tmp_sz);
- if (ret != 1)
- goto fail_decrypt;
+ fail_seal:
+ free(out->data);
+ fail_ctx:
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
+ fail_cipher:
+ clrbuf(*out);
+ return -ECRYPT;
+}
- out_sz += tmp_sz;
+/* 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)
+{
+ struct ossl_crypt_ctx ctx;
+ buffer_t ct;
+ const uint8_t * tag;
+ int in_sz;
- assert(out_sz <= in_sz);
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
- out->len = (size_t) out_sz;
+ memset(&ctx, 0, sizeof(ctx));
+
+ if (ossl_cipher_ctx_init(&ctx, nid) < 0)
+ goto fail_cipher;
+
+ if (in.len < (size_t) ctx.tagsz)
+ goto fail_ctx;
+
+ in_sz = (int) in.len - ctx.tagsz;
+
+ out->data = malloc((size_t) in_sz + EVP_MAX_BLOCK_LENGTH);
+ if (out->data == NULL)
+ goto fail_ctx;
+
+ ct.data = in.data;
+ ct.len = (size_t) in_sz;
+ tag = in.data + in_sz;
+
+ if (openssl_open(&ctx, key, nonce, aad, ct, tag, out) < 0)
+ goto fail_open;
+
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
return 0;
- fail_decrypt:
- EVP_CIPHER_CTX_cleanup(ctx);
- fail_decrypt_init:
+
+ fail_open:
free(out->data);
- fail_malloc:
+ fail_ctx:
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
+ fail_cipher:
clrbuf(*out);
return -ECRYPT;
}
-void * openssl_crypt_create_ctx(void)
+struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk)
{
- return (void *) EVP_CIPHER_CTX_new();
+ struct ossl_crypt_ctx * ctx;
+
+ assert(sk != NULL);
+ assert(sk->key != NULL);
+
+ ctx = malloc(sizeof(*ctx));
+ if (ctx == NULL)
+ goto fail_malloc;
+
+ memset(ctx, 0, sizeof(*ctx));
+
+ if (ossl_cipher_ctx_init(ctx, sk->nid) < 0)
+ goto fail_cipher;
+
+ return ctx;
+
+ fail_cipher:
+ free(ctx);
+ fail_malloc:
+ return NULL;
}
-void openssl_crypt_destroy_ctx(void * ctx)
+void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx)
{
- EVP_CIPHER_CTX_free((EVP_CIPHER_CTX *) ctx);
+ if (ctx == NULL)
+ return;
+
+ EVP_CIPHER_CTX_free(ctx->evp_ctx);
+ free(ctx);
+}
+
+int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx)
+{
+ assert(ctx != NULL);
+
+ return ctx->tagsz;
}
/* AUTHENTICATION */
@@ -343,7 +1148,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;
@@ -359,35 +1169,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)
{
@@ -447,7 +1280,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;
@@ -466,146 +1304,307 @@ int openssl_load_privkey_file(const char * path,
int openssl_load_privkey_str(const char * str,
void ** key)
{
- BIO * bio;
+ return load_pem_str(str, rd_privkey_bio, key);
+}
+
+int openssl_load_pubkey_file(const char * path,
+ void ** key)
+{
+ FILE * fp;
EVP_PKEY * pkey;
- bio = BIO_new(BIO_s_mem());
- if (bio == NULL)
- goto fail_bio;
+ fp = fopen(path, "r");
+ if (fp == NULL)
+ goto fail_file;
- if (BIO_write(bio, str, strlen(str)) < 0)
- goto fail_key;
+ pthread_cleanup_push(__cleanup_fclose, fp);
+
+ pkey = PEM_read_PUBKEY(fp, NULL, NULL, NULL);
+
+ pthread_cleanup_pop(false);
- pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
if (pkey == NULL)
goto fail_key;
- BIO_free(bio);
+ fclose(fp);
*key = (void *) pkey;
return 0;
fail_key:
- BIO_free(bio);
- fail_bio:
+ fclose(fp);
+ fail_file:
*key = NULL;
return -1;
}
-int openssl_load_pubkey_file(const char * path,
- void ** key)
+int openssl_load_pubkey_file_to_der(const char * path,
+ buffer_t * buf)
{
FILE * fp;
EVP_PKEY * pkey;
+ int ret;
+
+ assert(path != NULL);
+ assert(buf != NULL);
+
+ memset(buf, 0, sizeof(*buf));
fp = fopen(path, "r");
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;
- fclose(fp);
+ /* Extract public key bytes in DER format */
+ ret = get_pk_bytes_from_key(pkey, buf);
+ if (ret < 0)
+ goto fail_extract;
- *key = (void *) pkey;
+ EVP_PKEY_free(pkey);
+
+ fclose(fp);
return 0;
+
+ fail_extract:
+ EVP_PKEY_free(pkey);
fail_key:
fclose(fp);
fail_file:
- *key = NULL;
+ clrbuf(*buf);
return -1;
}
int openssl_load_pubkey_str(const char * str,
void ** key)
{
- BIO * bio;
- EVP_PKEY * pkey;
+ return load_pem_str(str, rd_pubkey_bio, key);
+}
- bio = BIO_new(BIO_s_mem());
- if (bio == NULL)
- goto fail_bio;
+int openssl_load_pubkey_raw_file(const char * path,
+ buffer_t * buf)
+{
+ FILE * fp;
+ uint8_t tmp_buf[CRYPT_KEY_BUFSZ];
+ size_t bytes_read;
+ const char * algo;
- if (BIO_write(bio, str, strlen(str)) < 0)
- goto fail_key;
+ assert(path != 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);
+
+ pthread_cleanup_pop(false);
+
+ if (bytes_read == 0)
+ goto fail_read;
+
+ /* Validate that this is a known hybrid KEM format */
+ algo = __openssl_hybrid_algo_from_len(bytes_read);
+ if (algo == NULL)
+ goto fail_read;
+
+ buf->data = malloc(bytes_read);
+ if (buf->data == NULL)
+ goto fail_malloc;
+
+ memcpy(buf->data, tmp_buf, bytes_read);
+ buf->len = bytes_read;
+
+ fclose(fp);
+ return 0;
+
+ fail_malloc:
+ fail_read:
+ fclose(fp);
+ fail_file:
+ clrbuf(*buf);
+ return -1;
+}
+
+/* Determine hybrid KEM algorithm from raw private key length */
+static const char * __openssl_hybrid_algo_from_sk_len(size_t len)
+{
+ switch(len) {
+ case X25519MLKEM768_SKSZ:
+ return "X25519MLKEM768";
+ case X448MLKEM1024_SKSZ:
+ return "X448MLKEM1024";
+ default:
+ break;
+ }
+
+ return NULL;
+}
+
+/* Wipe the raw-key staging buffer if a cancel aborts the read. */
+static void __cleanse_key_buf(void * o)
+{
+ OPENSSL_cleanse(o, CRYPT_KEY_BUFSZ);
+}
+
+int openssl_load_privkey_raw_file(const char * path,
+ void ** key)
+{
+ FILE * fp;
+ uint8_t tmp_buf[CRYPT_KEY_BUFSZ];
+ size_t bytes_read;
+ const char * algo;
+ EVP_PKEY * pkey;
+
+ assert(path != 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);
+
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+
+ if (bytes_read == 0)
+ goto fail_read;
+
+ /* Determine algorithm from key size */
+ algo = __openssl_hybrid_algo_from_sk_len(bytes_read);
+ if (algo == NULL)
+ goto fail_read;
+
+ pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algo, NULL,
+ tmp_buf, bytes_read);
+ /* Clear sensitive data from stack */
+ OPENSSL_cleanse(tmp_buf, bytes_read);
- pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
if (pkey == NULL)
- goto fail_key;
+ goto fail_read;
- BIO_free(bio);
+ fclose(fp);
*key = (void *) pkey;
return 0;
- fail_key:
- BIO_free(bio);
- fail_bio:
+
+ fail_read:
+ fclose(fp);
+ fail_file:
*key = NULL;
return -1;
}
-int openssl_cmp_key(const void * key1,
- const void * key2)
+int openssl_cmp_key(const EVP_PKEY * key1,
+ const EVP_PKEY * key2)
{
- EVP_PKEY * pkey1;
- EVP_PKEY * pkey2;
-
assert(key1 != NULL);
assert(key2 != NULL);
- pkey1 = (EVP_PKEY *) key1;
- pkey2 = (EVP_PKEY *) key2;
-
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
- return EVP_PKEY_eq(pkey1, pkey2) == 1 ? 0 : -1;
+ return EVP_PKEY_eq(key1, key2) == 1 ? 0 : -1;
#else
- return EVP_PKEY_cmp(pkey1, pkey2) == 1 ? 0 : -1;
+ return EVP_PKEY_cmp(key1, key2) == 1 ? 0 : -1;
#endif
}
-void openssl_free_key(void * key)
+void openssl_free_key(EVP_PKEY * key)
{
- EVP_PKEY_free((EVP_PKEY *) key);
+ EVP_PKEY_free(key);
}
int openssl_check_crt_name(void * crt,
const char * name)
{
- char * subj;
- char * cn;
- X509 * xcrt;
+ const unsigned char * cn;
+ ASN1_STRING * val;
+ 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 = ASN1_STRING_length(val);
- cn = strstr(subj, "CN=");
- if (cn == NULL)
- goto fail_cn;
+ if (len < 0 || (size_t) len != strlen(name))
+ return -1;
- if (strcmp(cn + 3, name) != 0)
- goto fail_cn;
+ if (memchr(cn, '\0', (size_t) len) != NULL)
+ return -1;
- free(subj);
+ if (memcmp(cn, name, (size_t) len) != 0)
+ return -1;
+
+ return 0;
+}
+
+int openssl_get_crt_name(void * crt,
+ char * name)
+{
+ const unsigned char * cn;
+ ASN1_STRING * val;
+ X509_NAME * nm;
+ int idx;
+ int len;
+
+ 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;
+
+ val = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(nm, idx));
+ cn = ASN1_STRING_get0_data(val);
+ len = ASN1_STRING_length(val);
+
+ if (len < 0)
+ return -1;
+
+ if ((size_t) len > NAME_SIZE)
+ return -ENAME;
+
+ /* Reject an embedded NUL that would truncate the parsed name. */
+ if (memchr(cn, '\0', (size_t) len) != NULL)
+ return -1;
+
+ 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,
char * str)
{
- BIO * bio;
- X509 * xcrt;
- char * p;
+ BIO * bio;
+ X509 * xcrt;
+ char * p;
+ ssize_t len;
xcrt = (X509 *) crt;
@@ -615,11 +1614,12 @@ int openssl_crt_str(const void * crt,
X509_print(bio, xcrt);
- BIO_get_mem_data(bio, &p);
- if (p == NULL)
+ len = (ssize_t) BIO_get_mem_data(bio, &p);
+ if (len <= 0 || p == NULL)
goto fail_p;
- sprintf(str, "%s", p);
+ memcpy(str, p, len);
+ str[len] = '\0';
BIO_free(bio);
@@ -633,25 +1633,33 @@ int openssl_crt_str(const void * crt,
int openssl_crt_der(const void * crt,
buffer_t * buf)
{
- int len;
+ uint8_t * p;
+ int len;
assert(crt != NULL);
assert(buf != NULL);
- len = i2d_X509((X509 *) crt, &buf->data);
+ /* Get the size by encoding to NULL */
+ len = i2d_X509((X509 *) crt, NULL);
if (len < 0)
- goto fail_der;
+ goto fail_len;
+ buf->data = malloc((size_t) len);
+ if (buf->data == NULL)
+ goto fail_malloc;
+
+ p = buf->data; /* i2d_X509 increments p */
+ i2d_X509((X509 *) crt, &p);
buf->len = (size_t) len;
return 0;
- fail_der:
+ fail_malloc:
+ fail_len:
clrbuf(*buf);
return -1;
}
-
void * openssl_auth_create_store(void)
{
return X509_STORE_new();
@@ -672,12 +1680,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;
@@ -687,7 +1726,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;
@@ -695,46 +1735,89 @@ 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_sign(void * pkp,
+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,
+ int nid)
+{
+ if (EVP_PKEY_get_id(pkey) < 0)
+ return NULL; /* Provider-based (PQC) */
+
+ if (nid == NID_undef)
+ return NULL; /* Classical requires explicit nid */
+
+ 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,
buffer_t * sig)
{
- EVP_PKEY * pkey;
- EVP_MD_CTX * mdctx;
- size_t required;
+ EVP_MD_CTX * mdctx;
+ const EVP_MD * md;
+ size_t required;
assert(pkp != NULL);
assert(sig != NULL);
- pkey = (EVP_PKEY *) pkp;
-
mdctx = EVP_MD_CTX_new();
if (!mdctx)
goto fail_ctx;
- if (EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1)
- goto fail_digest;
+ md = select_md(pkp, nid);
- if (EVP_DigestSignUpdate(mdctx, msg.data, msg.len) != 1)
+ if (EVP_DigestSignInit(mdctx, NULL, md, NULL, pkp) != 1)
goto fail_digest;
- if (EVP_DigestSignFinal(mdctx, NULL, &required) != 1)
+ /* Get required signature buffer size */
+ if (EVP_DigestSign(mdctx, NULL, &required, msg.data, msg.len) != 1)
goto fail_digest;
sig->data = malloc(required);
if (sig->data == NULL)
goto fail_digest;
- if (EVP_DigestSignFinal(mdctx, sig->data, &required) != 1)
+ if (EVP_DigestSign(mdctx, sig->data, &required, msg.data, msg.len) != 1)
goto fail_sign;
sig->len = required;
@@ -751,29 +1834,27 @@ int openssl_sign(void * pkp,
return -1;
}
-int openssl_verify_sig(void * pk,
- buffer_t msg,
- buffer_t sig)
+int openssl_verify_sig(EVP_PKEY * pk,
+ int nid,
+ buffer_t msg,
+ buffer_t sig)
{
- EVP_PKEY * pkey;
- EVP_MD_CTX * mdctx;
- int ret;
+ EVP_MD_CTX * mdctx;
+ const EVP_MD * md;
+ int ret;
assert(pk != NULL);
- pkey = (EVP_PKEY *) pk;
-
mdctx = EVP_MD_CTX_new();
if (!mdctx)
goto fail_ctx;
- if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1)
- goto fail_digest;
+ md = select_md(pk, nid);
- if (EVP_DigestVerifyUpdate(mdctx, msg.data, msg.len) != 1)
+ if (EVP_DigestVerifyInit(mdctx, NULL, md, NULL, pk) != 1)
goto fail_digest;
- ret = EVP_DigestVerifyFinal(mdctx, sig.data, sig.len);
+ ret = EVP_DigestVerify(mdctx, sig.data, sig.len, msg.data, msg.len);
if (ret != 1)
goto fail_digest;
@@ -786,3 +1867,67 @@ int openssl_verify_sig(void * pk,
clrbuf(sig);
return -1;
}
+
+ssize_t openssl_md_digest(int nid,
+ buffer_t in,
+ uint8_t * out)
+{
+ const EVP_MD * md;
+ unsigned int len;
+
+ assert(in.data != NULL);
+ assert(out != NULL);
+
+ md = EVP_get_digestbynid(nid);
+ if (md == NULL)
+ return -1;
+
+ if (EVP_Digest(in.data, in.len, out, &len, md, NULL) != 1)
+ return -1;
+
+ return (ssize_t) len;
+}
+
+ssize_t openssl_md_len(int nid)
+{
+ const EVP_MD * md;
+
+ md = EVP_get_digestbynid(nid);
+ if (md == NULL)
+ return -1;
+
+ return (ssize_t) EVP_MD_get_size(md);
+}
+
+int openssl_secure_malloc_init(size_t max,
+ size_t guard)
+{
+ return CRYPTO_secure_malloc_init(max, guard) == 1 ? 0 : -1;
+}
+
+void openssl_secure_malloc_fini(void)
+{
+ CRYPTO_secure_malloc_done();
+}
+
+void * openssl_secure_malloc(size_t size)
+{
+ return OPENSSL_secure_malloc(size);
+}
+
+void openssl_secure_free(void * ptr,
+ size_t size)
+{
+ OPENSSL_secure_clear_free(ptr, size);
+}
+
+void openssl_secure_clear(void * ptr,
+ size_t size)
+{
+ 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 d4ee73b9..e5cc35f7 100644
--- a/src/lib/crypt/openssl.h
+++ b/src/lib/crypt/openssl.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* OpenSSL based cryptographic operations
* Elliptic curve Diffie-Hellman key exchange
@@ -26,28 +26,80 @@
#ifndef OUROBOROS_LIB_CRYPT_OPENSSL_H
#define OUROBOROS_LIB_CRYPT_OPENSSL_H
-ssize_t openssl_ecdh_pkp_create(void ** pkp,
- uint8_t * pk);
-
-void openssl_ecdh_pkp_destroy(void * pkp);
-
-int openssl_ecdh_derive(void * pkp,
- buffer_t pk,
- uint8_t * s);
-
-int openssl_encrypt(void * ctx,
- uint8_t * key,
- buffer_t in,
- buffer_t * out);
-
-int openssl_decrypt(void * ctx,
- uint8_t * key,
- buffer_t in,
- buffer_t * out);
-
-void * openssl_crypt_create_ctx(void);
-
-void openssl_crypt_destroy_ctx(void * ctx);
+struct ossl_crypt_ctx;
+
+ssize_t openssl_pkp_create(const char * algo,
+ EVP_PKEY ** pkp,
+ uint8_t * pk);
+
+void openssl_pkp_destroy(EVP_PKEY * pkp);
+
+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);
+
+/* 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);
+
+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_get_algo_from_pk_raw(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);
+
+struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk);
+
+void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx);
+
+int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx);
/* AUTHENTICATION */
@@ -76,15 +128,25 @@ int openssl_load_pubkey_file(const char * path,
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,
+ buffer_t * buf);
-int openssl_cmp_key(const void * key1,
- const void * key2);
+int openssl_load_privkey_raw_file(const char * path,
+ void ** key);
-void openssl_free_key(void * key);
+int openssl_cmp_key(const EVP_PKEY * key1,
+ const EVP_PKEY * key2);
+
+void openssl_free_key(EVP_PKEY * key);
int openssl_check_crt_name(void * crt,
const char * name);
+int openssl_get_crt_name(void * crt,
+ char * name);
+
int openssl_crt_str(const void * crt,
char * str);
@@ -98,15 +160,54 @@ void openssl_auth_destroy_store(void * store);
int openssl_auth_add_crt_to_store(void * store,
void * crt);
+void * openssl_auth_create_chain(void);
+
+void openssl_auth_destroy_chain(void * chain);
+
+int openssl_auth_add_crt_to_chain(void * chain,
+ void * crt);
+
int openssl_verify_crt(void * store,
+ void * untrusted,
void * crt);
-int openssl_sign(void * pkp,
+int openssl_verify_crt_pin(void * store,
+ void * untrusted,
+ void * crt,
+ void * pin);
+
+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_verify_sig(void * pk,
- 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_len(int md_nid);
+
+/* Secure memory allocation */
+int openssl_secure_malloc_init(size_t max,
+ size_t guard);
+
+void openssl_secure_malloc_fini(void);
+
+void * openssl_secure_malloc(size_t size);
+
+void openssl_secure_free(void * ptr,
+ size_t size);
+
+void openssl_secure_clear(void * ptr,
+ size_t size);
+
+void openssl_cleanup(void);
#endif /* OUROBOROS_LIB_CRYPT_OPENSSL_H */
diff --git a/src/lib/dev.c b/src/lib/dev.c
index cb483aca..3fb8d831 100644
--- a/src/lib/dev.c
+++ b/src/lib/dev.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* API for applications
*
@@ -27,11 +27,15 @@
#endif
#include "config.h"
+#include "ssm.h"
+#include <ouroboros/atomics.h>
#include <ouroboros/bitmap.h>
#include <ouroboros/cep.h>
+#include <ouroboros/crc16.h>
#include <ouroboros/crypt.h>
#include <ouroboros/dev.h>
+#include <ouroboros/endian.h>
#include <ouroboros/errno.h>
#include <ouroboros/fccntl.h>
#include <ouroboros/flow.h>
@@ -44,32 +48,33 @@
#include <ouroboros/np1_flow.h>
#include <ouroboros/pthread.h>
#include <ouroboros/random.h>
-#include <ouroboros/serdes-irm.h>
-#include <ouroboros/shm_flow_set.h>
-#include <ouroboros/shm_rdrbuff.h>
-#include <ouroboros/shm_rbuff.h>
-#include <ouroboros/sockets.h>
-#include <ouroboros/utils.h>
#ifdef PROC_FLOW_STATS
#include <ouroboros/rib.h>
#endif
+#include <ouroboros/serdes-irm.h>
+#include <ouroboros/sockets.h>
+#include <ouroboros/ssm_flow_set.h>
+#include <ouroboros/ssm_pool.h>
+#include <ouroboros/ssm_rbuff.h>
+#include <ouroboros/tw.h>
+#include <ouroboros/utils.h>
+#include <assert.h>
#ifdef HAVE_LIBGCRYPT
#include <gcrypt.h>
#endif
-#include <assert.h>
-#include <stdlib.h>
-#include <string.h>
-#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
#include <sys/types.h>
#ifndef CLOCK_REALTIME_COARSE
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
#endif
-/* Partial read information. */
#define NO_PART -1
#define DONE_PART -2
@@ -77,29 +82,30 @@
#define SECMEMSZ 16384
#define MSGBUFSZ 2048
-/* map flow_ids to flow descriptors; track state of the flow */
struct fmap {
int fd;
- /* TODO: use actual flow state */
enum flow_state state;
};
-#define frcti_to_flow(frcti) \
- ((struct flow *)((uint8_t *) frcti - offsetof(struct flow, frcti)))
-
struct flow {
- struct list_head next;
-
struct flow_info info;
- struct shm_rbuff * rx_rb;
- struct shm_rbuff * tx_rb;
- struct shm_flow_set * set;
+ struct ssm_rbuff * rx_rb;
+ struct ssm_rbuff * tx_rb;
+ struct ssm_flow_set * set;
uint16_t oflags;
ssize_t part_idx;
struct crypt_ctx * crypt;
+ 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;
@@ -118,39 +124,33 @@ struct flow_set {
};
struct fqueue {
- struct flowevent fqueue[SHM_BUFFER_SIZE]; /* Safe copy from shm. */
+ struct flowevent fqueue[SSM_RBUFF_SIZE]; /* Safe copy from shm. */
size_t fqsize;
size_t next;
};
struct {
- struct shm_rdrbuff * rdrb;
- struct shm_flow_set * fqset;
+ struct ssm_pool * pool;
+ struct ssm_flow_set * fqset;
struct bmp * fds;
struct bmp * fqueues;
struct flow * flows;
struct fmap * id_to_fd;
- struct list_head flow_list;
pthread_mutex_t mtx;
pthread_cond_t cond;
- pthread_t tx;
- pthread_t rx;
- size_t n_frcti;
- fset_t * frct_set;
-
pthread_rwlock_t lock;
-} ai;
+} proc;
static void flow_destroy(struct fmap * p)
{
- pthread_mutex_lock(&ai.mtx);
+ pthread_mutex_lock(&proc.mtx);
if (p->state == FLOW_DESTROY) {
- pthread_mutex_unlock(&ai.mtx);
+ pthread_mutex_unlock(&proc.mtx);
return;
}
@@ -159,12 +159,12 @@ static void flow_destroy(struct fmap * p)
else
p->state = FLOW_NULL;
- pthread_cond_signal(&ai.cond);
+ pthread_cond_signal(&proc.cond);
- pthread_cleanup_push(__cleanup_mutex_unlock, &ai.mtx);
+ pthread_cleanup_push(__cleanup_mutex_unlock, &proc.mtx);
while (p->state != FLOW_NULL)
- pthread_cond_wait(&ai.cond, &ai.mtx);
+ pthread_cond_wait(&proc.cond, &proc.mtx);
p->fd = -1;
p->state = FLOW_INIT;
@@ -175,17 +175,17 @@ static void flow_destroy(struct fmap * p)
static void flow_set_state(struct fmap * p,
enum flow_state state)
{
- pthread_mutex_lock(&ai.mtx);
+ pthread_mutex_lock(&proc.mtx);
if (p->state == FLOW_DESTROY) {
- pthread_mutex_unlock(&ai.mtx);
+ pthread_mutex_unlock(&proc.mtx);
return;
}
p->state = state;
- pthread_cond_broadcast(&ai.cond);
+ pthread_cond_broadcast(&proc.cond);
- pthread_mutex_unlock(&ai.mtx);
+ pthread_mutex_unlock(&proc.mtx);
}
static enum flow_state flow_wait_assign(int flow_id)
@@ -193,26 +193,26 @@ static enum flow_state flow_wait_assign(int flow_id)
enum flow_state state;
struct fmap * p;
- p = &ai.id_to_fd[flow_id];
+ p = &proc.id_to_fd[flow_id];
- pthread_mutex_lock(&ai.mtx);
+ pthread_mutex_lock(&proc.mtx);
if (p->state == FLOW_ALLOCATED) {
- pthread_mutex_unlock(&ai.mtx);
+ pthread_mutex_unlock(&proc.mtx);
return FLOW_ALLOCATED;
}
if (p->state == FLOW_INIT)
p->state = FLOW_ALLOC_PENDING;
- pthread_cleanup_push(__cleanup_mutex_unlock, &ai.mtx);
+ pthread_cleanup_push(__cleanup_mutex_unlock, &proc.mtx);
while (p->state == FLOW_ALLOC_PENDING)
- pthread_cond_wait(&ai.cond, &ai.mtx);
+ pthread_cond_wait(&proc.cond, &proc.mtx);
if (p->state == FLOW_DESTROY) {
p->state = FLOW_NULL;
- pthread_cond_broadcast(&ai.cond);
+ pthread_cond_broadcast(&proc.cond);
}
state = p->state;
@@ -224,13 +224,13 @@ static enum flow_state flow_wait_assign(int flow_id)
return state;
}
-static int proc_announce(const char * prog)
+static int proc_announce(const struct proc_info * proc)
{
uint8_t buf[SOCK_BUF_SIZE];
buffer_t msg = {SOCK_BUF_SIZE, buf};
int err;
- if (proc_announce__irm_req_ser(&msg, prog) < 0)
+ if (proc_announce__irm_req_ser(&msg, proc) < 0)
return -ENOMEM;
err = send_recv_msg(&msg);
@@ -240,7 +240,7 @@ static int proc_announce(const char * prog)
return irm__irm_result_des(&msg);
}
-/* IRMd will clean up the mess if this fails */
+/* IRMd cleans up on failure. */
static void proc_exit(void)
{
uint8_t buf[SOCK_BUF_SIZE];
@@ -252,8 +252,8 @@ static void proc_exit(void)
send_recv_msg(&msg);
}
-static int sdb_encrypt(struct flow * flow,
- struct shm_du_buff * sdb)
+static int spb_encrypt(struct flow * flow,
+ struct ssm_pk_buff * spb)
{
buffer_t in;
buffer_t out;
@@ -261,19 +261,19 @@ static int sdb_encrypt(struct flow * flow,
uint8_t * tail;
if (flow->crypt == NULL)
- return 0; /* No encryption */
+ return 0;
- in.data = shm_du_buff_head(sdb);
- in.len = shm_du_buff_len(sdb);
+ in.data = ssm_pk_buff_head(spb);
+ in.len = ssm_pk_buff_len(spb);
if (crypt_encrypt(flow->crypt, in, &out) < 0)
goto fail_encrypt;
- head = shm_du_buff_head_alloc(sdb, IVSZ);
+ head = ssm_pk_buff_push(spb, flow->headsz);
if (head == NULL)
goto fail_alloc;
- tail = shm_du_buff_tail_alloc(sdb, (out.len - in.len) - IVSZ);
+ tail = ssm_pk_buff_push_tail(spb, flow->tailsz);
if (tail == NULL)
goto fail_alloc;
@@ -288,25 +288,24 @@ static int sdb_encrypt(struct flow * flow,
return -ECRYPT;
}
-static int sdb_decrypt(struct flow * flow,
- struct shm_du_buff * sdb)
+static int spb_decrypt(struct flow * flow,
+ struct ssm_pk_buff * spb)
{
buffer_t in;
buffer_t out;
uint8_t * head;
if (flow->crypt == NULL)
- return 0; /* No decryption */
+ return 0;
- in.data = shm_du_buff_head(sdb);
- in.len = shm_du_buff_len(sdb);
+ in.data = ssm_pk_buff_head(spb);
+ in.len = ssm_pk_buff_len(spb);
if (crypt_decrypt(flow->crypt, in, &out) < 0)
- return -ENOMEM;
-
+ return -ECRYPT;
- head = shm_du_buff_head_release(sdb, IVSZ) + IVSZ;
- shm_du_buff_tail_release(sdb, (in.len - out.len) - IVSZ);
+ head = ssm_pk_buff_pop(spb, flow->headsz) + flow->headsz;
+ ssm_pk_buff_pop_tail(spb, flow->tailsz);
memcpy(head, out.data, out.len);
@@ -315,195 +314,445 @@ static int sdb_decrypt(struct flow * flow,
return 0;
}
-#include "frct.c"
+/* tw_move under proc.lock rdlock; gates teardown vs in-flight fires. */
+static void tw_move_safe(void)
+{
+ pthread_rwlock_rdlock(&proc.lock);
-void * flow_tx(void * o)
+ pthread_cleanup_push(__cleanup_rwlock_unlock, &proc.lock);
+
+ tw_move();
+
+ pthread_cleanup_pop(1);
+}
+
+static int crc_add(struct ssm_pk_buff * spb,
+ size_t head_skip)
{
- struct timespec tic = TIMESPEC_INIT_NS(TICTIME);
+ uint8_t * head;
+ uint8_t * tail;
- (void) o;
+ tail = ssm_pk_buff_push_tail(spb, CRCLEN);
+ if (tail == NULL)
+ return -ENOMEM;
- while (true) {
- timerwheel_move();
+ head = ssm_pk_buff_head(spb) + head_skip;
- nanosleep(&tic, NULL);
- }
+ mem_hash(HASH_CRC32, tail, head, tail - head);
- return (void *) 0;
+ return 0;
}
-static void flow_send_keepalive(struct flow * flow,
- struct timespec now)
+static int crc_check(struct ssm_pk_buff * spb,
+ size_t head_skip)
{
- struct shm_du_buff * sdb;
- ssize_t idx;
- uint8_t * ptr;
+ uint32_t crc;
+ uint8_t * head;
+ uint8_t * tail;
- idx = shm_rdrbuff_alloc(ai.rdrb, 0, &ptr, &sdb);
- if (idx < 0)
- return;
+ if (ssm_pk_buff_len(spb) < head_skip + CRCLEN)
+ return 1;
- pthread_rwlock_wrlock(&ai.lock);
+ head = ssm_pk_buff_head(spb) + head_skip;
+ tail = ssm_pk_buff_pop_tail(spb, CRCLEN);
- flow->snd_act = now;
+ mem_hash(HASH_CRC32, &crc, head, tail - head);
+
+ return !(crc == *((uint32_t *) tail));
+}
+
+/* FRCT included here so it can use proc and dev.c statics directly. */
+#include "frct.c"
+
+/* Decrypt before any check so the plaintext is authoritative. */
+static bool invalid_pkt(struct flow * flow,
+ struct ssm_pk_buff * spb)
+{
+ const struct frct_pci * pci;
+ uint16_t flags;
+ size_t pci_total;
+
+ if (spb == NULL || ssm_pk_buff_len(spb) == 0)
+ return true;
+
+ if (spb_decrypt(flow, spb) < 0)
+ return true;
+
+ if (flow->frcti == NULL) {
+ if (flow->info.qs.ber == 0 && crc_check(spb, 0) != 0)
+ return true;
+ return false;
+ }
+
+ if (ssm_pk_buff_len(spb) < FRCT_PCILEN)
+ return true;
+
+ pci = (const struct frct_pci *) ssm_pk_buff_head(spb);
+ flags = ntoh16(pci->flags);
- if (shm_rbuff_write(flow->tx_rb, idx))
- shm_rdrbuff_remove(ai.rdrb, idx);
+ /* Untrusted flag read; mismatch on HCS will drop on corrupt. */
+ if (flags & FRCT_DATA)
+ pci_total = frcti_data_hdr_len(flow->frcti);
else
- shm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ pci_total = frcti_ctrl_hdr_len(flow->frcti);
- pthread_rwlock_unlock(&ai.lock);
+ if (ssm_pk_buff_len(spb) < pci_total)
+ return true;
+
+ if (frct_hcs_check(pci, flow->frcti) != 0)
+ return true;
+
+ /* HCS valid: CRC32 on SACK; or on DATA if ber = 0. */
+ if (flags & FRCT_SACK) {
+ if (crc_check(spb, pci_total) != 0)
+ return true;
+
+ } else if ((flags & FRCT_DATA) && flow->info.qs.ber == 0) {
+ if (crc_check(spb, pci_total) != 0)
+ return true;
+ }
+
+ return false;
}
-/* Needs rdlock on ai. */
-static void _flow_keepalive(struct flow * flow)
+static bool deadline_passed(const struct timespec * abs)
{
- struct timespec now;
- struct timespec s_act;
- struct timespec r_act;
- int flow_id;
- time_t timeo;
- uint32_t acl;
+ struct timespec now;
- s_act = flow->snd_act;
- r_act = flow->rcv_act;
+ if (abs == NULL)
+ return false;
- flow_id = flow->info.id;
- timeo = flow->info.qs.timeout;
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
- acl = shm_rbuff_get_acl(flow->rx_rb);
- if (timeo == 0 || acl & (ACL_FLOWPEER | ACL_FLOWDOWN))
- return;
+ return ts_diff_ns(&now, abs) >= 0;
+}
+
+/* Clamp the wait by min(dl, next tw expiry, now + TICTIME). */
+static void compute_wait_deadline(const struct timespec * dl,
+ struct timespec * out)
+{
+ struct timespec now;
+ struct timespec cap;
+ struct timespec expiry;
+ struct timespec tic = TIMESPEC_INIT_NS(TICTIME);
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ ts_add(&now, &tic, &cap);
- if (ts_diff_ns(&now, &r_act) > (int64_t) timeo * MILLION) {
- shm_rbuff_set_acl(flow->rx_rb, ACL_FLOWPEER);
- shm_flow_set_notify(ai.fqset, flow_id, FLOW_PEER);
- return;
- }
+ tw_next_expiry(&expiry);
+
+ *out = (ts_diff_ns(&cap, &expiry) < 0) ? expiry : cap;
+ if (dl != NULL && ts_diff_ns(out, dl) > 0)
+ *out = *dl;
+}
+
+static void flow_drain_rx_nb(struct flow * flow)
+{
+ ssize_t idx;
+ struct ssm_pk_buff * spb;
+ struct ssm_rbuff * rx_rb;
+ struct frcti * frcti;
+#ifdef PROC_FLOW_STATS
+ struct timespec t_a;
+ struct timespec t_b;
+#endif
+
+ if (flow->frcti != NULL)
+ STAT_BUMP(flow->frcti, drain_calls);
+
+ while (true) {
+ pthread_rwlock_rdlock(&proc.lock);
- if (ts_diff_ns(&now, &s_act) > (int64_t) timeo * (MILLION >> 2)) {
- pthread_rwlock_unlock(&ai.lock);
+ rx_rb = flow->rx_rb;
+ if (rx_rb == NULL) {
+ pthread_rwlock_unlock(&proc.lock);
+ return;
+ }
+
+ idx = ssm_rbuff_read(rx_rb);
+ if (idx < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return;
+ }
- flow_send_keepalive(flow, now);
+ spb = ssm_pool_get(proc.pool, idx);
+ if (invalid_pkt(flow, spb)) {
+ ssm_pool_remove(proc.pool, idx);
+ pthread_rwlock_unlock(&proc.lock);
+ continue;
+ }
- pthread_rwlock_rdlock(&ai.lock);
+ frcti = flow->frcti;
+ if (frcti != NULL) {
+#ifdef PROC_FLOW_STATS
+ clock_gettime(CLOCK_MONOTONIC, &t_a);
+ FRCTI_RCV(frcti, spb);
+ clock_gettime(CLOCK_MONOTONIC, &t_b);
+ STAT_ADD(frcti, rcv_proc_ns,
+ (size_t) ts_diff_ns(&t_b, &t_a));
+#else
+ FRCTI_RCV(frcti, spb);
+#endif
+ } else {
+ ssm_pool_remove(proc.pool, idx);
+ }
+
+ pthread_rwlock_unlock(&proc.lock);
+
+ /* Per-packet so the delayed-ACK fires on time in a burst. */
+#ifdef PROC_FLOW_STATS
+ clock_gettime(CLOCK_MONOTONIC, &t_a);
+ tw_move_safe();
+ clock_gettime(CLOCK_MONOTONIC, &t_b);
+ if (frcti != NULL)
+ STAT_ADD(frcti, tw_move_ns,
+ (size_t) ts_diff_ns(&t_b, &t_a));
+#else
+ tw_move_safe();
+#endif
}
}
-static void handle_keepalives(void)
+/* 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 list_head * p;
- struct list_head * h;
+ 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(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
+ if (flow->info.id < 0 || flow->crypt == NULL) {
+ pthread_rwlock_unlock(&proc.lock);
+ return;
+ }
- list_for_each_safe(p, h, &ai.flow_list) {
- struct flow * flow;
- flow = list_entry(p, struct flow, next);
- _flow_keepalive(flow);
+ /* 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;
}
- pthread_rwlock_unlock(&ai.lock);
-}
+ flow->rk_attempt = now;
+ info = flow->info;
+ pthread_rwlock_unlock(&proc.lock);
-static void __cleanup_fqueue_destroy(void * fq)
-{
- fqueue_destroy((fqueue_t *) fq);
+ 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);
}
-void * flow_rx(void * o)
+/* A clamp-timeout means tw work is due, not the caller deadline. */
+static int flow_rx_one(struct flow * flow,
+ struct timespec * abs)
{
- struct timespec tic = TIMESPEC_INIT_NS(TICTIME);
- int ret;
- struct fqueue * fq;
+ struct timespec wait_abs;
+ struct ssm_pk_buff * spb;
+ struct ssm_rbuff * rx_rb;
+ ssize_t idx;
- (void) o;
+ while (true) {
+ compute_wait_deadline(abs, &wait_abs);
- fq = fqueue_create();
+ /* rdlock gates flow_fini; FLOWDOWN preempts the block. */
+ pthread_rwlock_rdlock(&proc.lock);
- pthread_cleanup_push(__cleanup_fqueue_destroy, fq);
+ rx_rb = flow->rx_rb;
+ if (rx_rb == NULL) {
+ pthread_rwlock_unlock(&proc.lock);
+ return -EFLOWDOWN;
+ }
- /* fevent will filter all FRCT packets for us */
- while ((ret = fevent(ai.frct_set, fq, &tic)) != 0) {
- if (ret == -ETIMEDOUT) {
- handle_keepalives();
+ /* 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))
+ return -ETIMEDOUT;
+ tw_move_safe();
continue;
}
+ if (idx < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return idx;
+ }
+
+ spb = ssm_pool_get(proc.pool, idx);
+ if (invalid_pkt(flow, spb)) {
+ ssm_pool_remove(proc.pool, idx);
+ pthread_rwlock_unlock(&proc.lock);
+ continue;
+ }
+
+ if (flow->frcti != NULL)
+ FRCTI_RCV(flow->frcti, spb);
+ else
+ ssm_pool_remove(proc.pool, idx);
- while (fqueue_next(fq) >= 0)
- ; /* no need to act */
+ pthread_rwlock_unlock(&proc.lock);
+
+ tw_move_safe();
+ return 0;
}
+}
- pthread_cleanup_pop(true);
+/* 0 = window open; -EAGAIN = !block and would block; else flow_rx_one rc. */
+static __inline__ int flow_wait_window(struct flow * flow,
+ size_t n,
+ bool block,
+ struct timespec * dl)
+{
+ int rc;
- return (void *) 0;
+ while (true) {
+ flow_drain_rx_nb(flow);
+ if (FRCTI_IS_WINDOW_OPEN_N(flow->frcti, n))
+ return 0;
+ if (!block)
+ return -EAGAIN;
+ rc = flow_rx_one(flow, dl);
+ if (rc < 0)
+ return rc;
+ }
}
static void flow_clear(int fd)
{
- memset(&ai.flows[fd], 0, sizeof(ai.flows[fd]));
+ memset(&proc.flows[fd], 0, sizeof(proc.flows[fd]));
- ai.flows[fd].info.id = -1;
+ proc.flows[fd].info.id = -1;
}
-static void __flow_fini(int fd)
+/* Order before flow_fini's wrlock, which blocks on rdlock holders. */
+static void flow_quiesce(int fd)
{
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
+ struct ssm_rbuff * rx_rb = proc.flows[fd].rx_rb;
+ struct ssm_rbuff * tx_rb = proc.flows[fd].tx_rb;
- if (ai.flows[fd].frcti != NULL) {
- ai.n_frcti--;
- if (ai.n_frcti == 0) {
- pthread_cancel(ai.tx);
- pthread_join(ai.tx, NULL);
- }
+ if (rx_rb != NULL)
+ ssm_rbuff_set_bits(rx_rb, RB_FLOWDOWN);
- shm_flow_set_del(ai.fqset, 0, ai.flows[fd].info.id);
+ if (tx_rb != NULL)
+ ssm_rbuff_set_bits(tx_rb, RB_FLOWDOWN);
+}
- frcti_destroy(ai.flows[fd].frcti);
- }
+static void do_flow_fini(int fd)
+{
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
- if (ai.flows[fd].info.id != -1) {
- flow_destroy(&ai.id_to_fd[ai.flows[fd].info.id]);
- bmp_release(ai.fds, fd);
- }
+ if (proc.flows[fd].frcti != NULL)
+ frcti_destroy(proc.flows[fd].frcti);
- if (ai.flows[fd].rx_rb != NULL) {
- shm_rbuff_set_acl(ai.flows[fd].rx_rb, ACL_FLOWDOWN);
- shm_rbuff_close(ai.flows[fd].rx_rb);
+ if (proc.flows[fd].info.id != -1) {
+ flow_destroy(&proc.id_to_fd[proc.flows[fd].info.id]);
+ bmp_release(proc.fds, fd);
}
- if (ai.flows[fd].tx_rb != NULL) {
- shm_rbuff_set_acl(ai.flows[fd].tx_rb, ACL_FLOWDOWN);
- shm_rbuff_close(ai.flows[fd].tx_rb);
- }
+ if (proc.flows[fd].rx_rb != NULL)
+ ssm_rbuff_close(proc.flows[fd].rx_rb);
+
+ if (proc.flows[fd].tx_rb != NULL)
+ ssm_rbuff_close(proc.flows[fd].tx_rb);
- if (ai.flows[fd].set != NULL) {
- shm_flow_set_notify(ai.flows[fd].set,
- ai.flows[fd].info.id,
+ if (proc.flows[fd].set != NULL) {
+ ssm_flow_set_notify(proc.flows[fd].set,
+ proc.flows[fd].info.id,
FLOW_DEALLOC);
- shm_flow_set_close(ai.flows[fd].set);
+ ssm_flow_set_close(proc.flows[fd].set);
}
- crypt_destroy_ctx(ai.flows[fd].crypt);
-
- list_del(&ai.flows[fd].next);
+ crypt_destroy_ctx(proc.flows[fd].crypt);
flow_clear(fd);
}
static void flow_fini(int fd)
{
- pthread_rwlock_wrlock(&ai.lock);
+ flow_quiesce(fd);
+
+ pthread_rwlock_wrlock(&proc.lock);
+
+ do_flow_fini(fd);
+
+ pthread_rwlock_unlock(&proc.lock);
+}
+
+#define IS_ENCRYPTED(crypt) ((crypt)->nid != NID_undef)
+#define IS_ORDERED(info) ((info)->qs.service != SVC_RAW)
+#define IS_STREAM(info) ((info)->qs.service == SVC_STREAM)
+
+/* Raw MTU minus the wrapping (IV/Tag + optional CRC) dev.c adds. */
+static __inline__ size_t flow_user_mtu(const struct flow * flow,
+ size_t raw)
+{
+ size_t hdr;
- __flow_fini(fd);
+ hdr = flow->headsz + flow->tailsz;
+ if (flow->info.qs.ber == 0 && flow->crypt == NULL)
+ hdr += CRCLEN;
- pthread_rwlock_unlock(&ai.lock);
+ return raw > hdr ? raw - hdr : 0;
}
static int flow_init(struct flow_info * info,
- buffer_t * sk)
+ struct crypt_sk * sk,
+ time_t rtt_hint)
{
struct timespec now;
struct flow * flow;
@@ -512,27 +761,27 @@ static int flow_init(struct flow_info * info,
clock_gettime(PTHREAD_COND_CLOCK, &now);
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_wrlock(&proc.lock);
- fd = bmp_allocate(ai.fds);
- if (!bmp_is_id_valid(ai.fds, fd)) {
+ fd = bmp_allocate(proc.fds);
+ if (!bmp_is_id_valid(proc.fds, fd)) {
err = -EBADF;
goto fail_fds;
}
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
flow->info = *info;
- flow->rx_rb = shm_rbuff_open(info->n_pid, info->id);
+ flow->rx_rb = ssm_rbuff_open(info->n_pid, info->id);
if (flow->rx_rb == NULL)
goto fail_rx_rb;
- flow->tx_rb = shm_rbuff_open(info->n_1_pid, info->id);
+ flow->tx_rb = ssm_rbuff_open(info->n_1_pid, info->id);
if (flow->tx_rb == NULL)
goto fail_tx_rb;
- flow->set = shm_flow_set_open(info->n_1_pid);
+ flow->set = ssm_flow_set_open(info->n_1_pid);
if (flow->set == NULL)
goto fail_set;
@@ -541,56 +790,49 @@ static int flow_init(struct flow_info * info,
flow->snd_act = now;
flow->rcv_act = now;
flow->crypt = NULL;
+ flow->headsz = 0;
+ flow->tailsz = 0;
- if (sk!= NULL && sk->data != NULL) {
- assert(sk->len == SYMMKEYSZ);
- flow->crypt = crypt_create_ctx(sk->data);
+ if (IS_ENCRYPTED(sk)) {
+ flow->crypt = crypt_create_ctx(sk);
if (flow->crypt == NULL)
goto fail_crypt;
+ flow->headsz = crypt_get_headsz(flow->crypt);
+ flow->tailsz = crypt_get_tagsz(flow->crypt);
}
assert(flow->frcti == NULL);
- if (info->qs.in_order != 0) {
- flow->frcti = frcti_create(fd, DELT_A, DELT_R, info->mpl);
+ if (IS_ORDERED(&flow->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->qs, frct_mtu);
if (flow->frcti == NULL)
goto fail_frcti;
-
- if (shm_flow_set_add(ai.fqset, 0, info->id))
- goto fail_flow_set_add;
-
- ++ai.n_frcti;
- if (ai.n_frcti == 1 &&
- pthread_create(&ai.tx, NULL, flow_tx, NULL) < 0)
- goto fail_tx_thread;
}
- list_add_tail(&flow->next, &ai.flow_list);
-
- ai.id_to_fd[info->id].fd = fd;
+ proc.id_to_fd[info->id].fd = fd;
- flow_set_state(&ai.id_to_fd[info->id], FLOW_ALLOCATED);
+ flow_set_state(&proc.id_to_fd[info->id], FLOW_ALLOCATED);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return fd;
- fail_tx_thread:
- shm_flow_set_del(ai.fqset, 0, info->id);
- fail_flow_set_add:
- frcti_destroy(flow->frcti);
fail_frcti:
crypt_destroy_ctx(flow->crypt);
fail_crypt:
- shm_flow_set_close(flow->set);
+ ssm_flow_set_close(flow->set);
fail_set:
- shm_rbuff_close(flow->tx_rb);
+ ssm_rbuff_close(flow->tx_rb);
fail_tx_rb:
- shm_rbuff_close(flow->rx_rb);
+ ssm_rbuff_close(flow->rx_rb);
fail_rx_rb:
- bmp_release(ai.fds, fd);
+ bmp_release(proc.fds, fd);
fail_fds:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return err;
}
@@ -608,8 +850,10 @@ static void init(int argc,
char ** argv,
char ** envp)
{
- 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
@@ -625,7 +869,11 @@ static void init(int argc,
goto fail_prog;
}
- if (proc_announce(prog)) {
+ memset(&info, 0, sizeof(info));
+ info.pid = getpid();
+ strncpy(info.prog, prog, PROG_NAME_SIZE);
+
+ if (proc_announce(&info)) {
fprintf(stderr, "FATAL: Could not announce to IRMd.\n");
goto fail_prog;
}
@@ -640,74 +888,86 @@ static void init(int argc,
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
}
#endif
- ai.fds = bmp_create(PROG_MAX_FLOWS - PROG_RES_FDS, PROG_RES_FDS);
- if (ai.fds == NULL) {
+ proc.fds = bmp_create(PROC_MAX_FLOWS - PROC_RES_FDS, PROC_RES_FDS);
+ if (proc.fds == NULL) {
fprintf(stderr, "FATAL: Could not create fd bitmap.\n");
goto fail_fds;
}
- ai.fqueues = bmp_create(PROG_MAX_FQUEUES, 0);
- if (ai.fqueues == NULL) {
+ proc.fqueues = bmp_create(PROC_MAX_FQUEUES, 0);
+ if (proc.fqueues == NULL) {
fprintf(stderr, "FATAL: Could not create fqueue bitmap.\n");
goto fail_fqueues;
}
- ai.rdrb = shm_rdrbuff_open();
- if (ai.rdrb == NULL) {
+ if (is_ouroboros_member_uid(getuid()))
+ proc.pool = ssm_pool_open(0);
+ else
+ proc.pool = ssm_pool_open(getuid());
+
+ if (proc.pool == NULL) {
fprintf(stderr, "FATAL: Could not open packet buffer.\n");
goto fail_rdrb;
}
- ai.flows = malloc(sizeof(*ai.flows) * PROG_MAX_FLOWS);
- if (ai.flows == NULL) {
+ proc.flows = malloc(sizeof(*proc.flows) * PROC_MAX_FLOWS);
+ if (proc.flows == NULL) {
fprintf(stderr, "FATAL: Could not malloc flows.\n");
goto fail_flows;
}
- for (i = 0; i < PROG_MAX_FLOWS; ++i)
+ for (i = 0; i < PROC_MAX_FLOWS; ++i)
flow_clear(i);
- ai.id_to_fd = malloc(sizeof(*ai.id_to_fd) * SYS_MAX_FLOWS);
- if (ai.id_to_fd == NULL) {
+ proc.id_to_fd = malloc(sizeof(*proc.id_to_fd) * SYS_MAX_FLOWS);
+ if (proc.id_to_fd == NULL) {
fprintf(stderr, "FATAL: Could not malloc id_to_fd.\n");
goto fail_id_to_fd;
}
for (i = 0; i < SYS_MAX_FLOWS; ++i)
- ai.id_to_fd[i].state = FLOW_INIT;
+ proc.id_to_fd[i].state = FLOW_INIT;
- if (pthread_mutex_init(&ai.mtx, NULL)) {
+ if (pthread_mutex_init(&proc.mtx, NULL)) {
fprintf(stderr, "FATAL: Could not init mutex.\n");
goto fail_mtx;
}
- if (pthread_cond_init(&ai.cond, NULL) < 0) {
+ if (pthread_cond_init(&proc.cond, NULL) < 0) {
fprintf(stderr, "FATAL: Could not init condvar.\n");
goto fail_cond;
}
- if (pthread_rwlock_init(&ai.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;
}
- ai.fqset = shm_flow_set_open(getpid());
- if (ai.fqset == NULL) {
+ proc.fqset = ssm_flow_set_open(getpid());
+ if (proc.fqset == NULL) {
fprintf(stderr, "FATAL: Could not open flow set.\n");
goto fail_fqset;
}
- ai.frct_set = fset_create();
- if (ai.frct_set == NULL || ai.frct_set->idx != 0) {
- fprintf(stderr, "FATAL: Could not create FRCT set.\n");
- goto fail_frct_set;
- }
-
- if (timerwheel_init() < 0) {
+ if (tw_init() < 0) {
fprintf(stderr, "FATAL: Could not initialize timerwheel.\n");
goto fail_timerwheel;
}
+ if (crypt_secure_malloc_init(PROC_SECMEM_MAX) < 0) {
+ fprintf(stderr, "FATAL: Could not init secure malloc.\n");
+ goto fail_secmem;
+ }
+
#if defined PROC_FLOW_STATS
if (strstr(argv[0], "ipcpd") == NULL) {
sprintf(procstr, "proc.%d", getpid());
@@ -717,43 +977,38 @@ static void init(int argc,
}
}
#endif
- if (pthread_create(&ai.rx, NULL, flow_rx, NULL) < 0) {
- fprintf(stderr, "FATAL: Could not start monitor thread.\n");
- goto fail_monitor;
- }
-
- list_head_init(&ai.flow_list);
+ pthread_rwlockattr_destroy(&attr);
return;
- fail_monitor:
#if defined PROC_FLOW_STATS
- rib_fini();
fail_rib_init:
+ crypt_secure_malloc_fini();
#endif
- timerwheel_fini();
+ fail_secmem:
+ tw_fini();
fail_timerwheel:
- fset_destroy(ai.frct_set);
- fail_frct_set:
- shm_flow_set_close(ai.fqset);
+ ssm_flow_set_close(proc.fqset);
fail_fqset:
- pthread_rwlock_destroy(&ai.lock);
+ pthread_rwlock_destroy(&proc.lock);
fail_flow_lock:
- pthread_cond_destroy(&ai.cond);
+ pthread_rwlockattr_destroy(&attr);
+ fail_rwlock_attr:
+ pthread_cond_destroy(&proc.cond);
fail_cond:
- pthread_mutex_destroy(&ai.mtx);
+ pthread_mutex_destroy(&proc.mtx);
fail_mtx:
- free(ai.id_to_fd);
+ free(proc.id_to_fd);
fail_id_to_fd:
- free(ai.flows);
+ free(proc.flows);
fail_flows:
- shm_rdrbuff_close(ai.rdrb);
+ ssm_pool_close(proc.pool);
fail_rdrb:
- bmp_destroy(ai.fqueues);
+ bmp_destroy(proc.fqueues);
fail_fqueues:
- bmp_destroy(ai.fds);
+ bmp_destroy(proc.fds);
fail_fds:
- memset(&ai, 0, sizeof(ai));
+ memset(&proc, 0, sizeof(proc));
fail_prog:
exit(EXIT_FAILURE);
}
@@ -762,51 +1017,53 @@ static void fini(void)
{
int i;
- if (ai.fds == NULL)
+ if (proc.fds == NULL)
return;
- pthread_cancel(ai.rx);
- pthread_join(ai.rx, NULL);
+ /* Wake all in-flight readers/writers BEFORE wrlock acquire. */
+ for (i = 0; i < PROC_MAX_FLOWS; ++i)
+ if (proc.flows[i].info.id != -1)
+ flow_quiesce(i);
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_wrlock(&proc.lock);
- for (i = 0; i < PROG_MAX_FLOWS; ++i) {
- if (ai.flows[i].info.id != -1) {
+ for (i = 0; i < PROC_MAX_FLOWS; ++i) {
+ struct flow * flow = &proc.flows[i];
+ if (flow->info.id != -1) {
ssize_t idx;
- shm_rbuff_set_acl(ai.flows[i].rx_rb, ACL_FLOWDOWN);
- while ((idx = shm_rbuff_read(ai.flows[i].rx_rb)) >= 0)
- shm_rdrbuff_remove(ai.rdrb, idx);
- __flow_fini(i);
+ while ((idx = ssm_rbuff_read(flow->rx_rb)) >= 0)
+ ssm_pool_remove(proc.pool, idx);
+ do_flow_fini(i);
}
}
- pthread_cond_destroy(&ai.cond);
- pthread_mutex_destroy(&ai.mtx);
+ pthread_cond_destroy(&proc.cond);
+ pthread_mutex_destroy(&proc.mtx);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
#ifdef PROC_FLOW_STATS
rib_fini();
#endif
- timerwheel_fini();
+ crypt_secure_malloc_fini();
- fset_destroy(ai.frct_set);
+ tw_fini();
- shm_flow_set_close(ai.fqset);
+ ssm_flow_set_close(proc.fqset);
- pthread_rwlock_destroy(&ai.lock);
+ pthread_rwlock_destroy(&proc.lock);
- free(ai.flows);
- free(ai.id_to_fd);
+ free(proc.flows);
+ free(proc.id_to_fd);
- shm_rdrbuff_close(ai.rdrb);
+ ssm_pool_close(proc.pool);
- bmp_destroy(ai.fds);
- bmp_destroy(ai.fqueues);
+ bmp_destroy(proc.fds);
+ bmp_destroy(proc.fqueues);
proc_exit();
- memset(&ai, 0, sizeof(ai));
+ memset(&proc, 0, sizeof(proc));
}
#if defined(__MACH__) && defined(__APPLE__)
@@ -823,17 +1080,22 @@ __attribute__((section(FINI_SECTION))) __typeof__(fini) * __fini = fini;
int flow_accept(qosspec_t * qs,
const struct timespec * timeo)
{
- struct flow_info flow;
- uint8_t buf[SOCK_BUF_SIZE];
- buffer_t msg = {SOCK_BUF_SIZE, buf};
- buffer_t sk;
- int fd;
- int err;
+ struct flow_info flow;
+ struct crypt_sk crypt;
+ uint8_t buf[SOCK_BUF_SIZE];
+ buffer_t msg = {SOCK_BUF_SIZE, buf};
+ uint8_t key[SYMMKEYSZ];
+ int fd;
+ int err;
#ifdef QOS_DISABLE_CRC
if (qs != NULL)
qs->ber = 1;
#endif
+ /* STREAM cannot tolerate loss: drops create silent gaps. */
+ if (qs != NULL && qs->service == SVC_STREAM && qs->loss != 0)
+ return -EINVAL;
+
memset(&flow, 0, sizeof(flow));
flow.n_pid = getpid();
@@ -846,13 +1108,18 @@ int flow_accept(qosspec_t * qs,
if (err < 0)
return err;
- err = flow__irm_result_des(&msg, &flow, &sk);
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_RESP;
+
+ err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
return err;
- fd = flow_init(&flow, &sk);
+ /* No RTT in accept; rtt_hint=0 bootstraps from first ACK. */
+ fd = flow_init(&flow, &crypt, 0);
- freebuf(sk);
+ crypt_secure_clear(key, SYMMKEYSZ);
if (qs != NULL)
*qs = flow.qs;
@@ -864,17 +1131,23 @@ int flow_alloc(const char * dst,
qosspec_t * qs,
const struct timespec * timeo)
{
- struct flow_info flow;
- uint8_t buf[SOCK_BUF_SIZE];
- buffer_t msg = {SOCK_BUF_SIZE, buf};
- buffer_t sk; /* symmetric key */
- int fd;
- int err;
+ struct flow_info flow;
+ struct crypt_sk crypt;
+ uint8_t buf[SOCK_BUF_SIZE];
+ buffer_t msg = {SOCK_BUF_SIZE, buf};
+ uint8_t key[SYMMKEYSZ];
+ int fd;
+ int err;
+ struct timespec t0;
+ struct timespec t1;
#ifdef QOS_DISABLE_CRC
if (qs != NULL)
qs->ber = 1;
#endif
+ /* STREAM cannot tolerate loss: drops create silent gaps. */
+ if (qs != NULL && qs->service == SVC_STREAM && qs->loss != 0)
+ return -EINVAL;
memset(&flow, 0, sizeof(flow));
@@ -884,19 +1157,25 @@ int flow_alloc(const char * dst,
if (flow_alloc__irm_req_ser(&msg, &flow, dst, timeo))
return -ENOMEM;
+ clock_gettime(PTHREAD_COND_CLOCK, &t0);
+
err = send_recv_msg(&msg);
- if (err < 0) {
- printf("send_recv_msg error %d\n", err);
+ if (err < 0)
return err;
- }
- err = flow__irm_result_des(&msg, &flow, &sk);
+ clock_gettime(PTHREAD_COND_CLOCK, &t1);
+
+ 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, &sk);
+ fd = flow_init(&flow, &crypt, ts_diff_ns(&t1, &t0));
- freebuf(sk);
+ crypt_secure_clear(key, SYMMKEYSZ);
if (qs != NULL)
*qs = flow.qs;
@@ -907,11 +1186,13 @@ int flow_alloc(const char * dst,
int flow_join(const char * dst,
const struct timespec * timeo)
{
- struct flow_info flow;
- uint8_t buf[SOCK_BUF_SIZE];
- buffer_t msg = {SOCK_BUF_SIZE, buf};
- int fd;
- int err;
+ struct flow_info flow;
+ struct crypt_sk crypt;
+ uint8_t buf[SOCK_BUF_SIZE];
+ buffer_t msg = {SOCK_BUF_SIZE, buf};
+ uint8_t key[SYMMKEYSZ];
+ int fd;
+ int err;
memset(&flow, 0, sizeof(flow));
@@ -925,11 +1206,17 @@ int flow_join(const char * dst,
if (err < 0)
return err;
- err = flow__irm_result_des(&msg, &flow, NULL);
+ 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, NULL);
+ fd = flow_init(&flow, &crypt, 0);
+
+ crypt_secure_clear(key, SYMMKEYSZ);
return fd;
}
@@ -946,17 +1233,17 @@ int flow_dealloc(int fd)
struct flow * flow;
int err;
- if (fd < 0 || fd >= SYS_MAX_FLOWS )
+ if (fd < 0 || fd >= PROC_MAX_FLOWS )
return -EINVAL;
- memset(&info, 0, sizeof(flow));
+ memset(&info, 0, sizeof(info));
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
@@ -965,46 +1252,48 @@ int flow_dealloc(int fd)
flow->rcv_timesout = true;
flow->rcv_timeo = tic;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
flow_read(fd, buf, SOCK_BUF_SIZE);
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- timeo.tv_sec = frcti_dealloc(flow->frcti);
- while (timeo.tv_sec < 0) { /* keep the flow active for rtx */
- ssize_t ret;
+ while (FRCTI_LINGERING(flow->frcti)) {
+ ssize_t ret;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
ret = flow_read(fd, pkt, PKT_BUF_LEN);
- pthread_rwlock_rdlock(&ai.lock);
-
- timeo.tv_sec = frcti_dealloc(flow->frcti);
+ pthread_rwlock_rdlock(&proc.lock);
- if (ret == -EFLOWDOWN && timeo.tv_sec < 0)
- timeo.tv_sec = -timeo.tv_sec;
+ if (ret == -EFLOWDOWN)
+ break;
}
- pthread_cleanup_push(__cleanup_rwlock_unlock, &ai.lock);
+ timeo.tv_sec = FRCTI_DEALLOC(flow->frcti);
- shm_rbuff_fini(flow->tx_rb);
+ pthread_cleanup_push(__cleanup_rwlock_unlock, &proc.lock);
+
+ ssm_rbuff_fini(flow->tx_rb);
pthread_cleanup_pop(true);
info.id = flow->info.id;
info.n_pid = getpid();
- if (flow_dealloc__irm_req_ser(&msg, &info, &timeo) < 0)
- return -ENOMEM;
+ if (flow_dealloc__irm_req_ser(&msg, &info, &timeo) < 0) {
+ err = -ENOMEM;
+ goto out;
+ }
err = send_recv_msg(&msg);
if (err < 0)
- return err;
+ goto out;
err = irm__irm_result_des(&msg);
+ out:
flow_fini(fd);
return err;
@@ -1018,34 +1307,37 @@ int ipcp_flow_dealloc(int fd)
struct flow * flow;
int err;
- if (fd < 0 || fd >= SYS_MAX_FLOWS )
+ if (fd < 0 || fd >= PROC_MAX_FLOWS )
return -EINVAL;
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- memset(&info, 0, sizeof(flow));
+ memset(&info, 0, sizeof(info));
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
info.id = flow->info.id;
info.n_1_pid = flow->info.n_1_pid;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
- if (ipcp_flow_dealloc__irm_req_ser(&msg, &info) < 0)
- return -ENOMEM;
+ if (ipcp_flow_dealloc__irm_req_ser(&msg, &info) < 0) {
+ err = -ENOMEM;
+ goto out;
+ }
err = send_recv_msg(&msg);
if (err < 0)
- return err;
+ goto out;
err = irm__irm_result_des(&msg);
+ out:
flow_fini(fd);
return err;
@@ -1061,22 +1353,30 @@ 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;
-
- if (fd < 0 || fd >= SYS_MAX_FLOWS)
+ uint16_t old_acc;
+ uint16_t new_acc;
+ size_t max;
+ size_t * maxp;
+ size_t rsz;
+ size_t * rszp;
+ time_t rto;
+ time_t * rtop;
+ int rc;
+ bool emit_eos = false;
+
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -EBADF;
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
va_start(l, cmd);
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_wrlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
va_end(l);
return -ENOTALLOC;
}
@@ -1124,42 +1424,50 @@ int fccntl(int fd,
break;
case FLOWGRXQLEN:
qlen = va_arg(l, size_t *);
- *qlen = shm_rbuff_queued(flow->rx_rb);
+ *qlen = ssm_rbuff_queued(flow->rx_rb);
break;
case FLOWGTXQLEN:
qlen = va_arg(l, size_t *);
- *qlen = shm_rbuff_queued(flow->tx_rb);
+ *qlen = ssm_rbuff_queued(flow->tx_rb);
+ break;
+ case FLOWGMTU:
+ maxp = va_arg(l, size_t *);
+ if (maxp == NULL)
+ goto einval;
+ *maxp = flow_user_mtu(flow, flow->info.mtu);
break;
case FLOWSFLAGS:
+ old_acc = flow->oflags & FLOWFACCMODE;
flow->oflags = va_arg(l, uint32_t);
- rx_acl = shm_rbuff_get_acl(flow->rx_rb);
- tx_acl = shm_rbuff_get_acl(flow->rx_rb);
- /*
- * Making our own flow write only means making the
- * the other side of the flow read only.
- */
+ new_acc = flow->oflags & FLOWFACCMODE;
+
+ /* Defer EOS emit until after proc.lock is dropped: */
+ /* frcti_fin_snd may block on shm-pool/tx-rb. */
+ if (new_acc == FLOWFRDONLY
+ && old_acc != FLOWFRDONLY
+ && flow->frcti != NULL)
+ emit_eos = true;
+
+ /* 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_bits(flow->rx_rb, RB_WR);
+ else
+ ssm_rbuff_set_bits(flow->rx_rb, RB_WR);
if (flow->oflags & FLOWFDOWN) {
- rx_acl |= ACL_FLOWDOWN;
- tx_acl |= ACL_FLOWDOWN;
- shm_flow_set_notify(flow->set,
+ ssm_rbuff_set_bits(flow->rx_rb, RB_FLOWDOWN);
+ ssm_rbuff_set_bits(flow->tx_rb, RB_FLOWDOWN);
+ ssm_flow_set_notify(flow->set,
flow->info.id,
FLOW_DOWN);
} else {
- rx_acl &= ~ACL_FLOWDOWN;
- tx_acl &= ~ACL_FLOWDOWN;
- shm_flow_set_notify(flow->set,
+ ssm_rbuff_clr_bits(flow->rx_rb, RB_FLOWDOWN);
+ ssm_rbuff_clr_bits(flow->tx_rb, RB_FLOWDOWN);
+ ssm_flow_set_notify(flow->set,
flow->info.id,
FLOW_UP);
}
- shm_rbuff_set_acl(flow->rx_rb, rx_acl);
- shm_rbuff_set_acl(flow->tx_rb, tx_acl);
-
break;
case FLOWGFLAGS:
fflags = va_arg(l, uint32_t *);
@@ -1181,104 +1489,351 @@ int fccntl(int fd,
goto eperm;
*cflags = frcti_getflags(flow->frcti);
break;
+ case FRCTSMAXSDU:
+ max = va_arg(l, size_t);
+ if (flow->frcti == NULL)
+ goto eperm;
+ if (frcti_set_max_rcv_sdu(flow->frcti, max) < 0)
+ goto einval;
+ break;
+ case FRCTGMAXSDU:
+ maxp = va_arg(l, size_t *);
+ if (maxp == NULL)
+ goto einval;
+ if (flow->frcti == NULL)
+ goto eperm;
+ *maxp = frcti_get_max_rcv_sdu(flow->frcti);
+ break;
+ case FRCTSRRINGSZ:
+ rsz = va_arg(l, size_t);
+ if (flow->frcti == NULL)
+ goto eperm;
+ rc = frcti_set_rcv_ring_sz(flow->frcti, rsz);
+ if (rc < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ va_end(l);
+ return rc;
+ }
+ break;
+ case FRCTGRRINGSZ:
+ rszp = va_arg(l, size_t *);
+ if (rszp == NULL)
+ goto einval;
+ if (flow->frcti == NULL)
+ goto eperm;
+ *rszp = frcti_get_rcv_ring_sz(flow->frcti);
+ break;
+ case FRCTSRTOMIN:
+ if (flow->frcti == NULL)
+ goto eperm;
+ rto = va_arg(l, time_t);
+ rc = frcti_set_rto_min(flow->frcti, rto);
+ if (rc < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ va_end(l);
+ return rc;
+ }
+ break;
+ case FRCTGRTOMIN:
+ if (flow->frcti == NULL)
+ goto eperm;
+ rtop = va_arg(l, time_t *);
+ if (rtop == NULL)
+ goto einval;
+ *rtop = frcti_get_rto_min(flow->frcti);
+ break;
default:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
va_end(l);
return -ENOTSUP;
};
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
+
+ if (emit_eos)
+ frcti_fin_snd(flow->frcti);
va_end(l);
return 0;
einval:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
va_end(l);
return -EINVAL;
eperm:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
va_end(l);
return -EPERM;
}
-static int chk_crc(struct shm_du_buff * sdb)
+static int flow_tx_spb(struct flow * flow,
+ struct ssm_pk_buff * spb,
+ uint16_t flags,
+ bool block,
+ struct timespec * abstime)
{
- uint32_t crc;
- uint8_t * head = shm_du_buff_head(sdb);
- uint8_t * tail = shm_du_buff_tail_release(sdb, CRCLEN);
+ struct timespec now;
+ ssize_t idx;
+ size_t pci_total;
+ int ret;
- mem_hash(HASH_CRC32, &crc, head, tail - head);
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ flow->snd_act = now;
- return !(crc == *((uint32_t *) tail));
+ idx = ssm_pk_buff_get_off(spb);
+
+ if (ssm_pk_buff_len(spb) > 0) {
+ if (FRCTI_SND(flow->frcti, spb, flags) < 0)
+ goto enomem;
+
+ if (flow->info.qs.ber == 0) {
+ pci_total = flow->frcti != NULL
+ ? frcti_data_hdr_len(flow->frcti) : 0;
+ if (crc_add(spb, pci_total) != 0)
+ goto enomem;
+ }
+
+ if (spb_encrypt(flow, spb) < 0)
+ goto enomem;
+ }
+
+ if (!block)
+ ret = ssm_rbuff_write(flow->tx_rb, idx);
+ else
+ ret = ssm_rbuff_write_b(flow->tx_rb, idx, abstime);
+
+ if (ret < 0)
+ return ret;
+
+ ssm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ return 0;
+
+ enomem:
+ return -ENOMEM;
}
-static int add_crc(struct shm_du_buff * sdb)
+/* Per-fragment role for fragment i out of n; n == 1 yields SOLE. */
+static __inline__ uint16_t flow_frag_role(size_t i, size_t n)
{
- uint8_t * head = shm_du_buff_head(sdb);
- uint8_t * tail = shm_du_buff_tail_alloc(sdb, CRCLEN);
- if (tail == NULL)
- return -1;
+ if (n == 1)
+ return FRCT_FR_SOLE;
- mem_hash(HASH_CRC32, tail, head, tail - head);
+ if (i == 0)
+ return FRCT_FR_FIRST;
- return 0;
+ if (i + 1 == n)
+ return FRCT_FR_LAST;
+
+ return FRCT_FR_MID;
}
-static int flow_tx_sdb(struct flow * flow,
- struct shm_du_buff * sdb,
- bool block,
- struct timespec * abstime)
+static ssize_t flow_write_stream(struct flow * flow,
+ const void * buf,
+ size_t count,
+ int oflags,
+ struct timespec * dl)
{
- struct timespec now;
- ssize_t idx;
- int ret;
+ const uint8_t * src = buf;
+ size_t payload;
+ size_t off = 0;
+ bool block = !(oflags & FLOWFWNOBLOCK);
- clock_gettime(PTHREAD_COND_CLOCK, &now);
+ if (!FRCTI_IS_FRTX(flow->frcti))
+ return -EMSGSIZE;
- pthread_rwlock_wrlock(&ai.lock);
+ payload = FRCTI_PAYLOAD_CAP(flow->frcti);
- flow->snd_act = now;
+ while (off < count) {
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t idx;
+ size_t clen;
+ int ret;
- pthread_rwlock_unlock(&ai.lock);
+ ret = flow_wait_window(flow, 1, block, dl);
+ if (ret < 0)
+ return off > 0 ? (ssize_t) off : (ssize_t) ret;
- idx = shm_du_buff_get_idx(sdb);
+ clen = MIN(count - off, payload);
- pthread_rwlock_rdlock(&ai.lock);
+ if (block)
+ idx = ssm_pool_alloc_b(proc.pool, clen, &ptr,
+ &spb, dl);
+ else
+ idx = ssm_pool_alloc(proc.pool, clen, &ptr, &spb);
+ if (idx < 0)
+ return off > 0 ? (ssize_t) off : idx;
- if (shm_du_buff_len(sdb) > 0) {
- if (frcti_snd(flow->frcti, sdb) < 0)
- goto enomem;
+ memcpy(ptr, src + off, clen);
- if (sdb_encrypt(flow, sdb) < 0)
- goto enomem;
+ ret = flow_tx_spb(flow, spb, 0, block, dl);
+ if (ret < 0) {
+ ssm_pool_remove(proc.pool, idx);
+ return off > 0 ? (ssize_t) off : (ssize_t) ret;
+ }
- if (flow->info.qs.ber == 0 && add_crc(sdb) != 0)
- goto enomem;
+ off += clen;
}
- pthread_cleanup_push(__cleanup_rwlock_unlock, &ai.lock);
+ return (ssize_t) count;
+}
- if (!block)
- ret = shm_rbuff_write(flow->tx_rb, idx);
- else
- ret = shm_rbuff_write_b(flow->tx_rb, idx, abstime);
+/* Per-fragment flow_tx_spb loop. Raw flows refuse; FRCT splits the SDU. */
+static ssize_t flow_write_frag(struct flow * flow,
+ const void * buf,
+ size_t count,
+ int oflags,
+ struct timespec * dl)
+{
+ const uint8_t * src = buf;
+ size_t frag_payload;
+ size_t n;
+ size_t off = 0;
+ size_t i;
+ int ret;
+ bool block = !(oflags & FLOWFWNOBLOCK);
+ /* Raw flows carry no PCI; cannot fragment. */
+ if (flow->frcti == NULL)
+ return -EMSGSIZE;
+
+ frag_payload = FRCTI_PAYLOAD_CAP(flow->frcti);
+
+ /* 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. */
+ if (n > RQ_SIZE)
+ return -EMSGSIZE;
+
+ /* SDU-atomic FC: wait for n seqnos to avoid overshoot mid-SDU. */
+ ret = flow_wait_window(flow, n, block, dl);
if (ret < 0)
- shm_rdrbuff_remove(ai.rdrb, idx);
- else
- shm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ return (ssize_t) ret;
+
+ STAT_BUMP(flow->frcti, sdu_snd_frag);
+
+ for (i = 0; i < n; ++i) {
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t idx;
+ size_t clen;
+
+ clen = (i + 1 == n) ? (count - off) : frag_payload;
+
+ if (block)
+ idx = ssm_pool_alloc_b(proc.pool, clen, &ptr,
+ &spb, dl);
+ else
+ idx = ssm_pool_alloc(proc.pool, clen, &ptr, &spb);
+ if (idx < 0) {
+ if (off > 0)
+ STAT_BUMP(flow->frcti, sdu_snd_alloc);
+ return off > 0 ? (ssize_t) off : idx;
+ }
- pthread_cleanup_pop(true);
+ memcpy(ptr, src + off, clen);
+
+ 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;
+ }
+
+ off += clen;
+ }
+
+ return (ssize_t) count;
+}
+
+/*
+ * Initiator promotes on the install grace (it holds the key-confirm
+ * tag); responder waits for peer_synced, with a near-exhaustion floor.
+ */
+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 && !flow->rk_initiator) {
+ 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;
+}
-enomem:
- pthread_rwlock_unlock(&ai.lock);
- shm_rdrbuff_remove(ai.rdrb, idx);
- return -ENOMEM;
+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,
@@ -1290,139 +1845,250 @@ ssize_t flow_write(int fd,
int ret;
int flags;
struct timespec abs;
- struct timespec * abstime = NULL;
- struct shm_du_buff * sdb;
+ struct timespec now;
+ struct timespec * dl = NULL;
+ struct ssm_pk_buff * spb;
uint8_t * ptr;
if (buf == NULL && count != 0)
return -EINVAL;
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -EBADF;
- flow = &ai.flows[fd];
-
- clock_gettime(PTHREAD_COND_CLOCK, &abs);
+ flow = &proc.flows[fd];
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
+ flags = flow->oflags;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
if (flow->snd_timesout) {
- ts_add(&abs, &flow->snd_timeo, &abs);
- abstime = &abs;
+ ts_add(&now, &flow->snd_timeo, &abs);
+ dl = &abs;
}
- flags = flow->oflags;
-
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
if ((flags & FLOWFACCMODE) == FLOWFRDONLY)
return -EPERM;
- if (flags & FLOWFWNOBLOCK) {
- if (!frcti_is_window_open(flow->frcti))
- return -EAGAIN;
- idx = shm_rdrbuff_alloc(ai.rdrb, count, &ptr, &sdb);
- } else {
- ret = frcti_window_wait(flow->frcti, abstime);
+ 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) {
+ /* Pump rx_rb so a pure-writer processes ACKs. */
+ ret = flow_wait_window(flow, 1, !(flags & FLOWFWNOBLOCK), dl);
if (ret < 0)
return ret;
- idx = shm_rdrbuff_alloc_b(ai.rdrb, count, &ptr, &sdb, abstime);
+
+ if (count > 0 && FRCTI_IS_STREAM(flow->frcti))
+ return flow_write_stream(flow, buf, count, flags, dl);
+
+ if (FRCTI_NEEDS_FRAG(flow->frcti, count))
+ return flow_write_frag(flow, buf, count, flags, dl);
+ } else if (flow->info.mtu > 0
+ && count > flow_user_mtu(flow, flow->info.mtu)) {
+ /* Raw flows carry no PCI; refuse anything > one n-1 frame. */
+ return -EMSGSIZE;
}
+ if (flags & FLOWFWNOBLOCK)
+ idx = ssm_pool_alloc(proc.pool, count, &ptr, &spb);
+ else
+ idx = ssm_pool_alloc_b(proc.pool, count, &ptr, &spb, dl);
if (idx < 0)
return idx;
if (count > 0)
memcpy(ptr, buf, count);
- ret = flow_tx_sdb(flow, sdb, !(flags & FLOWFWNOBLOCK), abstime);
-
- return ret < 0 ? (ssize_t) ret : (ssize_t) count;
-}
-
-static bool invalid_pkt(struct flow * flow,
- struct shm_du_buff * sdb)
-{
- if (shm_du_buff_len(sdb) == 0)
- return true;
-
- if (flow->info.qs.ber == 0 && chk_crc(sdb) != 0)
- return true;
-
- if (sdb_decrypt(flow, sdb) < 0)
- return true;
+ 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 false;
+ return (ssize_t) count;
}
-static ssize_t flow_rx_sdb(struct flow * flow,
- struct shm_du_buff ** sdb,
+static ssize_t flow_rx_spb(struct flow * flow,
+ struct ssm_pk_buff ** spb,
bool block,
struct timespec * abstime)
{
ssize_t idx;
struct timespec now;
- idx = block ? shm_rbuff_read_b(flow->rx_rb, abstime) :
- shm_rbuff_read(flow->rx_rb);
+ idx = block ? ssm_rbuff_read_b(flow->rx_rb, abstime)
+ : ssm_rbuff_read(flow->rx_rb);
if (idx < 0)
return idx;
clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- pthread_rwlock_wrlock(&ai.lock);
-
flow->rcv_act = now;
- pthread_rwlock_unlock(&ai.lock);
-
- *sdb = shm_rdrbuff_get(ai.rdrb, idx);
+ *spb = ssm_pool_get(proc.pool, idx);
- if (invalid_pkt(flow, *sdb)) {
- shm_rdrbuff_remove(ai.rdrb, idx);
+ if (invalid_pkt(flow, *spb)) {
+ ssm_pool_remove(proc.pool, idx);
return -EAGAIN;
}
return idx;
}
+static ssize_t raw_flow_read_pkt(struct flow * flow,
+ bool block,
+ struct timespec * dl)
+{
+ struct ssm_pk_buff * spb;
+ struct timespec wait_abs;
+ 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)
+ return -EAGAIN;
+ } else {
+ compute_wait_deadline(dl, &wait_abs);
+ idx = ssm_rbuff_read_b(flow->rx_rb, &wait_abs);
+ if (idx == -ETIMEDOUT) {
+ if (deadline_passed(dl))
+ return -ETIMEDOUT;
+ continue;
+ }
+ if (idx < 0)
+ return idx;
+ }
+
+ spb = ssm_pool_get(proc.pool, idx);
+ if (!invalid_pkt(flow, spb))
+ return idx;
+
+ ssm_pool_remove(proc.pool, idx);
+ if (!block)
+ return -EAGAIN;
+ }
+}
+
+static ssize_t deliver_pkt(struct flow * flow,
+ struct ssm_pk_buff * spb,
+ ssize_t idx,
+ void * buf,
+ size_t count,
+ bool partrd)
+{
+ uint8_t * packet = ssm_pk_buff_head(spb);
+ ssize_t n = ssm_pk_buff_len(spb);
+
+ assert(n >= 0);
+
+ if (n <= (ssize_t) count) {
+ memcpy(buf, packet, n);
+ ipcp_spb_release(spb);
+ if (partrd && n == (ssize_t) count)
+ flow->part_idx = DONE_PART;
+ else
+ flow->part_idx = NO_PART;
+
+ return n;
+ }
+
+ if (partrd) {
+ memcpy(buf, packet, count);
+ ssm_pk_buff_pop(spb, n);
+ flow->part_idx = idx;
+ return count;
+ }
+
+ ipcp_spb_release(spb);
+ return -EMSGSIZE;
+}
+
+/* Drive frcti_consume until it delivers or errors. */
+static ssize_t flow_read_frcti(struct flow * flow,
+ void * buf,
+ size_t count,
+ bool block,
+ struct timespec * dl)
+{
+ struct timespec now;
+ ssize_t bytes;
+ int rc;
+
+ while (true) {
+ flow_drain_rx_nb(flow);
+ bytes = FRCTI_CONSUME(flow->frcti, buf, count);
+ if (bytes >= 0)
+ break;
+ if (bytes != -EAGAIN)
+ return bytes;
+ if (!block)
+ return -EAGAIN;
+ rc = flow_rx_one(flow, dl);
+ if (rc < 0)
+ return rc;
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ flow->rcv_act = now;
+
+ return bytes;
+}
+
ssize_t flow_read(int fd,
void * buf,
size_t count)
{
- ssize_t idx;
- ssize_t n;
- uint8_t * packet;
- struct shm_du_buff * sdb;
+ struct flow * flow;
+ struct ssm_pk_buff * spb;
struct timespec abs;
struct timespec now;
- struct timespec * abstime = NULL;
- struct flow * flow;
+ struct timespec * dl = NULL;
+ ssize_t idx;
bool block;
bool partrd;
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -EBADF;
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
if (flow->part_idx == DONE_PART) {
- pthread_rwlock_unlock(&ai.lock);
flow->part_idx = NO_PART;
+ pthread_rwlock_unlock(&proc.lock);
return 0;
}
@@ -1430,75 +2096,40 @@ ssize_t flow_read(int fd,
partrd = !(flow->oflags & FLOWFRNOPART);
if (flow->rcv_timesout) {
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
ts_add(&now, &flow->rcv_timeo, &abs);
- abstime = &abs;
- }
-
- idx = flow->part_idx;
- if (idx < 0) {
- while ((idx = frcti_queued_pdu(flow->frcti)) < 0) {
- pthread_rwlock_unlock(&ai.lock);
-
- idx = flow_rx_sdb(flow, &sdb, block, abstime);
- if (idx < 0) {
- if (block && idx != -EAGAIN)
- return idx;
- if (!block)
- return idx;
-
- pthread_rwlock_rdlock(&ai.lock);
- continue;
- }
-
- pthread_rwlock_rdlock(&ai.lock);
-
- frcti_rcv(flow->frcti, sdb);
- }
+ dl = &abs;
}
- sdb = shm_rdrbuff_get(ai.rdrb, idx);
+ pthread_rwlock_unlock(&proc.lock);
- pthread_rwlock_unlock(&ai.lock);
+ if (flow->crypt != NULL
+ && (ssm_rbuff_get_flags(flow->rx_rb) & RB_REKEY))
+ flow_rekey(flow);
- packet = shm_du_buff_head(sdb);
+ /* Advance TX off a stale epoch even on recv-mostly (ACK-only) flows. */
+ flow_tx_promote(flow);
- n = shm_du_buff_len(sdb);
+ tw_move_safe();
- assert(n >= 0);
-
- if (n <= (ssize_t) count) {
- memcpy(buf, packet, n);
- ipcp_sdb_release(sdb);
-
- pthread_rwlock_wrlock(&ai.lock);
+ idx = flow->part_idx;
+ if (idx < 0 && flow->frcti != NULL)
+ return flow_read_frcti(flow, buf, count, block, dl);
- flow->part_idx = (partrd && n == (ssize_t) count) ?
- DONE_PART : NO_PART;
+ if (idx < 0) {
+ idx = raw_flow_read_pkt(flow, block, dl);
+ if (idx < 0)
+ return idx;
+ }
- flow->rcv_act = now;
+ spb = ssm_pool_get(proc.pool, idx);
- pthread_rwlock_unlock(&ai.lock);
- return n;
- } else {
- if (partrd) {
- memcpy(buf, packet, count);
- shm_du_buff_head_release(sdb, n);
- pthread_rwlock_wrlock(&ai.lock);
- flow->part_idx = idx;
-
- flow->rcv_act = now;
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ flow->rcv_act = now;
- pthread_rwlock_unlock(&ai.lock);
- return count;
- } else {
- ipcp_sdb_release(sdb);
- return -EMSGSIZE;
- }
- }
+ return deliver_pkt(flow, spb, idx, buf, count, partrd);
}
-/* fqueue functions. */
-
struct flow_set * fset_create(void)
{
struct flow_set * set;
@@ -1507,20 +2138,20 @@ struct flow_set * fset_create(void)
if (set == NULL)
goto fail_malloc;
- assert(ai.fqueues);
+ assert(proc.fqueues);
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_wrlock(&proc.lock);
- set->idx = bmp_allocate(ai.fqueues);
- if (!bmp_is_id_valid(ai.fqueues, set->idx))
+ set->idx = bmp_allocate(proc.fqueues);
+ if (!bmp_is_id_valid(proc.fqueues, set->idx))
goto fail_bmp_alloc;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return set;
fail_bmp_alloc:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
free(set);
fail_malloc:
return NULL;
@@ -1533,11 +2164,11 @@ void fset_destroy(struct flow_set * set)
fset_zero(set);
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_wrlock(&proc.lock);
- bmp_release(ai.fqueues, set->idx);
+ bmp_release(proc.fqueues, set->idx);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
free(set);
}
@@ -1548,7 +2179,7 @@ struct fqueue * fqueue_create(void)
if (fq == NULL)
return NULL;
- memset(fq->fqueue, -1, SHM_BUFFER_SIZE * sizeof(*fq->fqueue));
+ memset(fq->fqueue, -1, SSM_RBUFF_SIZE * sizeof(*fq->fqueue));
fq->fqsize = 0;
fq->next = 0;
@@ -1565,7 +2196,7 @@ void fset_zero(struct flow_set * set)
if (set == NULL)
return;
- shm_flow_set_zero(ai.fqset, set->idx);
+ ssm_flow_set_zero(proc.fqset, set->idx);
}
int fset_add(struct flow_set * set,
@@ -1574,12 +2205,12 @@ int fset_add(struct flow_set * set,
struct flow * flow;
int ret;
- if (set == NULL || fd < 0 || fd >= SYS_MAX_FLOWS)
+ if (set == NULL || fd < 0 || fd >= PROC_MAX_FLOWS)
return -EINVAL;
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
ret = -EINVAL;
@@ -1587,21 +2218,21 @@ int fset_add(struct flow_set * set,
}
if (flow->frcti != NULL)
- shm_flow_set_del(ai.fqset, 0, ai.flows[fd].info.id);
+ ssm_flow_set_del(proc.fqset, 0, flow->info.id);
- ret = shm_flow_set_add(ai.fqset, set->idx, ai.flows[fd].info.id);
+ ret = ssm_flow_set_add(proc.fqset, set->idx, flow->info.id);
if (ret < 0)
goto fail;
- if (shm_rbuff_queued(ai.flows[fd].rx_rb))
- shm_flow_set_notify(ai.fqset, ai.flows[fd].info.id, FLOW_PKT);
+ if (ssm_rbuff_queued(flow->rx_rb))
+ ssm_flow_set_notify(proc.fqset, flow->info.id, FLOW_PKT);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return ret;
fail:
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return ret;
}
@@ -1610,99 +2241,112 @@ void fset_del(struct flow_set * set,
{
struct flow * flow;
- if (set == NULL || fd < 0 || fd >= SYS_MAX_FLOWS)
+ if (set == NULL || fd < 0 || fd >= PROC_MAX_FLOWS)
return;
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id >= 0)
- shm_flow_set_del(ai.fqset, set->idx, flow->info.id);
+ ssm_flow_set_del(proc.fqset, set->idx, flow->info.id);
if (flow->frcti != NULL)
- shm_flow_set_add(ai.fqset, 0, ai.flows[fd].info.id);
+ ssm_flow_set_add(proc.fqset, 0, flow->info.id);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
}
bool fset_has(const struct flow_set * set,
int fd)
{
- bool ret;
+ struct flow * flow;
+ bool ret;
- if (set == NULL || fd < 0 || fd >= SYS_MAX_FLOWS)
+ if (set == NULL || fd < 0 || fd >= PROC_MAX_FLOWS)
return false;
- pthread_rwlock_rdlock(&ai.lock);
+ flow = &proc.flows[fd];
- if (ai.flows[fd].info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
+
+ if (flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
return false;
}
- ret = (shm_flow_set_has(ai.fqset, set->idx, ai.flows[fd].info.id) == 1);
+ ret = (ssm_flow_set_has(proc.fqset, set->idx, flow->info.id) == 1);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return ret;
}
-/* Filter fqueue events for non-data packets */
static int fqueue_filter(struct fqueue * fq)
{
- struct shm_du_buff * sdb;
+ struct ssm_pk_buff * spb;
int fd;
ssize_t idx;
struct frcti * frcti;
+ int ret = 0;
+
+ /* proc.lock rdlock gates frcti_destroy via flow_fini wrlock. */
+ pthread_rwlock_rdlock(&proc.lock);
while (fq->next < fq->fqsize) {
- if (fq->fqueue[fq->next].event != FLOW_PKT)
- return 1;
+ 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;
+ }
- pthread_rwlock_rdlock(&ai.lock);
+ if (fq->fqueue[fq->next].event != FLOW_PKT) {
+ ret = 1;
+ goto out;
+ }
- fd = ai.id_to_fd[fq->fqueue[fq->next].flow_id].fd;
+ fd = proc.id_to_fd[fq->fqueue[fq->next].flow_id].fd;
if (fd < 0) {
++fq->next;
- pthread_rwlock_unlock(&ai.lock);
continue;
}
- frcti = ai.flows[fd].frcti;
+ frcti = proc.flows[fd].frcti;
if (frcti == NULL) {
- pthread_rwlock_unlock(&ai.lock);
- return 1;
+ ret = 1;
+ goto out;
}
- if (__frcti_pdu_ready(frcti) >= 0) {
- pthread_rwlock_unlock(&ai.lock);
- return 1;
+ if (FRCTI_PDU_READY(frcti)) {
+ ret = 1;
+ goto out;
}
- pthread_rwlock_unlock(&ai.lock);
-
- idx = flow_rx_sdb(&ai.flows[fd], &sdb, false, NULL);
+ idx = flow_rx_spb(&proc.flows[fd], &spb, false, NULL);
if (idx < 0)
- return 0;
+ goto out;
- pthread_rwlock_rdlock(&ai.lock);
+ spb = ssm_pool_get(proc.pool, idx);
- sdb = shm_rdrbuff_get(ai.rdrb, idx);
+ FRCTI_RCV(frcti, spb);
- __frcti_rcv(frcti, sdb);
-
- if (__frcti_pdu_ready(frcti) >= 0) {
- pthread_rwlock_unlock(&ai.lock);
- return 1;
+ if (FRCTI_PDU_READY(frcti)) {
+ ret = 1;
+ goto out;
}
- pthread_rwlock_unlock(&ai.lock);
-
++fq->next;
}
- return 0;
+ out:
+ pthread_rwlock_unlock(&proc.lock);
+ return ret;
}
int fqueue_next(struct fqueue * fq)
@@ -1719,15 +2363,15 @@ int fqueue_next(struct fqueue * fq)
if (fq->next != 0 && fqueue_filter(fq) == 0)
return -EPERM;
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
e = fq->fqueue + fq->next;
- fd = ai.id_to_fd[e->flow_id].fd;
+ fd = proc.id_to_fd[e->flow_id].fd;
++fq->next;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return fd;
}
@@ -1749,7 +2393,8 @@ ssize_t fevent(struct flow_set * set,
{
ssize_t ret = 0;
struct timespec abs;
- struct timespec * t = NULL;
+ struct timespec * dl = NULL;
+ struct timespec wait_abs;
if (set == NULL || fq == NULL)
return -EINVAL;
@@ -1757,17 +2402,26 @@ ssize_t fevent(struct flow_set * set,
if (fq->fqsize > 0 && fq->next != fq->fqsize)
return 1;
- clock_gettime(PTHREAD_COND_CLOCK, &abs);
-
if (timeo != NULL) {
- ts_add(&abs, timeo, &abs);
- t = &abs;
+ struct timespec now;
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ ts_add(&now, timeo, &abs);
+ dl = &abs;
}
while (ret == 0) {
- ret = shm_flow_set_wait(ai.fqset, set->idx, fq->fqueue, t);
- if (ret == -ETIMEDOUT)
- return -ETIMEDOUT;
+ tw_move_safe();
+
+ compute_wait_deadline(dl, &wait_abs);
+
+ ret = ssm_flow_set_wait(proc.fqset, set->idx,
+ fq->fqueue, &wait_abs);
+ if (ret == -ETIMEDOUT) {
+ if (deadline_passed(dl))
+ return -ETIMEDOUT;
+ ret = 0;
+ continue;
+ }
fq->fqsize = ret;
fq->next = 0;
@@ -1780,12 +2434,12 @@ ssize_t fevent(struct flow_set * set,
return 1;
}
-/* ipcp-dev functions. */
-
int np1_flow_alloc(pid_t n_pid,
int flow_id)
{
- struct flow_info flow;
+ struct flow_info flow;
+ struct crypt_sk crypt = { .nid = NID_undef, .key = NULL,
+ .epoch = 0, .role = CRYPT_ROLE_INIT };
memset(&flow, 0, sizeof(flow));
@@ -1793,9 +2447,10 @@ int np1_flow_alloc(pid_t n_pid,
flow.n_pid = getpid();
flow.qs = qos_np1;
flow.mpl = 0;
- flow.n_1_pid = n_pid; /* This "flow" is upside-down! */
+ /* np1 flow: n_1_pid is the upper. */
+ flow.n_1_pid = n_pid;
- return flow_init(&flow, NULL);
+ return flow_init(&flow, &crypt, 0);
}
int np1_flow_dealloc(int flow_id,
@@ -1803,19 +2458,14 @@ int np1_flow_dealloc(int flow_id,
{
int fd;
- /*
- * TODO: Don't pass timeo to the IPCP but wait in IRMd.
- * This will need async ops, waiting until we bootstrap
- * the IRMd over ouroboros.
- */
-
+ /* TODO: wait in IRMd, not here; needs async ops. */
sleep(timeo);
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- fd = ai.id_to_fd[flow_id].fd;
+ fd = proc.id_to_fd[flow_id].fd;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return fd;
}
@@ -1828,15 +2478,47 @@ int np1_flow_resp(int flow_id,
if (resp == 0 && flow_wait_assign(flow_id) != FLOW_ALLOCATED)
return -1;
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- fd = ai.id_to_fd[flow_id].fd;
+ fd = proc.id_to_fd[flow_id].fd;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
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];
@@ -1856,12 +2538,15 @@ 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)
{
struct flow_info flow;
- uint8_t buf[SOCK_BUF_SIZE];
- buffer_t msg = {SOCK_BUF_SIZE, buf};
- int err;
+ uint8_t buf[SOCK_BUF_SIZE];
+ buffer_t msg = {SOCK_BUF_SIZE, buf};
+ struct crypt_sk crypt;
+ uint8_t key[SYMMKEYSZ];
+ int err;
memset(&flow, 0, sizeof(flow));
@@ -1870,6 +2555,7 @@ int ipcp_flow_req_arr(const buffer_t * dst,
flow.n_1_pid = getpid();
flow.qs = qs;
flow.mpl = mpl;
+ flow.mtu = mtu;
if (ipcp_flow_req_arr__irm_req_ser(&msg, dst, &flow, data) < 0)
return -ENOMEM;
@@ -1878,22 +2564,56 @@ int ipcp_flow_req_arr(const buffer_t * dst,
if (err < 0)
return err;
- err = flow__irm_result_des(&msg, &flow, NULL);
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_INIT;
+
+ err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
return err;
- /* inverted for np1_flow */
+ /* np1 flows are not encrypted. */
+ assert(crypt.nid == NID_undef);
+
+ /* Inverted for np1_flow. */
flow.n_1_pid = flow.n_pid;
flow.n_pid = getpid();
flow.mpl = 0;
+ flow.mtu = 0;
flow.qs = qos_np1;
- return flow_init(&flow, NULL);
+ crypt.nid = NID_undef;
+
+ return flow_init(&flow, &crypt, 0);
+}
+
+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,
int response,
time_t mpl,
+ uint32_t mtu,
const buffer_t * data)
{
struct flow_info flow;
@@ -1901,15 +2621,16 @@ int ipcp_flow_alloc_reply(int fd,
buffer_t msg = {SOCK_BUF_SIZE, buf};
int err;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- flow.id = ai.flows[fd].info.id;
+ flow.id = proc.flows[fd].info.id;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
flow.mpl = mpl;
+ flow.mtu = mtu;
if (ipcp_flow_alloc_reply__irm_msg_ser(&msg, &flow, response, data) < 0)
return -ENOMEM;
@@ -1922,169 +2643,228 @@ int ipcp_flow_alloc_reply(int fd,
}
int ipcp_flow_read(int fd,
- struct shm_du_buff ** sdb)
+ struct ssm_pk_buff ** spb)
{
struct flow * flow;
ssize_t idx = -1;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
- assert(sdb);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
+ assert(spb);
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- assert(flow->info.id >= 0);
+ if (flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return -ENOTALLOC;
+ }
- while (frcti_queued_pdu(flow->frcti) < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ /* Raw flow: deliver the popped pkt directly (no FRCT rq). */
+ if (flow->frcti == NULL) {
+ idx = flow_rx_spb(flow, spb, false, NULL);
+ pthread_rwlock_unlock(&proc.lock);
+ return idx < 0 ? (int) idx : 0;
+ }
- idx = flow_rx_sdb(flow, sdb, false, NULL);
- if (idx < 0)
+ while (!FRCTI_PDU_READY(flow->frcti)) {
+ idx = flow_rx_spb(flow, spb, false, NULL);
+ if (idx < 0) {
+ pthread_rwlock_unlock(&proc.lock);
return idx;
+ }
- pthread_rwlock_rdlock(&ai.lock);
-
- frcti_rcv(flow->frcti, *sdb);
+ FRCTI_RCV(flow->frcti, *spb);
}
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return 0;
}
int ipcp_flow_write(int fd,
- struct shm_du_buff * sdb)
+ struct ssm_pk_buff * spb)
{
struct flow * flow;
int ret;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
- assert(sdb);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
+ assert(spb);
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_wrlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
if ((flow->oflags & FLOWFACCMODE) == FLOWFRDONLY) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -EPERM;
}
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
- ret = flow_tx_sdb(flow, sdb, true, NULL);
+ ret = flow_tx_spb(flow, spb, FRCT_FR_SOLE, true, NULL);
return ret;
}
+/* Copy src into dst_pool without consuming src. Caller owns both halves. */
+static int pool_dup_spb(struct ssm_pool * src_pool,
+ size_t src_off,
+ struct ssm_pool * dst_pool,
+ struct ssm_pk_buff ** dst_spb)
+{
+ struct ssm_pk_buff * src;
+ uint8_t * ptr;
+ size_t len;
+
+ src = ssm_pool_get(src_pool, src_off);
+ len = ssm_pk_buff_len(src);
+
+ if (ssm_pool_alloc(dst_pool, len, &ptr, dst_spb) < 0)
+ return -ENOMEM;
+
+ memcpy(ptr, ssm_pk_buff_head(src), len);
+
+ return 0;
+}
+
int np1_flow_read(int fd,
- struct shm_du_buff ** sdb)
+ struct ssm_pk_buff ** spb,
+ struct ssm_pool * pool)
{
- struct flow * flow;
- ssize_t idx = -1;
+ struct flow * flow;
+ ssize_t off;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
- assert(sdb);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
+ assert(spb);
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- assert(flow->info.id >= 0);
+ pthread_rwlock_rdlock(&proc.lock);
- pthread_rwlock_rdlock(&ai.lock);
+ if (flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return -ENOTALLOC;
+ }
- idx = shm_rbuff_read(flow->rx_rb);
- if (idx < 0) {
- pthread_rwlock_unlock(&ai.lock);
- return idx;
+ off = ssm_rbuff_read(flow->rx_rb);
+ if (off < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return off;
}
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
- *sdb = shm_rdrbuff_get(ai.rdrb, idx);
+ if (pool == NULL) {
+ *spb = ssm_pool_get(proc.pool, off);
+ } else {
+ /* Cross-pool copy: PUP -> GSPP */
+ if (pool_dup_spb(pool, off, proc.pool, spb) < 0) {
+ ssm_pool_remove(pool, off);
+ return -ENOMEM;
+ }
+ ssm_pool_remove(pool, off);
+ }
return 0;
}
int np1_flow_write(int fd,
- struct shm_du_buff * sdb)
+ struct ssm_pk_buff * spb,
+ struct ssm_pool * pool)
{
- struct flow * flow;
- int ret;
- ssize_t idx;
+ struct flow * flow;
+ struct ssm_pk_buff * dst;
+ int ret;
+ size_t off;
+ size_t dst_off;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
- assert(sdb);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
+ assert(spb);
- flow = &ai.flows[fd];
+ flow = &proc.flows[fd];
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -ENOTALLOC;
}
if ((flow->oflags & FLOWFACCMODE) == FLOWFRDONLY) {
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return -EPERM;
}
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
- idx = shm_du_buff_get_idx(sdb);
+ off = ssm_pk_buff_get_off(spb);
- ret = shm_rbuff_write_b(flow->tx_rb, idx, NULL);
- if (ret < 0)
- shm_rdrbuff_remove(ai.rdrb, idx);
- else
- shm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ if (pool == NULL) {
+ ret = ssm_rbuff_write_b(flow->tx_rb, off, NULL);
+ if (ret < 0)
+ return ret;
+ ssm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ } else {
+ /* Cross-pool copy: GSPP -> PUP. Src kept on error. */
+ if (pool_dup_spb(proc.pool, off, pool, &dst) < 0)
+ return -ENOMEM;
+ dst_off = ssm_pk_buff_get_off(dst);
+ ret = ssm_rbuff_write_b(flow->tx_rb, dst_off, NULL);
+ if (ret < 0) {
+ ssm_pool_remove(pool, dst_off);
+ return ret;
+ }
+ ssm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
+ ssm_pool_remove(proc.pool, off);
+ }
- return ret;
+ return 0;
}
-int ipcp_sdb_reserve(struct shm_du_buff ** sdb,
+int ipcp_spb_reserve(struct ssm_pk_buff ** spb,
size_t len)
{
- return shm_rdrbuff_alloc_b(ai.rdrb, len, NULL, sdb, NULL) < 0 ? -1 : 0;
+ return ssm_pool_alloc_b(proc.pool, len, NULL, spb, NULL) < 0
+ ? -1 : 0;
}
-void ipcp_sdb_release(struct shm_du_buff * sdb)
+void ipcp_spb_release(struct ssm_pk_buff * spb)
{
- shm_rdrbuff_remove(ai.rdrb, shm_du_buff_get_idx(sdb));
+ ssm_pool_remove(proc.pool, ssm_pk_buff_get_off(spb));
}
int ipcp_flow_fini(int fd)
{
- struct shm_rbuff * rx_rb;
+ struct ssm_rbuff * rx_rb;
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- if (ai.flows[fd].info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ if (proc.flows[fd].info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
return -1;
}
- shm_rbuff_set_acl(ai.flows[fd].rx_rb, ACL_FLOWDOWN);
- shm_rbuff_set_acl(ai.flows[fd].tx_rb, ACL_FLOWDOWN);
+ ssm_rbuff_set_bits(proc.flows[fd].rx_rb, RB_FLOWDOWN);
+ ssm_rbuff_set_bits(proc.flows[fd].tx_rb, RB_FLOWDOWN);
- shm_flow_set_notify(ai.flows[fd].set,
- ai.flows[fd].info.id,
+ ssm_flow_set_notify(proc.flows[fd].set,
+ proc.flows[fd].info.id,
FLOW_DEALLOC);
- rx_rb = ai.flows[fd].rx_rb;
+ rx_rb = proc.flows[fd].rx_rb;
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
if (rx_rb != NULL)
- shm_rbuff_fini(rx_rb);
+ ssm_rbuff_fini(rx_rb);
return 0;
}
@@ -2092,74 +2872,91 @@ int ipcp_flow_fini(int fd)
int ipcp_flow_get_qoscube(int fd,
qoscube_t * cube)
{
- assert(fd >= 0 && fd < SYS_MAX_FLOWS);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
assert(cube);
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- assert(ai.flows[fd].info.id >= 0);
+ assert(proc.flows[fd].info.id >= 0);
- *cube = qos_spec_to_cube(ai.flows[fd].info.qs);
+ *cube = qos_spec_to_cube(proc.flows[fd].info.qs);
- pthread_rwlock_unlock(&ai.lock);
+ pthread_rwlock_unlock(&proc.lock);
return 0;
}
+/* Not a snapshot: two atomic loads; caller keeps the fd live. */
size_t ipcp_flow_queued(int fd)
{
- size_t q;
-
- pthread_rwlock_rdlock(&ai.lock);
-
- assert(ai.flows[fd].info.id >= 0);
-
- q = shm_rbuff_queued(ai.flows[fd].tx_rb);
-
- pthread_rwlock_unlock(&ai.lock);
+ assert(fd >= 0 && fd < PROC_MAX_FLOWS);
+ assert(proc.flows[fd].info.id >= 0);
- return q;
+ return ssm_rbuff_queued(proc.flows[fd].tx_rb);
}
-ssize_t local_flow_read(int fd)
+int local_flow_transfer(int src_fd,
+ int dst_fd,
+ struct ssm_pool * src_pool,
+ struct ssm_pool * dst_pool)
{
- ssize_t ret;
-
- assert(fd >= 0);
-
- pthread_rwlock_rdlock(&ai.lock);
-
- ret = shm_rbuff_read(ai.flows[fd].rx_rb);
+ struct flow * src_flow;
+ struct flow * dst_flow;
+ struct ssm_pk_buff * dst_spb;
+ struct ssm_pool * sp;
+ struct ssm_pool * dp;
+ ssize_t off;
+ int ret;
- pthread_rwlock_unlock(&ai.lock);
+ assert(src_fd >= 0);
+ assert(dst_fd >= 0);
- return ret;
-}
-
-int local_flow_write(int fd,
- size_t idx)
-{
- struct flow * flow;
- int ret;
+ src_flow = &proc.flows[src_fd];
+ dst_flow = &proc.flows[dst_fd];
- assert(fd >= 0);
+ sp = src_pool == NULL ? proc.pool : src_pool;
+ dp = dst_pool == NULL ? proc.pool : dst_pool;
- flow = &ai.flows[fd];
+ pthread_rwlock_rdlock(&proc.lock);
- pthread_rwlock_rdlock(&ai.lock);
+ off = ssm_rbuff_read(src_flow->rx_rb);
+ if (off < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return off;
+ }
- if (flow->info.id < 0) {
- pthread_rwlock_unlock(&ai.lock);
+ if (dst_flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ ssm_pool_remove(sp, off);
return -ENOTALLOC;
}
- ret = shm_rbuff_write_b(flow->tx_rb, idx, NULL);
- if (ret == 0)
- shm_flow_set_notify(flow->set, flow->info.id, FLOW_PKT);
- else
- shm_rdrbuff_remove(ai.rdrb, idx);
+ pthread_rwlock_unlock(&proc.lock);
- pthread_rwlock_unlock(&ai.lock);
+ if (sp == dp) {
+ /* Same pool: zero-copy */
+ ret = ssm_rbuff_write_b(dst_flow->tx_rb, off, NULL);
+ if (ret < 0)
+ ssm_pool_remove(sp, off);
+ else
+ ssm_flow_set_notify(dst_flow->set,
+ dst_flow->info.id, FLOW_PKT);
+ } else {
+ /* Different pools: single copy */
+ if (pool_dup_spb(sp, off, dp, &dst_spb) < 0) {
+ ssm_pool_remove(sp, off);
+ return -ENOMEM;
+ }
+
+ ssm_pool_remove(sp, off);
+ off = ssm_pk_buff_get_off(dst_spb);
+ ret = ssm_rbuff_write_b(dst_flow->tx_rb, off, NULL);
+ if (ret < 0)
+ ssm_pool_remove(dp, off);
+ else
+ ssm_flow_set_notify(dst_flow->set,
+ dst_flow->info.id, FLOW_PKT);
+ }
return ret;
}
diff --git a/src/lib/frct.c b/src/lib/frct.c
index 08c5ea80..8eabfc67 100644
--- a/src/lib/frct.c
+++ b/src/lib/frct.c
@@ -1,7 +1,7 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
- * Flow and Retransmission Control
+ * Flow and Retransmission Control Task (FRCT)
*
* Dimitri Staessens <dimitri@ouroboros.rocks>
* Sander Vrijders <sander@ouroboros.rocks>
@@ -20,97 +20,416 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
-#include <ouroboros/endian.h>
+/* Included by dev.c; uses dev.c statics (proc, spb_encrypt, ...). */
#define DELT_RDV (100 * MILLION) /* ns */
-#define MAX_RDV (1 * BILLION) /* ns */
+#define MAX_RDV (1 * BILLION) /* ns */
+
+#define MAX_RTO_MUL 8 /* caps the RTO backoff shift */
+#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 NACK_COOLDOWN_NS (100 * MILLION) /* pre-DRF NACK cooldown */
+#define FRCT_TX_TIMEO_NS (250 * 1000) /* tx ring write deadline */
+#define ACK_DELAY_NS (2ULL * TICTIME) /* delayed-ACK fire delay */
#define FRCT "frct"
#define FRCT_PCILEN (sizeof(struct frct_pci))
#define FRCT_NAME_STRLEN 32
-struct frct_cr {
- uint32_t lwe; /* Left window edge */
- uint32_t rwe; /* Right window edge */
+/* Wire-protocol cap on SACK blocks per packet; binds both peers. */
+#define SACK_MAX_BLOCKS 2048
+#define SACK_BLOCK_SIZE (2 * sizeof(uint32_t))
+/* 2B count + 2B pad to 4-byte align the block list. */
+#define SACK_HDR_SIZE (sizeof(uint32_t))
+#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. */
+
+/* RFC 8985 §7.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. */
+#define REO_DECAY_PKTS 16
+/* DSACK seqno sanity: reject reports older/farther than one rcv window. */
+#define MAX_DSACK_LAG RQ_SIZE
+
+/* Signed ns elapsed; negative under concurrent update (no underflow). */
+static __inline__ int64_t ts_age_ns(uint64_t now_ns,
+ uint64_t then_ns)
+{
+ return (int64_t)(now_ns - then_ns);
+}
- uint8_t cflags;
- uint32_t seqno; /* SEQ to send, or last SEQ Ack'd */
+/* True iff strictly more than thr_ns elapsed since then_ns. */
+static __inline__ bool ts_aged_ns(uint64_t now_ns,
+ uint64_t then_ns,
+ uint64_t thr_ns)
+{
+ return ts_age_ns(now_ns, then_ns) > (int64_t) thr_ns;
+}
- struct timespec act; /* Last seen activity */
- time_t inact; /* Inactivity (s) */
-};
+/* FRCT r-timer: do not retransmit packet older than t_r (from first send). */
+#define RXM_AGED_OUT(t0, now_ns, t_r) \
+ ts_aged_ns((now_ns), (t0), (uint64_t)(t_r))
-struct frcti {
- int fd;
+/* FRCT a-timer: do not (re)transmit ACK after t_a from last data receive. */
+#define ACK_AGED_OUT(act, now_ns, t_a) \
+ ts_aged_ns((now_ns), (act), (uint64_t)(t_a))
- time_t mpl;
- time_t a;
- time_t r;
- time_t rdv;
-
- time_t srtt; /* Smoothed rtt */
- time_t mdev; /* Deviation */
- time_t rto; /* Retransmission timeout */
- uint32_t rttseq;
- struct timespec t_probe; /* Probe time */
- bool probe; /* Probe active */
-#ifdef PROC_FLOW_STATS
- size_t n_rtx; /* Number of rxm packets */
- size_t n_prb; /* Number of rtt probes */
- size_t n_rtt; /* Number of estimates */
- size_t n_dup; /* Duplicates received */
- size_t n_dak; /* Delayed ACKs received */
- size_t n_rdv; /* Number of rdv packets */
- size_t n_out; /* Packets out of window */
- size_t n_rqo; /* Packets out of rqueue */
-#endif
- struct frct_cr snd_cr;
- struct frct_cr rcv_cr;
+struct sack_args {
+ uint16_t n;
+ bool dsack; /* RFC 2883: block[0] is a DSACK report */
+ uint32_t ack;
+ uint32_t rwe;
+ uint32_t blocks[][2]; /* flexible — sized at alloc time */
+};
+/* NewReno-careful (RFC 6582) exit pad; gates RTT samples post-signal. */
+#define RTT_QUARANTINE 32
+#define RTTP_NONCE_LEN 16
- ssize_t rq[RQ_SIZE];
- pthread_rwlock_t lock;
+/* RTT-probe wire payload (after the FRCT PCI). */
+struct frct_rttp {
+ uint32_t probe_id; /* sender counter; 0 on reply */
+ uint32_t echo_id; /* peer's probe_id; 0 outbound */
+ uint8_t nonce[RTTP_NONCE_LEN]; /* random; echoed verbatim */
+} __attribute__((packed));
- bool open; /* Window open/closed */
- struct timespec t_wnd; /* Window closed time */
- struct timespec t_rdvs; /* Last rendez-vous sent */
- pthread_cond_t cond;
- pthread_mutex_t mtx;
-};
+#define RTTP_PAYLOAD sizeof(struct frct_rttp)
+#define RTTP_POS(id) ((id) & (RTTP_RING - 1))
+/*
+ * Flag values are assigned MSB-first on the wire (RFC convention):
+ * bit 0 = 0x8000 occupies wire-position 0 of the 16-bit flags
+ * field, bit 12 = 0x0008 is the last assigned bit, and the three
+ * LSBs (0x0007) are reserved.
+ */
enum frct_flags {
- FRCT_DATA = 0x01, /* PDU carries data */
- FRCT_DRF = 0x02, /* Data run flag */
- FRCT_ACK = 0x04, /* ACK field valid */
- FRCT_FC = 0x08, /* FC window valid */
- FRCT_RDVS = 0x10, /* Rendez-vous */
- FRCT_FFGM = 0x20, /* First Fragment */
- FRCT_MFGM = 0x40, /* More fragments */
+ FRCT_DATA = 0x8000, /* PDU carries data */
+ FRCT_DRF = 0x4000, /* Data run flag */
+ FRCT_ACK = 0x2000, /* ACK field valid */
+ FRCT_NACK = 0x1000, /* Neg-ACK: pci->seqno is arrival_seqno - 1 */
+ FRCT_FC = 0x0800, /* FC window valid */
+ FRCT_RDVS = 0x0400, /* Rendez-vous */
+ FRCT_FFGM = 0x0200, /* First fragment (begin) */
+ FRCT_LFGM = 0x0100, /* Last fragment (end) */
+ FRCT_RXM = 0x0080, /* Retransmission */
+ FRCT_SACK = 0x0040, /* SACK block list follows */
+ FRCT_RTTP = 0x0020, /* RTT probe / echo */
+ FRCT_KA = 0x0010, /* Keepalive */
+ FRCT_FIN = 0x0008, /* End of stream */
};
-struct frct_pci {
- uint8_t flags;
+/*
+ * DATA-packet fragment role (FFGM = begin, LFGM = end), SCTP-style:
+ * 1 1 = sole / un-fragmented SDU (begin AND end)
+ * 1 0 = first fragment of a multi-fragment SDU
+ * 0 0 = middle fragment
+ * 0 1 = last fragment
+ */
+#define FRCT_FR_MASK (FRCT_FFGM | FRCT_LFGM)
+#define FRCT_FR_SOLE (FRCT_FFGM | FRCT_LFGM)
+#define FRCT_FR_FIRST (FRCT_FFGM)
+#define FRCT_FR_MID (0)
+#define FRCT_FR_LAST (FRCT_LFGM)
+
+/* Default cap on a single reassembled SDU. App can raise via FRCTSMAXSDU */
+#define FRCT_MAX_SDU (1U << 20)
+
+/* Stream-mode PCI extension: [start, end) byte range on every DATA pkt. */
+struct frct_pci_stream {
+ uint32_t start;
+ uint32_t end;
+} __attribute__((packed));
+
+#define FRCT_PCI_STREAM_LEN (sizeof(struct frct_pci_stream))
- uint8_t pad; /* 24 bit window! */
- uint16_t window;
+/* Bytes following PCI: SACK list / RTTP nonce / control payload. */
+#define FRCT_BODY(pci) ((uint8_t *) (pci) + FRCT_PCILEN)
+/* Typed access to the stream PCI extension on stream DATA packets. */
+#define FRCT_SPCI(pci) \
+ ((struct frct_pci_stream *) ((uint8_t *) (pci) + FRCT_PCILEN))
+/* Push the FRCT header onto spb's head. */
+#define FRCT_HDR_PUSH(spb, frcti) \
+ ((struct frct_pci *) ssm_pk_buff_push((spb), \
+ frcti_data_hdr_len(frcti)))
+
+/* Pop a fixed-size header off spb's head; cast to type *. */
+#define FRCT_HDR_POP(spb, type) \
+ ((struct type *) ssm_pk_buff_pop((spb), sizeof(struct type)))
+
+/* Default / max per-flow stream rx ring (pow2); min N * per_pkt. */
+#define FRCT_STREAM_RING_MIN_PKTS 4
+#define FRCT_STREAM_RING_SZ (1U << 20) /* 1 MiB default */
+#define FRCT_STREAM_RING_SZ_MAX (1U << 27) /* 128 MiB */
+
+struct frct_pci {
+ uint16_t flags;
+ uint16_t hcs;
+
+ uint32_t window;
uint32_t seqno;
uint32_t ackno;
} __attribute__((packed));
+/* Stat counters; fold to no-ops without PROC_FLOW_STATS. */
#ifdef PROC_FLOW_STATS
+struct frcti_stat {
+ size_t rxm_rto; /* RTO-timer driven retransmits */
+ size_t rxm_rcv; /* RXM packets received (all) */
+ 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_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_arm_fail; /* rxm_arm: malloc failed */
+ size_t rxm_cancel; /* entries cancelled at teardown */
+ size_t rxm_tx_dead; /* RXM tx into terminal flow */
+ size_t tx_drop; /* frct_tx fail (any cause) */
+ size_t tx_drop_ack; /* bare ACK dropped */
+ size_t tx_drop_sack; /* SACK dropped */
+ size_t tx_drop_ka; /* keepalive dropped */
+ size_t tx_drop_rttp; /* RTT probe/echo dropped */
+ size_t tx_drop_nack; /* pre-DRF NACK dropped */
+ size_t tx_drop_rdv; /* rendez-vous dropped */
+ size_t tx_drop_other; /* anything not matched above */
+ size_t ack_snd; /* ACK packets sent (bare + SACK) */
+ size_t ack_fire; /* delayed-ACK timer fires */
+ size_t ack_supp_seqno; /* fire suppressed: seqno */
+ size_t ack_supp_inact; /* fire suppressed: inact */
+ size_t ack_supp_rate; /* fire suppressed: rate */
+ size_t ack_rcv; /* ACK packets received */
+ size_t ack_rtt; /* ACKs that fed RTT estimator */
+ size_t ack_dup_rcv; /* ACK packet wire dups dropped */
+ size_t dup_rcv; /* duplicates received */
+ size_t out_rcv; /* pkts out of window */
+ size_t rqo_rcv; /* pkts out of rqueue */
+ size_t ooo_rcv; /* OOO arrivals */
+ size_t sack_snd; /* SACK packets sent */
+ size_t sack_rcv; /* SACK packets received */
+ size_t dsack_snd; /* SACK pkts carrying a DSACK */
+ size_t dsack_rcv; /* DSACK blocks parsed */
+ size_t dsack_drop; /* DSACK blocks past MAX_DSACK_LAG */
+ size_t nack_snd; /* pre-DRF NACKs sent */
+ size_t nack_rcv; /* pre-DRF NACKs received */
+ size_t tlp_snd; /* tail loss probes sent */
+ size_t inact_drop; /* inactivity drop (NACK on cd) */
+ size_t drf_rebase; /* DRF-triggered window rebase */
+ size_t rq_released; /* slots cleared by release_rq */
+ size_t rttp_snd; /* RTT probes sent */
+ size_t rttp_rcv; /* RTT probe replies rcvd */
+ size_t rtt_smpl; /* RTT estimator samples */
+ size_t rdv_snd; /* rendez-vous packets sent */
+ size_t rdv_rcv; /* rendez-vous packets rcvd */
+ size_t ka_snd; /* keepalives sent */
+ size_t ka_rcv; /* keepalives received */
+ size_t sdu_snd_frag; /* writes that fragmented */
+ size_t sdu_snd_alloc; /* alloc fail truncated SDU send */
+ size_t sdu_snd_tx; /* tx fail truncated SDU send */
+ size_t frag_snd; /* fragments sent: FIRST/MID/LAST */
+ size_t frag_rcv; /* fragments stashed in rq[] */
+ size_t sdu_reasm; /* SDUs delivered reassembled */
+ size_t sdu_sole; /* SOLE SDUs delivered (n==1) */
+ size_t frag_drop; /* dropped at malformed run */
+ size_t strm_snd_byte; /* bytes sent on stream */
+ size_t strm_rcv_byte; /* bytes copied to ring */
+ size_t strm_dlv_byte; /* bytes delivered to reader */
+ size_t strm_drop; /* stream rcvs dropped */
+ size_t strm_fin_drop; /* stream FIN packets rejected */
+ /* Profiling instrumentation. */
+ size_t rcv_proc_ns; /* time inside FRCTI_RCV (ns) */
+ size_t tw_move_ns; /* time inside tw_move (ns) */
+ size_t drain_calls; /* flow_drain_rx_nb invocations */
+};
+
+#define STAT_BUMP(frcti, field) FETCH_ADD_RELAXED(&(frcti)->stat.field, 1)
+#define STAT_ADD(frcti, field, v) FETCH_ADD_RELAXED(&(frcti)->stat.field, (v))
+#define STAT_LOAD(frcti, field) LOAD_RELAXED(&(frcti)->stat.field)
+#else
+#define STAT_BUMP(frcti, field) ((void) (frcti))
+#define STAT_ADD(frcti, field, v) ((void) (frcti))
+#define STAT_LOAD(frcti, field) ((void) (frcti), (size_t) 0)
+#endif
+
+#define frcti_to_flow(f) (&proc.flows[(f)->fd])
+#define RTTP_RING 8
+#define RTTP_COLD_NS (100 * MILLION) /* cold-probe cadence */
+#define RQ_SLOT(seqno) ((seqno) & (RQ_SIZE - 1))
+
+struct rxm_entry;
+
+enum snd_slot_flags {
+ SND_RTX = 0x01, /* Any retransmit; Karn skips next RTT sample. */
+ SND_FAST_RXM = 0x02, /* Fast-retx one-shot gate per loss event. */
+ SND_TLP = 0x04, /* Tail loss probe; ACK resets rto_mul. */
+};
+
+struct snd_slot {
+ struct rxm_entry * rxm; /* RXM entry, NULL if none. */
+ uint64_t time; /* ts_to_ns of last send (any kind). */
+ uint8_t flags; /* SND_* bits above. */
+};
+
+/* Per-seqno reorder slot (FRTX) and stream-mode byte/FIN metadata. */
+struct rcv_slot {
+ ssize_t idx; /* spb idx; -1 = empty */
+ uint32_t start; /* stream byte start */
+ uint32_t end; /* stream byte end */
+ uint8_t fin; /* stream FIN bit */
+};
+
+struct frct_cr {
+ uint32_t lwe; /* Left window edge */
+ uint32_t rwe; /* Right window edge */
+
+ uint8_t cflags;
+ uint32_t seqno; /* SEQ to send, or last SEQ Ack'd */
+ uint32_t ackno; /* snd: ACK-pkt seqno; rcv: dedup */
+
+ uint64_t act; /* ts_to_ns of last activity */
+ uint64_t inact; /* Inactivity threshold (ns) */
+};
+
+struct frcti {
+ /* IMM: set once in frcti_create; read-only thereafter. */
+ int fd;
+ uint64_t t_mpl; /* MPL (ns) */
+ uint64_t t_a; /* a-timer (ns) */
+ uint64_t t_r; /* r-timer (ns) */
+ uint64_t t_rdv; /* RDV cooldown (ns) */
+ time_t ber; /* cached qs.ber */
+ bool lossy; /* qs.loss != 0 */
+ time_t qs_timeout; /* cached qs.timeout (ms) */
+ size_t frag_mtu; /* max FRCT pkt: PCI + payload */
+ uint16_t sack_n_max; /* SACK blocks that fit MTU */
+ bool stream;
+
+ /* All fields below are protected by lock (rwlock/LOAD_ACQUIRE). */
+ struct {
+ struct frct_cr snd_cr;
+ 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;
+ struct {
+ uint32_t id;
+ uint64_t ts; /* ts_to_ns send */
+ uint8_t nonce[RTTP_NONCE_LEN]; /* echoed back */
+ } probes[RTTP_RING];
+
+ /* rcv reassembly */
+ size_t max_rcv_sdu; /* max reasm bytes */
+ uint8_t * rcv_ring; /* lazy alloc */
+ size_t rcv_ring_sz; /* power of 2 */
+ uint32_t ring_seq_cap; /* ring/per_pkt */
+
+ uint32_t snd_byte_next;
+ bool snd_fin_sent;
+ uint32_t snd_fin_seqno;
+ uint32_t rcv_byte_next;
+ uint32_t rcv_byte_high; /* contiguous high */
+ uint32_t rcv_byte_fin; /* set when FIN */
+ bool rcv_fin_seen;
+
+ struct rcv_slot rcv_slots[RQ_SIZE];
+ struct snd_slot snd_slots[RQ_SIZE]; /* .rxm is ATOM */
+
+ /* rcv SACK dedup */
+ uint64_t t_snd_sack;
+ uint32_t sack_lwe; /* rcv lwe at SACK */
+ uint16_t sack_n; /* SACK block count */
+
+ /* RFC 2883 D-SACK: pending report (single-slot, latest). */
+ uint32_t dsack_seqno;
+ bool dsack_valid;
+
+ /* RFC 8985 §7.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 tlp_high_seq; /* §7.3: 0 = none */
+ uint8_t tlp_count; /* §7.3 per-episode */
+ uint64_t t_nack;
+ bool open; /* FC window state */
+ bool in_recovery;
+ uint32_t recovery_high; /* seqno @ entry */
+ uint32_t rack_fired_lwe; /* lwe @ last RACK */
+ struct timespec t_wnd; /* window-closed ts */
+ struct timespec t_last_rdv; /* last RDV sent */
+ struct list_head rxm_list; /* live rxm entries */
+
+ pthread_rwlock_t lock;
+ };
+
+ /* Read/written via __atomic without holding lock. */
+ uint64_t t_ka_rcv; /* ts_to_ns of last KA rx */
+ uint8_t ack_pending; /* delayed-ACK dedup */
+ uint8_t tlp_pending; /* TLP arm dedup (lazy) */
+
+ /* Timer entries; ownership belongs to the tw module. */
+ struct tw_entry ack_tw; /* delayed-ACK timer */
+ struct tw_entry ka_tw; /* keepalive timer */
+ struct tw_entry tlp_tw; /* tail-loss probe timer */
+
+#ifdef PROC_FLOW_STATS
+ /* STAT: lock-free relaxed atomic counters. */
+ struct frcti_stat stat;
+#endif
+};
+
+#ifdef PROC_FLOW_STATS
+
+__attribute__((cold))
static int frct_rib_read(const char * path,
char * buf,
size_t len)
{
+ struct frcti * frcti;
struct timespec now;
+ uint64_t now_ns;
char * entry;
- struct flow * flow;
- struct frcti * frcti;
int fd;
-
- (void) len;
+ int written;
+ /* Snapshot under the locks; format outside (pure userspace). */
+ struct {
+ uint64_t t_mpl;
+ uint64_t t_a;
+ uint64_t t_r;
+ time_t srtt;
+ time_t mdev;
+ time_t rto;
+ time_t min_rtt;
+ struct frct_cr snd_cr;
+ struct frct_cr rcv_cr;
+ size_t rx_q_now;
+ size_t tx_q_now;
+ struct frcti_stat stat;
+ } s;
entry = strstr(path, RIB_SEPARATOR);
assert(entry);
@@ -118,23 +437,50 @@ static int frct_rib_read(const char * path,
fd = atoi(path);
- flow = &ai.flows[fd];
-
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
+ return 0;
- pthread_rwlock_rdlock(&ai.lock);
+ pthread_rwlock_rdlock(&proc.lock);
- frcti = flow->frcti;
+ frcti = proc.flows[fd].frcti;
+ if (frcti == NULL) {
+ pthread_rwlock_unlock(&proc.lock);
+ return 0;
+ }
+
+ s.t_mpl = frcti->t_mpl;
+ s.t_a = frcti->t_a;
+ s.t_r = frcti->t_r;
+
+ s.rx_q_now = proc.flows[fd].rx_rb != NULL
+ ? ssm_rbuff_queued(proc.flows[fd].rx_rb) : 0;
+ s.tx_q_now = proc.flows[fd].tx_rb != NULL
+ ? ssm_rbuff_queued(proc.flows[fd].tx_rb) : 0;
pthread_rwlock_rdlock(&frcti->lock);
- sprintf(buf,
- "Maximum packet lifetime (ns): %20ld\n"
- "Max time to Ack (ns): %20ld\n"
- "Max time to Retransmit (ns): %20ld\n"
+ s.srtt = frcti->srtt;
+ s.mdev = frcti->mdev;
+ s.rto = frcti->rto;
+ s.min_rtt = frcti->min_rtt;
+ s.snd_cr = frcti->snd_cr;
+ s.rcv_cr = frcti->rcv_cr;
+ s.stat = frcti->stat;
+
+ pthread_rwlock_unlock(&frcti->lock);
+ pthread_rwlock_unlock(&proc.lock);
+
+ written = snprintf(buf, len,
+ "Maximum packet lifetime (ns): %20" PRIu64 "\n"
+ "Max time to Ack (ns): %20" PRIu64 "\n"
+ "Max time to Retransmit (ns): %20" PRIu64 "\n"
"Smoothed rtt (ns): %20ld\n"
"RTT standard deviation (ns): %20ld\n"
"Retransmit timeout RTO (ns): %20ld\n"
+ "Minimum RTT (RACK base, ns): %20ld\n"
"Sender left window edge: %20u\n"
"Sender right window edge: %20u\n"
"Sender inactive (ns): %20lld\n"
@@ -143,44 +489,132 @@ static int frct_rib_read(const char * path,
"Receiver right window edge: %20u\n"
"Receiver inactive (ns): %20lld\n"
"Receiver last ack: %20u\n"
- "Number of pkt retransmissions: %20zu\n"
- "Number of rtt probes: %20zu\n"
- "Number of rtt estimates: %20zu\n"
- "Number of duplicates received: %20zu\n"
- "Number of delayed acks received: %20zu\n"
- "Number of rendez-vous sent: %20zu\n"
- "Number of packets out of window: %20zu\n"
- "Number of packets out of rqueue: %20zu\n",
- frcti->mpl,
- frcti->a,
- frcti->r,
- frcti->srtt,
- frcti->mdev,
- frcti->rto,
- frcti->snd_cr.lwe,
- frcti->snd_cr.rwe,
- ts_diff_ns(&now, &frcti->snd_cr.act),
- frcti->snd_cr.seqno,
- frcti->rcv_cr.lwe,
- frcti->rcv_cr.rwe,
- ts_diff_ns(&now, &frcti->rcv_cr.act),
- frcti->rcv_cr.seqno,
- frcti->n_rtx,
- frcti->n_prb,
- frcti->n_rtt,
- frcti->n_dup,
- frcti->n_dak,
- frcti->n_rdv,
- frcti->n_out,
- frcti->n_rqo);
-
- pthread_rwlock_unlock(&flow->frcti->lock);
-
- pthread_rwlock_unlock(&ai.lock);
-
- return strlen(buf);
+ "RXM (RTO-driven) sent: %20zu\n"
+ "RXM packets received: %20zu\n"
+ " duplicates received: %20zu\n"
+ "RXM (SACK mechanism) sent: %20zu\n"
+ "RXM (RACK-driven) sent: %20zu\n"
+ "RXM (DupThresh-driven) sent: %20zu\n"
+ "RXM (NACK-driven) sent: %20zu\n"
+ "ACK packets sent: %20zu\n"
+ "Delayed-ACK timer fires: %20zu\n"
+ " suppressed (seqno): %20zu\n"
+ " suppressed (inact): %20zu\n"
+ " suppressed (rate): %20zu\n"
+ "ACK packets received: %20zu\n"
+ " fed RTT estimator: %20zu\n"
+ " wire dups dropped: %20zu\n"
+ "Duplicates received: %20zu\n"
+ "Out-of-window pkts received: %20zu\n"
+ "Out-of-rqueue pkts received: %20zu\n"
+ "OOO arrivals: %20zu\n"
+ "SACKs sent: %20zu\n"
+ "SACKs received: %20zu\n"
+ "D-SACKs sent: %20zu\n"
+ "D-SACKs received: %20zu\n"
+ "D-SACK out-of-range dropped: %20zu\n"
+ "Pre-DRF NACKs sent: %20zu\n"
+ "Pre-DRF NACKs received: %20zu\n"
+ "Tail loss probes sent: %20zu\n"
+ "Inactivity drops (silent): %20zu\n"
+ "DRF window rebases: %20zu\n"
+ "rq slots cleared by release_rq: %20zu\n"
+ "RTT probes sent: %20zu\n"
+ "RTT probe replies received: %20zu\n"
+ "RTT estimator samples: %20zu\n"
+ "Rendez-vous packets sent: %20zu\n"
+ "Rendez-vous packets received: %20zu\n"
+ "Keepalives sent: %20zu\n"
+ "Keepalives received: %20zu\n"
+ "SDU writes fragmented: %20zu\n"
+ " alloc fail mid-SDU: %20zu\n"
+ " tx fail mid-SDU: %20zu\n"
+ "Fragments sent: %20zu\n"
+ "Fragments received: %20zu\n"
+ "SDUs delivered reassembled: %20zu\n"
+ "SDUs delivered (SOLE): %20zu\n"
+ "Fragments dropped (malformed): %20zu\n"
+ "Stream bytes sent: %20zu\n"
+ "Stream bytes received: %20zu\n"
+ "Stream bytes delivered: %20zu\n"
+ "Stream packets dropped: %20zu\n"
+ "Stream FINs dropped: %20zu\n"
+ "FRCTI_RCV time (ns): %20zu\n"
+ "tw_move time (ns): %20zu\n"
+ "drain_rx_nb calls: %20zu\n"
+ "RX rbuff queued: %20zu\n"
+ "TX rbuff queued: %20zu\n"
+ "RXM-due entries: %20zu\n"
+ " bail (acked): %20zu\n"
+ " bail (unowned): %20zu\n"
+ " bail (aged): %20zu\n"
+ " bail (defer): %20zu\n"
+ "RXM-arm malloc failures: %20zu\n"
+ "RXM cancels (teardown): %20zu\n"
+ "RXM tx into dead flow: %20zu\n"
+ "Tx ring drops (any cause): %20zu\n"
+ " ack: %20zu\n"
+ " sack: %20zu\n"
+ " ka: %20zu\n"
+ " rttp: %20zu\n"
+ " nack: %20zu\n"
+ " rdv: %20zu\n"
+ " other: %20zu\n",
+ /* Check getattr size below when adding stats. */
+ s.t_mpl, s.t_a, s.t_r,
+ s.srtt, s.mdev, s.rto, s.min_rtt,
+ s.snd_cr.lwe, s.snd_cr.rwe,
+ (long long)(now_ns - s.snd_cr.act),
+ s.snd_cr.seqno,
+ s.rcv_cr.lwe, s.rcv_cr.rwe,
+ (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_nack,
+ s.stat.ack_snd, s.stat.ack_fire,
+ s.stat.ack_supp_seqno, s.stat.ack_supp_inact,
+ s.stat.ack_supp_rate,
+ s.stat.ack_rcv, s.stat.ack_rtt, s.stat.ack_dup_rcv,
+ s.stat.dup_rcv, s.stat.out_rcv, s.stat.rqo_rcv,
+ s.stat.ooo_rcv,
+ s.stat.sack_snd, s.stat.sack_rcv,
+ s.stat.dsack_snd, s.stat.dsack_rcv, s.stat.dsack_drop,
+ s.stat.nack_snd, s.stat.nack_rcv, s.stat.tlp_snd,
+ s.stat.inact_drop, s.stat.drf_rebase, s.stat.rq_released,
+ s.stat.rttp_snd, s.stat.rttp_rcv, s.stat.rtt_smpl,
+ s.stat.rdv_snd, s.stat.rdv_rcv,
+ s.stat.ka_snd, s.stat.ka_rcv,
+ s.stat.sdu_snd_frag, s.stat.sdu_snd_alloc, s.stat.sdu_snd_tx,
+ s.stat.frag_snd, s.stat.frag_rcv,
+ s.stat.sdu_reasm, s.stat.sdu_sole, s.stat.frag_drop,
+ s.stat.strm_snd_byte, s.stat.strm_rcv_byte,
+ s.stat.strm_dlv_byte,
+ s.stat.strm_drop, s.stat.strm_fin_drop,
+ s.stat.rcv_proc_ns, s.stat.tw_move_ns,
+ s.stat.drain_calls,
+ s.rx_q_now, s.tx_q_now,
+ 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_arm_fail,
+ s.stat.rxm_cancel,
+ s.stat.rxm_tx_dead, s.stat.tx_drop,
+ s.stat.tx_drop_ack, s.stat.tx_drop_sack,
+ s.stat.tx_drop_ka, s.stat.tx_drop_rttp,
+ s.stat.tx_drop_nack, s.stat.tx_drop_rdv,
+ s.stat.tx_drop_other);
+
+ if (written < 0)
+ return 0;
+
+ if ((size_t) written >= len)
+ return (int) (len - 1);
+
+ return written;
}
+__attribute__((cold))
static int frct_rib_readdir(char *** buf)
{
*buf = malloc(sizeof(**buf));
@@ -199,13 +633,14 @@ static int frct_rib_readdir(char *** buf)
return -ENOMEM;
}
+__attribute__((cold))
static int frct_rib_getattr(const char * path,
struct rib_attr * attr)
{
(void) path;
- (void) attr;
- attr->size = 1189;
+ /* Must be >= the sprintf output in frct_rib_read. */
+ attr->size = 8192;
attr->mtime = 0;
return 0;
@@ -220,128 +655,1168 @@ static struct rib_ops r_ops = {
#endif /* PROC_FLOW_STATS */
-static bool before(uint32_t seq1,
- uint32_t seq2)
+static __inline__ bool before(uint32_t s1, uint32_t s2)
{
- return (int32_t)(seq1 - seq2) < 0;
+ return (int32_t)(s1 - s2) < 0;
}
-static bool after(uint32_t seq1,
- uint32_t seq2)
+static __inline__ bool after(uint32_t s1, uint32_t s2)
{
- return (int32_t)(seq2 - seq1) < 0;
+ return (int32_t)(s2 - s1) < 0;
}
-static void __send_frct_pkt(int fd,
- uint8_t flags,
- uint32_t ackno,
- uint32_t rwe)
+static __inline__ bool within(uint32_t seq, uint32_t lo, uint32_t hi)
{
- struct shm_du_buff * sdb;
- struct frct_pci * pci;
- ssize_t idx;
- struct flow * f;
+ return after(seq, lo) && !after(seq, hi);
+}
- /* Raw calls needed to bypass frcti. */
-#ifdef RXM_BLOCKING
- idx = shm_rdrbuff_alloc_b(ai.rdrb, sizeof(*pci), NULL, &sdb, NULL);
-#else
- idx = shm_rdrbuff_alloc(ai.rdrb, sizeof(*pci), NULL, &sdb);
-#endif
- if (idx < 0)
+static __inline__ bool in_window(uint32_t seq, const struct frct_cr * cr)
+{
+ return !before(seq, cr->lwe) && before(seq, cr->rwe);
+}
+
+/* DRF arrival that stays within the current receive epoch. */
+static __inline__ bool same_epoch_drf(uint32_t seq,
+ uint16_t flags,
+ const struct frct_cr * cr)
+{
+ if (cr->lwe == cr->rwe)
+ return false;
+
+ return (flags & FRCT_RXM) || in_window(seq, cr);
+}
+
+/*
+ * 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).
+ * 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;
+ uint64_t R = mult * (base / 4);
+
+ R = MAX(R, (uint64_t) MIN_REORDER_NS);
+ R = MIN(R, (uint64_t) frcti->srtt);
+
+ return R;
+}
+
+static __inline__ int frct_spb_reserve(size_t len,
+ struct ssm_pk_buff ** spb)
+{
+ ssize_t idx = ssm_pool_alloc_b(proc.pool, len, NULL, spb, NULL);
+
+ return idx < 0 ? (int) idx : 0;
+}
+
+static __inline__ void frct_spb_release(struct ssm_pk_buff * spb)
+{
+ ssm_pool_remove(proc.pool, ssm_pk_buff_get_off(spb));
+}
+
+static __inline__ void frct_spb_release_idx(size_t idx)
+{
+ ssm_pool_remove(proc.pool, idx);
+}
+
+/* Fetch the spb stashed at the rq slot for seqno. */
+static __inline__ struct ssm_pk_buff * rq_frag(const struct frcti * frcti,
+ uint32_t seqno)
+{
+ return ssm_pool_get(proc.pool, frcti->rcv_slots[RQ_SLOT(seqno)].idx);
+}
+
+static __inline__ size_t frcti_data_hdr_len(const struct frcti * frcti)
+{
+ return FRCT_PCILEN + (frcti->stream ? FRCT_PCI_STREAM_LEN : 0);
+}
+
+static __inline__ size_t frcti_ctrl_hdr_len(const struct frcti * frcti)
+{
+ (void) frcti;
+
+ return FRCT_PCILEN;
+}
+
+/*
+ * HCS at offset 2 inside PCI. Covers flags (bytes 0..1) and
+ * window/seqno/ackno (bytes 4..15), plus SPCI for stream DATA.
+ */
+static void frct_hcs_set(struct frct_pci * pci,
+ bool stream)
+{
+ uint16_t hcs = 0;
+ size_t tail;
+
+ tail = sizeof(*pci) - sizeof(pci->flags) - sizeof(pci->hcs);
+ if (stream)
+ tail += FRCT_PCI_STREAM_LEN;
+
+ crc16_ccitt_false(&hcs, pci, sizeof(pci->flags));
+ crc16_ccitt_false(&hcs, &pci->window, tail);
+
+ pci->hcs = hton16(hcs);
+}
+
+static int frct_hcs_check(const struct frct_pci * pci,
+ const struct frcti * frcti)
+{
+ uint16_t hcs = 0;
+ uint16_t flags;
+ size_t tail;
+
+ /* Untrusted flag read; mismatch on HCS will drop on corrupt. */
+ flags = ntoh16(pci->flags);
+
+ tail = sizeof(*pci) - sizeof(pci->flags) - sizeof(pci->hcs);
+ if (frcti->stream && (flags & FRCT_DATA))
+ tail += FRCT_PCI_STREAM_LEN;
+
+ crc16_ccitt_false(&hcs, pci, sizeof(pci->flags));
+ crc16_ccitt_false(&hcs, &pci->window, tail);
+
+ return hcs != ntoh16(pci->hcs);
+}
+
+/* Bump tx_drop plus the per-frame-type counter matching `flags`. */
+static void frct_tx_drop_bump(struct frcti * frcti,
+ uint16_t flags)
+{
+ STAT_BUMP(frcti, tx_drop);
+
+ if (flags & FRCT_SACK) {
+ STAT_BUMP(frcti, tx_drop_sack);
return;
+ }
- pci = (struct frct_pci *) shm_du_buff_head(sdb);
- memset(pci, 0, sizeof(*pci));
+ if (flags & FRCT_KA) {
+ STAT_BUMP(frcti, tx_drop_ka);
+ return;
+ }
- *((uint32_t *) pci) = hton32(rwe);
+ if (flags & FRCT_RTTP) {
+ STAT_BUMP(frcti, tx_drop_rttp);
+ return;
+ }
- pci->flags = flags;
- pci->ackno = hton32(ackno);
+ if (flags & FRCT_NACK) {
+ STAT_BUMP(frcti, tx_drop_nack);
+ return;
+ }
+
+ if (flags & FRCT_RDVS) {
+ STAT_BUMP(frcti, tx_drop_rdv);
+ return;
+ }
+
+ if (flags & FRCT_ACK) {
+ STAT_BUMP(frcti, tx_drop_ack);
+ return;
+ }
- f = &ai.flows[fd];
+ STAT_BUMP(frcti, tx_drop_other);
+}
+
+static int frct_tx(struct frcti * frcti, struct ssm_pk_buff * spb)
+{
+ struct flow * f = frcti_to_flow(frcti);
+ const struct frct_pci * pci;
+ const struct timespec * dl = NULL;
+ struct timespec now;
+ struct timespec intv = TIMESPEC_INIT_NS(FRCT_TX_TIMEO_NS);
+ struct timespec deadline;
+ uint16_t flags;
+ ssize_t idx;
+ int ret = -ENOMEM;
+
+ pci = (const struct frct_pci *) ssm_pk_buff_head(spb);
+ flags = ntoh16(pci->flags);
+
+ /* CRC32 covers plaintext body; PCI is in HCS. Pre-encrypt. */
+ if (flags & FRCT_SACK) {
+ if (crc_add(spb, frcti_ctrl_hdr_len(frcti)) != 0)
+ goto fail;
+ } else if ((flags & FRCT_DATA) && f->info.qs.ber == 0) {
+ if (crc_add(spb, frcti_data_hdr_len(frcti)) != 0)
+ goto fail;
+ }
- if (sdb_encrypt(f, sdb) < 0)
+ if (spb_encrypt(f, spb) < 0)
goto fail;
-#ifdef RXM_BLOCKING
- if (shm_rbuff_write_b(f->tx_rb, idx, NULL))
-#else
- if (shm_rbuff_write(f->tx_rb, idx))
-#endif
+ idx = ssm_pk_buff_get_off(spb);
+
+ /* DATA blocks; control times out so a full ring can't stall 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 (ret < 0)
goto fail;
- shm_flow_set_notify(f->set, f->info.id, FLOW_PKT);
+ ssm_flow_set_notify(f->set, f->info.id, FLOW_PKT);
- return;
+ return 0;
fail:
- ipcp_sdb_release(sdb);
- return;
+ frct_tx_drop_bump(frcti, flags);
+ ssm_pool_remove(proc.pool, ssm_pk_buff_get_off(spb));
+ return ret;
+}
+
+__attribute__((cold))
+static void frct_mark_flow_down(struct frcti * frcti)
+{
+ struct flow * f = frcti_to_flow(frcti);
+
+ if (f->rx_rb != NULL)
+ ssm_rbuff_set_bits(f->rx_rb, RB_FLOWDOWN);
+
+ if (f->tx_rb != NULL)
+ ssm_rbuff_set_bits(f->tx_rb, RB_FLOWDOWN);
+}
+
+__attribute__((cold))
+static void frct_mark_peer_dead(struct frcti * frcti)
+{
+ struct flow * f = frcti_to_flow(frcti);
+
+ if (f->rx_rb != NULL)
+ ssm_rbuff_set_bits(f->rx_rb, RB_FLOWPEER);
+
+ if (proc.fqset != NULL)
+ ssm_flow_set_notify(proc.fqset, f->info.id, FLOW_PEER);
+}
+
+static __inline__ int frct_ctrl_alloc(struct ssm_pk_buff ** spb,
+ struct frct_pci ** pci,
+ size_t payload_len)
+{
+ if (frct_spb_reserve(FRCT_PCILEN + payload_len, spb) < 0)
+ return -1;
+
+ *pci = (struct frct_pci *) ssm_pk_buff_head(*spb);
+ memset(*pci, 0, FRCT_PCILEN);
+
+ return 0;
+}
+
+/*
+ * Advertised rwe. Stream mode clamps to lwe + ring_seq_cap so the
+ * byte-equivalent fits the rx ring. Caller holds at least the rdlock.
+ */
+static __inline__ uint32_t frcti_advert_rwe(struct frcti * frcti)
+{
+ uint32_t rwe;
+ uint32_t cap;
+
+ rwe = frcti->rcv_cr.rwe;
+
+ if (!frcti->stream)
+ return rwe;
+
+ cap = frcti->rcv_cr.lwe + frcti->ring_seq_cap;
+
+ return before(cap, rwe) ? cap : rwe;
+}
+
+static void frcti_pkt_snd(struct frcti * frcti,
+ uint16_t flags,
+ uint32_t ackno,
+ uint32_t rwe)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+
+ if (frct_ctrl_alloc(&spb, &pci, 0) < 0)
+ return;
+
+ pci->flags = hton16(flags);
+ pci->window = hton32(rwe);
+ pci->ackno = hton32(ackno);
+ if (flags & FRCT_ACK) {
+ /* reuse ackno for the sequence number of delayed ACK */
+ ackno = FETCH_ADD_RELAXED(&frcti->snd_cr.ackno, 1);
+ pci->seqno = hton32(ackno + 1);
+ }
+
+ frct_hcs_set(pci, false);
+
+ frct_tx(frcti, spb);
+}
+
+/* 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 floor;
+
+ if (rtt_hint > 0) {
+ rtt_hint = MAX(rtt_hint, (time_t) RTT_BOOT_NS);
+ frcti->srtt = rtt_hint;
+ 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;
+ } else {
+ /* Boot from first ACK. */
+ frcti->srtt = 0;
+ frcti->mdev = RTT_BOOT_NS;
+ frcti->rto = MAX((time_t) INITIAL_RTO, frcti->rto_min);
+ frcti->min_rtt = 0;
+ }
+
+ 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)
+{
+ if (frcti->min_rtt == 0)
+ return true;
+
+ if (mrtt < frcti->min_rtt)
+ return true;
+
+ return ts_aged_ns(now_ns, frcti->t_min_rtt, MIN_RTT_WIN_NS);
+}
+
+/* Linux-style windowed-min refresh of RACK.min_RTT. */
+static __inline__ void min_rtt_update(struct frcti * frcti,
+ time_t mrtt,
+ uint64_t now_ns)
+{
+ if (!min_rtt_stale(frcti, mrtt, now_ns))
+ return;
+
+ frcti->min_rtt = mrtt;
+ frcti->t_min_rtt = now_ns;
+}
+
+static void rtt_update(struct frcti * frcti,
+ time_t mrtt,
+ uint64_t now_ns)
+{
+ time_t srtt = frcti->srtt;
+ time_t rttvar = frcti->mdev;
+ time_t floor;
+ time_t rto;
+
+ if (srtt == 0) {
+ srtt = mrtt;
+ rttvar = mrtt >> 1;
+ } else {
+ /* RFC 6298 symmetric EWMA. */
+ time_t delta = mrtt - srtt;
+ srtt += (delta >> 3);
+ delta = (ABS(delta) - rttvar) >> 2;
+#ifdef FRCT_LINUX_RTT_ESTIMATOR
+ if (delta < 0)
+ delta >>= 3;
+#endif
+ rttvar += delta;
+ }
+ STAT_BUMP(frcti, rtt_smpl);
+ frcti->srtt = MAX(SRTT_FLOOR_NS, srtt);
+ frcti->mdev = MAX(MDEV_FLOOR_NS, rttvar);
+
+ min_rtt_update(frcti, mrtt, now_ns);
+
+ floor = MAX(frcti->rto_min, 2 * frcti->srtt);
+ rto = MAX(floor, frcti->srtt + (frcti->mdev << MDEV_MUL));
+
+ STORE_RELEASE(&frcti->rto, rto);
+ STORE_RELEASE(&frcti->rto_mul, 0);
+}
+
+/* Fill probes[pos], return new probe_id; 0 on entropy failure. Wrlock. */
+static uint32_t rttp_alloc_probe(struct frcti * frcti,
+ uint64_t now_ns,
+ uint8_t nonce[RTTP_NONCE_LEN])
+{
+ uint32_t probe_id;
+ size_t pos;
+
+ if (random_buffer(nonce, RTTP_NONCE_LEN) < 0)
+ return 0;
+
+ probe_id = frcti->probe_id_next++;
+ if (probe_id == 0)
+ probe_id = frcti->probe_id_next++;
+
+ pos = RTTP_POS(probe_id);
+ frcti->probes[pos].id = probe_id;
+ frcti->probes[pos].ts = now_ns;
+ memcpy(frcti->probes[pos].nonce, nonce, RTTP_NONCE_LEN);
+ frcti->t_snd_probe = now_ns;
+
+ STAT_BUMP(frcti, rttp_snd);
+
+ return probe_id;
+}
+
+/* Caller wrlock; out args valid on true (caller emits post-unlock). */
+static bool rtt_probe_arm(struct frcti * frcti,
+ uint64_t now_ns,
+ uint32_t * probe_id,
+ uint8_t nonce[RTTP_NONCE_LEN])
+{
+ if (frcti->srtt == 0)
+ return false;
+
+ if (!after(frcti->snd_cr.seqno, frcti->snd_cr.lwe))
+ return false;
+
+ if (!ts_aged_ns(now_ns, frcti->t_rcv_rtt,
+ 2u * (uint64_t) frcti->srtt))
+ return false;
+
+ if (!ts_aged_ns(now_ns, frcti->t_snd_probe,
+ (uint64_t) frcti->srtt))
+ return false;
+
+ *probe_id = rttp_alloc_probe(frcti, now_ns, nonce);
+
+ return *probe_id != 0;
+}
+
+static void frcti_rttp_snd(struct frcti * frcti,
+ uint32_t probe_id,
+ uint32_t echo_id,
+ const uint8_t * nonce)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+ struct frct_rttp * rttp;
+
+ if (frct_ctrl_alloc(&spb, &pci, RTTP_PAYLOAD) < 0)
+ return;
+
+ pci->flags = hton16(FRCT_RTTP);
+
+ frct_hcs_set(pci, false);
+
+ rttp = (struct frct_rttp *) FRCT_BODY(pci);
+ rttp->probe_id = hton32(probe_id);
+ rttp->echo_id = hton32(echo_id);
+ memcpy(rttp->nonce, nonce, sizeof(rttp->nonce));
+
+ frct_tx(frcti, spb);
+}
+
+struct rxm_entry {
+ struct tw_entry tw;
+ struct list_head next; /* in frcti->rxm_list */
+ struct frcti * frcti;
+ uint32_t seqno;
+ uint64_t t0;
+ size_t len;
+ uint8_t pkt[]; /* flexible — sized at alloc time */
+};
+
+static void rxm_entry_destroy(struct rxm_entry * r)
+{
+ free(r);
+}
+
+static bool rxm_still_owned(struct frcti * frcti,
+ size_t pos,
+ struct rxm_entry * r)
+{
+ return LOAD_ACQUIRE(&frcti->snd_slots[pos].rxm) == r;
+}
+
+/*
+ * 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.
+ */
+static uint64_t rxm_next_deadline(struct frcti * frcti,
+ uint64_t now_ns)
+{
+ time_t rto = LOAD_RELAXED(&frcti->rto);
+ uint8_t rto_mul = LOAD_RELAXED(&frcti->rto_mul);
+
+ return now_ns + ((uint64_t) rto << rto_mul);
+}
+
+/* Copy pkt, set FRCT_RXM, refresh ackno, re-seal HCS. */
+static struct ssm_pk_buff * rxm_pkt_prepare(const void * pkt,
+ size_t len,
+ uint32_t rcv_lwe,
+ bool stream)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+ uint16_t flags;
+
+ if (frct_spb_reserve(len, &spb) < 0)
+ return NULL;
+
+ pci = (struct frct_pci *) ssm_pk_buff_head(spb);
+ memcpy(pci, pkt, len);
+
+ flags = ntoh16(pci->flags) | FRCT_RXM;
+ pci->flags = hton16(flags);
+ pci->ackno = hton32(rcv_lwe);
+
+ frct_hcs_set(pci, stream);
+
+ return spb;
+}
+
+/* Caller must NOT hold frcti->lock. */
+static void rxm_snd(struct frcti * frcti,
+ uint32_t seqno,
+ const void * pkt,
+ size_t len)
+{
+ struct ssm_pk_buff * spb;
+ struct timespec now;
+ struct snd_slot * slot;
+ uint32_t snd_lwe;
+ uint32_t rcv_lwe;
+ size_t pos;
+ int ret;
+
+ snd_lwe = LOAD_RELAXED(&frcti->snd_cr.lwe);
+ rcv_lwe = LOAD_RELAXED(&frcti->rcv_cr.lwe);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ pos = RQ_SLOT(seqno);
+ slot = &frcti->snd_slots[pos];
+
+ slot->time = TS_TO_UINT64(now);
+ /* RTO supersedes any pending TLP/fast-rxm on this slot. */
+ slot->flags = (slot->flags & ~(SND_FAST_RXM | SND_TLP)) | SND_RTX;
+ /* §7.3: RTO supersedes TLP probes and ends the probe episode. */
+ frcti->tlp_high_seq = 0;
+ frcti->tlp_count = 0;
+
+ frcti->rtt_lwe = seqno + 1;
+
+ /* Only the HoL retransmit bumps the global RTO backoff. */
+ 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. */
+ if (seqno == snd_lwe)
+ frcti->reo_wnd_mult = 1;
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ STAT_BUMP(frcti, rxm_rto);
+
+ spb = rxm_pkt_prepare(pkt, len, rcv_lwe, frcti->stream);
+ if (spb == NULL)
+ return;
+
+ /* ETIMEDOUT/ENOMEM: let r-timer drive teardown. */
+ ret = frct_tx(frcti, spb);
+ if (ret == -EFLOWDOWN || ret == -ENOTALLOC)
+ STAT_BUMP(frcti, rxm_tx_dead);
+}
+
+static void rxm_due(void * arg)
+{
+ struct rxm_entry * r = arg;
+ struct frcti * frcti = r->frcti;
+ struct timespec now;
+ uint64_t now_ns;
+ uint32_t snd_lwe;
+ size_t pos = RQ_SLOT(r->seqno);
+
+ STAT_BUMP(frcti, rxm_due_count);
+
+ snd_lwe = LOAD_RELAXED(&frcti->snd_cr.lwe);
+
+ /* Already ACK'd: expected for the steady-state majority. */
+ if (before(r->seqno, snd_lwe)) {
+ STAT_BUMP(frcti, rxm_due_acked);
+ goto cleanup;
+ }
+
+ /* SACK/RACK-cleared the slot (caller NULL'd snd_slots[pos].rxm). */
+ if (!rxm_still_owned(frcti, pos, r)) {
+ STAT_BUMP(frcti, rxm_due_unowned);
+ goto cleanup;
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ /* R-timer expired: peer unreachable. */
+ if (RXM_AGED_OUT(r->t0, now_ns, frcti->t_r)) {
+ STAT_BUMP(frcti, rxm_due_aged);
+ frct_mark_flow_down(frcti);
+ goto cleanup;
+ }
+
+ /* 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);
+ return;
+ }
+
+ rxm_snd(frcti, r->seqno, r->pkt, r->len);
+
+ /* Re-check ownership: fire path may have replaced our entry. */
+ if (rxm_still_owned(frcti, pos, r)) {
+ uint64_t anchor;
+
+ /* Per-slot anchor breaks co-fire re-bin. */
+ anchor = frcti->snd_slots[pos].time;
+ tw_post(&r->tw, rxm_next_deadline(frcti, anchor), rxm_due, r);
+ return;
+ }
+
+ cleanup:
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ if (rxm_still_owned(frcti, pos, r))
+ STORE_RELEASE(&frcti->snd_slots[pos].rxm, NULL);
+
+ list_del(&r->next);
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ rxm_entry_destroy(r);
+}
+
+/* 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;
+
+ 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);
+
+ 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);
+}
+
+static void rxm_cancel_all(struct frcti * frcti)
+{
+ struct list_head * p;
+ struct list_head * t;
+
+ list_for_each_safe(p, t, &frcti->rxm_list) {
+ struct rxm_entry * r = list_entry(p, struct rxm_entry, next);
+ list_del(&r->next);
+ tw_cancel(&r->tw);
+ rxm_entry_destroy(r);
+ STAT_BUMP(frcti, rxm_cancel);
+ }
}
-static void send_frct_pkt(struct frcti * frcti)
+static __inline__ void sack_block_put(uint8_t * payload,
+ uint16_t i,
+ uint32_t s,
+ uint32_t e)
+{
+ uint32_t * blk = (uint32_t *)
+ (payload + SACK_HDR_SIZE + i * SACK_BLOCK_SIZE);
+
+ blk[0] = hton32(s);
+ blk[1] = hton32(e);
+}
+
+static __inline__ void sack_block_get(const uint8_t * payload,
+ uint16_t i,
+ uint32_t * s,
+ uint32_t * e)
+{
+ const uint32_t * blk = (const uint32_t *)
+ (payload + SACK_HDR_SIZE + i * SACK_BLOCK_SIZE);
+
+ *s = ntoh32(blk[0]);
+ *e = ntoh32(blk[1]);
+}
+
+/*
+ * Build SACK blocks for ranges *above* rcv_cr.lwe. Wire invariant
+ * (see doc/frct.txt §1.3): every block produced here satisfies
+ * blocks[i].start > rcv_cr.lwe = ackno, which makes the "first block
+ * below ackno" convention used to mark a D-SACK (RFC 2883 §4 case 1)
+ * unambiguous. Caller holds frcti->lock.
+ */
+static uint16_t sack_blocks_build(struct frcti * frcti,
+ uint32_t blocks[][2],
+ uint16_t max_n)
+{
+ const struct rcv_slot * slots = frcti->rcv_slots;
+ uint32_t s;
+ uint32_t end;
+ uint16_t n = 0;
+
+ s = frcti->rcv_cr.lwe + 1;
+ end = frcti->rcv_cr.lwe + RQ_SIZE;
+ if (after(end, frcti->rcv_cr.rwe))
+ end = frcti->rcv_cr.rwe;
+
+ while (before(s, end) && n < max_n) {
+ while (before(s, end) && slots[RQ_SLOT(s)].idx == -1)
+ ++s;
+
+ if (!before(s, end))
+ break;
+
+ blocks[n][0] = s;
+ while (before(s, end) && slots[RQ_SLOT(s)].idx != -1)
+ ++s;
+ blocks[n][1] = s;
+ ++n;
+ }
+
+ return n;
+}
+
+/*
+ * Prepend the pending D-SACK report (if any) as block[0]; clear flag.
+ * Returns the number of slots consumed at the head (0 or 1). Caller
+ * holds wrlock.
+ */
+static __inline__ uint16_t dsack_consume(struct frcti * frcti,
+ uint32_t blocks[][2])
+{
+ if (!frcti->dsack_valid || frcti->sack_n_max == 0)
+ return 0;
+
+ blocks[0][0] = frcti->dsack_seqno;
+ blocks[0][1] = frcti->dsack_seqno + 1;
+ frcti->dsack_valid = false;
+ return 1;
+}
+
+/* Caller must NOT hold frcti->lock. */
+static void frcti_sack_snd(struct frcti * frcti,
+ const struct sack_args * sa)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+ buffer_t buf;
+ uint16_t i;
+
+ assert(sa->n <= SACK_MAX_BLOCKS);
+
+ buf.len = SACK_HDR_SIZE + sa->n * SACK_BLOCK_SIZE;
+
+ if (frct_ctrl_alloc(&spb, &pci, buf.len) < 0)
+ return;
+
+ pci->flags = hton16(FRCT_ACK | FRCT_FC | FRCT_SACK);
+ pci->window = hton32(sa->rwe);
+ pci->ackno = hton32(sa->ack);
+ pci->seqno = hton32(FETCH_ADD_RELAXED(&frcti->snd_cr.ackno, 1) + 1);
+
+ frct_hcs_set(pci, false);
+
+ buf.data = FRCT_BODY(pci);
+ memset(buf.data, 0, SACK_HDR_SIZE);
+ *(uint16_t *) buf.data = hton16(sa->n);
+ 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);
+}
+
+static void ack_snd(struct frcti * frcti,
+ bool with_sack)
{
struct timespec now;
+ uint64_t now_ns;
time_t diff;
uint32_t ackno;
uint32_t rwe;
- int fd;
+ struct sack_args * sa = NULL;
+ size_t sa_sz;
+ bool sacking = false;
assert(frcti);
+ STAT_BUMP(frcti, ack_fire);
+
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ if (with_sack && frcti->sack_n_max > 0) {
+ sa_sz = sizeof(*sa) + frcti->sack_n_max * sizeof(sa->blocks[0]);
+ sa = malloc(sa_sz);
+ /* If alloc fails, fall through and send a bare cum-ACK. */
+ }
pthread_rwlock_wrlock(&frcti->lock);
- if (!after(frcti->rcv_cr.lwe, frcti->rcv_cr.seqno)) {
+ /* D-SACK rides through cum-ACK freshness; signal is the duplicate. */
+ if (!after(frcti->rcv_cr.lwe, frcti->rcv_cr.seqno)
+ && !frcti->dsack_valid) {
pthread_rwlock_unlock(&frcti->lock);
- return;
+ STAT_BUMP(frcti, ack_supp_seqno);
+ goto out;
}
- fd = frcti->fd;
ackno = frcti->rcv_cr.lwe;
- rwe = frcti->rcv_cr.rwe;
+ rwe = frcti_advert_rwe(frcti);
- diff = ts_diff_ns(&now, &frcti->rcv_cr.act);
- if (diff > frcti->a) {
+ if (ACK_AGED_OUT(frcti->rcv_cr.act, now_ns, frcti->t_a)) {
pthread_rwlock_unlock(&frcti->lock);
- return;
+ STAT_BUMP(frcti, ack_supp_inact);
+ goto out;
}
- diff = ts_diff_ns(&now, &frcti->snd_cr.act);
- if (diff < TICTIME) {
+ diff = (time_t) ts_age_ns(now_ns, frcti->snd_cr.act);
+ if (diff < TICTIME && !frcti->dsack_valid) {
pthread_rwlock_unlock(&frcti->lock);
- return;
+ STAT_BUMP(frcti, ack_supp_rate);
+ goto out;
}
+ /* RFC 2018: piggyback SACK on timer ACK; dedup unchanged board. */
+ if (sa == NULL || (frcti->sack_n == 0 && !frcti->dsack_valid))
+ goto no_sack;
+
+ sa->dsack = false;
+ sa->n = dsack_consume(frcti, sa->blocks);
+ if (sa->n == 1)
+ sa->dsack = true;
+
+ sa->n += sack_blocks_build(frcti, sa->blocks + sa->n,
+ frcti->sack_n_max - sa->n);
+ if (sa->n == 0)
+ goto no_sack;
+
+ if (!sa->dsack && ackno == frcti->sack_lwe && sa->n == frcti->sack_n)
+ goto no_sack;
+
+ sa->ack = ackno;
+ sa->rwe = rwe;
+ frcti->sack_lwe = ackno;
+ frcti->sack_n = sa->n;
+ frcti->t_snd_sack = now_ns;
+ sacking = true;
+
+ no_sack:
frcti->rcv_cr.seqno = frcti->rcv_cr.lwe;
pthread_rwlock_unlock(&frcti->lock);
- __send_frct_pkt(fd, FRCT_ACK | FRCT_FC, ackno, rwe);
+ STAT_BUMP(frcti, ack_snd);
+
+ if (sacking) {
+ STAT_BUMP(frcti, sack_snd);
+ if (sa->dsack)
+ STAT_BUMP(frcti, dsack_snd);
+ frcti_sack_snd(frcti, sa);
+ } else {
+ frcti_pkt_snd(frcti, FRCT_ACK | FRCT_FC, ackno, rwe);
+ }
+
+ out:
+ free(sa);
}
-static void __send_rdv(int fd)
+/* Delayed-ACK timer: per-flow, dedup'd via atomic test-and-set. */
+static void ack_due(void * arg)
{
- __send_frct_pkt(fd, FRCT_RDVS, 0, 0);
+ struct frcti * frcti = arg;
+
+ __atomic_clear(&frcti->ack_pending, __ATOMIC_RELAXED);
+
+ ack_snd(frcti, true);
}
-static struct frcti * frcti_create(int fd,
- time_t a,
- time_t r,
- time_t mpl)
+static int ack_arm(struct frcti * frcti)
{
- struct frcti * frcti;
- ssize_t idx;
- struct timespec now;
- pthread_condattr_t cattr;
+ struct timespec now;
+ uint64_t deadline;
+
+ if (__atomic_test_and_set(&frcti->ack_pending, __ATOMIC_RELAXED))
+ return 0;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ deadline = TS_TO_UINT64(now) + ACK_DELAY_NS;
+
+ tw_post(&frcti->ack_tw, deadline, ack_due, frcti);
+
+ return 0;
+}
+
+/* Forward decl breaks the keepalive cycle: ka_arm <-> ka_due. */
+static void ka_due(void * arg);
+
+static int ka_arm(struct frcti * frcti)
+{
+ struct timespec now;
+ uint64_t now_ns;
+ uint64_t timeo_ns;
+ uint64_t snd_ns;
+ uint64_t rcv_ns;
+ uint64_t deadline;
+
+ timeo_ns = (uint64_t) frcti->qs_timeout * MILLION; /* IMM */
+ snd_ns = LOAD_RELAXED(&frcti->snd_cr.act) + timeo_ns / 4;
+ rcv_ns = LOAD_RELAXED(&frcti->rcv_cr.act) + timeo_ns;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+ deadline = MIN(snd_ns, rcv_ns);
+ if (deadline <= now_ns)
+ deadline = now_ns + timeo_ns / 4;
+
+ tw_post(&frcti->ka_tw, deadline, ka_due, frcti);
+
+ return 0;
+}
+
+__attribute__((cold))
+static void ka_snd(struct frcti * frcti)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+ struct timespec now;
+ uint64_t now_ns;
+ time_t timeo_ns;
+ uint64_t rcv_act;
+ uint64_t ka_rcv;
+ int64_t rcv_idle;
+ int64_t snd_idle;
+ uint32_t ackno;
+
+ assert(frcti);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ timeo_ns = (time_t)(frcti->qs_timeout) * MILLION; /* IMM */
+ rcv_act = LOAD_RELAXED(&frcti->rcv_cr.act);
+ ka_rcv = LOAD_RELAXED(&frcti->t_ka_rcv);
+ rcv_idle = ts_age_ns(now_ns, rcv_act > ka_rcv ? rcv_act : ka_rcv);
+ snd_idle = ts_age_ns(now_ns, LOAD_RELAXED(&frcti->snd_cr.act));
+
+ if (rcv_idle > timeo_ns) {
+ frct_mark_peer_dead(frcti);
+ return;
+ }
+
+ if (snd_idle <= timeo_ns / 4) {
+ ka_arm(frcti);
+ return;
+ }
+
+ if (frct_ctrl_alloc(&spb, &pci, 0) < 0) {
+ ka_arm(frcti);
+ return;
+ }
+
+ ackno = LOAD_RELAXED(&frcti->rcv_cr.lwe);
+
+ pci->flags = hton16(FRCT_KA | FRCT_ACK);
+ pci->ackno = hton32(ackno);
+
+ frct_hcs_set(pci, false);
+
+ STAT_BUMP(frcti, ka_snd);
+ frct_tx(frcti, spb);
+
+ ka_arm(frcti);
+}
+
+/* Keepalive timer: re-posted by the fire callback itself. */
+static void ka_due(void * arg)
+{
+ ka_snd((struct frcti *) arg);
+}
+
+static void frcti_rdv_snd(struct frcti * frcti)
+{
+ frcti_pkt_snd(frcti, FRCT_RDVS, 0, 0);
+}
+
+#define HAS_RESCNTL(cr) ((cr)->cflags & FRCTFRESCNTL)
+static bool frcti_is_window_open(struct frcti * frcti)
+{
+ struct frct_cr * snd_cr = &frcti->snd_cr;
+ struct timespec now;
+ time_t diff;
+ bool ret = false;
+
+ if (!HAS_RESCNTL(snd_cr))
+ return true;
+
+ if (before(snd_cr->seqno, LOAD_RELAXED(&snd_cr->rwe)))
+ return true;
+
+ /* Window may be closed; wrlock for RDV state mutations. */
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ if (before(snd_cr->seqno, snd_cr->rwe)) {
+ ret = true;
+ goto unlock;
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ if (frcti->open) {
+ frcti->open = false;
+ frcti->t_wnd = now;
+ frcti->t_last_rdv = now;
+ goto unlock;
+ }
+
+ diff = ts_diff_ns(&now, &frcti->t_wnd);
+ if (diff > MAX_RDV)
+ goto unlock;
+
+ diff = ts_diff_ns(&now, &frcti->t_last_rdv);
+ if (diff > (time_t) frcti->t_rdv) {
+ frcti->t_last_rdv = now;
+ frcti_rdv_snd(frcti);
+ STAT_BUMP(frcti, rdv_snd);
+ }
+ unlock:
+ pthread_rwlock_unlock(&frcti->lock);
+
+ return ret;
+}
+
+/* n contiguous seqnos free? No RDV: the n=1 path drives it. */
+static bool frcti_is_window_open_n(struct frcti * frcti,
+ size_t n)
+{
+ struct frct_cr * snd_cr = &frcti->snd_cr;
+
+ if (!HAS_RESCNTL(snd_cr))
+ return true;
+
+ if (n <= 1)
+ return frcti_is_window_open(frcti);
+
+ return before(snd_cr->seqno + (uint32_t)(n - 1),
+ LOAD_RELAXED(&snd_cr->rwe));
+}
+
+static void release_rq(struct frcti * frcti)
+{
+ size_t i;
+
+ for (i = 0; i < RQ_SIZE; ++i) {
+ if (frcti->rcv_slots[i].idx == -1)
+ continue;
+
+ /* Stream rq entries are sentinels (no spb owned). */
+ if (!frcti->stream)
+ frct_spb_release_idx(frcti->rcv_slots[i].idx);
+
+ frcti->rcv_slots[i].idx = -1;
+ STAT_BUMP(frcti, rq_released);
+ }
+}
+
+static __inline__ bool stream_ring_sz_ok(struct frcti * frcti,
+ size_t n)
+{
+ size_t per_pkt;
+
+ if (n > FRCT_STREAM_RING_SZ_MAX)
+ return false;
+
+ if ((n & (n - 1)) != 0)
+ return false;
+
+ per_pkt = frcti->frag_mtu - frcti_data_hdr_len(frcti);
+
+ return n >= FRCT_STREAM_RING_MIN_PKTS * per_pkt;
+}
+
+/* Default ring sized for full RQ_SIZE seqno window; pow2, capped. */
+static size_t default_stream_ring_sz(size_t per_pkt)
+{
+ size_t need;
+ size_t sz;
+
+ need = (size_t) RQ_SIZE * per_pkt;
+ sz = FRCT_STREAM_RING_SZ;
+
+ while (sz < need && sz < FRCT_STREAM_RING_SZ_MAX)
+ sz <<= 1;
+
+ return sz;
+}
+
+struct frcti * frcti_create(int fd,
+ uint64_t a,
+ uint64_t r,
+ uint64_t mpl,
+ time_t rtt_hint,
+ qosspec_t qs,
+ uint32_t mtu)
+{
+ struct frcti * frcti;
+ ssize_t idx;
+ struct timespec now;
+ uint64_t now_ns;
+ size_t bb;
+ size_t per_pkt;
#ifdef PROC_FLOW_STATS
- char frctstr[FRCT_NAME_STRLEN + 1];
+ char frctstr[FRCT_NAME_STRLEN + 1];
#endif
- mpl *= MILLION;
- a *= BILLION;
- r *= BILLION;
+ mpl *= MILLION; /* ms -> ns */
+ a *= MILLION; /* ms -> ns */
+ r *= MILLION; /* ms -> ns */
frcti = malloc(sizeof(*frcti));
if (frcti == NULL)
@@ -349,56 +1824,77 @@ static struct frcti * frcti_create(int fd,
memset(frcti, 0, sizeof(*frcti));
+ list_head_init(&frcti->rxm_list);
+
if (pthread_rwlock_init(&frcti->lock, NULL))
goto fail_lock;
- if (pthread_mutex_init(&frcti->mtx, NULL))
- goto fail_mutex;
-
- if (pthread_condattr_init(&cattr))
- goto fail_cattr;
-#ifndef __APPLE__
- pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
-#endif
- if (pthread_cond_init(&frcti->cond, &cattr))
- goto fail_cond;
-
#ifdef PROC_FLOW_STATS
sprintf(frctstr, "%d", fd);
if (rib_reg(frctstr, &r_ops))
goto fail_rib_reg;
#endif
- pthread_condattr_destroy(&cattr);
for (idx = 0; idx < RQ_SIZE; ++idx)
- frcti->rq[idx] = -1;
+ frcti->rcv_slots[idx].idx = -1;
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ frcti->t_mpl = mpl;
+ frcti->t_a = a;
+ frcti->t_r = r;
+ frcti->t_rdv = DELT_RDV;
+ frcti->fd = fd;
+ frcti->ber = (time_t) qs.ber;
+ frcti->lossy = (qs.loss != 0);
+ frcti->qs_timeout = (time_t) qs.timeout;
+
+ frcti->frag_mtu = (size_t) mtu;
+
+ /* Cap blocks per SACK at what fits in the per-flow frag_mtu. */
+ bb = (frcti->frag_mtu - FRCT_PCILEN - SACK_HDR_SIZE)
+ / 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;
+
+ frcti->stream = (qs.service == SVC_STREAM);
+ if (frcti->stream) {
+ per_pkt = frcti->frag_mtu - frcti_data_hdr_len(frcti);
+ frcti->rcv_ring_sz = default_stream_ring_sz(per_pkt);
+ frcti->ring_seq_cap =
+ (uint32_t) (frcti->rcv_ring_sz / per_pkt);
+ }
- frcti->mpl = mpl;
- frcti->a = a;
- frcti->r = r;
- frcti->rdv = DELT_RDV;
- frcti->fd = fd;
-
-
- frcti->rttseq = 0;
- frcti->probe = false;
-
- frcti->srtt = 0; /* Updated on first ACK */
- frcti->mdev = 10 * MILLION; /* Updated on first ACK */
- frcti->rto = BILLION; /* Initial rxm will be after 1 s */
-#ifdef PROC_FLOW_STATS
- frcti->n_rtx = 0;
- frcti->n_prb = 0;
- frcti->n_rtt = 0;
- frcti->n_dup = 0;
- frcti->n_dak = 0;
- frcti->n_rdv = 0;
- frcti->n_out = 0;
- frcti->n_rqo = 0;
-#endif
- if (ai.flows[fd].info.qs.loss == 0) {
+ frcti->rto_min = (time_t) MAX(RTO_MIN, 1ULL << RXMQ_RES);
+ rtt_init(frcti, rtt_hint);
+ frcti->t_min_rtt = now_ns;
+ frcti->probe_id_next = 1;
+ frcti->t_rcv_rtt = now_ns;
+ frcti->t_snd_probe = now_ns;
+ frcti->t_snd_sack = 0;
+ frcti->sack_lwe = 0;
+ frcti->sack_n = 0;
+ frcti->dsack_seqno = 0;
+ frcti->dsack_valid = false;
+ frcti->reo_wnd_mult = 1;
+ frcti->dsack_lwe_snap = 0;
+ frcti->t_last_reo_widen = 0;
+ /* So the first pre-DRF NACK fires without waiting cooldown. */
+ frcti->t_nack = now_ns - BILLION;
+ frcti->in_recovery = false;
+ frcti->recovery_high = 0;
+ frcti->rack_fired_lwe = 0;
+
+ tw_init_entry(&frcti->ack_tw);
+ tw_init_entry(&frcti->ka_tw);
+ tw_init_entry(&frcti->tlp_tw);
+
+ if (!frcti->lossy) {
frcti->snd_cr.cflags |= FRCTFRTX | FRCTFLINGER;
frcti->rcv_cr.cflags |= FRCTFRTX;
}
@@ -406,24 +1902,31 @@ static struct frcti * frcti_create(int fd,
frcti->snd_cr.cflags |= FRCTFRESCNTL;
frcti->snd_cr.rwe = START_WINDOW;
+ if (frcti->lossy)
+ frcti->snd_cr.rwe = RQ_SIZE;
- frcti->snd_cr.inact = (3 * mpl + a + r) / BILLION + 1; /* s */
- frcti->snd_cr.act.tv_sec = now.tv_sec - (frcti->snd_cr.inact + 1);
+ frcti->snd_cr.inact = 3 * mpl + a + r + BILLION; /* ns */
+ frcti->snd_cr.act = now_ns - frcti->snd_cr.inact - BILLION;
- frcti->rcv_cr.inact = (2 * mpl + a + r) / BILLION + 1; /* s */
- frcti->rcv_cr.act.tv_sec = now.tv_sec - (frcti->rcv_cr.inact + 1);
+ frcti->rcv_cr.inact = 2 * mpl + a + r + BILLION; /* ns */
+ frcti->rcv_cr.act = now_ns - frcti->rcv_cr.inact - BILLION;
+
+ frcti->t_ka_rcv = now_ns;
+
+ /* qs_timeout == 0: no KA, silent peer crash goes undetected. */
+ if (frcti->qs_timeout > 0) {
+ if (ka_arm(frcti) < 0)
+ goto fail_ka_arm;
+ }
return frcti;
+ fail_ka_arm:
#ifdef PROC_FLOW_STATS
+ sprintf(frctstr, "%d", fd);
+ rib_unreg(frctstr);
fail_rib_reg:
- pthread_cond_destroy(&frcti->cond);
#endif
- fail_cond:
- pthread_condattr_destroy(&cattr);
- fail_cattr:
- pthread_mutex_destroy(&frcti->mtx);
- fail_mutex:
pthread_rwlock_destroy(&frcti->lock);
fail_lock:
free(frcti);
@@ -431,21 +1934,55 @@ static struct frcti * frcti_create(int fd,
return NULL;
}
-static void frcti_destroy(struct frcti * frcti)
+void frcti_destroy(struct frcti * frcti)
{
#ifdef PROC_FLOW_STATS
char frctstr[FRCT_NAME_STRLEN + 1];
+#endif
+ /* Drop every wheel entry referencing frcti before freeing it. */
+ rxm_cancel_all(frcti);
+ tw_cancel(&frcti->ack_tw);
+ tw_cancel(&frcti->ka_tw);
+ tw_cancel(&frcti->tlp_tw);
+
+#if defined(PROC_FLOW_STATS) && defined(FRCT_DEBUG_STDOUT)
+ 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_due=%zu acked=%zu unowned=%zu aged=%zu defer=%zu "
+ "cancel=%zu arm_fail=%zu inflight=%u "
+ "nack_snd=%zu nack_rcv=%zu inact_drop=%zu "
+ "drf_rebase=%zu rq_released=%zu\n",
+ (int) getpid(), frcti->fd,
+ frcti->stat.sdu_snd_frag, frcti->stat.sdu_reasm,
+ frcti->stat.sdu_sole,
+ 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_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_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.drf_rebase, frcti->stat.rq_released);
+#endif
+
+ release_rq(frcti);
+ free(frcti->rcv_ring);
+#ifdef PROC_FLOW_STATS
sprintf(frctstr, "%d", frcti->fd);
rib_unreg(frctstr);
#endif
- pthread_cond_destroy(&frcti->cond);
- pthread_mutex_destroy(&frcti->mtx);
pthread_rwlock_destroy(&frcti->lock);
free(frcti);
}
-static uint16_t frcti_getflags(struct frcti * frcti)
+uint16_t frcti_getflags(struct frcti * frcti)
{
uint16_t ret;
@@ -453,89 +1990,92 @@ static uint16_t frcti_getflags(struct frcti * frcti)
pthread_rwlock_rdlock(&frcti->lock);
- ret = frcti->snd_cr.cflags;
+ ret = frcti->snd_cr.cflags & FRCTFMASK;
pthread_rwlock_unlock(&frcti->lock);
return ret;
}
-static void frcti_setflags(struct frcti * frcti,
- uint16_t flags)
+void frcti_setflags(struct frcti * frcti,
+ uint16_t flags)
{
- flags |= FRCTFRTX; /* Should not be set by command */
-
assert(frcti);
- pthread_rwlock_wrlock(&frcti->lock);
+ flags &= FRCTFSETMASK;
- frcti->snd_cr.cflags &= FRCTFRTX; /* Zero other flags */
+ pthread_rwlock_wrlock(&frcti->lock);
- frcti->snd_cr.cflags &= flags;
+ frcti->snd_cr.cflags = (frcti->snd_cr.cflags & ~FRCTFSETMASK) | flags;
pthread_rwlock_unlock(&frcti->lock);
}
-#define frcti_queued_pdu(frcti) \
- (frcti == NULL ? idx : __frcti_queued_pdu(frcti))
+size_t frcti_get_max_rcv_sdu(struct frcti * frcti)
+{
+ size_t ret;
-#define frcti_snd(frcti, sdb) \
- (frcti == NULL ? 0 : __frcti_snd(frcti, sdb))
+ assert(frcti);
-#define frcti_rcv(frcti, sdb) \
- (frcti == NULL ? 0 : __frcti_rcv(frcti, sdb))
+ pthread_rwlock_rdlock(&frcti->lock);
+ ret = frcti->max_rcv_sdu;
+ pthread_rwlock_unlock(&frcti->lock);
-#define frcti_dealloc(frcti) \
- (frcti == NULL ? 0 : __frcti_dealloc(frcti))
+ return ret;
+}
-#define frcti_is_window_open(frcti) \
- (frcti == NULL ? true : __frcti_is_window_open(frcti))
+int frcti_set_max_rcv_sdu(struct frcti * frcti,
+ size_t max)
+{
+ assert(frcti);
-#define frcti_window_wait(frcti, abstime) \
- (frcti == NULL ? 0 : __frcti_window_wait(frcti, abstime))
+ if (max == 0)
+ return -EINVAL;
+ pthread_rwlock_wrlock(&frcti->lock);
+ frcti->max_rcv_sdu = max;
+ pthread_rwlock_unlock(&frcti->lock);
+
+ return 0;
+}
-static bool __frcti_is_window_open(struct frcti * frcti)
+size_t frcti_get_rcv_ring_sz(struct frcti * frcti)
{
- struct frct_cr * snd_cr = &frcti->snd_cr;
- bool ret = true;
+ size_t ret;
+
+ assert(frcti);
pthread_rwlock_rdlock(&frcti->lock);
+ ret = frcti->rcv_ring_sz;
+ pthread_rwlock_unlock(&frcti->lock);
- if (snd_cr->cflags & FRCTFRESCNTL)
- ret = before(snd_cr->seqno, snd_cr->rwe);
+ return ret;
+}
- if (!ret) {
- struct timespec now;
+/* Set before any stream byte has been delivered; -EBUSY otherwise. */
+int frcti_set_rcv_ring_sz(struct frcti * frcti,
+ size_t n)
+{
+ int ret = 0;
+ size_t per_pkt;
- clock_gettime(PTHREAD_COND_CLOCK, &now);
+ assert(frcti);
- pthread_mutex_lock(&frcti->mtx);
- if (frcti->open) {
- frcti->open = false;
- frcti->t_wnd = now;
- frcti->t_rdvs = now;
- } else {
- time_t diff;
- diff = ts_diff_ns(&now, &frcti->t_wnd);
- if (diff > MAX_RDV) {
- pthread_mutex_unlock(&frcti->mtx);
- pthread_rwlock_unlock(&frcti->lock);
- return false;
- }
-
- diff = ts_diff_ns(&now, &frcti->t_rdvs);
- if (diff > frcti->rdv) {
- frcti->t_rdvs = now;
- __send_rdv(frcti->fd);
-#ifdef PROC_FLOW_STATS
- frcti->n_rdv++;
-#endif
+ if (!frcti->stream)
+ return -ENOTSUP;
- }
- }
+ if (!stream_ring_sz_ok(frcti, n))
+ return -EINVAL;
+
+ per_pkt = frcti->frag_mtu - frcti_data_hdr_len(frcti);
+
+ pthread_rwlock_wrlock(&frcti->lock);
- pthread_mutex_unlock(&frcti->mtx);
+ if (frcti->rcv_ring != NULL) {
+ ret = -EBUSY;
+ } else {
+ frcti->rcv_ring_sz = n;
+ frcti->ring_seq_cap = (uint32_t) (n / per_pkt);
}
pthread_rwlock_unlock(&frcti->lock);
@@ -543,392 +2083,2138 @@ static bool __frcti_is_window_open(struct frcti * frcti)
return ret;
}
-static int __frcti_window_wait(struct frcti * frcti,
- struct timespec * abstime)
+time_t frcti_get_rto_min(struct frcti * frcti)
{
- struct frct_cr * snd_cr = &frcti->snd_cr;
- int ret = 0;
+ time_t v;
+
+ assert(frcti);
pthread_rwlock_rdlock(&frcti->lock);
+ v = frcti->rto_min;
+ pthread_rwlock_unlock(&frcti->lock);
- if (!(snd_cr->cflags & FRCTFRESCNTL)) {
- pthread_rwlock_unlock(&frcti->lock);
+ return v;
+}
+
+/* Floor at the timer-wheel resolution; finer granularity is unrepresentable. */
+int frcti_set_rto_min(struct frcti * frcti,
+ time_t rto_min)
+{
+ time_t floor = (time_t) (1ULL << RXMQ_RES);
+ time_t rto_floor;
+ time_t rto;
+
+ assert(frcti);
+
+ if (rto_min < floor)
+ return -EINVAL;
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ frcti->rto_min = rto_min;
+ if (frcti->srtt > 0) {
+ rto_floor = MAX(rto_min, 2 * frcti->srtt);
+ rto = MAX(rto_floor,
+ frcti->srtt + (frcti->mdev << MDEV_MUL));
+ STORE_RELEASE(&frcti->rto, rto);
+ } else if (frcti->rto < rto_min) {
+ STORE_RELEASE(&frcti->rto, rto_min);
+ }
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ return 0;
+}
+
+/* Re-arm a fresh rxm so a lost fast-retx still recovers via RTO. */
+static void sack_rxm_snd(struct frcti * frcti,
+ void * pkt,
+ size_t len)
+{
+ struct ssm_pk_buff * spb;
+ const struct frct_pci * pci;
+ struct rxm_entry * rxm;
+ uint32_t rcv_lwe;
+ uint32_t seqno;
+ int ret;
+
+ rcv_lwe = LOAD_RELAXED(&frcti->rcv_cr.lwe);
+
+ spb = rxm_pkt_prepare(pkt, len, rcv_lwe, frcti->stream);
+ if (spb == NULL)
+ return;
+
+ pci = (const struct frct_pci *) ssm_pk_buff_head(spb);
+ seqno = ntoh32(pci->seqno);
+
+ 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);
+ if (ret == -EFLOWDOWN || ret == -ENOTALLOC)
+ STAT_BUMP(frcti, rxm_tx_dead);
+}
+
+/* Additive HoL emit; original snd_slots[hp].rxm stays armed (NewReno). */
+static int fast_rxm_send(struct frcti * frcti,
+ void * pkt,
+ size_t len)
+{
+ struct ssm_pk_buff * spb;
+ uint32_t rcv_lwe;
+
+ rcv_lwe = LOAD_RELAXED(&frcti->rcv_cr.lwe);
+
+ spb = rxm_pkt_prepare(pkt, len, rcv_lwe, frcti->stream);
+ if (spb == NULL)
return 0;
+
+ return frct_tx(frcti, spb);
+}
+
+/* PCI bytes survive head_release at receive; just rewind the pointer. */
+static __inline__ uint16_t frag_role_peek(struct ssm_pk_buff * spb)
+{
+ const struct frct_pci * pci;
+
+ assert(ssm_pk_buff_head(spb) != NULL);
+
+ pci = (const struct frct_pci *) (ssm_pk_buff_head(spb) - FRCT_PCILEN);
+
+ return ntoh16(pci->flags) & FRCT_FR_MASK;
+}
+
+enum frag_state {
+ FRAG_NOT_READY, /* head missing / FIRST..LAST run incomplete */
+ FRAG_DELIVER, /* *count fragments form a deliverable SDU */
+ FRAG_DROP, /* *count fragments at lwe are malformed */
+};
+
+/*
+ * On a gap in the run: FRTX waits (NOT_READY); best-effort scans forward
+ * for the next FIRST/SOLE and returns DROP for the broken prefix. *count
+ * gets the offset from the trailing edge. NOT_READY if no later run is
+ * in window. Caller rdlock.
+ */
+static enum frag_state frag_inspect_gap(struct frcti * frcti,
+ size_t start,
+ size_t * count)
+{
+ const struct rcv_slot * slots = frcti->rcv_slots;
+ struct ssm_pk_buff * spb;
+ uint32_t k;
+ uint16_t role;
+ size_t m;
+
+ if (frcti->rcv_cr.cflags & FRCTFRTX)
+ return FRAG_NOT_READY;
+
+ k = frcti->rcv_cr.rwe - RQ_SIZE;
+
+ for (m = start; m < RQ_SIZE; ++m) {
+ if (slots[RQ_SLOT(k + m)].idx == -1)
+ continue;
+
+ spb = rq_frag(frcti, k + m);
+ role = frag_role_peek(spb);
+
+ if (role == FRCT_FR_SOLE || role == FRCT_FR_FIRST) {
+ if (m == 0)
+ return FRAG_NOT_READY;
+
+ *count = m;
+ return FRAG_DROP;
+ }
}
- while (snd_cr->seqno == snd_cr->rwe && ret != -ETIMEDOUT) {
- struct timespec now;
- pthread_rwlock_unlock(&frcti->lock);
- pthread_mutex_lock(&frcti->mtx);
+ return FRAG_NOT_READY;
+}
- if (frcti->open) {
- clock_gettime(PTHREAD_COND_CLOCK, &now);
+/*
+ * Inspect rq[lwe..]; set *count and return DELIVER/DROP/NOT_READY. DROP
+ * covers broken prefixes (mid/last at HoL, FIRST..[non-LAST]..new-FIRST).
+ * Non-FRTX flows skip past gaps to the next FIRST/SOLE. Caller rdlock.
+ */
+static enum frag_state frag_run_inspect(struct frcti * frcti,
+ size_t * count)
+{
+ const struct rcv_slot * slots = frcti->rcv_slots;
+ struct ssm_pk_buff * spb;
+ uint32_t k = frcti->rcv_cr.rwe - RQ_SIZE;
+ uint16_t role;
+ size_t n = 0;
+
+ if (slots[RQ_SLOT(k)].idx == -1)
+ return frag_inspect_gap(frcti, 0, count);
+
+ spb = rq_frag(frcti, k);
+ role = frag_role_peek(spb);
+
+ if (role == FRCT_FR_SOLE) {
+ *count = 1;
+ return FRAG_DELIVER;
+ }
+
+ if (role != FRCT_FR_FIRST) {
+ *count = 1;
+ return FRAG_DROP;
+ }
+
+ while (true) {
+ if (n == RQ_SIZE || slots[RQ_SLOT(k + n)].idx == -1)
+ return frag_inspect_gap(frcti, n, count);
+
+ spb = rq_frag(frcti, k + n);
+ role = frag_role_peek(spb);
+ ++n;
- frcti->t_wnd = now;
- frcti->t_rdvs = now;
- frcti->open = false;
+ if (role == FRCT_FR_LAST) {
+ *count = n;
+ return FRAG_DELIVER;
}
- pthread_cleanup_push(__cleanup_mutex_unlock, &frcti->mtx);
+ if (n > 1 && role != FRCT_FR_MID) {
+ /* SOLE or new FIRST mid-run: drop the prefix. */
+ *count = n - 1;
+ return FRAG_DROP;
+ }
+ }
+}
+
+/* Caller wrlock. Delivery edge is implicit: rwe - RQ_SIZE. */
+static void frag_drop(struct frcti * frcti,
+ size_t count)
+{
+ uint32_t k = frcti->rcv_cr.rwe - RQ_SIZE;
+ uint32_t edge;
+ size_t i;
- ret = -__timedwait(&frcti->cond, &frcti->mtx, abstime);
+ for (i = 0; i < count; ++i) {
+ size_t pos = RQ_SLOT(k + i);
- pthread_cleanup_pop(false);
+ if (frcti->rcv_slots[pos].idx == -1)
+ continue;
- if (ret == -ETIMEDOUT) {
- time_t diff;
+ frct_spb_release_idx(frcti->rcv_slots[pos].idx);
+ frcti->rcv_slots[pos].idx = -1;
+ }
+
+ frcti->rcv_cr.rwe += count;
- clock_gettime(PTHREAD_COND_CLOCK, &now);
+ /* Drop may span a gap; pull lwe up to preserve rwe - RQ_SIZE <= lwe. */
+ edge = frcti->rcv_cr.rwe - RQ_SIZE;
+ if (before(frcti->rcv_cr.lwe, edge))
+ STORE_RELEASE(&frcti->rcv_cr.lwe, edge);
+}
- diff = ts_diff_ns(&now, &frcti->t_wnd);
- if (diff > MAX_RDV) {
- pthread_mutex_unlock(&frcti->mtx);
- return -ECONNRESET; /* write fails! */
- }
+/* Copy `count` fragments at rq[lwe..] into buf; release + advance lwe. */
+static size_t frag_gather(struct frcti * frcti,
+ size_t count,
+ uint8_t * buf)
+{
+ struct ssm_pk_buff * frag;
+ size_t off = 0;
+ size_t i;
+ uint32_t k = frcti->rcv_cr.rwe - RQ_SIZE;
+
+ for (i = 0; i < count; ++i) {
+ size_t pos = RQ_SLOT(k + i);
+ size_t flen;
+
+ frag = rq_frag(frcti, k + i);
+ flen = ssm_pk_buff_len(frag);
+ memcpy(buf + off, ssm_pk_buff_head(frag), flen);
+ off += flen;
+ frct_spb_release_idx(frcti->rcv_slots[pos].idx);
+ frcti->rcv_slots[pos].idx = -1;
+ }
- diff = ts_diff_ns(&now, &frcti->t_rdvs);
- if (diff > frcti->rdv) {
- frcti->t_rdvs = now;
- __send_rdv(frcti->fd);
- }
+ frcti->rcv_cr.rwe += count;
+
+ return off;
+}
+
+/* Caller holds lock. */
+static size_t frag_total_len(struct frcti * frcti,
+ size_t count,
+ bool * overflow)
+{
+ struct ssm_pk_buff * frag;
+ size_t total = 0;
+ size_t i;
+ uint32_t k = frcti->rcv_cr.rwe - RQ_SIZE;
+
+ *overflow = false;
+
+ for (i = 0; i < count; ++i) {
+ size_t flen;
+
+ frag = rq_frag(frcti, k + i);
+ flen = ssm_pk_buff_len(frag);
+ if (total + flen < total) {
+ *overflow = true;
+ return 0;
}
+ total += flen;
+ }
+
+ return total;
+}
+
+/*
+ * Process a delivered slot at lwe: latch FIN if acceptable,
+ * advance byte_high (clamped to byte_fin once latched).
+ */
+static __inline__ void stream_deliver_slot(struct frcti * frcti,
+ size_t lp)
+{
+ uint32_t end;
+
+ end = frcti->rcv_slots[lp].end;
+
+ if (frcti->rcv_slots[lp].fin) {
+ if (end == frcti->rcv_byte_high && !frcti->rcv_fin_seen) {
+ frcti->rcv_fin_seen = true;
+ frcti->rcv_byte_fin = end;
+ } else {
+ STAT_BUMP(frcti, strm_fin_drop);
+ }
+ }
+
+ if (frcti->rcv_fin_seen && after(end, frcti->rcv_byte_fin))
+ end = frcti->rcv_byte_fin;
+
+ frcti->rcv_byte_high = end;
+}
+
+/* Two-segment memcpy from buf into the rx ring at byte offset start. */
+static void stream_ring_write(struct frcti * frcti,
+ uint32_t start,
+ buffer_t buf)
+{
+ size_t mask = frcti->rcv_ring_sz - 1;
+ size_t off = start & mask;
+
+ if (off + buf.len <= frcti->rcv_ring_sz) {
+ memcpy(frcti->rcv_ring + off, buf.data, buf.len);
+ } else {
+ size_t first = frcti->rcv_ring_sz - off;
+ memcpy(frcti->rcv_ring + off, buf.data, first);
+ memcpy(frcti->rcv_ring, buf.data + first, buf.len - first);
+ }
+}
+
+/* Two-segment memcpy from the rx ring at byte offset start into buf. */
+static void stream_ring_read(struct frcti * frcti,
+ uint32_t start,
+ buffer_t buf)
+{
+ size_t mask = frcti->rcv_ring_sz - 1;
+ size_t off = start & mask;
+
+ if (off + buf.len <= frcti->rcv_ring_sz) {
+ memcpy(buf.data, frcti->rcv_ring + off, buf.len);
+ } else {
+ size_t first = frcti->rcv_ring_sz - off;
+ memcpy(buf.data, frcti->rcv_ring + off, first);
+ memcpy(buf.data + first, frcti->rcv_ring, buf.len - first);
+ }
+}
+
+/* Deliver-or-drop one stashed slot at lwe; advance lwe/rwe. Caller wrlock. */
+static void stream_advance_lwe(struct frcti * frcti)
+{
+ size_t lp;
+
+ lp = RQ_SLOT(frcti->rcv_cr.lwe);
+
+ if (frcti->rcv_slots[lp].start != frcti->rcv_byte_high)
+ STAT_BUMP(frcti, strm_drop);
+ else
+ stream_deliver_slot(frcti, lp);
+
+ frcti->rcv_slots[lp].fin = 0;
+ frcti->rcv_slots[lp].idx = -1;
+ STORE_RELEASE(&frcti->rcv_cr.lwe, frcti->rcv_cr.lwe + 1);
+ frcti->rcv_cr.rwe++;
+}
+
+/*
+ * Validate a stream DATA packet before stashing. Returns 0 if the
+ * packet may be written into rcv_ring + rq[], -1 otherwise.
+ */
+static __inline__ int stream_stash_check(struct frcti * frcti,
+ uint32_t start,
+ uint32_t end,
+ size_t plen,
+ uint16_t flags)
+{
+ if (end - start != (uint32_t) plen)
+ return -1;
+
+ /* FIN MUST be 0-byte. */
+ if ((flags & FRCT_FIN) && plen != 0)
+ return -1;
+
+ /* Post-EOS: no further FIN once latched. */
+ if (frcti->rcv_fin_seen && (flags & FRCT_FIN))
+ return -1;
+
+ /* Post-EOS: reject data at or past byte_fin. */
+ if (frcti->rcv_fin_seen && !before(start, frcti->rcv_byte_fin))
+ return -1;
+
+ /* Stale: peer is behind the delivered edge. */
+ if (before(end, frcti->rcv_byte_next))
+ return -1;
+
+ /* Exact-edge: only an empty-stream FIN is meaningful. */
+ if (end == frcti->rcv_byte_next && !(flags & FRCT_FIN))
+ return -1;
+
+ if (end - frcti->rcv_byte_next > frcti->rcv_ring_sz)
+ return -1;
+
+ return 0;
+}
+
+/*
+ * Stream-mode DATA receive: validate, stash payload in rcv_ring, mark
+ * rq[pos], advance lwe through any newly-contiguous run. Returns 0
+ * (spb released) or -1 (caller releases). Caller wrlock.
+ */
+static int frcti_stream_data_rcv(struct frcti * frcti,
+ struct ssm_pk_buff * spb,
+ size_t pos,
+ uint16_t flags)
+{
+ struct frct_pci_stream * spci;
+ uint32_t start;
+ uint32_t end;
+ buffer_t buf;
+ size_t skip;
+
+ if (ssm_pk_buff_len(spb) < FRCT_PCI_STREAM_LEN)
+ return -1;
+
+ if (frcti->rcv_ring == NULL) {
+ frcti->rcv_ring = calloc(1, frcti->rcv_ring_sz);
+ if (frcti->rcv_ring == NULL)
+ return -ENOMEM;
+ }
+
+ spci = FRCT_HDR_POP(spb, frct_pci_stream);
+ start = ntoh32(spci->start);
+ end = ntoh32(spci->end);
+
+ buf.data = ssm_pk_buff_head(spb);
+ buf.len = ssm_pk_buff_len(spb);
- pthread_mutex_unlock(&frcti->mtx);
- pthread_rwlock_rdlock(&frcti->lock);
+ if (stream_stash_check(frcti, start, end, buf.len, flags) < 0)
+ return -1;
+
+ /* Trim front-overlap with already-delivered region. */
+ if (before(start, frcti->rcv_byte_next)) {
+ skip = frcti->rcv_byte_next - start;
+ buf.data += skip;
+ buf.len -= skip;
+ start = frcti->rcv_byte_next;
}
+ stream_ring_write(frcti, start, buf);
+ STAT_ADD(frcti, strm_rcv_byte, buf.len);
+
+ frcti->rcv_slots[pos].idx = 1;
+ frcti->rcv_slots[pos].start = start;
+ frcti->rcv_slots[pos].end = end;
+ frcti->rcv_slots[pos].fin = (flags & FRCT_FIN) ? 1 : 0;
+
+ while (frcti->rcv_slots[RQ_SLOT(frcti->rcv_cr.lwe)].idx != -1)
+ stream_advance_lwe(frcti);
+
+ frct_spb_release(spb);
+
+ return 0;
+}
+
+/*
+ * DATA receive: stash idx at rq[pos], advance lwe through any
+ * contiguous run. Caller wrlock.
+ */
+static void frcti_data_stash(struct frcti * frcti,
+ ssize_t idx,
+ size_t pos,
+ uint16_t flags)
+{
+ frcti->rcv_slots[pos].idx = idx;
+
+ if ((flags & FRCT_FR_MASK) != FRCT_FR_SOLE)
+ STAT_BUMP(frcti, frag_rcv);
+
+ /* lwe = cum-ACK edge; advance per fragment through contiguous run. */
+ while (before(frcti->rcv_cr.lwe, frcti->rcv_cr.rwe)
+ && frcti->rcv_slots[RQ_SLOT(frcti->rcv_cr.lwe)].idx != -1)
+ STORE_RELEASE(&frcti->rcv_cr.lwe, frcti->rcv_cr.lwe + 1);
+}
+
+/* Stream consume: copy up to `count` contiguous bytes from ring into buf. */
+static ssize_t frcti_consume_stream(struct frcti * frcti,
+ uint8_t * buf,
+ size_t count)
+{
+ size_t avail;
+ size_t copy;
+ ssize_t ret;
+ buffer_t dst;
+
+ assert(frcti);
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ avail = (size_t) (frcti->rcv_byte_high - frcti->rcv_byte_next);
+ if (avail == 0) {
+ /* EOS drained: signal EOF to the reader. */
+ if (frcti->rcv_fin_seen
+ && frcti->rcv_byte_next == frcti->rcv_byte_fin)
+ ret = 0;
+ else
+ ret = -EAGAIN;
+ goto unlock;
+ }
+
+ copy = MIN(avail, count);
+
+ dst.data = buf;
+ dst.len = copy;
+ stream_ring_read(frcti, frcti->rcv_byte_next, dst);
+
+ frcti->rcv_byte_next += (uint32_t) copy;
+ STAT_ADD(frcti, strm_dlv_byte, copy);
+
+ ret = (ssize_t) copy;
+
+ unlock:
pthread_rwlock_unlock(&frcti->lock);
return ret;
}
-static ssize_t __frcti_queued_pdu(struct frcti * frcti)
+/*
+ * FRTX consume: copy next ready PDU (full SDU or nothing). Returns bytes,
+ * -EAGAIN (no PDU), or -EMSGSIZE (oversize: run dropped to unblock flow).
+ */
+static ssize_t frcti_consume(struct frcti * frcti,
+ uint8_t * buf,
+ size_t count)
{
- ssize_t idx;
- size_t pos;
+ size_t n;
+ size_t total;
+ bool overflow;
+ enum frag_state st;
+ ssize_t ret;
assert(frcti);
- /* See if we already have the next PDU. */
pthread_rwlock_wrlock(&frcti->lock);
- pos = frcti->rcv_cr.lwe & (RQ_SIZE - 1);
-
- idx = frcti->rq[pos];
- if (idx != -1) {
- ++frcti->rcv_cr.lwe;
- ++frcti->rcv_cr.rwe;
- frcti->rq[pos] = -1;
+ while (true) {
+ st = frag_run_inspect(frcti, &n);
+ if (st == FRAG_NOT_READY) {
+ ret = -EAGAIN;
+ goto unlock;
+ }
+ if (st == FRAG_DROP) {
+ 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);
+ frag_drop(frcti, n);
+ ret = -EMSGSIZE;
+ goto unlock;
+ }
+ ret = (ssize_t) frag_gather(frcti, n, buf);
+ if (n > 1)
+ STAT_BUMP(frcti, sdu_reasm);
+ else
+ STAT_BUMP(frcti, sdu_sole);
+ goto unlock;
}
+ unlock:
pthread_rwlock_unlock(&frcti->lock);
- return idx;
+ return ret;
}
-static ssize_t __frcti_pdu_ready(struct frcti * frcti)
+static bool frcti_pdu_ready(struct frcti * frcti)
{
- ssize_t idx;
- size_t pos;
+ size_t pos;
+ size_t count;
+ bool ready;
assert(frcti);
- /* See if we already have the next PDU. */
pthread_rwlock_rdlock(&frcti->lock);
- pos = frcti->rcv_cr.lwe & (RQ_SIZE - 1);
- idx = frcti->rq[pos];
+ if (frcti->stream) {
+ ready = frcti->rcv_byte_high != frcti->rcv_byte_next;
+ pthread_rwlock_unlock(&frcti->lock);
+ return ready;
+ }
+
+ if (frag_run_inspect(frcti, &count) != FRAG_DELIVER) {
+ /* Drop case: frcti_consume will handle it; not ready. */
+ pthread_rwlock_unlock(&frcti->lock);
+ return false;
+ }
+
+ pos = RQ_SLOT(frcti->rcv_cr.rwe - RQ_SIZE);
+ ready = frcti->rcv_slots[pos].idx != -1;
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ return ready;
+}
+
+/* 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)
+
+/* Have srtt: probe when peer quiet for > 2*srtt and last probe > srtt. */
+#define PROBE_DUE_WARM(frcti, now_ns) \
+ ((now_ns) - (frcti)->t_rcv_rtt > 2u * (uint64_t)(frcti)->srtt \
+ && (now_ns) - (frcti)->t_snd_probe > (uint64_t)(frcti)->srtt)
+
+/* Seeds srtt for receive-only sides so they don't fall back to 1 s RTO. */
+__attribute__((cold))
+static void frcti_rcv_probe(struct frcti * frcti,
+ uint64_t now_ns)
+{
+ uint32_t probe_id;
+ uint8_t nonce[RTTP_NONCE_LEN] = { 0 };
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ if (frcti->srtt == 0 && !PROBE_DUE_COLD(frcti, now_ns)) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+
+ if (frcti->srtt != 0 && !PROBE_DUE_WARM(frcti, now_ns)) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+
+ probe_id = rttp_alloc_probe(frcti, now_ns, nonce);
pthread_rwlock_unlock(&frcti->lock);
- return idx;
+ if (probe_id != 0)
+ frcti_rttp_snd(frcti, probe_id, 0, nonce);
}
-#include <timerwheel.c>
+/* Echo at slot `pos` matches our probe: id, slot, nonce all intact. */
+static __inline__ bool probe_echo_matches(struct frcti * frcti,
+ size_t pos,
+ uint32_t echo_id,
+ const uint8_t nonce[RTTP_NONCE_LEN])
+{
+ if (frcti->probes[pos].id != echo_id)
+ return false;
+
+ if (frcti->probes[pos].ts == 0)
+ return false;
+
+ return memcmp(frcti->probes[pos].nonce, nonce, RTTP_NONCE_LEN) == 0;
+}
/*
- * Send a final ACK for everything that has not been ACK'd.
- * If the flow should be kept active for retransmission,
- * the returned time will be negative.
+ * RTT probe (echo_id == 0): bounce the nonce back to peer.
+ * RTT echo (echo_id != 0): verify nonce + feed sample.
*/
-static time_t __frcti_dealloc(struct frcti * frcti)
+static void frcti_rttp_rcv(struct frcti * frcti,
+ buffer_t pkt,
+ uint64_t now_ns)
{
- struct timespec now;
- time_t wait;
- int ackno;
- int fd = -1;
+ const struct frct_rttp * rttp;
+ uint32_t probe_id;
+ uint32_t echo_id;
+ uint8_t nonce[RTTP_NONCE_LEN];
+ size_t ring_pos;
+ int64_t elapsed;
+ uint64_t sample;
+
+ if (pkt.len < RTTP_PAYLOAD)
+ return;
+
+ rttp = (const struct frct_rttp *) pkt.data;
+ probe_id = ntoh32(rttp->probe_id);
+ echo_id = ntoh32(rttp->echo_id);
+
+ /* Forged/malformed: bouncing this would loop on echo_id == 0. */
+ if (probe_id == 0 && echo_id == 0)
+ return;
+
+ memcpy(nonce, rttp->nonce, sizeof(nonce));
+
+ if (echo_id == 0) {
+ /* Probe: echo back with same nonce so peer can verify. */
+ STAT_BUMP(frcti, rttp_rcv);
+ frcti_rttp_snd(frcti, 0, probe_id, nonce);
+ return;
+ }
+
+ ring_pos = RTTP_POS(echo_id);
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ if (!probe_echo_matches(frcti, ring_pos, echo_id, nonce)) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+
+ elapsed = ts_age_ns(now_ns, frcti->probes[ring_pos].ts);
+ frcti->probes[ring_pos].ts = 0;
+ frcti->t_rcv_rtt = now_ns;
+
+ if (elapsed <= 0) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+ sample = (uint64_t) elapsed;
+
+ /* Clamp probe sample to RTT_CLAMP_MUL * srtt to avoid poisoning. */
+ if (frcti->srtt > 0)
+ sample = MIN(sample, (uint64_t) frcti->srtt * RTT_CLAMP_MUL);
+
+ rtt_update(frcti, sample, now_ns);
+
+ pthread_rwlock_unlock(&frcti->lock);
+}
+
+/* Honours piggybacked ACK on the KA. */
+static void frcti_ka_rcv(struct frcti * frcti,
+ const struct frct_pci * pci,
+ uint64_t now_ns,
+ uint16_t flags)
+{
+ uint32_t ka_ackno;
+
+ STORE_RELEASE(&frcti->t_ka_rcv, now_ns);
+ STAT_BUMP(frcti, ka_rcv);
+
+ if (!(flags & FRCT_ACK))
+ return;
+
+ ka_ackno = ntoh32(pci->ackno);
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ if (within(ka_ackno, frcti->snd_cr.lwe, frcti->snd_cr.seqno))
+ STORE_RELEASE(&frcti->snd_cr.lwe, ka_ackno);
+
+ pthread_rwlock_unlock(&frcti->lock);
+}
+
+/*
+ * Additive HoL re-emit (carries DRF); runs before rcv_cr->act
+ * refresh so it doesn't pre-empt peer's first DRF.
+ */
+__attribute__((cold))
+static void frcti_nack_rcv(struct frcti * frcti)
+{
+ struct timespec now;
+ uint64_t now_ns;
+ size_t hp;
+ struct rxm_entry * rxm;
+ void * pkt_copy = NULL;
+ size_t pkt_len = 0;
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ STAT_BUMP(frcti, nack_rcv);
+
+ if (frcti->snd_cr.seqno == frcti->snd_cr.lwe) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+
+ hp = RQ_SLOT(frcti->snd_cr.lwe);
+ rxm = LOAD_ACQUIRE(&frcti->snd_slots[hp].rxm);
+ if (rxm == NULL || RXM_AGED_OUT(rxm->t0, now_ns, frcti->t_r)) {
+ pthread_rwlock_unlock(&frcti->lock);
+ return;
+ }
+
+ pkt_copy = malloc(rxm->len);
+ if (pkt_copy != NULL) {
+ memcpy(pkt_copy, rxm->pkt, rxm->len);
+ pkt_len = rxm->len;
+ /* Karn: suppress RTT sample. NACK supersedes pending TLP. */
+ frcti->snd_slots[hp].flags =
+ (frcti->snd_slots[hp].flags & ~SND_TLP)
+ | SND_RTX | SND_FAST_RXM;
+ frcti->rtt_lwe = frcti->snd_cr.lwe + 1;
+ STAT_BUMP(frcti, rxm_nack);
+ }
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ if (pkt_copy != NULL) {
+ int ret = fast_rxm_send(frcti, pkt_copy, pkt_len);
+ if (ret == -EFLOWDOWN || ret == -ENOTALLOC)
+ STAT_BUMP(frcti, rxm_tx_dead);
+ free(pkt_copy);
+ }
+}
+
+__attribute__((cold))
+static void frcti_rdv_rcv(struct frcti * frcti)
+{
+ uint32_t rwe;
pthread_rwlock_rdlock(&frcti->lock);
- ackno = frcti->rcv_cr.lwe;
- if (frcti->rcv_cr.lwe != frcti->rcv_cr.seqno)
- fd = frcti->fd;
+ rwe = frcti_advert_rwe(frcti);
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ STAT_BUMP(frcti, rdv_rcv);
+
+ frcti_pkt_snd(frcti, FRCT_FC, 0, rwe);
+}
+
+/* §7.2: PTO = 2*SRTT + max delayed-ACK delay; fallback when unseeded. */
+static __inline__ uint64_t tlp_pto(const struct frcti * frcti)
+{
+ if (frcti->srtt > 0)
+ return 2ULL * (uint64_t) frcti->srtt + ACK_DELAY_NS;
+
+ return NACK_COOLDOWN_NS;
+}
+
+/*
+ * RFC 8985 §7: lazy probe. Re-evaluate on fire — if sender was active
+ * within PTO, re-post; else probe HoL once and hand off to RTO.
+ */
+__attribute__((cold))
+static void tlp_due(void * arg)
+{
+ struct frcti * frcti = arg;
+ struct timespec now;
+ uint64_t now_ns;
+ uint64_t pto;
+ uint64_t rto_at;
+ size_t hp;
+ struct rxm_entry * rxm;
+ void * pkt_copy = NULL;
+ size_t pkt_len = 0;
+ bool re_post = false;
+ uint64_t deadline = 0;
- wait = MAX(frcti->rcv_cr.inact - now.tv_sec + frcti->rcv_cr.act.tv_sec,
- frcti->snd_cr.inact - now.tv_sec + frcti->snd_cr.act.tv_sec);
- wait = MAX(wait, 0);
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ pthread_rwlock_wrlock(&frcti->lock);
- if (frcti->snd_cr.cflags & FRCTFLINGER
- && before(frcti->snd_cr.lwe, frcti->snd_cr.seqno))
- wait = -wait;
+ 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;
+
+ if (frcti->tlp_count >= MAX_TLP_PER_EP)
+ goto unlock;
+
+ pto = tlp_pto(frcti);
+
+ /* §7.2: anchor PTO on most recent send; defer if still active. */
+ if (now_ns < frcti->snd_cr.act + pto) {
+ deadline = frcti->snd_cr.act + pto;
+ re_post = true;
+ goto unlock;
+ }
+
+ hp = RQ_SLOT(frcti->snd_cr.lwe);
+ rxm = LOAD_ACQUIRE(&frcti->snd_slots[hp].rxm);
+ if (rxm == NULL || RXM_AGED_OUT(rxm->t0, now_ns, frcti->t_r))
+ 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));
+ if (rto_at <= now_ns)
+ goto unlock;
+
+ pkt_copy = malloc(rxm->len);
+ if (pkt_copy != NULL) {
+ 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->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;
+ frcti->tlp_count++;
+ STAT_BUMP(frcti, tlp_snd);
+ }
+
+ unlock:
pthread_rwlock_unlock(&frcti->lock);
- if (fd != -1)
- __send_frct_pkt(fd, FRCT_ACK, ackno, 0);
+ if (pkt_copy != NULL) {
+ fast_rxm_send(frcti, pkt_copy, pkt_len);
+ free(pkt_copy);
+ }
+
+ if (re_post)
+ tw_post(&frcti->tlp_tw, deadline, tlp_due, frcti);
+ else
+ __atomic_clear(&frcti->tlp_pending, __ATOMIC_RELAXED);
+}
+
+/* §7.2 lazy: post once per quiet period. tlp_due re-evaluates on fire. */
+static int tlp_arm(struct frcti * frcti)
+{
+ struct timespec now;
+ uint64_t now_ns;
+ uint64_t pto;
+ uint64_t deadline;
+
+ /* §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;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ pto = tlp_pto(frcti);
+
+ deadline = LOAD_RELAXED(&frcti->snd_cr.act) + pto;
+ if (deadline <= now_ns)
+ deadline = now_ns + pto;
- return wait;
+ tw_post(&frcti->tlp_tw, deadline, tlp_due, frcti);
+
+ return 0;
}
-static int __frcti_snd(struct frcti * frcti,
- struct shm_du_buff * sdb)
+/*
+ * FC window advert from any flag-bearing packet. Caps at lwe + RQ_SIZE,
+ * rejects backward shrink (forged/stale FC), marks window open.
+ * Caller wrlock.
+ */
+static __inline__ void frcti_fc_rcv(struct frcti * frcti,
+ const struct frct_pci * pci)
{
- struct frct_pci * pci;
- struct timespec now;
- struct frct_cr * snd_cr;
- struct frct_cr * rcv_cr;
- uint32_t seqno;
- bool rtx;
+ struct frct_cr * snd_cr;
+ uint32_t rwe;
+ uint32_t rwe_max;
+
+ snd_cr = &frcti->snd_cr;
+ rwe = ntoh32(pci->window);
+ rwe_max = snd_cr->lwe + RQ_SIZE;
+
+ if (after(rwe, rwe_max))
+ rwe = rwe_max;
+
+ /* Reject backward shrink (forged/stale FC). */
+ if (before(rwe, snd_cr->rwe))
+ rwe = snd_cr->rwe;
+
+ STORE_RELAXED(&snd_cr->rwe, rwe);
+ frcti->open = true;
+}
+
+/* Packet copies captured under frcti->lock; emitted after release. */
+struct pending {
+ buffer_t fast_rxm;
+ buffer_t sack_rxm[SACK_RXM_MAX];
+ size_t sack_rxm_cnt;
+};
+
+/* RFC 6582 §3.2: seal recovery_high on entry; do not extend on new gaps. */
+static void recovery_enter(struct frcti * frcti)
+{
+ if (frcti->in_recovery)
+ return;
+
+ frcti->in_recovery = true;
+ frcti->recovery_high = frcti->snd_cr.seqno + RTT_QUARANTINE;
+}
+
+/* True when cum-ACK clears recovery_high or all in-flight ACKed. */
+static bool recovery_exit_reached(struct frcti * frcti,
+ uint32_t ackno)
+{
+ if (!frcti->in_recovery)
+ return false;
+
+ if (!before(ackno, frcti->recovery_high))
+ return true;
+
+ return ackno == frcti->snd_cr.seqno;
+}
+
+/* RTT sample gate: Karn + SACK-consume + don't-seed. */
+static bool rtt_sample_eligible(struct frcti * frcti,
+ size_t p,
+ uint16_t flags,
+ uint32_t lwe)
+{
+ 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;
+}
+
+#define RXM_SLOT_EMPTY(rxm) ((rxm) == NULL)
+#define FAST_RXM_STAGED(pending) ((pending)->fast_rxm.data != NULL)
+#define RXM_FAST_DONE(flags) (((flags) & SND_FAST_RXM) != 0)
+
+/* RACK fast retransmit on cum-ACK: HoL aged past R, not yet retransmitted. */
+static void fast_rxm_consider(struct frcti * frcti,
+ uint64_t now_ns,
+ struct pending * pending)
+{
+ struct rxm_entry * rxm;
+ struct snd_slot * slot;
+ size_t hp;
+ uint64_t R;
+ bool rack_ok;
+
+ hp = RQ_SLOT(frcti->snd_cr.lwe);
+ slot = &frcti->snd_slots[hp];
+ rxm = LOAD_ACQUIRE(&slot->rxm);
+ R = rack_reorder_window(frcti);
+
+ 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)
+ return;
+
+ /* HoL aged past t_r; let rxm_due tear the flow down. */
+ if (RXM_AGED_OUT(rxm->t0, now_ns, frcti->t_r))
+ return;
+
+ /* Already on it. */
+ if (FAST_RXM_STAGED(pending) || RXM_FAST_DONE(slot->flags))
+ return;
+
+ recovery_enter(frcti);
+
+ pending->fast_rxm.data = malloc(rxm->len);
+ if (pending->fast_rxm.data == NULL)
+ return;
+
+ pending->fast_rxm.len = rxm->len;
+ 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)
+ STAT_BUMP(frcti, rxm_rack);
+ else
+ STAT_BUMP(frcti, rxm_dupthresh);
+}
+
+/* Caller holds wrlock; RACK fast retransmit queued in pending. */
+__attribute__((hot))
+static void frcti_ack_rcv(struct frcti * frcti,
+ const struct frct_pci * pci,
+ uint16_t flags,
+ uint64_t now_ns,
+ struct pending * pending)
+{
+ uint32_t ackno;
+ uint32_t lwe;
+ size_t p;
+ size_t fresh;
+
+ if (!(flags & FRCT_DATA))
+ STAT_BUMP(frcti, ack_rcv);
+
+ ackno = ntoh32(pci->ackno);
+ if (ackno == frcti->snd_cr.lwe) {
+ /* RFC 8985 §6.2: only on scoreboard change. */
+ if (frcti->snd_cr.lwe != frcti->rack_fired_lwe) {
+ fast_rxm_consider(frcti, now_ns, pending);
+ frcti->rack_fired_lwe = frcti->snd_cr.lwe;
+ }
+ return;
+ }
+
+ if (!within(ackno, frcti->snd_cr.lwe, frcti->snd_cr.seqno))
+ return;
+
+ lwe = frcti->snd_cr.lwe;
+ p = RQ_SLOT(lwe);
+
+ STORE_RELEASE(&frcti->snd_cr.lwe, ackno);
+
+ /* §7.3: cum-ACK past the probed seqno resolves the TLP. */
+ if (frcti->tlp_high_seq != 0
+ && !before(ackno, frcti->tlp_high_seq))
+ frcti->tlp_high_seq = 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. */
+ fresh = ackno - frcti->dsack_lwe_snap;
+ if (frcti->reo_wnd_mult > 1 && fresh >= REO_DECAY_PKTS) {
+ uint8_t half = frcti->reo_wnd_mult >> 1;
+ frcti->reo_wnd_mult = half < 1 ? 1 : half;
+ 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: SACK-above-lwe count is per-recovery-episode. */
+ frcti->dup_thresh = 0;
+
+ /* Karn-skip on retx; TLP ACK clears rto_mul (no CC backoff). */
+ if ((frcti->snd_slots[p].flags & SND_RTX) == 0
+ || (frcti->snd_slots[p].flags & SND_TLP) != 0)
+ STORE_RELEASE(&frcti->rto_mul, 0);
+
+ if (recovery_exit_reached(frcti, ackno))
+ frcti->in_recovery = false;
+
+ if (rtt_sample_eligible(frcti, p, flags, lwe)) {
+ int64_t mrtt = ts_age_ns(now_ns, frcti->snd_slots[p].time);
+ if (mrtt > 0) {
+ if (!(flags & FRCT_DATA))
+ STAT_BUMP(frcti, ack_rtt);
+ rtt_update(frcti, (time_t) mrtt, now_ns);
+ frcti->t_rcv_rtt = now_ns;
+ }
+ }
+}
+
+/* Skip k == lwe under clamp: NULLing HoL from a stale SACK wedges it. */
+static uint32_t sack_mark_blocks(struct frcti * frcti,
+ const uint8_t * payload,
+ uint16_t n,
+ uint32_t * newly_marked)
+{
+ uint32_t hi_sacked = frcti->snd_cr.lwe;
+ uint32_t marked = 0;
+ uint16_t i;
+
+ for (i = 0; i < n; ++i) {
+ uint32_t s;
+ uint32_t e;
+ uint32_t k;
+ bool clamped;
+
+ sack_block_get(payload, i, &s, &e);
+
+ if (!before(s, e))
+ continue;
+
+ clamped = before(s, frcti->snd_cr.lwe);
+ if (clamped)
+ s = frcti->snd_cr.lwe;
+ if (after(e, frcti->snd_cr.seqno))
+ e = frcti->snd_cr.seqno;
+
+ for (k = s; before(k, e); ++k) {
+ size_t kp = RQ_SLOT(k);
+ uint64_t t_k;
+ if (clamped && k == frcti->snd_cr.lwe)
+ continue;
+ if (LOAD_ACQUIRE(&frcti->snd_slots[kp].rxm) == NULL)
+ continue;
+ STORE_RELEASE(&frcti->snd_slots[kp].rxm, NULL);
+ frcti->snd_slots[kp].flags = 0;
+ marked++;
+ /* RACK.fack: latest SACK-confirmed send-time. */
+ t_k = frcti->snd_slots[kp].time;
+ if (t_k > frcti->t_latest_ack)
+ frcti->t_latest_ack = t_k;
+ }
+
+ if (after(e, hi_sacked))
+ hi_sacked = e;
+ }
+
+ *newly_marked = marked;
+ return hi_sacked;
+}
+
+/* Queue once per loss event (SND_FAST_RXM gates). Emit after unlock. */
+static void sack_queue_rxm(struct frcti * frcti,
+ uint32_t hi_sacked,
+ uint64_t now_ns,
+ struct pending * pending)
+{
+ uint64_t R = rack_reorder_window(frcti);
+ uint32_t k;
+ bool rack_ok;
+
+ for (k = frcti->snd_cr.lwe; before(k, hi_sacked); ++k) {
+ struct rxm_entry * rxm;
+ size_t kp = RQ_SLOT(k);
+ size_t cnt = pending->sack_rxm_cnt;
+ size_t rack_age;
+
+ rxm = LOAD_ACQUIRE(&frcti->snd_slots[kp].rxm);
+
+ if (cnt >= SACK_RXM_MAX)
+ break;
+
+ if (rxm == NULL)
+ continue;
+
+ if (frcti->snd_slots[kp].flags & SND_FAST_RXM)
+ continue;
+
+ 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)
+ continue;
+
+ if (rack_ok)
+ STAT_BUMP(frcti, rxm_rack);
+ else
+ STAT_BUMP(frcti, rxm_dupthresh);
+
+ pending->sack_rxm[cnt].data = malloc(rxm->len);
+ if (pending->sack_rxm[cnt].data == NULL)
+ break;
+
+ pending->sack_rxm[cnt].len = rxm->len;
+ memcpy(pending->sack_rxm[cnt].data, rxm->pkt, rxm->len);
+ pending->sack_rxm_cnt++;
+ /* NULL slot so the original timer self-cleans. */
+ STORE_RELEASE(&frcti->snd_slots[kp].rxm, NULL);
+ frcti->snd_slots[kp].time = now_ns;
+ frcti->snd_slots[kp].flags |= SND_RTX | SND_FAST_RXM;
+ frcti->rtt_lwe = k + 1;
+ }
+}
+
+/*
+ * RFC 2883 D-SACK detector. Returns true iff block[0] is a D-SACK
+ * report:
+ * case 1: blocks[0].start < pkt_ackno (strictly below cum-ACK).
+ * case 2: blocks[0] is a strict sub-range of some blocks[i>0].
+ * MAX_DSACK_LAG bounds case-1 distance to one rcv window (sanity).
+ */
+static bool sack_is_dsack(struct frcti * frcti,
+ const uint8_t * payload,
+ uint16_t n,
+ uint32_t pkt_ackno)
+{
+ uint32_t s0;
+ uint32_t e0;
+ uint16_t i;
+
+ if (n == 0)
+ return false;
+
+ sack_block_get(payload, 0, &s0, &e0);
+ if (!before(s0, e0))
+ return false;
+
+ if (before(s0, pkt_ackno)) {
+ if ((pkt_ackno - s0) <= (uint32_t) MAX_DSACK_LAG)
+ return true;
+ STAT_BUMP(frcti, dsack_drop);
+ return false;
+ }
+
+ for (i = 1; i < n; ++i) {
+ uint32_t si;
+ uint32_t ei;
+
+ sack_block_get(payload, i, &si, &ei);
+ if (!before(si, ei))
+ continue;
+ if (!before(s0, si) && !after(e0, ei)
+ && (s0 != si || e0 != ei))
+ return true;
+ }
+
+ return false;
+}
+
+/* RFC 8985 §7.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)
+{
+ time_t srtt = frcti->srtt;
+
+ /* Snap is unconditional: feeds the per-D-SACK decay clock. */
+ frcti->dsack_lwe_snap = frcti->snd_cr.lwe;
+
+ if (srtt > 0
+ && now_ns - frcti->t_last_reo_widen <= (uint64_t) srtt)
+ return;
+
+ if (frcti->reo_wnd_mult < REO_WND_MULT_MAX)
+ frcti->reo_wnd_mult++;
+
+ frcti->t_last_reo_widen = now_ns;
+}
+
+/* Caller holds wrlock; retransmits queued for post-unlock emission. */
+static void frcti_sack_rcv(struct frcti * frcti,
+ buffer_t pkt,
+ uint32_t pkt_ackno,
+ uint64_t now_ns,
+ struct pending * pending)
+{
+ uint32_t hi_sacked;
+ uint32_t marked;
+ uint16_t n;
+ bool dsack;
+ uint16_t n_real;
+
+ if (pkt.len < SACK_HDR_SIZE)
+ return;
+
+ n = ntoh16(*(const uint16_t *) pkt.data);
+ if (n > SACK_MAX_BLOCKS)
+ return;
+
+ if (pkt.len < SACK_HDR_SIZE + (size_t) n * SACK_BLOCK_SIZE)
+ return;
+
+ STAT_BUMP(frcti, sack_rcv);
+
+ dsack = sack_is_dsack(frcti, pkt.data, n, pkt_ackno);
+ n_real = n - (dsack ? 1 : 0);
+
+ if (dsack) {
+ STAT_BUMP(frcti, dsack_rcv);
+ reo_wnd_on_dsack(frcti, now_ns);
+ }
+
+ /* DSACK-only carries no new gap; don't enter recovery. */
+ if (n_real > 0)
+ recovery_enter(frcti);
+
+ marked = 0;
+ hi_sacked = sack_mark_blocks(frcti, pkt.data, n, &marked);
+ frcti->dup_thresh += marked;
+
+ if (after(hi_sacked, frcti->snd_cr.lwe))
+ sack_queue_rxm(frcti, hi_sacked, now_ns, pending);
+}
+
+/* Emit and free queued packet copies. */
+static void pending_flush(struct frcti * frcti,
+ struct pending * pending)
+{
+ size_t i;
+
+ for (i = 0; i < pending->sack_rxm_cnt; ++i) {
+ sack_rxm_snd(frcti, pending->sack_rxm[i].data,
+ pending->sack_rxm[i].len);
+ free(pending->sack_rxm[i].data);
+ }
+
+ if (pending->fast_rxm.data != NULL) {
+ int ret = fast_rxm_send(frcti, pending->fast_rxm.data,
+ pending->fast_rxm.len);
+ if (ret == -EFLOWDOWN || ret == -ENOTALLOC)
+ STAT_BUMP(frcti, rxm_tx_dead);
+ free(pending->fast_rxm.data);
+ }
+}
+
+/* Pre-DRF NACK: ask peer to retransmit HoL; seqno is informational. */
+static void frcti_nack_snd(struct frcti * frcti,
+ uint32_t seqno_unseen)
+{
+ struct ssm_pk_buff * spb;
+ struct frct_pci * pci;
+
+ if (frct_ctrl_alloc(&spb, &pci, 0) < 0)
+ return;
+
+ pci->flags = hton16(FRCT_NACK);
+ pci->seqno = hton32(seqno_unseen);
+
+ frct_hcs_set(pci, false);
+
+ frct_tx(frcti, spb);
+}
+
+enum frct_act {
+ FRCT_ACTIVE,
+ FRCT_INACT_NEED_NACK,
+ FRCT_INACT_DROP,
+};
+
+/* On rcv inactivity: rebase on DRF, or arm pre-DRF NACK. Caller wrlock. */
+static enum frct_act rcv_inact_check(struct frcti * frcti,
+ uint16_t flags,
+ uint32_t seqno,
+ uint64_t now_ns)
+{
+ struct frct_cr * rcv_cr = &frcti->rcv_cr;
+ uint64_t cd;
+
+ if (!ts_aged_ns(now_ns, rcv_cr->act, rcv_cr->inact))
+ return FRCT_ACTIVE;
+
+ if (flags & FRCT_DRF) {
+ if (same_epoch_drf(seqno, flags, rcv_cr))
+ return FRCT_ACTIVE;
+
+ /* Bootstrap or fresh epoch: rebase. */
+ STAT_BUMP(frcti, drf_rebase);
+ release_rq(frcti);
+ STORE_RELEASE(&rcv_cr->lwe, seqno);
+ rcv_cr->rwe = seqno + RQ_SIZE;
+ rcv_cr->seqno = seqno;
+ return FRCT_ACTIVE;
+ }
+
+ if (!(flags & FRCT_DATA))
+ return FRCT_ACTIVE;
+
+ /* Pre-DRF: nudge sender with NACK (rate-limited). */
+ cd = frcti->srtt > 0 ? (uint64_t) frcti->srtt : NACK_COOLDOWN_NS;
+ if (!ts_aged_ns(now_ns, frcti->t_nack, cd))
+ return FRCT_INACT_DROP;
+
+ frcti->t_nack = now_ns;
+ STAT_BUMP(frcti, nack_snd);
+
+ return FRCT_INACT_NEED_NACK;
+}
+
+/* Both modes: bounded accept into rq[seqno]. Caller wrlock. */
+__attribute__((hot))
+static bool rq_accept(struct frcti * frcti,
+ uint32_t seqno,
+ size_t pos,
+ uint16_t flags)
+{
+ struct frct_cr * rcv_cr = &frcti->rcv_cr;
+
+ if (!before(seqno, rcv_cr->rwe)) {
+ STAT_BUMP(frcti, out_rcv);
+ return false;
+ }
+
+ if (!before(seqno, rcv_cr->lwe + RQ_SIZE)) {
+ STAT_BUMP(frcti, rqo_rcv);
+ return false;
+ }
+
+ if (frcti->rcv_slots[pos].idx != -1) {
+ if (flags & FRCT_RXM)
+ STAT_BUMP(frcti, rxm_dup_rcv);
+ else
+ STAT_BUMP(frcti, dup_rcv);
+ /* RFC 2883 §4 case 2: in-window dup; sub-range marker. */
+ frcti->dsack_seqno = seqno;
+ frcti->dsack_valid = true;
+ return false;
+ }
+
+ return true;
+}
+
+/* OOO arrival; throttle by min_gap + scoreboard dedup. */
+static bool sack_check(struct frcti * frcti,
+ uint32_t seqno,
+ uint64_t now_ns,
+ struct sack_args * out)
+{
+ struct frct_cr * rcv_cr = &frcti->rcv_cr;
+ uint64_t min_gap;
+ uint16_t n;
+
+ if (!after(seqno, rcv_cr->lwe))
+ return false;
+
+ STAT_BUMP(frcti, ooo_rcv);
+
+ /* SACK carries cum-ACK; bound by t_a like any other ACK. */
+ if (ACK_AGED_OUT(rcv_cr->act, now_ns, frcti->t_a))
+ return false;
+
+ /* srtt/8 gate starved recovery under burst loss; floor to save CPU. */
+ min_gap = (uint64_t) SACK_MIN_GAP_NS;
+
+ if (!ts_aged_ns(now_ns, frcti->t_snd_sack, min_gap))
+ return false;
+
+ out->dsack = false;
+ 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);
+ frcti->t_snd_sack = now_ns;
+ frcti->sack_lwe = rcv_cr->lwe;
+ frcti->sack_n = n;
+
+ return true;
+}
+
+/* Wire-dup of fresh DATA at an already-ACKed seqno. */
+static __inline__ bool is_dup_data(uint16_t flags,
+ uint32_t seqno,
+ uint32_t lwe)
+{
+ if (!(flags & FRCT_DATA))
+ return false;
+
+ if (flags & FRCT_RXM)
+ return false;
+
+ return before(seqno, lwe);
+}
+
+/*
+ * Wire-dup ACK packet: same seqno as the previous emission. Updates
+ * the dedup ackno on a fresh ACK; caller drops on true.
+ */
+static __inline__ bool is_dup_ack(struct frcti * frcti,
+ uint16_t flags,
+ uint32_t seqno)
+{
+ if (flags & FRCT_DATA)
+ return false;
+
+ if (!(flags & FRCT_ACK))
+ return false;
+
+ if (seqno == frcti->rcv_cr.ackno)
+ return true;
+
+ frcti->rcv_cr.ackno = seqno;
+
+ return false;
+}
+
+/* Caller wrlock. */
+__attribute__((cold))
+static void seqno_rotate(struct frcti * frcti,
+ uint64_t now_ns)
+{
+ struct frct_cr * snd_cr = &frcti->snd_cr;
+
+ 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;
+
+ /* Avoid colliding with peer's current rcv window. */
+ do {
+ random_buffer(&snd_cr->seqno, sizeof(snd_cr->seqno));
+ } while (in_window(snd_cr->seqno, snd_cr));
+ STORE_RELEASE(&snd_cr->lwe, snd_cr->seqno);
+ STORE_RELAXED(&snd_cr->rwe, snd_cr->lwe + START_WINDOW);
+ frcti->rtt_lwe = snd_cr->seqno;
+ frcti->in_recovery = false;
+ frcti->recovery_high = snd_cr->seqno;
+}
+
+__attribute__((hot))
+static int frcti_snd(struct frcti * frcti,
+ struct ssm_pk_buff * spb,
+ uint16_t flags)
+{
+ struct frct_pci * pci;
+ struct frct_pci_stream * spci = NULL;
+ 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;
+ uint64_t now_ns;
+ int64_t rcv_idle;
+ uint32_t probe_id = 0;
+ uint8_t probe_nonce[RTTP_NONCE_LEN] = { 0 };
+ bool probe;
+ size_t payload_len = 0;
assert(frcti);
- assert(shm_du_buff_len(sdb) != 0);
+ /* Stream mode permits 0-byte sends for the EOS marker. */
+ assert(ssm_pk_buff_len(spb) != 0 || frcti->stream);
snd_cr = &frcti->snd_cr;
rcv_cr = &frcti->rcv_cr;
- timerwheel_move();
+ tw_move_safe();
- pci = (struct frct_pci *) shm_du_buff_head_alloc(sdb, FRCT_PCILEN);
+ if (frcti->stream)
+ payload_len = ssm_pk_buff_len(spb);
+
+ pci = FRCT_HDR_PUSH(spb, frcti);
if (pci == NULL)
return -ENOMEM;
- memset(pci, 0, sizeof(*pci));
+ /* 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;
+ }
+ }
+
+ memset(pci, 0, FRCT_PCILEN);
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
pthread_rwlock_wrlock(&frcti->lock);
rtx = snd_cr->cflags & FRCTFRTX;
- pci->flags |= FRCT_DATA;
+ pci_flags |= FRCT_DATA;
+ if (!frcti->stream)
+ pci_flags |= (flags & FRCT_FR_MASK);
- /* Set DRF if there are no unacknowledged packets. */
- if (snd_cr->seqno == snd_cr->lwe)
- pci->flags |= FRCT_DRF;
+ if (!frcti->stream && (flags & FRCT_FR_MASK) != FRCT_FR_SOLE)
+ STAT_BUMP(frcti, frag_snd);
- /* Choose a new sequence number if sender inactivity expired. */
- if (now.tv_sec - snd_cr->act.tv_sec > snd_cr->inact) {
- /* There are no unacknowledged packets. */
- assert(snd_cr->seqno == snd_cr->lwe);
- random_buffer(&snd_cr->seqno, sizeof(snd_cr->seqno));
- snd_cr->lwe = snd_cr->seqno;
- snd_cr->rwe = snd_cr->lwe + START_WINDOW;
+ if (frcti->stream) {
+ spci = FRCT_SPCI(pci);
+
+ if (flags & FRCT_FIN)
+ pci_flags |= FRCT_FIN;
+
+ spci->start = hton32(frcti->snd_byte_next);
+ frcti->snd_byte_next += (uint32_t) payload_len;
+ spci->end = hton32(frcti->snd_byte_next);
+ STAT_ADD(frcti, strm_snd_byte, payload_len);
}
+ if (snd_cr->seqno == snd_cr->lwe)
+ pci_flags |= FRCT_DRF;
+
+ seqno_rotate(frcti, now_ns);
+
seqno = snd_cr->seqno;
pci->seqno = hton32(seqno);
- if (now.tv_sec - rcv_cr->act.tv_sec < rcv_cr->inact) {
- pci->flags |= FRCT_FC;
- *((uint32_t *) pci) |= hton32(rcv_cr->rwe & 0x00FFFFFF);
+ rcv_idle = ts_age_ns(now_ns, rcv_cr->act);
+
+ if (rcv_idle < (int64_t) rcv_cr->inact) {
+ pci_flags |= FRCT_FC;
+ pci->window = hton32(frcti_advert_rwe(frcti));
}
if (!rtx) {
- snd_cr->lwe++;
+ STORE_RELEASE(&snd_cr->lwe, snd_cr->lwe + 1);
+ STORE_RELEASE(&snd_cr->rwe, snd_cr->lwe + RQ_SIZE);
} else {
- if (!frcti->probe) {
- frcti->rttseq = snd_cr->seqno;
- frcti->t_probe = now;
- frcti->probe = true;
-#ifdef PROC_FLOW_STATS
- frcti->n_prb++;
-#endif
- }
- if ((now.tv_sec - rcv_cr->act.tv_sec) * BILLION <= frcti->a) {
- pci->flags |= FRCT_ACK;
+ size_t p = RQ_SLOT(seqno);
+ frcti->snd_slots[p].time = now_ns;
+ /* Fresh send clears RTX bits. */
+ frcti->snd_slots[p].flags = 0;
+ if (rcv_idle <= (int64_t) frcti->t_a) {
+ pci_flags |= FRCT_ACK;
pci->ackno = hton32(rcv_cr->lwe);
rcv_cr->seqno = rcv_cr->lwe;
}
}
+ pci->flags = hton16(pci_flags);
+
+ frct_hcs_set(pci, frcti->stream);
+
snd_cr->seqno++;
- snd_cr->act = now;
+ STORE_RELEASE(&snd_cr->act, now_ns);
+
+ probe = rtt_probe_arm(frcti, now_ns, &probe_id, probe_nonce);
pthread_rwlock_unlock(&frcti->lock);
- if (rtx)
- timerwheel_rxm(frcti, seqno, sdb);
+ if (probe)
+ frcti_rttp_snd(frcti, probe_id, 0, probe_nonce);
+
+ if (rtx) {
+ assert(rxm != NULL);
+ rxm_arm(frcti, seqno, rxm, spb);
+ tlp_arm(frcti);
+ }
return 0;
}
-static void rtt_estimator(struct frcti * frcti,
- time_t mrtt)
+/* Stream FIN is armed for rxm; needs to be in window. */
+static __inline__ bool stream_fin_blocked(struct frcti * frcti)
{
- time_t srtt = frcti->srtt;
- time_t rttvar = frcti->mdev;
+ if (!frcti->stream)
+ return false;
- if (srtt == 0) { /* first measurement */
- srtt = mrtt;
- rttvar = mrtt >> 1;
- } else {
- time_t delta = mrtt - srtt;
- srtt += (delta >> 3);
- delta = (ABS(delta) - rttvar) >> 2;
-#ifdef FRCT_LINUX_RTT_ESTIMATOR
- if (delta < 0)
- delta >>= 3;
-#endif
- rttvar += delta;
+ 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
+ * in pci->ackno (sender packs via frcti_pkt_snd's ackno parameter).
+ */
+static void frcti_fin_snd(struct frcti * frcti)
+{
+ struct ssm_pk_buff * spb;
+ bool already;
+ uint32_t fin_seqno;
+
+ if (!(frcti->snd_cr.cflags & FRCTFLINGER))
+ return;
+
+ 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;
}
-#ifdef PROC_FLOW_STATS
- frcti->n_rtt++;
-#endif
- frcti->srtt = MAX(1000L, srtt);
- frcti->mdev = MAX(100L, rttvar);
- frcti->rto = MAX(RTO_MIN, frcti->srtt + (frcti->mdev << MDEV_MUL));
-}
-
-/* Always queues the next application packet on the RQ. */
-static void __frcti_rcv(struct frcti * frcti,
- struct shm_du_buff * sdb)
-{
- ssize_t idx;
- size_t pos;
- struct frct_pci * pci;
- struct timespec now;
- struct frct_cr * rcv_cr;
- struct frct_cr * snd_cr;
- uint32_t seqno;
- uint32_t ackno;
- uint32_t rwe;
- int fd = -1;
- assert(frcti);
+ frcti->snd_fin_sent = true;
+ fin_seqno = frcti->snd_cr.seqno;
+
+ if (!already && !frcti->stream)
+ frcti->snd_fin_seqno = fin_seqno;
+ pthread_rwlock_unlock(&frcti->lock);
+
+ if (already)
+ return;
+
+ if (!frcti->stream) {
+ frcti_pkt_snd(frcti, FRCT_FIN, fin_seqno, 0);
+ return;
+ }
+
+ if (frct_spb_reserve(frcti_data_hdr_len(frcti), &spb) < 0)
+ return;
+
+ /* Reset spb to 0-len so frcti_snd's head_alloc populates PCI. */
+ ssm_pk_buff_truncate(spb, 0);
+
+ if (frcti_snd(frcti, spb, FRCT_FIN) < 0) {
+ frct_spb_release(spb);
+ return;
+ }
+
+ if (frct_tx(frcti, spb) < 0)
+ return;
+
+ pthread_rwlock_wrlock(&frcti->lock);
+
+ frcti->snd_fin_seqno = frcti->snd_cr.seqno - 1;
+
+ pthread_rwlock_unlock(&frcti->lock);
+}
+
+static bool final_ack_due(struct frcti * frcti,
+ struct frct_cr * rcv_cr,
+ uint64_t now_ns)
+{
+ if (rcv_cr->lwe == rcv_cr->seqno)
+ return false;
+
+ if (ACK_AGED_OUT(rcv_cr->act, now_ns, frcti->t_a))
+ return false;
+
+ return true;
+}
+
+/* Snd-side has FLINGER cflag and unACK'd data below the FIN/seqno. */
+static __inline__ bool snd_drain_pending(struct frct_cr * snd_cr,
+ uint32_t edge)
+{
+ if (!(snd_cr->cflags & FRCTFLINGER))
+ return false;
+
+ return before(snd_cr->lwe, edge);
+}
+
+/* Peer is still active and we haven't seen their FIN yet. */
+static __inline__ bool rcv_drain_pending(struct frcti * frcti,
+ struct frct_cr * rcv_cr,
+ uint64_t now_ns)
+{
+ if (frcti->rcv_fin_seen)
+ return false;
+
+ return !ts_aged_ns(now_ns, rcv_cr->act, rcv_cr->inact);
+}
+
+/* Drain-loop predicate: snd-side unACK'd data OR peer still active. */
+static bool frcti_lingering(struct frcti * frcti)
+{
+ struct timespec now;
+ struct frct_cr * snd_cr;
+ struct frct_cr * rcv_cr;
+ uint32_t edge;
+ uint64_t now_ns;
+ bool snd_linger;
+ bool rcv_linger;
+
+ /* Idempotent; emits FIN once per side, both stream and msg. */
+ frcti_fin_snd(frcti);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ pthread_rwlock_rdlock(&frcti->lock);
+
+ snd_cr = &frcti->snd_cr;
rcv_cr = &frcti->rcv_cr;
+
+ if (frcti->snd_fin_sent)
+ edge = frcti->snd_fin_seqno;
+ else
+ edge = snd_cr->seqno;
+
+ snd_linger = snd_drain_pending(snd_cr, edge);
+ rcv_linger = rcv_drain_pending(frcti, rcv_cr, now_ns);
+
+ pthread_rwlock_unlock(&frcti->lock);
+
+ return snd_linger || rcv_linger;
+}
+
+static time_t frcti_dealloc(struct frcti * frcti)
+{
+ struct timespec now;
+ struct frct_cr * snd_cr;
+ struct frct_cr * rcv_cr;
+ int ackno;
+ bool due;
+ int64_t now_ns;
+ int64_t rcv;
+ int64_t snd;
+
snd_cr = &frcti->snd_cr;
+ rcv_cr = &frcti->rcv_cr;
+
+ /* Idempotent; usually already sent by frcti_lingering. */
+ frcti_fin_snd(frcti);
clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
- pci = (struct frct_pci *) shm_du_buff_head_release(sdb, FRCT_PCILEN);
+ pthread_rwlock_rdlock(&frcti->lock);
- idx = shm_du_buff_get_idx(sdb);
- seqno = ntoh32(pci->seqno);
- pos = seqno & (RQ_SIZE - 1);
+ ackno = rcv_cr->lwe;
+ rcv = (int64_t)(rcv_cr->act + rcv_cr->inact) - now_ns;
+ snd = (int64_t)(snd_cr->act + snd_cr->inact) - now_ns;
+ due = final_ack_due(frcti, rcv_cr, now_ns);
- pthread_rwlock_wrlock(&frcti->lock);
+ pthread_rwlock_unlock(&frcti->lock);
- if (now.tv_sec - rcv_cr->act.tv_sec > rcv_cr->inact) {
- if (pci->flags & FRCT_DRF) { /* New run. */
- rcv_cr->lwe = seqno;
- rcv_cr->rwe = seqno + RQ_SIZE;
- rcv_cr->seqno = seqno;
- } else if (pci->flags & FRCT_DATA) {
- goto drop_packet;
- }
- }
+ if (due)
+ frcti_pkt_snd(frcti, FRCT_ACK, ackno, 0);
- rcv_cr->act = now;
+ return (time_t) MAX((MAX(rcv, snd) / BILLION), 0);
+}
- /* For now, just send an immediate window update. */
- if (pci->flags & FRCT_RDVS) {
- fd = frcti->fd;
- rwe = rcv_cr->rwe;
- pthread_rwlock_unlock(&frcti->lock);
+__attribute__((hot))
+static void frcti_rcv(struct frcti * frcti,
+ struct ssm_pk_buff * spb)
+{
+ ssize_t idx;
+ size_t pos;
+ struct frct_pci * pci;
+ struct timespec now;
+ uint64_t now_ns;
+ struct frct_cr * rcv_cr;
+ uint32_t seqno;
+ uint16_t flags;
+ buffer_t pkt;
+ struct pending pending = { 0 };
+ bool in_order;
+ struct sack_args * sa = NULL;
+ bool send_sack = false;
- __send_frct_pkt(fd, FRCT_FC, 0, rwe);
+ assert(frcti);
- shm_rdrbuff_remove(ai.rdrb, idx);
+ rcv_cr = &frcti->rcv_cr;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ if (ssm_pk_buff_len(spb) < FRCT_PCILEN) {
+ frct_spb_release(spb);
return;
}
- if (pci->flags & FRCT_ACK) {
- ackno = ntoh32(pci->ackno);
- if (after(ackno, frcti->snd_cr.lwe))
- frcti->snd_cr.lwe = ackno;
+ pci = FRCT_HDR_POP(spb, frct_pci);
- if (frcti->probe && after(ackno, frcti->rttseq)) {
-#ifdef PROC_FLOW_STATS
- if (!(pci->flags & FRCT_DATA))
- frcti->n_dak++;
-#endif
- rtt_estimator(frcti, ts_diff_ns(&now, &frcti->t_probe));
- frcti->probe = false;
- }
+ idx = ssm_pk_buff_get_off(spb);
+ seqno = ntoh32(pci->seqno);
+ pos = RQ_SLOT(seqno);
+
+ flags = ntoh16(pci->flags);
+
+ pkt.data = ssm_pk_buff_head(spb);
+ pkt.len = ssm_pk_buff_len(spb);
+
+ if (flags & FRCT_RXM)
+ STAT_BUMP(frcti, rxm_rcv);
+
+ /* Stateless / lock-free dispatches. spb released via ctrl_done. */
+ if (flags & FRCT_KA) {
+ frcti_ka_rcv(frcti, pci, now_ns, flags);
+ goto ctrl_done;
}
- if (pci->flags & FRCT_FC) {
- uint32_t rwe;
+ if (flags & FRCT_RTTP) {
+ frcti_rttp_rcv(frcti, pkt, now_ns);
+ goto ctrl_done;
+ }
+
+ if (flags & FRCT_NACK) {
+ frcti_nack_rcv(frcti);
+ goto ctrl_done;
+ }
- rwe = ntoh32(*((uint32_t *)pci) & hton32(0x00FFFFFF));
- rwe |= snd_cr->rwe & 0xFF000000;
+ if (flags & FRCT_RDVS) {
+ frcti_rdv_rcv(frcti);
+ goto ctrl_done;
+ }
- /* Rollover for 24 bit */
- if (before(rwe, snd_cr->rwe) && snd_cr->rwe - rwe > 0x007FFFFF)
- rwe += 0x01000000;
+ /* Msg-mode FIN: control packet, FIN seqno carried in pci->ackno. */
+ if ((flags & FRCT_FIN) && !(flags & FRCT_DATA)) {
+ pthread_rwlock_wrlock(&frcti->lock);
+ if (!frcti->rcv_fin_seen) {
+ frcti->rcv_fin_seen = true;
+ frcti->rcv_byte_fin = ntoh32(pci->ackno);
+ }
+ pthread_rwlock_unlock(&frcti->lock);
+ goto ctrl_done;
+ }
- snd_cr->rwe = rwe;
+ pthread_rwlock_wrlock(&frcti->lock);
- pthread_mutex_lock(&frcti->mtx);
- if (!frcti->open) {
- frcti->open = true;
- pthread_cond_broadcast(&frcti->cond);
+ /* rcv_inact_check is a no-op for non-DATA non-DRF packets. */
+ if (flags & (FRCT_DATA | FRCT_DRF)) {
+ switch (rcv_inact_check(frcti, flags, seqno, now_ns)) {
+ case FRCT_INACT_NEED_NACK:
+ pthread_rwlock_unlock(&frcti->lock);
+ frcti_nack_snd(frcti, seqno - 1);
+ frct_spb_release(spb);
+ return;
+ case FRCT_INACT_DROP:
+ STAT_BUMP(frcti, inact_drop);
+ goto drop_packet;
+ case FRCT_ACTIVE:
+ /* FALLTHRU */
+ default:
+ break;
}
- pthread_mutex_unlock(&frcti->mtx);
}
- if (!(pci->flags & FRCT_DATA))
+ /* DATA-only act refresh: non-DATA would lock out DRF rebase. */
+ if (flags & FRCT_DATA)
+ STORE_RELEASE(&rcv_cr->act, now_ns);
+
+ /* Wire-dup ACK packet: same seqno as the previous emission. */
+ if (is_dup_ack(frcti, flags, seqno)) {
+ STAT_BUMP(frcti, ack_dup_rcv);
+ goto drop_packet;
+ }
+
+ /* Wire-dup of DATA: piggybacked ACK info already processed. */
+ if (is_dup_data(flags, seqno, rcv_cr->lwe)) {
+ rcv_cr->seqno = seqno;
+ STAT_BUMP(frcti, dup_rcv);
+ /* RFC 2883 §4 case 1: dup below cum-ACK. */
+ frcti->dsack_seqno = seqno;
+ frcti->dsack_valid = true;
+ goto drop_packet;
+ }
+
+ if (flags & FRCT_ACK)
+ frcti_ack_rcv(frcti, pci, flags, now_ns, &pending);
+
+ if (flags & FRCT_SACK)
+ frcti_sack_rcv(frcti, pkt, ntoh32(pci->ackno),
+ now_ns, &pending);
+
+ if (flags & FRCT_FC)
+ frcti_fc_rcv(frcti, pci);
+
+ if (!(flags & FRCT_DATA))
goto drop_packet;
if (before(seqno, rcv_cr->lwe)) {
- rcv_cr->seqno = seqno; /* Ensures we send a new ACK. */
-#ifdef PROC_FLOW_STATS
- frcti->n_dup++;
-#endif
+ /* Bump rcv_cr.seqno to force ack_snd to fire on the dup. */
+ rcv_cr->seqno = seqno;
+ if (flags & FRCT_RXM)
+ STAT_BUMP(frcti, rxm_dup_rcv);
+ else
+ STAT_BUMP(frcti, dup_rcv);
+ /* RFC 2883 §4 case 1: dup below cum-ACK. */
+ frcti->dsack_seqno = seqno;
+ frcti->dsack_valid = true;
goto drop_packet;
}
- if (rcv_cr->cflags & FRCTFRTX) {
+ if (!rq_accept(frcti, seqno, pos, flags))
+ goto drop_packet;
- if (!before(seqno, rcv_cr->rwe)) { /* Out of window. */
-#ifdef PROC_FLOW_STATS
- frcti->n_out++;
-#endif
+ if (frcti->stream) {
+ if (frcti_stream_data_rcv(frcti, spb, pos, flags) < 0) {
+ STAT_BUMP(frcti, strm_drop);
goto drop_packet;
}
-
- if (!before(seqno, rcv_cr->lwe + RQ_SIZE)) {
-#ifdef PROC_FLOW_STATS
- frcti->n_rqo++;
-#endif
- goto drop_packet; /* Out of rq. */
- }
- if (frcti->rq[pos] != -1) {
-#ifdef PROC_FLOW_STATS
- frcti->n_dup++;
-#endif
- goto drop_packet; /* Duplicate in rq. */
- }
- fd = frcti->fd;
+ /* spb consumed by stash; do not release in drop path. */
+ spb = NULL;
} else {
- rcv_cr->lwe = seqno;
+ frcti_data_stash(frcti, idx, pos, flags);
}
- frcti->rq[pos] = idx;
+ /* Lazy alloc: only OOO arrivals can trigger a SACK send. */
+ if (after(seqno, rcv_cr->lwe) && frcti->sack_n_max > 0) {
+ size_t sa_sz = sizeof(*sa)
+ + frcti->sack_n_max * sizeof(sa->blocks[0]);
+ sa = malloc(sa_sz);
+ /* If alloc fails, sack_check sees NULL and we skip SACK. */
+ }
+
+ send_sack = sa != NULL && sack_check(frcti, seqno, now_ns, sa);
+ in_order = !after(seqno, rcv_cr->lwe);
pthread_rwlock_unlock(&frcti->lock);
- if (fd != -1)
- timerwheel_delayed_ack(fd, frcti);
+ if (send_sack) {
+ STAT_BUMP(frcti, sack_snd);
+ if (sa->dsack)
+ STAT_BUMP(frcti, dsack_snd);
+ frcti_sack_snd(frcti, sa);
+ } else if (in_order) {
+ ack_arm(frcti);
+ }
+
+ if ((flags & FRCT_ACK) && frcti->snd_cr.seqno != frcti->snd_cr.lwe)
+ tlp_arm(frcti);
+
+ pending_flush(frcti, &pending);
+ frcti_rcv_probe(frcti, now_ns);
+
+ free(sa);
+ return;
+
+ ctrl_done:
+ frct_spb_release(spb);
return;
drop_packet:
pthread_rwlock_unlock(&frcti->lock);
- shm_rdrbuff_remove(ai.rdrb, idx);
- send_frct_pkt(frcti);
- return;
+ frct_spb_release(spb);
+ /* with_sack=true: ack_snd no-ops if neither dsack nor SACK is due. */
+ ack_snd(frcti, true);
+
+ pending_flush(frcti, &pending);
+ free(sa);
}
+
+/* NULL-shim macros for the no-FRCT case. */
+
+#define FRCTI_SND(frcti, spb, flags) \
+ ((frcti) == NULL ? 0 : frcti_snd((frcti), (spb), (flags)))
+
+#define FRCTI_RCV(frcti, spb) \
+ do { \
+ if ((frcti) != NULL) \
+ frcti_rcv((frcti), (spb)); \
+ } while (0)
+
+#define FRCTI_PDU_READY(frcti) \
+ ((frcti) != NULL && frcti_pdu_ready(frcti))
+
+#define FRCTI_CONSUME(frcti, buf, count) \
+ ((frcti) == NULL ? (ssize_t) -EAGAIN \
+ : (frcti)->stream \
+ ? frcti_consume_stream((frcti), (buf), (count)) \
+ : frcti_consume((frcti), (buf), (count)))
+
+#define FRCTI_IS_FRTX(frcti) \
+ ((frcti) != NULL && ((frcti)->rcv_cr.cflags & FRCTFRTX))
+
+#define FRCTI_IS_STREAM(frcti) ((frcti) != NULL && (frcti)->stream)
+
+#define FRCTI_PAYLOAD_CAP(frcti) \
+ ((frcti)->frag_mtu - frcti_data_hdr_len(frcti))
+
+#define FRCTI_NEEDS_FRAG(frcti, count) \
+ ((frcti) != NULL && (count) > FRCTI_PAYLOAD_CAP(frcti))
+
+#define FRCTI_IS_WINDOW_OPEN(frcti) \
+ ((frcti) == NULL ? true : frcti_is_window_open(frcti))
+
+#define FRCTI_IS_WINDOW_OPEN_N(frcti, n) \
+ ((frcti) == NULL ? true : frcti_is_window_open_n((frcti), (n)))
+
+#define FRCTI_LINGERING(frcti) \
+ ((frcti) == NULL ? false : frcti_lingering(frcti))
+
+#define FRCTI_DEALLOC(frcti) \
+ ((frcti) == NULL ? (time_t) 0 : frcti_dealloc(frcti))
+
diff --git a/src/lib/hash.c b/src/lib/hash.c
index b465f894..903474df 100644
--- a/src/lib/hash.c
+++ b/src/lib/hash.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Hashing
*
@@ -39,6 +39,9 @@
#include <ouroboros/md5.h>
#include <ouroboros/sha3.h>
#endif
+#include <ouroboros/crc8.h>
+#include <ouroboros/crc16.h>
+#include <ouroboros/crc64.h>
#include <string.h>
#include <assert.h>
#include <stdbool.h>
@@ -69,6 +72,14 @@ int hash_len_tbl [] = {
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
return (uint16_t) gcry_md_get_algo_dlen(gcry_algo_tbl[algo]);
#else
@@ -81,12 +92,36 @@ void mem_hash(enum hash_algo algo,
const uint8_t * buf,
size_t len)
{
-#ifdef HAVE_LIBGCRYPT
- gcry_md_hash_buffer(gcry_algo_tbl[algo], dst, buf, len);
-#else
+#ifndef HAVE_LIBGCRYPT
struct sha3_ctx sha3_ctx;
struct md5_ctx md5_ctx;
+#endif
+ if (algo == HASH_CRC8) {
+ uint8_t crc = 0;
+
+ crc8_autosar(&crc, buf, len);
+ *(uint8_t *) dst = crc;
+ return;
+ }
+ if (algo == HASH_CRC16) {
+ uint16_t crc = 0;
+
+ crc16_ccitt_false(&crc, buf, len);
+ *(uint16_t *) dst = htobe16(crc);
+ return;
+ }
+
+ if (algo == HASH_CRC64) {
+ uint64_t crc = 0;
+
+ crc64_nvme(&crc, buf, len);
+ *(uint64_t *) dst = htobe64(crc);
+ return;
+ }
+#ifdef HAVE_LIBGCRYPT
+ gcry_md_hash_buffer(gcry_algo_tbl[algo], dst, buf, len);
+#else
switch (algo) {
case HASH_CRC32:
memset(dst, 0, CRC32_HASH_LEN);
@@ -131,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 8333d0d3..c62701aa 100644
--- a/src/lib/irm.c
+++ b/src/lib/irm.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API to instruct the IRM
*
@@ -614,7 +614,7 @@ ssize_t irm_list_names(struct name_info ** names)
return 0;
}
- *names = malloc(nr * sizeof(**names));
+ *names = calloc(nr, sizeof(**names));
if (*names == NULL) {
irm_msg__free_unpacked(recv_msg, NULL);
return -ENOMEM;
diff --git a/src/lib/list.c b/src/lib/list.c
deleted file mode 100644
index 62b2eb27..00000000
--- a/src/lib/list.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Simple doubly linked list implementation.
- *
- * 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 <ouroboros/list.h>
-
-#include <stddef.h>
-
-void list_head_init(struct list_head * h)
-{
- h->nxt = h;
- h->prv = h;
-}
-
-static void add_list(struct list_head * n,
- struct list_head * prv,
- struct list_head * nxt)
-{
- nxt->prv = n;
- n->nxt = nxt;
- n->prv = prv;
- prv->nxt = n;
-}
-
-static void del_list(struct list_head * prv,
- struct list_head * nxt)
-{
- nxt->prv = prv;
- prv->nxt = nxt;
-}
-
-void list_add(struct list_head * n,
- struct list_head * h)
-{
- add_list(n, h, h->nxt);
-}
-
-void list_add_tail(struct list_head * n,
- struct list_head * h)
-{
- add_list(n, h->prv, h);
-}
-
-void list_del(struct list_head * e)
-{
- del_list(e->prv, e->nxt);
- e->nxt = e->prv = e;
-}
-
-bool list_is_empty(const struct list_head * h)
-{
- return h->nxt == h;
-}
-
-void list_move(struct list_head * n,
- struct list_head * h)
-{
- del_list(n->prv, n->nxt);
- add_list(n, h, h->nxt);
-}
diff --git a/src/lib/lockfile.c b/src/lib/lockfile.c
index cf6d3c94..0c18dfc1 100644
--- a/src/lib/lockfile.c
+++ b/src/lib/lockfile.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Lockfile for Ouroboros
*
diff --git a/src/lib/logs.c b/src/lib/logs.c
index d90bcd63..af03de10 100644
--- a/src/lib/logs.c
+++ b/src/lib/logs.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Logging facilities
*
diff --git a/src/lib/md5.c b/src/lib/md5.c
index ad0dd4d7..2412b909 100644
--- a/src/lib/md5.c
+++ b/src/lib/md5.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* MD5 algorithm
*
diff --git a/src/lib/notifier.c b/src/lib/notifier.c
index 4fccd371..10bdb28d 100644
--- a/src/lib/notifier.c
+++ b/src/lib/notifier.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Notifier event system using callbacks
*
diff --git a/src/lib/pb/cep.proto b/src/lib/pb/cep.proto
index d31cf4f7..14a85cfa 100644
--- a/src/lib/pb/cep.proto
+++ b/src/lib/pb/cep.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Message for Connection Information in OCEP
*
diff --git a/src/lib/pb/enroll.proto b/src/lib/pb/enroll.proto
index 60e964c6..37226b4d 100644
--- a/src/lib/pb/enroll.proto
+++ b/src/lib/pb/enroll.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Enrollment protocol
*
diff --git a/src/lib/pb/ipcp.proto b/src/lib/pb/ipcp.proto
index c2c7f48b..afee4f91 100644
--- a/src/lib/pb/ipcp.proto
+++ b/src/lib/pb/ipcp.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Messages sent to IPCPds
*
@@ -39,6 +39,7 @@ enum ipcp_msg_code {
IPCP_CONNECT = 10;
IPCP_DISCONNECT = 11;
IPCP_REPLY = 12;
+ IPCP_FLOW_UPDATE = 13;
}
message ipcp_msg {
@@ -54,6 +55,7 @@ message ipcp_msg {
optional int32 response = 10;
optional string comp = 11;
optional uint32 timeo_sec = 12;
- optional sint32 mpl = 13;
+ optional sint32 mpl = 13; /* MPL in ms. */
optional int32 result = 14;
+ optional uint32 uid = 15; /* 0 = GSPP, >0 = PUP uid */
}
diff --git a/src/lib/pb/ipcp_config.proto b/src/lib/pb/ipcp_config.proto
index 1308c6d1..eac4da37 100644
--- a/src/lib/pb/ipcp_config.proto
+++ b/src/lib/pb/ipcp_config.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Layer configuration message
*
diff --git a/src/lib/pb/irm.proto b/src/lib/pb/irm.proto
index 75f5f350..f54bc9ea 100644
--- a/src/lib/pb/irm.proto
+++ b/src/lib/pb/irm.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Messages sent to IRMd
*
@@ -53,6 +53,8 @@ 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;
}
message timespec_msg {
@@ -88,11 +90,15 @@ message irm_msg {
repeated ipcp_list_msg ipcps = 17;
repeated name_info_msg names = 18;
optional timespec_msg timeo = 19;
- optional sint32 mpl = 20;
+ optional sint32 mpl = 20; /* MPL in ms. */
optional string comp = 21;
optional bytes pk = 22; /* piggyback */
- optional bytes symmkey = 23;
- optional uint32 timeo_sec = 24;
- optional uint32 timeo_nsec = 25;
- optional sint32 result = 26;
+ optional uint32 timeo_sec = 23;
+ optional uint32 timeo_nsec = 24;
+ 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 */
}
diff --git a/src/lib/pb/model.proto b/src/lib/pb/model.proto
index 7b06e434..4c1564a5 100644
--- a/src/lib/pb/model.proto
+++ b/src/lib/pb/model.proto
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Model description messages
*
@@ -28,18 +28,20 @@ message qosspec_msg {
required uint32 availability = 3; /* Class of 9s. */
required uint32 loss = 4; /* Packet loss. */
required uint32 ber = 5; /* Bit error rate, ppb. */
- required uint32 in_order = 6; /* In-order delivery. */
+ required uint32 service = 6; /* enum qos_service. */
required uint32 max_gap = 7; /* In ms. */
required uint32 timeout = 8; /* Timeout in ms. */
}
message flow_info_msg {
- required uint32 id = 1;
- required uint32 n_pid = 2;
- required uint32 n_1_pid = 3;
- required uint32 mpl = 4;
- required uint32 state = 5;
- required qosspec_msg qos = 6;
+ required uint32 id = 1;
+ required uint32 n_pid = 2;
+ required uint32 n_1_pid = 3;
+ required uint32 mpl = 4; /* MPL in ms. */
+ required uint32 state = 5;
+ required qosspec_msg qos = 6;
+ required uint32 uid = 7;
+ required uint32 mtu = 8; /* Layer MTU (bytes). */
}
message name_info_msg {
diff --git a/src/lib/protobuf.c b/src/lib/protobuf.c
index 6df4e810..a824d357 100644
--- a/src/lib/protobuf.c
+++ b/src/lib/protobuf.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Protobuf syntax conversion
*
@@ -23,6 +23,8 @@
#define _DEFAULT_SOURCE
#include <ouroboros/protobuf.h>
+#include <ouroboros/crypt.h>
+#include <ouroboros/proc.h>
#include <stdlib.h>
#include <string.h>
@@ -73,12 +75,14 @@ flow_info_msg_t * flow_info_s_to_msg(const struct flow_info * s)
flow_info_msg__init(msg);
- msg->id = s->id;
- msg->n_pid = s->n_pid;
- msg->n_1_pid = s->n_1_pid;
- msg->mpl = s->mpl;
- msg->state = s->state;
- msg->qos = qos_spec_s_to_msg(&s->qs);
+ msg->id = s->id;
+ msg->n_pid = s->n_pid;
+ msg->n_1_pid = s->n_1_pid;
+ msg->mpl = s->mpl;
+ msg->state = s->state;
+ msg->uid = s->uid;
+ msg->mtu = s->mtu;
+ msg->qos = qos_spec_s_to_msg(&s->qs);
if (msg->qos == NULL)
goto fail_msg;
@@ -96,11 +100,15 @@ struct flow_info flow_info_msg_to_s(const flow_info_msg_t * msg)
assert(msg != NULL);
+ memset(&s, 0, sizeof(s));
+
s.id = msg->id;
s.n_pid = msg->n_pid;
s.n_1_pid = msg->n_1_pid;
s.mpl = msg->mpl;
s.state = msg->state;
+ s.uid = msg->uid;
+ s.mtu = msg->mtu;
s.qs = qos_spec_msg_to_s(msg->qos);
return s;
@@ -131,7 +139,7 @@ name_info_msg_t * name_info_s_to_msg(const struct name_info * info)
goto fail_msg;
msg->ckey = strdup(info->c.key);
- if (msg->skey == NULL)
+ if (msg->ckey == NULL)
goto fail_msg;
msg->ccrt = strdup(info->c.crt);
@@ -155,6 +163,8 @@ struct name_info name_info_msg_to_s(const name_info_msg_t * msg)
assert(msg != NULL);
assert(strlen(msg->name) <= NAME_SIZE);
+ memset(&s, 0, sizeof(s));
+
strcpy(s.name, msg->name);
strcpy(s.s.key, msg->skey);
strcpy(s.s.crt, msg->scrt);
@@ -544,7 +554,7 @@ struct udp4_config udp4_config_msg_to_s(const udp4_config_msg_t * msg)
return s;
}
-#define IN6_LEN sizeof(struct in6_addr)
+#define IN6_LEN (size_t) sizeof(struct in6_addr)
udp6_config_msg_t * udp6_config_s_to_msg(const struct udp6_config * s)
{
udp6_config_msg_t * msg;
@@ -749,7 +759,7 @@ qosspec_msg_t * qos_spec_s_to_msg(const struct qos_spec * s)
msg->availability = s->availability;
msg->loss = s->loss;
msg->ber = s->ber;
- msg->in_order = s->in_order;
+ msg->service = s->service;
msg->max_gap = s->max_gap;
msg->timeout = s->timeout;
@@ -767,7 +777,7 @@ struct qos_spec qos_spec_msg_to_s(const qosspec_msg_t * msg)
s.availability = msg->availability;
s.loss = msg->loss;
s.ber = msg->ber;
- s.in_order = msg->in_order;
+ s.service = msg->service;
s.max_gap = msg->max_gap;
s.timeout = msg->timeout;
diff --git a/src/lib/qoscube.c b/src/lib/qoscube.c
index 267b3a87..5d7ae17d 100644
--- a/src/lib/qoscube.c
+++ b/src/lib/qoscube.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Quality of Service cube
*
@@ -29,15 +29,11 @@
qoscube_t qos_spec_to_cube(qosspec_t qs)
{
- if (qs.delay <= qos_voice.delay &&
- qs.bandwidth <= qos_voice.bandwidth &&
- qs.availability >= qos_voice.availability &&
- qs.max_gap <= qos_voice.max_gap)
+ if (qs.delay <= 50 && qs.bandwidth <= 100000
+ && qs.availability >= 5 && qs.max_gap <= 50)
return QOS_CUBE_VOICE;
- else if (qs.delay <= qos_video.delay &&
- qs.bandwidth <= qos_video.bandwidth &&
- qs.availability >= qos_video.availability &&
- qs.max_gap <= qos_video.max_gap)
+ else if (qs.delay <= 100 && qs.availability >= 3
+ && qs.max_gap <= 100)
return QOS_CUBE_VIDEO;
else
return QOS_CUBE_BE;
diff --git a/src/lib/random.c b/src/lib/random.c
index 2dc5f02f..a132f470 100644
--- a/src/lib/random.c
+++ b/src/lib/random.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Pseudo random generator
*
@@ -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,13 +44,28 @@ 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;
#elif defined(HAVE_OPENSSL_RNG)
- if (len > 0 && len < INT_MAX)
- return RAND_bytes((unsigned char *) buf, (int) len);
- return -1;
+ if (len == 0 || len >= INT_MAX)
+ return -1;
+
+ return RAND_bytes((unsigned char *) buf, (int) len) == 1 ? 0 : -1;
#endif
}
diff --git a/src/lib/rib.c b/src/lib/rib.c
index 97a20f47..6e421397 100644
--- a/src/lib/rib.c
+++ b/src/lib/rib.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* RIB export using FUSE
*
@@ -112,14 +112,14 @@ static int rib_read(const char * path,
(void) info;
(void) offset;
- pthread_rwlock_wrlock(&rib.lock);
+ pthread_rwlock_rdlock(&rib.lock);
list_for_each(p, &rib.reg_comps) {
struct reg_comp * r = list_entry(p, struct reg_comp, next);
if (strcmp(comp, r->path) == 0) {
- int ret = r->ops->read(path + 1, buf, size);
+ struct rib_ops * ops = r->ops;
pthread_rwlock_unlock(&rib.lock);
- return ret;
+ return ops->read(path + 1, buf, size);
}
}
@@ -160,19 +160,25 @@ static int rib_readdir(const char * path,
ssize_t len;
ssize_t i;
struct reg_comp * c;
+ struct rib_ops * ops;
c = list_entry(p, struct reg_comp, next);
if (strcmp(path + 1, c->path) != 0)
continue;
- assert(c->ops->readdir != NULL);
+ ops = c->ops;
+
+ assert(ops->readdir != NULL);
+
+ pthread_rwlock_unlock(&rib.lock);
- len = c->ops->readdir(&dir_entries);
+ len = ops->readdir(&dir_entries);
if (len < 0)
- break;
+ return 0;
for (i = 0; i < len; ++i)
filler(buf, dir_entries[i], NULL, 0);
freepp(char, dir_entries, len);
+ return 0;
}
}
diff --git a/src/lib/serdes-irm.c b/src/lib/serdes-irm.c
index 3aea0617..1d9b4dec 100644
--- a/src/lib/serdes-irm.c
+++ b/src/lib/serdes-irm.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros IRM Protocol - serialization/deserialization
*
@@ -24,6 +24,7 @@
#include "config.h"
+#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
#include <ouroboros/serdes-irm.h>
#include <ouroboros/protobuf.h>
@@ -135,14 +136,11 @@ int flow_join__irm_req_ser(buffer_t * buf,
int flow__irm_result_des(buffer_t * buf,
struct flow_info * flow,
- buffer_t * sk)
+ struct crypt_sk * sk)
{
irm_msg_t * msg;
int err;
- if (sk != NULL)
- sk->data = NULL;
-
msg = irm_msg__unpack(NULL, buf->len, buf->data);
if (msg == NULL) {
err = -EIRMD;
@@ -166,12 +164,62 @@ int flow__irm_result_des(buffer_t * buf,
*flow = flow_info_msg_to_s(msg->flow_info);
- if (sk != NULL) {
- sk->len = msg->symmkey.len;
- sk->data = msg->symmkey.data;
+ if (msg->has_cipher_nid)
+ sk->nid = msg->cipher_nid;
+ else
+ sk->nid = NID_undef;
+
+ if (msg->sym_key.len == SYMMKEYSZ)
+ memcpy(sk->key, msg->sym_key.data, SYMMKEYSZ);
+ 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;
+ }
- msg->symmkey.data = NULL;
- msg->symmkey.len = 0;
+ *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);
@@ -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)
{
@@ -289,8 +375,8 @@ int ipcp_create_r__irm_req_ser(buffer_t * buf,
return -ENOMEM;
}
-int proc_announce__irm_req_ser(buffer_t * buf,
- const char * prog)
+int proc_announce__irm_req_ser(buffer_t * buf,
+ const struct proc_info * proc)
{
irm_msg_t * msg;
size_t len;
@@ -303,8 +389,8 @@ int proc_announce__irm_req_ser(buffer_t * buf,
msg->code = IRM_MSG_CODE__IRM_PROC_ANNOUNCE;
msg->has_pid = true;
- msg->pid = getpid();
- msg->prog = strdup(prog);
+ msg->pid = proc->pid;
+ msg->prog = strdup(proc->prog);
if (msg->prog == NULL)
goto fail_msg;
@@ -398,6 +484,56 @@ 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;
+}
+
+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/serdes-oep.c b/src/lib/serdes-oep.c
index 8a836b3b..3d191494 100644
--- a/src/lib/serdes-oep.c
+++ b/src/lib/serdes-oep.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros Enrollment Protocol - serialization/deserialization
*
diff --git a/src/lib/sha3.c b/src/lib/sha3.c
index b9d6b07f..f406124e 100644
--- a/src/lib/sha3.c
+++ b/src/lib/sha3.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* SHA3 algorithm
*
diff --git a/src/lib/shm_rbuff.c b/src/lib/shm_rbuff.c
deleted file mode 100644
index 22cff41c..00000000
--- a/src/lib/shm_rbuff.c
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Ring buffer implementations for incoming packets
- *
- * 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/shm_rbuff.h>
-#include <ouroboros/lockfile.h>
-#include <ouroboros/errno.h>
-#include <ouroboros/fccntl.h>
-#include <ouroboros/pthread.h>
-#include <ouroboros/time.h>
-
-#include <assert.h>
-#include <fcntl.h>
-#include <signal.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-
-#define FN_MAX_CHARS 255
-
-#define SHM_RB_FILE_SIZE ((SHM_RBUFF_SIZE) * sizeof(ssize_t) \
- + 3 * sizeof(size_t) \
- + sizeof(pthread_mutex_t) \
- + 2 * sizeof (pthread_cond_t))
-
-#define shm_rbuff_used(rb) ((*rb->head + (SHM_RBUFF_SIZE) - *rb->tail) \
- & ((SHM_RBUFF_SIZE) - 1))
-#define shm_rbuff_free(rb) (shm_rbuff_used(rb) + 1 < (SHM_RBUFF_SIZE))
-#define shm_rbuff_empty(rb) (*rb->head == *rb->tail)
-#define head_el_ptr(rb) (rb->shm_base + *rb->head)
-#define tail_el_ptr(rb) (rb->shm_base + *rb->tail)
-
-struct shm_rbuff {
- ssize_t * shm_base; /* start of entry */
- size_t * head; /* start of ringbuffer head */
- size_t * tail; /* start of ringbuffer tail */
- size_t * acl; /* access control */
- pthread_mutex_t * lock; /* lock all free space in shm */
- pthread_cond_t * add; /* packet arrived */
- pthread_cond_t * del; /* packet removed */
- pid_t pid; /* pid of the owner */
- int flow_id; /* flow_id of the flow */
-};
-
-#define MM_FLAGS (PROT_READ | PROT_WRITE)
-
-static struct shm_rbuff * rbuff_create(pid_t pid,
- int flow_id,
- int flags)
-{
- struct shm_rbuff * rb;
- int fd;
- ssize_t * shm_base;
- char fn[FN_MAX_CHARS];
-
- sprintf(fn, SHM_RBUFF_PREFIX "%d.%d", pid, flow_id);
-
- rb = malloc(sizeof(*rb));
- if (rb == NULL)
- goto fail_malloc;
-
- fd = shm_open(fn, flags, 0666);
- if (fd == -1)
- goto fail_open;
-
- if ((flags & O_CREAT) && ftruncate(fd, SHM_RB_FILE_SIZE) < 0)
- goto fail_truncate;
-
- shm_base = mmap(NULL, SHM_RB_FILE_SIZE, MM_FLAGS, MAP_SHARED, fd, 0);
- if (shm_base == MAP_FAILED)
- goto fail_truncate;
-
- close(fd);
-
- rb->shm_base = shm_base;
- rb->head = (size_t *) (rb->shm_base + (SHM_RBUFF_SIZE));
- rb->tail = rb->head + 1;
- rb->acl = rb->tail + 1;
- rb->lock = (pthread_mutex_t *) (rb->acl + 1);
- rb->add = (pthread_cond_t *) (rb->lock + 1);
- rb->del = rb->add + 1;
- rb->pid = pid;
- rb->flow_id = flow_id;
-
- return rb;
-
- fail_truncate:
- close(fd);
- if (flags & O_CREAT)
- shm_unlink(fn);
- fail_open:
- free(rb);
- fail_malloc:
- return NULL;
-}
-
-static void rbuff_destroy(struct shm_rbuff * rb)
-{
- munmap(rb->shm_base, SHM_RB_FILE_SIZE);
-
- free(rb);
-}
-
-struct shm_rbuff * shm_rbuff_create(pid_t pid,
- int flow_id)
-{
- struct shm_rbuff * rb;
- pthread_mutexattr_t mattr;
- pthread_condattr_t cattr;
- mode_t mask;
-
- mask = umask(0);
-
- rb = rbuff_create(pid, flow_id, O_CREAT | O_EXCL | O_RDWR);
-
- umask(mask);
-
- if (rb == NULL)
- goto fail_rb;
-
- if (pthread_mutexattr_init(&mattr))
- 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->lock, &mattr))
- goto fail_mutex;
-
- if (pthread_condattr_init(&cattr))
- 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))
- goto fail_add;
-
- if (pthread_cond_init(rb->del, &cattr))
- goto fail_del;
-
- *rb->acl = ACL_RDWR;
- *rb->head = 0;
- *rb->tail = 0;
-
- rb->pid = pid;
- rb->flow_id = flow_id;
-
- pthread_mutexattr_destroy(&mattr);
- pthread_condattr_destroy(&cattr);
-
- return rb;
-
- fail_del:
- pthread_cond_destroy(rb->add);
- fail_add:
- pthread_condattr_destroy(&cattr);
- fail_cattr:
- pthread_mutex_destroy(rb->lock);
- fail_mutex:
- pthread_mutexattr_destroy(&mattr);
- fail_mattr:
- shm_rbuff_destroy(rb);
- fail_rb:
- return NULL;
-}
-
-struct shm_rbuff * shm_rbuff_open(pid_t pid,
- int flow_id)
-{
- return rbuff_create(pid, flow_id, O_RDWR);
-}
-
-void shm_rbuff_close(struct shm_rbuff * rb)
-{
- assert(rb);
-
- rbuff_destroy(rb);
-}
-
-#if (defined(SHM_RBUFF_LOCKLESS) && \
- (defined(__GNUC__) || defined (__clang__)))
-#include "shm_rbuff_ll.c"
-#else
-#include "shm_rbuff_pthr.c"
-#endif
diff --git a/src/lib/shm_rbuff_ll.c b/src/lib/shm_rbuff_ll.c
deleted file mode 100644
index 46a5314e..00000000
--- a/src/lib/shm_rbuff_ll.c
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Lockless ring buffer for incoming packets
- *
- * 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 RB_HEAD __sync_fetch_and_add(rb->head, 0)
-#define RB_TAIL __sync_fetch_and_add(rb->tail, 0)
-
-void shm_rbuff_destroy(struct shm_rbuff * rb)
-{
- char fn[FN_MAX_CHARS];
-
- assert(rb);
-
- sprintf(fn, SHM_RBUFF_PREFIX "%d.%d", rb->pid, rb->flow_id);
-
- __sync_bool_compare_and_swap(rb->acl, *rb->acl, ACL_FLOWDOWN);
-
- pthread_cond_broadcast(rb->del);
- pthread_cond_broadcast(rb->add);
-
- shm_rbuff_close(rb);
-
- shm_unlink(fn);
-}
-
-int shm_rbuff_write(struct shm_rbuff * rb,
- size_t idx)
-{
- size_t ohead;
- size_t nhead;
- bool was_empty = false;
-
- assert(rb);
- assert(idx < SHM_BUFFER_SIZE);
-
- if (__sync_fetch_and_add(rb->acl, 0) != ACL_RDWR) {
- if (__sync_fetch_and_add(rb->acl, 0) & ACL_FLOWDOWN)
- return -EFLOWDOWN;
- else if (__sync_fetch_and_add(rb->acl, 0) & ACL_RDONLY)
- return -ENOTALLOC;
- }
-
- if (!shm_rbuff_free(rb))
- return -EAGAIN;
-
- if (shm_rbuff_empty(rb))
- was_empty = true;
-
- nhead = RB_HEAD;
-
- *(rb->shm_base + nhead) = (ssize_t) idx;
-
- do {
- ohead = nhead;
- nhead = (ohead + 1) & ((SHM_RBUFF_SIZE) - 1);
- nhead = __sync_val_compare_and_swap(rb->head, ohead, nhead);
- } while (nhead != ohead);
-
- if (was_empty)
- pthread_cond_broadcast(rb->add);
-
- return 0;
-}
-
-/* FIXME: this is a copy of the pthr implementation */
-int shm_rbuff_write_b(struct shm_rbuff * rb,
- size_t idx,
- const struct timespec * abstime)
-{
- int ret = 0;
-
- assert(rb);
- assert(idx < SHM_BUFFER_SIZE);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- if (*rb->acl != ACL_RDWR) {
- if (*rb->acl & ACL_FLOWDOWN)
- ret = -EFLOWDOWN;
- else if (*rb->acl & ACL_RDONLY)
- ret = -ENOTALLOC;
- goto err;
- }
-
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (!shm_rbuff_free(rb) && ret != -ETIMEDOUT) {
- ret = -__timedwait(rb->add, rb->lock, abstime);
-#ifdef HAVE_ROBUST_MUTEX
- if (ret == -EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- }
-
- if (shm_rbuff_empty(rb))
- pthread_cond_broadcast(rb->add);
-
- if (ret != -ETIMEDOUT) {
- *head_el_ptr(rb) = (ssize_t) idx;
- *rb->head = (*rb->head + 1) & ((SHM_RBUFF_SIZE) -1);
- }
-
- pthread_cleanup_pop(true);
-
- return ret;
- err:
- pthread_mutex_unlock(rb->lock);
- return ret;
-}
-
-ssize_t shm_rbuff_read(struct shm_rbuff * rb)
-{
- size_t otail;
- size_t ntail;
-
- assert(rb);
-
- if (shm_rbuff_empty(rb)) {
- if (_sync_fetch_and_add(rb->acl, 0) & ACL_FLOWDOWN)
- return -EFLOWDOWN;
-
- if (_sync_fetch_and_add(rb->acl, 0) & ACL_FLOWPEER)
- return -EFLOWPEER;
-
- return -EAGAIN;
- }
-
- ntail = RB_TAIL;
-
- do {
- otail = ntail;
- ntail = (otail + 1) & ((SHM_RBUFF_SIZE) - 1);
- ntail = __sync_val_compare_and_swap(rb->tail, otail, ntail);
- } while (ntail != otail);
-
- pthread_cond_broadcast(rb->del);
-
- return *(rb->shm_base + ntail);
-}
-
-ssize_t shm_rbuff_read_b(struct shm_rbuff * rb,
- const struct timespec * abstime)
-{
- ssize_t idx = -1;
-
- assert(rb);
-
- /* try a non-blocking read first */
- idx = shm_rbuff_read(rb);
- if (idx != -EAGAIN)
- return idx;
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (shm_rbuff_empty(rb) && (idx != -ETIMEDOUT)) {
- idx = -__timedwait(rb->add, rb->lock, abstime);
-#ifdef HAVE_ROBUST_MUTEX
- if (idx == -EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- }
-
- if (idx != -ETIMEDOUT) {
- /* do a nonblocking read */
- idx = shm_rbuff_read(rb);
- assert(idx >= 0);
- }
-
- pthread_cleanup_pop(true);
-
- return idx;
-}
-
-void shm_rbuff_set_acl(struct shm_rbuff * rb,
- uint32_t flags)
-{
- assert(rb);
-
- __sync_bool_compare_and_swap(rb->acl, *rb->acl, flags);
-}
-
-uint32_t shm_rbuff_get_acl(struct shm_rbuff * rb)
-{
- assert(rb);
-
- return __sync_fetch_and_add(rb->acl, 0);
-}
-
-void shm_rbuff_fini(struct shm_rbuff * rb)
-{
- assert(rb);
-
- if (shm_rbuff_empty(rb))
- return;
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (!shm_rbuff_empty(rb))
-#ifndef HAVE_ROBUST_MUTEX
- pthread_cond_wait(rb->del, rb->lock);
-#else
- if (pthread_cond_wait(rb->del, rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- pthread_cleanup_pop(true);
-}
-
-size_t shm_rbuff_queued(struct shm_rbuff * rb)
-{
- assert(rb);
-
- return shm_rbuff_used(rb);
-}
diff --git a/src/lib/shm_rbuff_pthr.c b/src/lib/shm_rbuff_pthr.c
deleted file mode 100644
index b543fb07..00000000
--- a/src/lib/shm_rbuff_pthr.c
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Ring buffer for incoming packets
- *
- * 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/.
- */
-
-void shm_rbuff_destroy(struct shm_rbuff * rb)
-{
- char fn[FN_MAX_CHARS];
-
- assert(rb != NULL);
-
-#ifdef CONFIG_OUROBOROS_DEBUG
- pthread_mutex_lock(rb->lock);
-
- *rb->acl = *rb->acl & ACL_FLOWDOWN;
-
- pthread_cond_broadcast(rb->del);
- pthread_cond_broadcast(rb->add);
-
- pthread_mutex_unlock(rb->lock);
-#endif
- sprintf(fn, SHM_RBUFF_PREFIX "%d.%d", rb->pid, rb->flow_id);
-
- shm_rbuff_close(rb);
-
- shm_unlink(fn);
-}
-
-int shm_rbuff_write(struct shm_rbuff * rb,
- size_t idx)
-{
- int ret = 0;
-
- assert(rb != NULL);
- assert(idx < SHM_BUFFER_SIZE);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- if (*rb->acl != ACL_RDWR) {
- if (*rb->acl & ACL_FLOWDOWN)
- ret = -EFLOWDOWN;
- else if (*rb->acl & ACL_RDONLY)
- ret = -ENOTALLOC;
- goto err;
- }
-
- if (!shm_rbuff_free(rb)) {
- ret = -EAGAIN;
- goto err;
- }
-
- if (shm_rbuff_empty(rb))
- pthread_cond_broadcast(rb->add);
-
- *head_el_ptr(rb) = (ssize_t) idx;
- *rb->head = (*rb->head + 1) & ((SHM_RBUFF_SIZE) - 1);
-
- pthread_mutex_unlock(rb->lock);
-
- return 0;
- err:
- pthread_mutex_unlock(rb->lock);
- return ret;
-}
-
-int shm_rbuff_write_b(struct shm_rbuff * rb,
- size_t idx,
- const struct timespec * abstime)
-{
- int ret = 0;
-
- assert(rb != NULL);
- assert(idx < SHM_BUFFER_SIZE);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- if (*rb->acl != ACL_RDWR) {
- if (*rb->acl & ACL_FLOWDOWN)
- ret = -EFLOWDOWN;
- else if (*rb->acl & ACL_RDONLY)
- ret = -ENOTALLOC;
- goto err;
- }
-
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (!shm_rbuff_free(rb)
- && ret != -ETIMEDOUT
- && !(*rb->acl & ACL_FLOWDOWN)) {
- ret = -__timedwait(rb->del, rb->lock, abstime);
-#ifdef HAVE_ROBUST_MUTEX
- if (ret == -EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- }
-
- if (ret != -ETIMEDOUT) {
- if (shm_rbuff_empty(rb))
- pthread_cond_broadcast(rb->add);
- *head_el_ptr(rb) = (ssize_t) idx;
- *rb->head = (*rb->head + 1) & ((SHM_RBUFF_SIZE) - 1);
- }
-
- pthread_cleanup_pop(true);
-
- return ret;
- err:
- pthread_mutex_unlock(rb->lock);
- return ret;
-}
-
-static int check_rb_acl(struct shm_rbuff * rb)
-{
- assert(rb != NULL);
-
- if (*rb->acl & ACL_FLOWDOWN)
- return -EFLOWDOWN;
-
- if (*rb->acl & ACL_FLOWPEER)
- return -EFLOWPEER;
-
- return -EAGAIN;
-}
-
-ssize_t shm_rbuff_read(struct shm_rbuff * rb)
-{
- ssize_t ret = 0;
-
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- if (shm_rbuff_empty(rb)) {
- ret = check_rb_acl(rb);
- pthread_mutex_unlock(rb->lock);
- return ret;
- }
-
- ret = *tail_el_ptr(rb);
- *rb->tail = (*rb->tail + 1) & ((SHM_RBUFF_SIZE) - 1);
- pthread_cond_broadcast(rb->del);
-
- pthread_mutex_unlock(rb->lock);
-
- return ret;
-}
-
-ssize_t shm_rbuff_read_b(struct shm_rbuff * rb,
- const struct timespec * abstime)
-{
- ssize_t idx = -1;
-
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- if (shm_rbuff_empty(rb) && (*rb->acl & ACL_FLOWDOWN)) {
- pthread_mutex_unlock(rb->lock);
- return -EFLOWDOWN;
- }
-
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (shm_rbuff_empty(rb) &&
- idx != -ETIMEDOUT &&
- check_rb_acl(rb) == -EAGAIN) {
- idx = -__timedwait(rb->add, rb->lock, abstime);
-#ifdef HAVE_ROBUST_MUTEX
- if (idx == -EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- }
-
- if (!shm_rbuff_empty(rb)) {
- idx = *tail_el_ptr(rb);
- *rb->tail = (*rb->tail + 1) & ((SHM_RBUFF_SIZE) - 1);
- pthread_cond_broadcast(rb->del);
- } else if (idx != -ETIMEDOUT) {
- idx = check_rb_acl(rb);
- }
-
- pthread_cleanup_pop(true);
-
- assert(idx != -EAGAIN);
-
- return idx;
-}
-
-void shm_rbuff_set_acl(struct shm_rbuff * rb,
- uint32_t flags)
-{
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- *rb->acl = (size_t) flags;
-
- pthread_cond_broadcast(rb->del);
- pthread_cond_broadcast(rb->add);
-
- pthread_mutex_unlock(rb->lock);
-}
-
-uint32_t shm_rbuff_get_acl(struct shm_rbuff * rb)
-{
- uint32_t flags;
-
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- flags = (uint32_t) *rb->acl;
-
- pthread_mutex_unlock(rb->lock);
-
- return flags;
-}
-
-void shm_rbuff_fini(struct shm_rbuff * rb)
-{
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->lock);
-
- while (!shm_rbuff_empty(rb))
-#ifndef HAVE_ROBUST_MUTEX
- pthread_cond_wait(rb->del, rb->lock);
-#else
- if (pthread_cond_wait(rb->del, rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
- pthread_cleanup_pop(true);
-}
-
-size_t shm_rbuff_queued(struct shm_rbuff * rb)
-{
- size_t ret;
-
- assert(rb != NULL);
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rb->lock);
-#else
- if (pthread_mutex_lock(rb->lock) == EOWNERDEAD)
- pthread_mutex_consistent(rb->lock);
-#endif
-
- ret = shm_rbuff_used(rb);
-
- pthread_mutex_unlock(rb->lock);
-
- return ret;
-}
diff --git a/src/lib/shm_rdrbuff.c b/src/lib/shm_rdrbuff.c
deleted file mode 100644
index 7ad1bd2e..00000000
--- a/src/lib/shm_rdrbuff.c
+++ /dev/null
@@ -1,610 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Random Deletion Ring Buffer for Data Units
- *
- * 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/errno.h>
-#include <ouroboros/pthread.h>
-#include <ouroboros/shm_rdrbuff.h>
-
-#include <assert.h>
-#include <fcntl.h>
-#include <signal.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-
-#define SHM_BLOCKS_SIZE ((SHM_BUFFER_SIZE) * SHM_RDRB_BLOCK_SIZE)
-#define SHM_FILE_SIZE (SHM_BLOCKS_SIZE + 2 * sizeof(size_t) \
- + sizeof(pthread_mutex_t) + 2 * sizeof(pthread_cond_t) \
- + sizeof(pid_t))
-#define DU_BUFF_OVERHEAD (DU_BUFF_HEADSPACE + DU_BUFF_TAILSPACE)
-
-#define get_head_ptr(rdrb) \
- idx_to_du_buff_ptr(rdrb, *rdrb->head)
-
-#define get_tail_ptr(rdrb) \
- idx_to_du_buff_ptr(rdrb, *rdrb->tail)
-
-#define idx_to_du_buff_ptr(rdrb, idx) \
- ((struct shm_du_buff *) (rdrb->shm_base + idx * SHM_RDRB_BLOCK_SIZE))
-
-#define shm_rdrb_used(rdrb) \
- (((*rdrb->head + (SHM_BUFFER_SIZE) - *rdrb->tail) + 1) \
- & ((SHM_BUFFER_SIZE) - 1))
-
-#define shm_rdrb_free(rdrb, i) \
- (shm_rdrb_used(rdrb) + i < (SHM_BUFFER_SIZE))
-
-#define shm_rdrb_empty(rdrb) \
- (*rdrb->tail == *rdrb->head)
-
-struct shm_du_buff {
- size_t size;
-#ifdef SHM_RDRB_MULTI_BLOCK
- size_t blocks;
-#endif
- size_t du_head;
- size_t du_tail;
- size_t refs;
- size_t idx;
-};
-
-struct shm_rdrbuff {
- uint8_t * shm_base; /* start of blocks */
- size_t * head; /* start of ringbuffer head */
- size_t * tail; /* start of ringbuffer tail */
- pthread_mutex_t * lock; /* lock all free space in shm */
- pthread_cond_t * healthy; /* flag when packet is read */
- pid_t * pid; /* pid of the irmd owner */
-};
-
-static void garbage_collect(struct shm_rdrbuff * rdrb)
-{
-#ifdef SHM_RDRB_MULTI_BLOCK
- struct shm_du_buff * sdb;
- while (!shm_rdrb_empty(rdrb) &&
- (sdb = get_tail_ptr(rdrb))->refs == 0)
- *rdrb->tail = (*rdrb->tail + sdb->blocks)
- & ((SHM_BUFFER_SIZE) - 1);
-#else
- while (!shm_rdrb_empty(rdrb) && get_tail_ptr(rdrb)->refs == 0)
- *rdrb->tail = (*rdrb->tail + 1) & ((SHM_BUFFER_SIZE) - 1);
-#endif
- pthread_cond_broadcast(rdrb->healthy);
-}
-
-#ifdef HAVE_ROBUST_MUTEX
-static void sanitize(struct shm_rdrbuff * rdrb)
-{
- --get_head_ptr(rdrb)->refs;
- garbage_collect(rdrb);
- pthread_mutex_consistent(rdrb->lock);
-}
-#endif
-
-static char * rdrb_filename(void)
-{
- char * str;
-
- str = malloc(strlen(SHM_RDRB_NAME) + 1);
- if (str == NULL)
- return NULL;
-
- sprintf(str, "%s", SHM_RDRB_NAME);
-
- return str;
-}
-
-void shm_rdrbuff_close(struct shm_rdrbuff * rdrb)
-{
- assert(rdrb);
-
- munmap(rdrb->shm_base, SHM_FILE_SIZE);
- free(rdrb);
-}
-
-void shm_rdrbuff_destroy(struct shm_rdrbuff * rdrb)
-{
- char * shm_rdrb_fn;
-
- assert(rdrb);
-
- if (getpid() != *rdrb->pid && kill(*rdrb->pid, 0) == 0) {
- free(rdrb);
- return;
- }
-
- shm_rdrbuff_close(rdrb);
-
- shm_rdrb_fn = rdrb_filename();
- if (shm_rdrb_fn == NULL)
- return;
-
- shm_unlink(shm_rdrb_fn);
- free(shm_rdrb_fn);
-}
-
-#define MM_FLAGS (PROT_READ | PROT_WRITE)
-
-static struct shm_rdrbuff * rdrb_create(int flags)
-{
- struct shm_rdrbuff * rdrb;
- int fd;
- uint8_t * shm_base;
- char * shm_rdrb_fn;
-
- shm_rdrb_fn = rdrb_filename();
- if (shm_rdrb_fn == NULL)
- goto fail_fn;
-
- rdrb = malloc(sizeof *rdrb);
- if (rdrb == NULL)
- goto fail_rdrb;
-
- fd = shm_open(shm_rdrb_fn, flags, 0666);
- if (fd == -1)
- goto fail_open;
-
- if ((flags & O_CREAT) && ftruncate(fd, SHM_FILE_SIZE) < 0)
- goto fail_truncate;
-
- shm_base = mmap(NULL, SHM_FILE_SIZE, MM_FLAGS, MAP_SHARED, fd, 0);
- if (shm_base == MAP_FAILED)
- goto fail_truncate;
-
- close(fd);
-
- rdrb->shm_base = shm_base;
- rdrb->head = (size_t *) ((uint8_t *) rdrb->shm_base + SHM_BLOCKS_SIZE);
- rdrb->tail = rdrb->head + 1;
- rdrb->lock = (pthread_mutex_t *) (rdrb->tail + 1);
- rdrb->healthy = (pthread_cond_t *) (rdrb->lock + 1);
- rdrb->pid = (pid_t *) (rdrb->healthy + 1);
-
- free(shm_rdrb_fn);
-
- return rdrb;
-
- fail_truncate:
- close(fd);
- if (flags & O_CREAT)
- shm_unlink(shm_rdrb_fn);
- fail_open:
- free(rdrb);
- fail_rdrb:
- free(shm_rdrb_fn);
- fail_fn:
- return NULL;
-}
-
-struct shm_rdrbuff * shm_rdrbuff_create(void)
-{
- struct shm_rdrbuff * rdrb;
- mode_t mask;
- pthread_mutexattr_t mattr;
- pthread_condattr_t cattr;
-
- mask = umask(0);
-
- rdrb = rdrb_create(O_CREAT | O_EXCL | O_RDWR);
-
- umask(mask);
-
- if (rdrb == NULL)
- goto fail_rdrb;
-
- if (pthread_mutexattr_init(&mattr))
- 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(rdrb->lock, &mattr))
- goto fail_mutex;
-
- if (pthread_condattr_init(&cattr))
- goto fail_cattr;
-
- pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
-#ifndef __APPLE__
- pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
-#endif
- if (pthread_cond_init(rdrb->healthy, &cattr))
- goto fail_healthy;
-
- *rdrb->head = 0;
- *rdrb->tail = 0;
-
- *rdrb->pid = getpid();
-
- pthread_mutexattr_destroy(&mattr);
- pthread_condattr_destroy(&cattr);
-
- return rdrb;
-
- fail_healthy:
- pthread_condattr_destroy(&cattr);
- fail_cattr:
- pthread_mutex_destroy(rdrb->lock);
- fail_mutex:
- pthread_mutexattr_destroy(&mattr);
- fail_mattr:
- shm_rdrbuff_destroy(rdrb);
- fail_rdrb:
- return NULL;
-}
-
-struct shm_rdrbuff * shm_rdrbuff_open(void)
-{
- return rdrb_create(O_RDWR);
-}
-
-void shm_rdrbuff_purge(void)
-{
- char * shm_rdrb_fn;
-
- shm_rdrb_fn = rdrb_filename();
- if (shm_rdrb_fn == NULL)
- return;
-
- shm_unlink(shm_rdrb_fn);
- free(shm_rdrb_fn);
-}
-
-ssize_t shm_rdrbuff_alloc(struct shm_rdrbuff * rdrb,
- size_t len,
- uint8_t ** ptr,
- struct shm_du_buff ** psdb)
-{
- struct shm_du_buff * sdb;
- size_t size = DU_BUFF_OVERHEAD + len;
-#ifdef SHM_RDRB_MULTI_BLOCK
- size_t blocks = 0;
- size_t padblocks = 0;
-#endif
- ssize_t sz = size + sizeof(*sdb);
-
- assert(rdrb);
- assert(psdb);
-
-#ifndef SHM_RDRB_MULTI_BLOCK
- if (sz > SHM_RDRB_BLOCK_SIZE)
- return -EMSGSIZE;
-#else
- while (sz > 0) {
- sz -= SHM_RDRB_BLOCK_SIZE;
- ++blocks;
- }
-#endif
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rdrb->lock);
-#else
- if (pthread_mutex_lock(rdrb->lock) == EOWNERDEAD)
- sanitize(rdrb);
-#endif
-#ifdef SHM_RDRB_MULTI_BLOCK
- if (blocks + *rdrb->head > (SHM_BUFFER_SIZE))
- padblocks = (SHM_BUFFER_SIZE) - *rdrb->head;
-
- if (!shm_rdrb_free(rdrb, blocks + padblocks)) {
-#else
- if (!shm_rdrb_free(rdrb, 1)) {
-#endif
- pthread_mutex_unlock(rdrb->lock);
- return -EAGAIN;
- }
-
-#ifdef SHM_RDRB_MULTI_BLOCK
- if (padblocks) {
- sdb = get_head_ptr(rdrb);
- sdb->size = 0;
- sdb->blocks = padblocks;
- sdb->refs = 0;
- sdb->du_head = 0;
- sdb->du_tail = 0;
- sdb->idx = *rdrb->head;
-
- *rdrb->head = 0;
- }
-#endif
- sdb = get_head_ptr(rdrb);
- sdb->refs = 1;
- sdb->idx = *rdrb->head;
-#ifdef SHM_RDRB_MULTI_BLOCK
- sdb->blocks = blocks;
-
- *rdrb->head = (*rdrb->head + blocks) & ((SHM_BUFFER_SIZE) - 1);
-#else
- *rdrb->head = (*rdrb->head + 1) & ((SHM_BUFFER_SIZE) - 1);
-#endif
- pthread_mutex_unlock(rdrb->lock);
-
- sdb->size = size;
- sdb->du_head = DU_BUFF_HEADSPACE;
- sdb->du_tail = sdb->du_head + len;
-
- *psdb = sdb;
- if (ptr != NULL)
- *ptr = (uint8_t *) (sdb + 1) + sdb->du_head;
-
- return sdb->idx;
-}
-
-ssize_t shm_rdrbuff_alloc_b(struct shm_rdrbuff * rdrb,
- size_t len,
- uint8_t ** ptr,
- struct shm_du_buff ** psdb,
- const struct timespec * abstime)
-{
- struct shm_du_buff * sdb;
- size_t size = DU_BUFF_OVERHEAD + len;
-#ifdef SHM_RDRB_MULTI_BLOCK
- size_t blocks = 0;
- size_t padblocks = 0;
-#endif
- ssize_t sz = size + sizeof(*sdb);
- int ret = 0;
-
- assert(rdrb);
- assert(psdb);
-
-#ifndef SHM_RDRB_MULTI_BLOCK
- if (sz > SHM_RDRB_BLOCK_SIZE)
- return -EMSGSIZE;
-#else
- while (sz > 0) {
- sz -= SHM_RDRB_BLOCK_SIZE;
- ++blocks;
- }
-#endif
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rdrb->lock);
-#else
- if (pthread_mutex_lock(rdrb->lock) == EOWNERDEAD)
- sanitize(rdrb);
-#endif
- pthread_cleanup_push(__cleanup_mutex_unlock, rdrb->lock);
-
-#ifdef SHM_RDRB_MULTI_BLOCK
- if (blocks + *rdrb->head > (SHM_BUFFER_SIZE))
- padblocks = (SHM_BUFFER_SIZE) - *rdrb->head;
-
- while (!shm_rdrb_free(rdrb, blocks + padblocks) && ret != ETIMEDOUT) {
-#else
- while (!shm_rdrb_free(rdrb, 1) && ret != ETIMEDOUT) {
-#endif
- ret = __timedwait(rdrb->healthy, rdrb->lock, abstime);
-#ifdef SHM_RDRB_MULTI_BLOCK
- if (blocks + *rdrb->head > (SHM_BUFFER_SIZE))
- padblocks = (SHM_BUFFER_SIZE) - *rdrb->head;
-#endif
- }
-
- if (ret != ETIMEDOUT) {
-#ifdef SHM_RDRB_MULTI_BLOCK
- if (padblocks) {
- sdb = get_head_ptr(rdrb);
- sdb->size = 0;
- sdb->blocks = padblocks;
- sdb->refs = 0;
- sdb->du_head = 0;
- sdb->du_tail = 0;
- sdb->idx = *rdrb->head;
-
- *rdrb->head = 0;
- }
-#endif
- sdb = get_head_ptr(rdrb);
- sdb->refs = 1;
- sdb->idx = *rdrb->head;
-#ifdef SHM_RDRB_MULTI_BLOCK
- sdb->blocks = blocks;
-
- *rdrb->head = (*rdrb->head + blocks) & ((SHM_BUFFER_SIZE) - 1);
-#else
- *rdrb->head = (*rdrb->head + 1) & ((SHM_BUFFER_SIZE) - 1);
-#endif
- }
-
- pthread_cleanup_pop(true);
-
- if (ret == ETIMEDOUT)
- return -ETIMEDOUT;
-
- sdb->size = size;
- sdb->du_head = DU_BUFF_HEADSPACE;
- sdb->du_tail = sdb->du_head + len;
-
- *psdb = sdb;
- if (ptr != NULL)
- *ptr = (uint8_t *) (sdb + 1) + sdb->du_head;
-
- return sdb->idx;
-}
-
-ssize_t shm_rdrbuff_read(uint8_t ** dst,
- struct shm_rdrbuff * rdrb,
- size_t idx)
-{
- struct shm_du_buff * sdb;
-
- assert(dst);
- assert(rdrb);
- assert(idx < (SHM_BUFFER_SIZE));
-
- sdb = idx_to_du_buff_ptr(rdrb, idx);
- *dst = ((uint8_t *) (sdb + 1)) + sdb->du_head;
-
- return (ssize_t) (sdb->du_tail - sdb->du_head);
-}
-
-struct shm_du_buff * shm_rdrbuff_get(struct shm_rdrbuff * rdrb,
- size_t idx)
-{
- assert(rdrb);
- assert(idx < (SHM_BUFFER_SIZE));
-
- return idx_to_du_buff_ptr(rdrb, idx);
-}
-
-int shm_rdrbuff_remove(struct shm_rdrbuff * rdrb,
- size_t idx)
-{
- struct shm_du_buff * sdb;
-
- assert(rdrb);
- assert(idx < (SHM_BUFFER_SIZE));
-
-#ifndef HAVE_ROBUST_MUTEX
- pthread_mutex_lock(rdrb->lock);
-#else
- if (pthread_mutex_lock(rdrb->lock) == EOWNERDEAD)
- sanitize(rdrb);
-#endif
- /* assert(!shm_rdrb_empty(rdrb)); */
-
- sdb = idx_to_du_buff_ptr(rdrb, idx);
-
- if (sdb->refs == 1) { /* only stack needs it, can be removed */
- sdb->refs = 0;
- if (idx == *rdrb->tail)
- garbage_collect(rdrb);
- }
-
- pthread_mutex_unlock(rdrb->lock);
-
- return 0;
-}
-
-size_t shm_du_buff_get_idx(struct shm_du_buff * sdb)
-{
- assert(sdb);
-
- return sdb->idx;
-}
-
-uint8_t * shm_du_buff_head(struct shm_du_buff * sdb)
-{
- assert(sdb);
-
- return (uint8_t *) (sdb + 1) + sdb->du_head;
-}
-
-uint8_t * shm_du_buff_tail(struct shm_du_buff * sdb)
-{
- assert(sdb);
-
- return (uint8_t *) (sdb + 1) + sdb->du_tail;
-}
-
-size_t shm_du_buff_len(struct shm_du_buff * sdb)
-{
- assert(sdb);
-
- return sdb->du_tail - sdb->du_head;
-}
-
-uint8_t * shm_du_buff_head_alloc(struct shm_du_buff * sdb,
- size_t size)
-{
- assert(sdb);
-
- if (sdb->du_head < size)
- return NULL;
-
- sdb->du_head -= size;
-
- return (uint8_t *) (sdb + 1) + sdb->du_head;
-}
-
-uint8_t * shm_du_buff_tail_alloc(struct shm_du_buff * sdb,
- size_t size)
-{
- uint8_t * buf;
-
- assert(sdb);
-
- if (sdb->du_tail + size >= sdb->size)
- return NULL;
-
- buf = (uint8_t *) (sdb + 1) + sdb->du_tail;
-
- sdb->du_tail += size;
-
- return buf;
-}
-
-uint8_t * shm_du_buff_head_release(struct shm_du_buff * sdb,
- size_t size)
-{
- uint8_t * buf;
-
- assert(sdb);
- assert(!(size > sdb->du_tail - sdb->du_head));
-
- buf = (uint8_t *) (sdb + 1) + sdb->du_head;
-
- sdb->du_head += size;
-
- return buf;
-}
-
-uint8_t * shm_du_buff_tail_release(struct shm_du_buff * sdb,
- size_t size)
-{
- assert(sdb);
- assert(!(size > sdb->du_tail - sdb->du_head));
-
- sdb->du_tail -= size;
-
- return (uint8_t *) (sdb + 1) + sdb->du_tail;
-}
-
-void shm_du_buff_truncate(struct shm_du_buff * sdb,
- size_t len)
-{
- assert(sdb);
- assert(len <= sdb->size);
-
- sdb->du_tail = sdb->du_head + len;
-}
-
-int shm_du_buff_wait_ack(struct shm_du_buff * sdb)
-{
- __sync_add_and_fetch(&sdb->refs, 1);
-
- return 0;
-}
-
-int shm_du_buff_ack(struct shm_du_buff * sdb)
-{
- __sync_sub_and_fetch(&sdb->refs, 1);
- return 0;
-}
diff --git a/src/lib/sockets.c b/src/lib/sockets.c
index 5dfbcb5c..46586911 100644
--- a/src/lib/sockets.c
+++ b/src/lib/sockets.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The sockets layer to communicate between daemons
*
diff --git a/src/lib/shm_flow_set.c b/src/lib/ssm/flow_set.c
index 39913fd1..2e33b408 100644
--- a/src/lib/shm_flow_set.c
+++ b/src/lib/ssm/flow_set.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Management of flow_sets for fqueue
*
@@ -23,11 +23,12 @@
#define _POSIX_C_SOURCE 200809L
#include "config.h"
+#include "ssm.h"
#include <ouroboros/errno.h>
#include <ouroboros/lockfile.h>
#include <ouroboros/pthread.h>
-#include <ouroboros/shm_flow_set.h>
+#include <ouroboros/ssm_flow_set.h>
#include <ouroboros/time.h>
#include <assert.h>
@@ -54,17 +55,17 @@
#define FN_MAX_CHARS 255
#define FS_PROT (PROT_READ | PROT_WRITE)
-#define QUEUESIZE ((SHM_BUFFER_SIZE) * sizeof(struct flowevent))
+#define QUEUESIZE ((SSM_RBUFF_SIZE) * sizeof(struct flowevent))
-#define SHM_FSET_FILE_SIZE (SYS_MAX_FLOWS * sizeof(ssize_t) \
- + PROG_MAX_FQUEUES * sizeof(size_t) \
- + PROG_MAX_FQUEUES * sizeof(pthread_cond_t) \
- + PROG_MAX_FQUEUES * QUEUESIZE \
+#define SSM_FSET_FILE_SIZE (SYS_MAX_FLOWS * sizeof(ssize_t) \
+ + PROC_MAX_FQUEUES * sizeof(size_t) \
+ + PROC_MAX_FQUEUES * sizeof(pthread_cond_t) \
+ + PROC_MAX_FQUEUES * QUEUESIZE \
+ sizeof(pthread_mutex_t))
-#define fqueue_ptr(fs, idx) (fs->fqueues + (SHM_BUFFER_SIZE) * idx)
+#define fqueue_ptr(fs, idx) (fs->fqueues + (SSM_RBUFF_SIZE) * idx)
-struct shm_flow_set {
+struct ssm_flow_set {
ssize_t * mtable;
size_t * heads;
pthread_cond_t * conds;
@@ -74,15 +75,15 @@ struct shm_flow_set {
pid_t pid;
};
-static struct shm_flow_set * flow_set_create(pid_t pid,
+static struct ssm_flow_set * flow_set_create(pid_t pid,
int oflags)
{
- struct shm_flow_set * set;
+ struct ssm_flow_set * set;
ssize_t * shm_base;
char fn[FN_MAX_CHARS];
int fd;
- sprintf(fn, SHM_FLOW_SET_PREFIX "%d", pid);
+ sprintf(fn, SSM_FLOW_SET_PREFIX "%d", pid);
set = malloc(sizeof(*set));
if (set == NULL)
@@ -92,10 +93,10 @@ static struct shm_flow_set * flow_set_create(pid_t pid,
if (fd == -1)
goto fail_shm_open;
- if ((oflags & O_CREAT) && ftruncate(fd, SHM_FSET_FILE_SIZE) < 0)
+ if ((oflags & O_CREAT) && ftruncate(fd, SSM_FSET_FILE_SIZE) < 0)
goto fail_truncate;
- shm_base = mmap(NULL, SHM_FSET_FILE_SIZE, FS_PROT, MAP_SHARED, fd, 0);
+ shm_base = mmap(NULL, SSM_FSET_FILE_SIZE, FS_PROT, MAP_SHARED, fd, 0);
if (shm_base == MAP_FAILED)
goto fail_mmap;
@@ -103,10 +104,10 @@ static struct shm_flow_set * flow_set_create(pid_t pid,
set->mtable = shm_base;
set->heads = (size_t *) (set->mtable + SYS_MAX_FLOWS);
- set->conds = (pthread_cond_t *)(set->heads + PROG_MAX_FQUEUES);
- set->fqueues = (struct flowevent *) (set->conds + PROG_MAX_FQUEUES);
+ set->conds = (pthread_cond_t *)(set->heads + PROC_MAX_FQUEUES);
+ set->fqueues = (struct flowevent *) (set->conds + PROC_MAX_FQUEUES);
set->lock = (pthread_mutex_t *)
- (set->fqueues + PROG_MAX_FQUEUES * (SHM_BUFFER_SIZE));
+ (set->fqueues + PROC_MAX_FQUEUES * (SSM_RBUFF_SIZE));
return set;
@@ -121,9 +122,9 @@ static struct shm_flow_set * flow_set_create(pid_t pid,
return NULL;
}
-struct shm_flow_set * shm_flow_set_create(pid_t pid)
+struct ssm_flow_set * ssm_flow_set_create(pid_t pid)
{
- struct shm_flow_set * set;
+ struct ssm_flow_set * set;
pthread_mutexattr_t mattr;
pthread_condattr_t cattr;
mode_t mask;
@@ -163,7 +164,7 @@ struct shm_flow_set * shm_flow_set_create(pid_t pid)
if (pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK))
goto fail_condattr_set;
#endif
- for (i = 0; i < PROG_MAX_FQUEUES; ++i) {
+ for (i = 0; i < PROC_MAX_FQUEUES; ++i) {
set->heads[i] = 0;
if (pthread_cond_init(&set->conds[i], &cattr))
goto fail_init;
@@ -184,44 +185,44 @@ struct shm_flow_set * shm_flow_set_create(pid_t pid)
fail_mattr_set:
pthread_mutexattr_destroy(&mattr);
fail_mutexattr_init:
- shm_flow_set_destroy(set);
+ ssm_flow_set_destroy(set);
fail_set:
return NULL;
}
-struct shm_flow_set * shm_flow_set_open(pid_t pid)
+struct ssm_flow_set * ssm_flow_set_open(pid_t pid)
{
return flow_set_create(pid, O_RDWR);
}
-void shm_flow_set_destroy(struct shm_flow_set * set)
+void ssm_flow_set_destroy(struct ssm_flow_set * set)
{
char fn[FN_MAX_CHARS];
assert(set);
- sprintf(fn, SHM_FLOW_SET_PREFIX "%d", set->pid);
+ sprintf(fn, SSM_FLOW_SET_PREFIX "%d", set->pid);
- shm_flow_set_close(set);
+ ssm_flow_set_close(set);
shm_unlink(fn);
}
-void shm_flow_set_close(struct shm_flow_set * set)
+void ssm_flow_set_close(struct ssm_flow_set * set)
{
assert(set);
- munmap(set->mtable, SHM_FSET_FILE_SIZE);
+ munmap(set->mtable, SSM_FSET_FILE_SIZE);
free(set);
}
-void shm_flow_set_zero(struct shm_flow_set * set,
+void ssm_flow_set_zero(struct ssm_flow_set * set,
size_t idx)
{
ssize_t i = 0;
assert(set);
- assert(idx < PROG_MAX_FQUEUES);
+ assert(idx < PROC_MAX_FQUEUES);
pthread_mutex_lock(set->lock);
@@ -235,13 +236,13 @@ void shm_flow_set_zero(struct shm_flow_set * set,
}
-int shm_flow_set_add(struct shm_flow_set * set,
+int ssm_flow_set_add(struct ssm_flow_set * set,
size_t idx,
int flow_id)
{
assert(set);
assert(!(flow_id < 0) && flow_id < SYS_MAX_FLOWS);
- assert(idx < PROG_MAX_FQUEUES);
+ assert(idx < PROC_MAX_FQUEUES);
pthread_mutex_lock(set->lock);
@@ -257,13 +258,13 @@ int shm_flow_set_add(struct shm_flow_set * set,
return 0;
}
-void shm_flow_set_del(struct shm_flow_set * set,
+void ssm_flow_set_del(struct ssm_flow_set * set,
size_t idx,
int flow_id)
{
assert(set);
assert(!(flow_id < 0) && flow_id < SYS_MAX_FLOWS);
- assert(idx < PROG_MAX_FQUEUES);
+ assert(idx < PROC_MAX_FQUEUES);
pthread_mutex_lock(set->lock);
@@ -273,7 +274,7 @@ void shm_flow_set_del(struct shm_flow_set * set,
pthread_mutex_unlock(set->lock);
}
-int shm_flow_set_has(struct shm_flow_set * set,
+int ssm_flow_set_has(struct ssm_flow_set * set,
size_t idx,
int flow_id)
{
@@ -281,7 +282,7 @@ int shm_flow_set_has(struct shm_flow_set * set,
assert(set);
assert(!(flow_id < 0) && flow_id < SYS_MAX_FLOWS);
- assert(idx < PROG_MAX_FQUEUES);
+ assert(idx < PROC_MAX_FQUEUES);
pthread_mutex_lock(set->lock);
@@ -293,37 +294,45 @@ int shm_flow_set_has(struct shm_flow_set * set,
return ret;
}
-void shm_flow_set_notify(struct shm_flow_set * set,
+void ssm_flow_set_notify(struct ssm_flow_set * set,
int flow_id,
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);
}
-ssize_t shm_flow_set_wait(const struct shm_flow_set * set,
+ssize_t ssm_flow_set_wait(const struct ssm_flow_set * set,
size_t idx,
struct flowevent * fqueue,
const struct timespec * abstime)
@@ -331,7 +340,7 @@ ssize_t shm_flow_set_wait(const struct shm_flow_set * set,
ssize_t ret = 0;
assert(set);
- assert(idx < PROG_MAX_FQUEUES);
+ assert(idx < PROC_MAX_FQUEUES);
assert(fqueue);
#ifndef HAVE_ROBUST_MUTEX
diff --git a/src/lib/ssm/pool.c b/src/lib/ssm/pool.c
new file mode 100644
index 00000000..705de147
--- /dev/null
+++ b/src/lib/ssm/pool.c
@@ -0,0 +1,919 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Secure Shared Memory Infrastructure (SSMI) Packet Buffer
+ *
+ * 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/errno.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/ssm_pool.h>
+
+#include "ssm.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#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 },
+ { (1 << 9), SSM_GSPP_512_BLOCKS },
+ { (1 << 10), SSM_GSPP_1K_BLOCKS },
+ { (1 << 11), SSM_GSPP_2K_BLOCKS },
+ { (1 << 12), SSM_GSPP_4K_BLOCKS },
+ { (1 << 14), SSM_GSPP_16K_BLOCKS },
+ { (1 << 16), SSM_GSPP_64K_BLOCKS },
+ { (1 << 18), SSM_GSPP_256K_BLOCKS },
+ { (1 << 20), SSM_GSPP_1M_BLOCKS },
+};
+
+/* Per-User Pool (PUP) configuration */
+static const struct ssm_size_class_cfg ssm_pup_cfg[SSM_POOL_MAX_CLASSES] = {
+ { (1 << 8), SSM_PUP_256_BLOCKS },
+ { (1 << 9), SSM_PUP_512_BLOCKS },
+ { (1 << 10), SSM_PUP_1K_BLOCKS },
+ { (1 << 11), SSM_PUP_2K_BLOCKS },
+ { (1 << 12), SSM_PUP_4K_BLOCKS },
+ { (1 << 14), SSM_PUP_16K_BLOCKS },
+ { (1 << 16), SSM_PUP_64K_BLOCKS },
+ { (1 << 18), SSM_PUP_256K_BLOCKS },
+ { (1 << 20), SSM_PUP_1M_BLOCKS },
+};
+
+#define PTR_TO_OFFSET(pool_base, ptr) \
+ ((uintptr_t)(ptr) - (uintptr_t)(pool_base))
+
+#define OFFSET_TO_PTR(pool_base, offset) \
+ ((offset == 0) ? NULL : (void *)((uintptr_t)(pool_base) + offset))
+
+#define GET_SHARD_FOR_PID(pid) ((int)((pid) % SSM_POOL_SHARDS))
+
+
+#define SSM_FILE_SIZE (SSM_POOL_TOTAL_SIZE + sizeof(struct _ssm_pool_hdr))
+#define SSM_GSPP_FILE_SIZE (SSM_GSPP_TOTAL_SIZE + sizeof(struct _ssm_pool_hdr))
+#define SSM_PUP_FILE_SIZE (SSM_PUP_TOTAL_SIZE + sizeof(struct _ssm_pool_hdr))
+
+#define IS_GSPP(uid) ((uid) == SSM_GSPP_UID)
+#define GET_POOL_TOTAL_SIZE(uid) (IS_GSPP(uid) ? SSM_GSPP_TOTAL_SIZE \
+ : SSM_PUP_TOTAL_SIZE)
+#define GET_POOL_FILE_SIZE(uid) (IS_GSPP(uid) ? SSM_GSPP_FILE_SIZE \
+ : SSM_PUP_FILE_SIZE)
+#define GET_POOL_CFG(uid) (IS_GSPP(uid) ? ssm_gspp_cfg : ssm_pup_cfg)
+
+#define NEEDS_CHOWN(uid, gid) ((uid) != geteuid() || (gid) != getegid())
+
+struct ssm_pool {
+ uint8_t * shm_base; /* start of blocks */
+ struct _ssm_pool_hdr * hdr; /* shared memory header */
+ void * pool_base; /* base of the memory pool */
+ uid_t uid; /* user owner (0 = GSPP) */
+ size_t total_size; /* total data size */
+};
+
+static __inline__
+struct ssm_pk_buff * list_remove_head(struct _ssm_list_head * head,
+ void * base)
+{
+ uint32_t off;
+ uint32_t next_off;
+ struct ssm_pk_buff * blk;
+
+ assert(head != NULL);
+ assert(base != NULL);
+
+ off = LOAD(&head->head_offset);
+ if (off == 0)
+ return NULL;
+
+ /* Validate offset is within pool bounds */
+ if (off >= SSM_POOL_TOTAL_SIZE)
+ return NULL;
+
+ blk = OFFSET_TO_PTR(base, off);
+ next_off = LOAD(&blk->next_offset);
+
+
+
+ STORE(&head->head_offset, next_off);
+ STORE(&head->count, LOAD(&head->count) - 1);
+
+ return blk;
+}
+static __inline__ void list_add_head(struct _ssm_list_head * head,
+ struct ssm_pk_buff * blk,
+ void * base)
+{
+ uint32_t off;
+ uint32_t old;
+
+ assert(head != NULL);
+ assert(blk != NULL);
+ assert(base != NULL);
+
+ off = (uint32_t) PTR_TO_OFFSET(base, blk);
+ old = LOAD(&head->head_offset);
+
+ STORE(&blk->next_offset, old);
+ STORE(&head->head_offset, off);
+ STORE(&head->count, LOAD(&head->count) + 1);
+}
+
+static __inline__ int find_size_class_for_offset(struct ssm_pool * pool,
+ size_t offset)
+{
+ int c;
+
+ assert(pool != NULL);
+
+ for (c = 0; c < SSM_POOL_MAX_CLASSES; c++) {
+ struct _ssm_size_class * sc = &pool->hdr->size_classes[c];
+
+ if (sc->object_size == 0)
+ continue;
+
+ if (offset >= sc->pool_start &&
+ offset < sc->pool_start + sc->pool_size)
+ return c;
+ }
+
+ return -1;
+}
+
+static void init_size_classes(struct ssm_pool * pool)
+{
+ const struct ssm_size_class_cfg * cfg;
+ struct _ssm_size_class * sc;
+ struct _ssm_shard * shard;
+ pthread_mutexattr_t mattr;
+ pthread_condattr_t cattr;
+ uint8_t * region;
+ size_t offset;
+ int c; /* class iterator */
+ int s; /* shard iterator */
+ size_t i;
+
+ assert(pool != NULL);
+
+ /* Check if already initialized */
+ if (LOAD(&pool->hdr->initialized) != 0)
+ return;
+
+ cfg = GET_POOL_CFG(pool->uid);
+
+ pthread_mutexattr_init(&mattr);
+ pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
+#ifdef HAVE_ROBUST_MUTEX
+ pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST);
+#endif
+ pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_INHERIT);
+
+ pthread_condattr_init(&cattr);
+ pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
+#ifndef __APPLE__
+ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
+#endif
+ offset = 0;
+
+ for (c = 0; c < SSM_POOL_MAX_CLASSES; c++) {
+ if (cfg[c].blocks == 0)
+ continue;
+
+ sc = &pool->hdr->size_classes[c];
+
+ sc->object_size = cfg[c].size;
+ sc->pool_start = offset;
+ sc->pool_size = cfg[c].size * cfg[c].blocks;
+ sc->object_count = cfg[c].blocks;
+
+ /* Initialize all shards */
+ for (s = 0; s < SSM_POOL_SHARDS; s++) {
+ shard = &sc->shards[s];
+
+ STORE(&shard->free_list.head_offset, 0);
+ STORE(&shard->free_list.count, 0);
+ STORE(&shard->free_count, 0);
+
+ pthread_mutex_init(&shard->mtx, &mattr);
+ pthread_cond_init(&shard->cond, &cattr);
+ }
+
+ /* Lazy distribution: put all blocks in shard 0 initially */
+ region = pool->shm_base + offset;
+
+ for (i = 0; i < sc->object_count; ++i) {
+ struct ssm_pk_buff * blk;
+
+ blk = (struct ssm_pk_buff *)
+ (region + i * sc->object_size);
+
+ 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,
+ pool->pool_base);
+ FETCH_ADD(&sc->shards[0].free_count, 1);
+ }
+
+ offset += sc->pool_size;
+ }
+
+ /* Mark as initialized - acts as memory barrier */
+ STORE(&pool->hdr->initialized, 1);
+
+ pthread_mutexattr_destroy(&mattr);
+ pthread_condattr_destroy(&cattr);
+}
+
+/*
+ * Reclaim all blocks allocated by a specific pid in a size class.
+ * Called with shard mutex held.
+ */
+static size_t reclaim_pid_from_sc(struct _ssm_size_class * sc,
+ struct _ssm_shard * shard,
+ void * pool_base,
+ pid_t pid)
+{
+ uint8_t * region;
+ 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;
+ 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)
+ 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;
+}
+
+void ssm_pool_reclaim_orphans(struct ssm_pool * pool,
+ pid_t pid)
+{
+ size_t sc_idx;
+
+ if (pool == NULL || pid <= 0)
+ return;
+
+ for (sc_idx = 0; sc_idx < SSM_POOL_MAX_CLASSES; sc_idx++) {
+ struct _ssm_size_class * sc;
+ struct _ssm_shard * shard;
+
+ sc = &pool->hdr->size_classes[sc_idx];
+ if (sc->object_count == 0)
+ continue;
+
+ /* Reclaim to shard 0 for simplicity */
+ shard = &sc->shards[0];
+ robust_mutex_lock(&shard->mtx);
+ reclaim_pid_from_sc(sc, shard, pool->pool_base, pid);
+ pthread_mutex_unlock(&shard->mtx);
+ }
+}
+
+static __inline__
+struct ssm_pk_buff * try_alloc_from_shard(struct _ssm_shard * shard,
+ void * base)
+{
+ struct ssm_pk_buff * blk;
+
+ robust_mutex_lock(&shard->mtx);
+
+ if (LOAD(&shard->free_count) > 0) {
+ blk = list_remove_head(&shard->free_list, base);
+ if (blk != NULL) {
+ FETCH_SUB(&shard->free_count, 1);
+ return blk; /* Caller must unlock */
+ }
+ FETCH_SUB(&shard->free_count, 1);
+ }
+
+ pthread_mutex_unlock(&shard->mtx);
+ return NULL;
+}
+
+static __inline__ ssize_t init_block(struct ssm_pool * pool,
+ struct _ssm_size_class * sc,
+ struct _ssm_shard * shard,
+ struct ssm_pk_buff * blk,
+ size_t len,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb)
+{
+ 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;
+ blk->pk_tail = blk->pk_head + (uint32_t) len;
+ blk->off = (uint32_t) PTR_TO_OFFSET(pool->pool_base, blk);
+
+ pthread_mutex_unlock(&shard->mtx);
+
+ *spb = blk;
+ if (ptr != NULL)
+ *ptr = blk->data + blk->pk_head;
+
+ return blk->off;
+}
+
+/* Non-blocking allocation from size class */
+static ssize_t alloc_from_sc(struct ssm_pool * pool,
+ int idx,
+ size_t len,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb)
+{
+ struct _ssm_size_class * sc;
+ struct ssm_pk_buff * blk;
+ int local;
+ int s;
+
+ assert(pool != NULL);
+ assert(idx >= 0 && idx < SSM_POOL_MAX_CLASSES);
+ assert(spb != NULL);
+
+ sc = &pool->hdr->size_classes[idx];
+ local = GET_SHARD_FOR_PID(getpid());
+
+ for (s = 0; s < SSM_POOL_SHARDS; s++) {
+ struct _ssm_shard * shard;
+ int idx;
+
+ idx = (local + s) % SSM_POOL_SHARDS;
+ shard = &sc->shards[idx];
+
+ blk = try_alloc_from_shard(shard, pool->pool_base);
+ if (blk != NULL)
+ return init_block(pool, sc, shard, blk, len, ptr, spb);
+ }
+
+ return -EAGAIN;
+}
+
+/* Blocking allocation from size class */
+static ssize_t alloc_from_sc_b(struct ssm_pool * pool,
+ int idx,
+ size_t len,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb,
+ const struct timespec * abstime)
+{
+ struct _ssm_size_class * sc;
+ struct _ssm_shard * shard;
+ struct ssm_pk_buff * blk = NULL;
+ int local;
+ int s;
+ int ret = 0;
+
+ assert(pool != NULL);
+ assert(idx >= 0 && idx < SSM_POOL_MAX_CLASSES);
+ assert(spb != NULL);
+
+ sc = &pool->hdr->size_classes[idx];
+ local = GET_SHARD_FOR_PID(getpid());
+
+ while (blk == NULL && ret != ETIMEDOUT) {
+ /* Try non-blocking allocation from any shard */
+ for (s = 0; s < SSM_POOL_SHARDS && blk == NULL; s++) {
+ shard = &sc->shards[(local + s) % SSM_POOL_SHARDS];
+ blk = try_alloc_from_shard(shard, pool->pool_base);
+ }
+
+ if (blk != NULL)
+ break;
+
+ /* Nothing available, wait for signal */
+ shard = &sc->shards[local];
+ robust_mutex_lock(&shard->mtx);
+ ret = robust_wait(&shard->cond, &shard->mtx, abstime);
+ pthread_mutex_unlock(&shard->mtx);
+ }
+
+ if (ret == ETIMEDOUT)
+ return -ETIMEDOUT;
+
+ return init_block(pool, sc, shard, blk, len, ptr, spb);
+}
+
+/* Generate pool filename: uid=0 for GSPP, uid>0 for PUP */
+static char * pool_filename(uid_t uid)
+{
+ char base[64];
+
+ if (IS_GSPP(uid))
+ snprintf(base, sizeof(base), "%s", SSM_GSPP_NAME);
+ else
+ snprintf(base, sizeof(base), SSM_PUP_NAME_FMT, (int) uid);
+
+ return strdup(base);
+}
+
+void ssm_pool_close(struct ssm_pool * pool)
+{
+ size_t file_size;
+
+ assert(pool != NULL);
+
+ file_size = GET_POOL_FILE_SIZE(pool->uid);
+
+ munmap(pool->shm_base, file_size);
+ free(pool);
+}
+
+void ssm_pool_destroy(struct ssm_pool * pool)
+{
+ char * fn;
+
+ assert(pool != NULL);
+
+ if (getpid() != pool->hdr->pid && kill(pool->hdr->pid, 0) == 0) {
+ ssm_pool_close(pool);
+ return;
+ }
+
+ fn = pool_filename(pool->uid);
+ if (fn == NULL) {
+ ssm_pool_close(pool);
+ return;
+ }
+
+ ssm_pool_close(pool);
+
+ shm_unlink(fn);
+ free(fn);
+}
+
+#define MM_FLAGS (PROT_READ | PROT_WRITE)
+static struct ssm_pool * __pool_create(const char * name,
+ int flags,
+ uid_t uid,
+ gid_t gid,
+ mode_t mode)
+{
+ struct ssm_pool * pool;
+ int fd;
+ uint8_t * shm_base;
+ size_t file_size;
+ size_t total_size;
+
+ file_size = GET_POOL_FILE_SIZE(uid);
+ total_size = GET_POOL_TOTAL_SIZE(uid);
+
+ pool = malloc(sizeof(*pool));
+ if (pool == NULL)
+ goto fail_pool;
+
+ fd = shm_open(name, flags, mode);
+ if (fd == -1)
+ goto fail_open;
+
+ if (flags & O_CREAT) {
+ if (ftruncate(fd, (off_t) file_size) < 0)
+ goto fail_truncate;
+ if (NEEDS_CHOWN(uid, gid) && fchown(fd, uid, gid) < 0)
+ goto fail_truncate;
+ }
+
+ shm_base = mmap(NULL, file_size, MM_FLAGS, MAP_SHARED, fd, 0);
+ if (shm_base == MAP_FAILED)
+ goto fail_truncate;
+
+ pool->shm_base = shm_base;
+ pool->pool_base = shm_base;
+ pool->hdr = (struct _ssm_pool_hdr *) (shm_base + total_size);
+ pool->uid = uid;
+ pool->total_size = total_size;
+
+ if (flags & O_CREAT)
+ pool->hdr->mapped_addr = shm_base;
+
+ close(fd);
+
+ return pool;
+
+ fail_truncate:
+ close(fd);
+ if (flags & O_CREAT)
+ shm_unlink(name);
+ fail_open:
+ free(pool);
+ fail_pool:
+ return NULL;
+}
+
+struct ssm_pool * ssm_pool_create(uid_t uid,
+ gid_t gid)
+{
+ struct ssm_pool * pool;
+ char * fn;
+ mode_t mask;
+ mode_t mode;
+ pthread_mutexattr_t mattr;
+ pthread_condattr_t cattr;
+
+ fn = pool_filename(uid);
+ if (fn == NULL)
+ goto fail_fn;
+
+ mode = IS_GSPP(uid) ? 0660 : 0600;
+ mask = umask(0);
+
+ pool = __pool_create(fn, O_CREAT | O_EXCL | O_RDWR, uid, gid, mode);
+
+ umask(mask);
+
+ if (pool == NULL)
+ goto fail_pool;
+
+ if (pthread_mutexattr_init(&mattr))
+ 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(&pool->hdr->mtx, &mattr))
+ goto fail_mutex;
+
+ if (pthread_condattr_init(&cattr))
+ goto fail_cattr;
+
+ pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
+#ifndef __APPLE__
+ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
+#endif
+ if (pthread_cond_init(&pool->hdr->healthy, &cattr))
+ goto fail_healthy;
+
+ pool->hdr->pid = getpid();
+ STORE(&pool->hdr->initialized, 0);
+
+ init_size_classes(pool);
+
+ pthread_mutexattr_destroy(&mattr);
+ pthread_condattr_destroy(&cattr);
+ free(fn);
+
+ return pool;
+
+ fail_healthy:
+ pthread_condattr_destroy(&cattr);
+ fail_cattr:
+ pthread_mutex_destroy(&pool->hdr->mtx);
+ fail_mutex:
+ pthread_mutexattr_destroy(&mattr);
+ fail_mattr:
+ ssm_pool_close(pool);
+ shm_unlink(fn);
+ fail_pool:
+ free(fn);
+ fail_fn:
+ return NULL;
+}
+
+struct ssm_pool * ssm_pool_open(uid_t uid)
+{
+ struct ssm_pool * pool;
+ char * fn;
+
+ fn = pool_filename(uid);
+ if (fn == NULL)
+ return NULL;
+
+ pool = __pool_create(fn, O_RDWR, uid, 0, 0);
+ if (pool != NULL)
+ init_size_classes(pool);
+
+ free(fn);
+
+ return pool;
+}
+
+void ssm_pool_gspp_purge(void)
+{
+ char * fn;
+
+ fn = pool_filename(SSM_GSPP_UID);
+ if (fn == NULL)
+ return;
+
+ shm_unlink(fn);
+ free(fn);
+}
+
+int ssm_pool_mlock(struct ssm_pool * pool)
+{
+ size_t file_size;
+
+ assert(pool != NULL);
+
+ file_size = GET_POOL_FILE_SIZE(pool->uid);
+
+ return mlock(pool->shm_base, file_size);
+}
+
+ssize_t ssm_pool_alloc(struct ssm_pool * pool,
+ size_t count,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb)
+{
+ int idx;
+
+ assert(pool != NULL);
+ assert(spb != NULL);
+
+ idx = select_size_class(pool->hdr, count);
+ if (idx >= 0)
+ return alloc_from_sc(pool, idx, count, ptr, spb);
+
+ return -EMSGSIZE;
+}
+
+ssize_t ssm_pool_alloc_b(struct ssm_pool * pool,
+ size_t count,
+ uint8_t ** ptr,
+ struct ssm_pk_buff ** spb,
+ const struct timespec * abstime)
+{
+ int idx;
+
+ assert(pool != NULL);
+ assert(spb != NULL);
+
+ idx = select_size_class(pool->hdr, count);
+ if (idx >= 0)
+ return alloc_from_sc_b(pool, idx, count, ptr, spb, abstime);
+
+ return -EMSGSIZE;
+}
+
+ssize_t ssm_pool_read(uint8_t ** dst,
+ struct ssm_pool * pool,
+ size_t off)
+{
+ struct ssm_pk_buff * blk;
+
+ assert(dst != NULL);
+ assert(pool != NULL);
+
+ blk = OFFSET_TO_PTR(pool->pool_base, off);
+ if (blk == NULL)
+ return -EINVAL;
+
+ *dst = blk->data + blk->pk_head;
+
+ return (ssize_t) (blk->pk_tail - blk->pk_head);
+}
+
+struct ssm_pk_buff * ssm_pool_get(struct ssm_pool * pool,
+ size_t off)
+{
+ struct ssm_pk_buff * blk;
+
+ assert(pool != NULL);
+
+ if (off == 0 || off >= pool->total_size)
+ return NULL;
+
+ blk = OFFSET_TO_PTR(pool->pool_base, off);
+ if (blk == NULL)
+ return NULL;
+
+ if (LOAD(&blk->refcount) == 0)
+ return NULL;
+
+ return blk;
+}
+
+int ssm_pool_remove(struct ssm_pool * pool,
+ size_t off)
+{
+ struct ssm_pk_buff * blk;
+ struct _ssm_size_class * sc;
+ struct _ssm_shard * shard;
+ int sc_idx;
+ int shard_idx;
+ uint16_t old_ref;
+
+ assert(pool != NULL);
+
+ if (off == 0 || off >= pool->total_size)
+ return -EINVAL;
+
+ blk = OFFSET_TO_PTR(pool->pool_base, off);
+ if (blk == NULL)
+ return -EINVAL;
+
+ sc_idx = find_size_class_for_offset(pool, off);
+ if (sc_idx < 0)
+ return -EINVAL;
+
+ sc = &pool->hdr->size_classes[sc_idx];
+
+ /* Free to allocator's shard (lazy distribution in action) */
+ shard_idx = GET_SHARD_FOR_PID(blk->allocator_pid);
+ shard = &sc->shards[shard_idx];
+
+ robust_mutex_lock(&shard->mtx);
+
+ old_ref = FETCH_SUB(&blk->refcount, 1);
+ if (old_ref > 1) {
+ /* Still referenced */
+ pthread_mutex_unlock(&shard->mtx);
+ return 0;
+ }
+
+ blk->allocator_pid = 0;
+#ifdef CONFIG_OUROBOROS_DEBUG
+ if (old_ref == 0) {
+ /* Underflow - double free attempt */
+ pthread_mutex_unlock(&shard->mtx);
+ abort();
+ }
+
+ /* Poison fields to detect use-after-free */
+ blk->pk_head = 0xDEAD;
+ blk->pk_tail = 0xBEEF;
+#endif
+ list_add_head(&shard->free_list, blk, pool->pool_base);
+ FETCH_ADD(&shard->free_count, 1);
+
+ pthread_cond_signal(&shard->cond);
+
+ pthread_mutex_unlock(&shard->mtx);
+
+ return 0;
+}
+
+size_t ssm_pk_buff_get_off(const struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ return spb->off;
+}
+
+uint8_t * ssm_pk_buff_head(const struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ return (uint8_t *) spb->data + spb->pk_head;
+}
+
+uint8_t * ssm_pk_buff_tail(const struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ return (uint8_t *) spb->data + spb->pk_tail;
+}
+
+size_t ssm_pk_buff_len(const struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ return spb->pk_tail - spb->pk_head;
+}
+
+uint8_t * ssm_pk_buff_push(struct ssm_pk_buff * spb,
+ size_t size)
+{
+ assert(spb != NULL);
+
+ if (spb->pk_head < size)
+ return NULL;
+
+ spb->pk_head -= size;
+
+ return spb->data + spb->pk_head;
+}
+
+uint8_t * ssm_pk_buff_push_tail(struct ssm_pk_buff * spb,
+ size_t size)
+{
+ uint8_t * buf;
+
+ assert(spb != NULL);
+
+ if (spb->pk_tail + size >= spb->size)
+ return NULL;
+
+ buf = spb->data + spb->pk_tail;
+
+ spb->pk_tail += size;
+
+ return buf;
+}
+
+uint8_t * ssm_pk_buff_pop(struct ssm_pk_buff * spb,
+ size_t size)
+{
+ uint8_t * buf;
+
+ assert(spb != NULL);
+ assert(!(size > spb->pk_tail - spb->pk_head));
+
+ buf = spb->data + spb->pk_head;
+
+ spb->pk_head += size;
+
+ return buf;
+}
+
+uint8_t * ssm_pk_buff_pop_tail(struct ssm_pk_buff * spb,
+ size_t size)
+{
+ assert(spb != NULL);
+ assert(!(size > spb->pk_tail - spb->pk_head));
+
+ spb->pk_tail -= size;
+
+ return spb->data + spb->pk_tail;
+}
+
+void ssm_pk_buff_truncate(struct ssm_pk_buff * spb,
+ size_t len)
+{
+ assert(spb != NULL);
+ assert(len <= spb->size);
+
+ spb->pk_tail = spb->pk_head + len;
+}
+
+int ssm_pk_buff_wait_ack(struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ FETCH_ADD(&spb->refcount, 1);
+
+ return 0;
+}
+
+int ssm_pk_buff_ack(struct ssm_pk_buff * spb)
+{
+ assert(spb != NULL);
+
+ FETCH_SUB(&spb->refcount, 1);
+
+ return 0;
+}
diff --git a/src/lib/ssm/rbuff.c b/src/lib/ssm/rbuff.c
new file mode 100644
index 00000000..04978d82
--- /dev/null
+++ b/src/lib/ssm/rbuff.c
@@ -0,0 +1,519 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Ring buffer implementations for incoming packets
+ *
+ * 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 "ssm.h"
+
+#include <ouroboros/ssm_rbuff.h>
+#include <ouroboros/lockfile.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/fccntl.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/time.h>
+
+#include <assert.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#define FN_MAX_CHARS 255
+
+#define SSM_RBUFF_FILESIZE ((SSM_RBUFF_SIZE) * sizeof(ssize_t) \
+ + 3 * sizeof(size_t) \
+ + sizeof(pthread_mutex_t) \
+ + 2 * sizeof(pthread_cond_t))
+
+#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))
+#define TAIL_IDX(rb) (LOAD_ACQUIRE(rb->tail))
+#define ADVANCE_HEAD(rb) \
+ (STORE_RELEASE(rb->head, MODB(LOAD_RELAXED(rb->head) + 1)))
+#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))
+
+struct ssm_rbuff {
+ ssize_t * shm_base; /* start of shared memory */
+ size_t * head; /* start of ringbuffer */
+ size_t * tail;
+ 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 */
+};
+
+#define MM_FLAGS (PROT_READ | PROT_WRITE)
+
+static struct ssm_rbuff * rbuff_create(pid_t pid,
+ int flow_id,
+ int flags)
+{
+ struct ssm_rbuff * rb;
+ int fd;
+ ssize_t * shm_base;
+ char fn[FN_MAX_CHARS];
+
+ sprintf(fn, SSM_RBUFF_PREFIX "%d.%d", pid, flow_id);
+
+ rb = malloc(sizeof(*rb));
+ if (rb == NULL)
+ goto fail_malloc;
+
+ fd = shm_open(fn, flags, 0666);
+ if (fd == -1)
+ goto fail_open;
+
+ if ((flags & O_CREAT) && ftruncate(fd, SSM_RBUFF_FILESIZE) < 0)
+ goto fail_truncate;
+
+ shm_base = mmap(NULL, SSM_RBUFF_FILESIZE, MM_FLAGS, MAP_SHARED, fd, 0);
+
+ close(fd);
+
+ rb->shm_base = shm_base;
+ rb->head = (size_t *) (rb->shm_base + (SSM_RBUFF_SIZE));
+ rb->tail = (size_t *) (rb->head + 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;
+
+ return rb;
+
+ fail_truncate:
+ close(fd);
+ if (flags & O_CREAT)
+ shm_unlink(fn);
+ fail_open:
+ free(rb);
+ fail_malloc:
+ return NULL;
+}
+
+static void rbuff_destroy(struct ssm_rbuff * rb)
+{
+ munmap(rb->shm_base, SSM_RBUFF_FILESIZE);
+
+ free(rb);
+}
+
+struct ssm_rbuff * ssm_rbuff_create(pid_t pid,
+ int flow_id)
+{
+ struct ssm_rbuff * rb;
+ pthread_mutexattr_t mattr;
+ pthread_condattr_t cattr;
+ mode_t mask;
+
+ mask = umask(0);
+
+ rb = rbuff_create(pid, flow_id, O_CREAT | O_EXCL | O_RDWR);
+
+ umask(mask);
+
+ if (rb == NULL)
+ goto fail_rb;
+
+ if (pthread_mutexattr_init(&mattr))
+ 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))
+ goto fail_mutex;
+
+ if (pthread_condattr_init(&cattr))
+ 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))
+ goto fail_add;
+
+ if (pthread_cond_init(rb->del, &cattr))
+ goto fail_del;
+
+ *rb->flags = RB_RDWR;
+ *rb->head = 0;
+ *rb->tail = 0;
+
+ rb->pid = pid;
+ rb->flow_id = flow_id;
+
+ pthread_mutexattr_destroy(&mattr);
+ pthread_condattr_destroy(&cattr);
+
+ return rb;
+
+ fail_del:
+ pthread_cond_destroy(rb->add);
+ fail_add:
+ pthread_condattr_destroy(&cattr);
+ fail_cattr:
+ pthread_mutex_destroy(rb->mtx);
+ fail_mutex:
+ pthread_mutexattr_destroy(&mattr);
+ fail_mattr:
+ ssm_rbuff_destroy(rb);
+ fail_rb:
+ return NULL;
+}
+
+void ssm_rbuff_destroy(struct ssm_rbuff * rb)
+{
+ char fn[FN_MAX_CHARS];
+
+ assert(rb != NULL);
+
+ sprintf(fn, SSM_RBUFF_PREFIX "%d.%d", rb->pid, rb->flow_id);
+
+ ssm_rbuff_close(rb);
+
+ shm_unlink(fn);
+}
+
+struct ssm_rbuff * ssm_rbuff_open(pid_t pid,
+ int flow_id)
+{
+ return rbuff_create(pid, flow_id, O_RDWR);
+}
+
+void ssm_rbuff_close(struct ssm_rbuff * rb)
+{
+ assert(rb);
+
+ /*
+ * Caller must set RB_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 };
+ nanosleep(&tic, NULL);
+ }
+
+ rbuff_destroy(rb);
+}
+
+/* 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);
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+}
+
+int ssm_rbuff_write(struct ssm_rbuff * rb,
+ size_t off)
+{
+ size_t flags;
+ bool was_empty;
+ int ret = 0;
+
+ assert(rb != NULL);
+
+ __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+
+ flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+ if (flags != RB_RDWR) {
+ if (flags & RB_FLOWDOWN) {
+ ret = -EFLOWDOWN;
+ goto fail_flags;
+ }
+ if (!(flags & RB_WR)) {
+ ret = -ENOTALLOC;
+ goto fail_flags;
+ }
+ }
+
+ robust_mutex_lock(rb->mtx);
+
+ if (IS_FULL(rb)) {
+ ret = -EAGAIN;
+ goto fail_mutex;
+ }
+
+ was_empty = IS_EMPTY(rb);
+
+ HEAD(rb) = (ssize_t) off;
+ ADVANCE_HEAD(rb);
+
+ if (was_empty)
+ pthread_cond_broadcast(rb->add);
+
+ pthread_mutex_unlock(rb->mtx);
+
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+ return 0;
+
+ fail_mutex:
+ pthread_mutex_unlock(rb->mtx);
+ fail_flags:
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+ return ret;
+}
+
+int ssm_rbuff_write_b(struct ssm_rbuff * rb,
+ size_t off,
+ const struct timespec * abstime)
+{
+ size_t flags;
+ int ret = 0;
+ bool was_empty;
+
+ assert(rb != NULL);
+
+ __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+
+ flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+ if (flags != RB_RDWR) {
+ if (flags & RB_FLOWDOWN) {
+ ret = -EFLOWDOWN;
+ goto fail_flags;
+ }
+ if (!(flags & RB_WR)) {
+ ret = -ENOTALLOC;
+ goto fail_flags;
+ }
+ }
+
+ robust_mutex_lock(rb->mtx);
+
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
+
+ while (IS_FULL(rb) && ret != -ETIMEDOUT) {
+ flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+ if (flags & RB_FLOWDOWN) {
+ ret = -EFLOWDOWN;
+ break;
+ }
+ ret = -robust_wait(rb->del, rb->mtx, abstime);
+ }
+
+ pthread_cleanup_pop(false);
+
+ if (ret != -ETIMEDOUT && ret != -EFLOWDOWN) {
+ was_empty = IS_EMPTY(rb);
+ HEAD(rb) = (ssize_t) off;
+ ADVANCE_HEAD(rb);
+ if (was_empty)
+ pthread_cond_broadcast(rb->add);
+ }
+
+ pthread_mutex_unlock(rb->mtx);
+
+ fail_flags:
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+ return ret;
+}
+
+static int check_rb_flags(struct ssm_rbuff * rb)
+{
+ size_t flags;
+
+ assert(rb != NULL);
+
+ flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+
+ if (flags & RB_FLOWDOWN)
+ return -EFLOWDOWN;
+
+ if (flags & RB_FLOWPEER)
+ return -EFLOWPEER;
+
+ if (!(flags & RB_RD))
+ return -ENOTALLOC;
+
+ return -EAGAIN;
+}
+
+ssize_t ssm_rbuff_read(struct ssm_rbuff * rb)
+{
+ ssize_t ret;
+
+ assert(rb != NULL);
+
+ __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+
+ if (IS_EMPTY(rb)) {
+ ret = check_rb_flags(rb);
+ goto out;
+ }
+
+ robust_mutex_lock(rb->mtx);
+
+ if (IS_EMPTY(rb)) {
+ pthread_mutex_unlock(rb->mtx);
+ ret = check_rb_flags(rb);
+ goto out;
+ }
+
+ ret = TAIL(rb);
+ ADVANCE_TAIL(rb);
+
+ pthread_cond_broadcast(rb->del);
+
+ pthread_mutex_unlock(rb->mtx);
+
+ out:
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+ return ret;
+}
+
+ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb,
+ const struct timespec * abstime)
+{
+ ssize_t idx = -1;
+ size_t flags;
+
+ assert(rb != NULL);
+
+ __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+
+ flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+ if (IS_EMPTY(rb) && (flags & RB_FLOWDOWN)) {
+ idx = -EFLOWDOWN;
+ goto out;
+ }
+
+ robust_mutex_lock(rb->mtx);
+
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
+
+ while (IS_EMPTY(rb) &&
+ idx != -ETIMEDOUT &&
+ check_rb_flags(rb) == -EAGAIN) {
+ idx = -robust_wait(rb->add, rb->mtx, abstime);
+ }
+
+ pthread_cleanup_pop(false);
+
+ if (!IS_EMPTY(rb)) {
+ idx = TAIL(rb);
+ ADVANCE_TAIL(rb);
+ pthread_cond_broadcast(rb->del);
+ } else if (idx != -ETIMEDOUT) {
+ idx = check_rb_flags(rb);
+ }
+
+ pthread_mutex_unlock(rb->mtx);
+
+ assert(idx != -EAGAIN);
+
+ out:
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+ return idx;
+}
+
+void ssm_rbuff_set_bits(struct ssm_rbuff * rb,
+ uint32_t bits)
+{
+ assert(rb != NULL);
+
+ robust_mutex_lock(rb->mtx);
+ __atomic_fetch_or(rb->flags, (size_t) bits, __ATOMIC_SEQ_CST);
+ pthread_cond_broadcast(rb->add);
+ pthread_cond_broadcast(rb->del);
+ pthread_mutex_unlock(rb->mtx);
+}
+
+void ssm_rbuff_clr_bits(struct ssm_rbuff * rb,
+ uint32_t bits)
+{
+ assert(rb != NULL);
+
+ robust_mutex_lock(rb->mtx);
+ __atomic_fetch_and(rb->flags, ~(size_t) bits, __ATOMIC_SEQ_CST);
+ 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) __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
+}
+
+void ssm_rbuff_fini(struct ssm_rbuff * rb)
+{
+ assert(rb != NULL);
+
+ __atomic_fetch_add(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+
+ robust_mutex_lock(rb->mtx);
+
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
+
+ while (!IS_EMPTY(rb))
+ robust_wait(rb->del, rb->mtx, NULL);
+
+ pthread_cleanup_pop(false);
+
+ pthread_mutex_unlock(rb->mtx);
+
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+}
+
+size_t ssm_rbuff_queued(struct ssm_rbuff * rb)
+{
+ assert(rb != NULL);
+
+ return QUEUED(rb);
+}
+
+int ssm_rbuff_mlock(struct ssm_rbuff * rb)
+{
+ assert(rb != NULL);
+
+ return mlock(rb->shm_base, SSM_RBUFF_FILESIZE);
+}
diff --git a/src/lib/ssm/ssm.h.in b/src/lib/ssm/ssm.h.in
new file mode 100644
index 00000000..57febae4
--- /dev/null
+++ b/src/lib/ssm/ssm.h.in
@@ -0,0 +1,190 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Secure Shared Memory configuration
+ *
+ * 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_SSM_H
+#define OUROBOROS_LIB_SSM_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdatomic.h>
+#include <sys/types.h>
+
+/* Pool naming configuration */
+#define SSM_PREFIX "@SSM_PREFIX@"
+#define SSM_GSPP_NAME "@SSM_GSPP_NAME@"
+#define SSM_PUP_NAME_FMT "@SSM_PUP_NAME_FMT@"
+#define SSM_GSPP_UID 0
+
+/* Legacy SSM constants */
+#define SSM_RBUFF_PREFIX "@SSM_RBUFF_PREFIX@"
+#define SSM_FLOW_SET_PREFIX "@SSM_FLOW_SET_PREFIX@"
+#define SSM_POOL_NAME "@SSM_POOL_NAME@"
+#define SSM_RBUFF_SIZE @SSM_RBUFF_SIZE@
+
+/* Packet buffer space reservation */
+#define SSM_PK_BUFF_HEADSPACE @SSM_PK_BUFF_HEADSPACE@
+#define SSM_PK_BUFF_TAILSPACE @SSM_PK_BUFF_TAILSPACE@
+
+/* Global Shared Packet Pool (GSPP) - for privileged processes */
+#define SSM_GSPP_256_BLOCKS @SSM_GSPP_256_BLOCKS@
+#define SSM_GSPP_512_BLOCKS @SSM_GSPP_512_BLOCKS@
+#define SSM_GSPP_1K_BLOCKS @SSM_GSPP_1K_BLOCKS@
+#define SSM_GSPP_2K_BLOCKS @SSM_GSPP_2K_BLOCKS@
+#define SSM_GSPP_4K_BLOCKS @SSM_GSPP_4K_BLOCKS@
+#define SSM_GSPP_16K_BLOCKS @SSM_GSPP_16K_BLOCKS@
+#define SSM_GSPP_64K_BLOCKS @SSM_GSPP_64K_BLOCKS@
+#define SSM_GSPP_256K_BLOCKS @SSM_GSPP_256K_BLOCKS@
+#define SSM_GSPP_1M_BLOCKS @SSM_GSPP_1M_BLOCKS@
+#define SSM_GSPP_TOTAL_SIZE @SSM_GSPP_TOTAL_SIZE@
+
+/* Per-User Pool (PUP) - for unprivileged applications */
+#define SSM_PUP_256_BLOCKS @SSM_PUP_256_BLOCKS@
+#define SSM_PUP_512_BLOCKS @SSM_PUP_512_BLOCKS@
+#define SSM_PUP_1K_BLOCKS @SSM_PUP_1K_BLOCKS@
+#define SSM_PUP_2K_BLOCKS @SSM_PUP_2K_BLOCKS@
+#define SSM_PUP_4K_BLOCKS @SSM_PUP_4K_BLOCKS@
+#define SSM_PUP_16K_BLOCKS @SSM_PUP_16K_BLOCKS@
+#define SSM_PUP_64K_BLOCKS @SSM_PUP_64K_BLOCKS@
+#define SSM_PUP_256K_BLOCKS @SSM_PUP_256K_BLOCKS@
+#define SSM_PUP_1M_BLOCKS @SSM_PUP_1M_BLOCKS@
+#define SSM_PUP_TOTAL_SIZE @SSM_PUP_TOTAL_SIZE@
+
+/* Legacy pool blocks (same as GSPP for compatibility) */
+#define SSM_POOL_256_BLOCKS @SSM_POOL_256_BLOCKS@
+#define SSM_POOL_512_BLOCKS @SSM_POOL_512_BLOCKS@
+#define SSM_POOL_1K_BLOCKS @SSM_POOL_1K_BLOCKS@
+#define SSM_POOL_2K_BLOCKS @SSM_POOL_2K_BLOCKS@
+#define SSM_POOL_4K_BLOCKS @SSM_POOL_4K_BLOCKS@
+#define SSM_POOL_16K_BLOCKS @SSM_POOL_16K_BLOCKS@
+#define SSM_POOL_64K_BLOCKS @SSM_POOL_64K_BLOCKS@
+#define SSM_POOL_256K_BLOCKS @SSM_POOL_256K_BLOCKS@
+#define SSM_POOL_1M_BLOCKS @SSM_POOL_1M_BLOCKS@
+#define SSM_POOL_TOTAL_SIZE @SSM_POOL_TOTAL_SIZE@
+
+/* 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
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <pthread.h>
+
+#include <ouroboros/pthread.h>
+
+static __inline__ void robust_mutex_lock(pthread_mutex_t * mtx)
+{
+#ifndef HAVE_ROBUST_MUTEX
+ pthread_mutex_lock(mtx);
+#else
+ if (pthread_mutex_lock(mtx) == EOWNERDEAD)
+ pthread_mutex_consistent(mtx);
+#endif
+}
+
+static __inline__ int robust_wait(pthread_cond_t * cond,
+ pthread_mutex_t * mtx,
+ const struct timespec * abstime)
+{
+ int ret = __timedwait(cond, mtx, abstime);
+#ifdef HAVE_ROBUST_MUTEX
+ if (ret == EOWNERDEAD)
+ pthread_mutex_consistent(mtx);
+#endif
+ return ret;
+}
+
+/* Packet buffer structure used by pool, rbuff, and tests */
+struct ssm_pk_buff {
+ uint32_t next_offset; /* List linkage (pool < 4GB) */
+ uint16_t refcount; /* Reference count (app + rtx) */
+ pid_t allocator_pid; /* For orphan detection */
+ uint32_t size; /* Block size (max 1MB) */
+ 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 */
+};
+
+/* Size class configuration table */
+struct ssm_size_class_cfg {
+ size_t size;
+ size_t blocks;
+};
+
+struct _ssm_list_head {
+ uint32_t head_offset;
+ uint32_t count;
+};
+
+struct _ssm_shard {
+ pthread_mutex_t mtx;
+ pthread_cond_t cond;
+ struct _ssm_list_head free_list;
+ size_t free_count;
+};
+
+struct _ssm_size_class {
+ struct _ssm_shard shards[SSM_POOL_SHARDS];
+ size_t object_size;
+ size_t pool_start;
+ size_t pool_size;
+ size_t object_count;
+};
+
+struct _ssm_pool_hdr {
+ pthread_mutex_t mtx;
+ pthread_cond_t healthy;
+ pid_t pid;
+ uint32_t initialized;
+ void * mapped_addr;
+ struct _ssm_size_class size_classes[SSM_POOL_MAX_CLASSES];
+};
+
+#define SSM_PK_BUFF_TOTALSPACE (SSM_PK_BUFF_HEADSPACE + SSM_PK_BUFF_TAILSPACE)
+static __inline__ int select_size_class(struct _ssm_pool_hdr * hdr,
+ size_t len)
+{
+ size_t sz;
+ int i;
+
+ sz = sizeof(struct ssm_pk_buff) + SSM_PK_BUFF_TOTALSPACE + len;
+
+ for (i = 0; i < SSM_POOL_MAX_CLASSES; i++) {
+ struct _ssm_size_class * sc = &hdr->size_classes[i];
+ if (sc->object_size > 0 && sz <= sc->object_size)
+ return i;
+ }
+
+ return -1;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OUROBOROS_LIB_SSM_H */
diff --git a/src/lib/ssm/tests/CMakeLists.txt b/src/lib/ssm/tests/CMakeLists.txt
new file mode 100644
index 00000000..d622d41c
--- /dev/null
+++ b/src/lib/ssm/tests/CMakeLists.txt
@@ -0,0 +1,21 @@
+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
+ pool_test.c
+ pool_sharding_test.c
+ rbuff_test.c
+ flow_set_test.c
+ )
+
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test ouroboros-common)
+
+add_dependencies(build_tests ${PARENT_DIR}_test)
+
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/lib/ssm/tests/flow_set_test.c b/src/lib/ssm/tests/flow_set_test.c
new file mode 100644
index 00000000..1bb97b2f
--- /dev/null
+++ b/src/lib/ssm/tests/flow_set_test.c
@@ -0,0 +1,255 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the SSM flow set
+ *
+ * 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"
+#include "ssm.h"
+
+#include <test/test.h>
+#include <ouroboros/ssm_flow_set.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/time.h>
+
+#include <stdio.h>
+#include <unistd.h>
+#include <pthread.h>
+
+static int test_ssm_flow_set_create_destroy(void)
+{
+ struct ssm_flow_set * set;
+ pid_t pid;
+
+ TEST_START();
+
+ pid = getpid();
+
+ set = ssm_flow_set_create(pid);
+ if (set == NULL) {
+ printf("Failed to create flow set.\n");
+ goto fail;
+ }
+
+ ssm_flow_set_destroy(set);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_flow_set_add_del_has(void)
+{
+ struct ssm_flow_set * set;
+ pid_t pid;
+ size_t idx = 0;
+ int flow_id = 42;
+
+ TEST_START();
+
+ pid = getpid();
+
+ set = ssm_flow_set_create(pid);
+ if (set == NULL) {
+ printf("Failed to create flow set.\n");
+ goto fail;
+ }
+
+ if (ssm_flow_set_has(set, idx, flow_id)) {
+ printf("Flow should not be in set initially.\n");
+ goto fail_destroy;
+ }
+
+ if (ssm_flow_set_add(set, idx, flow_id) < 0) {
+ printf("Failed to add flow to set.\n");
+ goto fail_destroy;
+ }
+
+ if (!ssm_flow_set_has(set, idx, flow_id)) {
+ printf("Flow should be in set after add.\n");
+ goto fail_destroy;
+ }
+
+ /* Adding same flow again should fail */
+ if (ssm_flow_set_add(set, idx, flow_id) != -EPERM) {
+ printf("Should not be able to add flow twice.\n");
+ goto fail_destroy;
+ }
+
+ ssm_flow_set_del(set, idx, flow_id);
+
+ if (ssm_flow_set_has(set, idx, flow_id)) {
+ printf("Flow should not be in set after delete.\n");
+ goto fail_destroy;
+ }
+
+ ssm_flow_set_destroy(set);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+fail_destroy:
+ ssm_flow_set_destroy(set);
+fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_flow_set_zero(void)
+{
+ struct ssm_flow_set * set;
+ pid_t pid;
+ size_t idx = 0;
+ int flow_id1 = 10;
+ int flow_id2 = 20;
+
+ TEST_START();
+
+ pid = getpid();
+
+ set = ssm_flow_set_create(pid);
+ if (set == NULL) {
+ printf("Failed to create flow set.\n");
+ goto fail;
+ }
+
+ if (ssm_flow_set_add(set, idx, flow_id1) < 0) {
+ printf("Failed to add flow1 to set.\n");
+ goto fail_destroy;
+ }
+
+ if (ssm_flow_set_add(set, idx, flow_id2) < 0) {
+ printf("Failed to add flow2 to set.\n");
+ goto fail_destroy;
+ }
+
+ ssm_flow_set_zero(set, idx);
+
+ if (ssm_flow_set_has(set, idx, flow_id1)) {
+ printf("Flow1 should not be in set after zero.\n");
+ goto fail_destroy;
+ }
+
+ if (ssm_flow_set_has(set, idx, flow_id2)) {
+ printf("Flow2 should not be in set after zero.\n");
+ goto fail_destroy;
+ }
+
+ ssm_flow_set_destroy(set);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+fail_destroy:
+ ssm_flow_set_destroy(set);
+fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_flow_set_notify_wait(void)
+{
+ struct ssm_flow_set * set;
+ pid_t pid;
+ size_t idx = 0;
+ int flow_id = 100;
+ struct flowevent events[SSM_RBUFF_SIZE];
+ struct timespec timeout;
+ ssize_t ret;
+
+ TEST_START();
+
+ pid = getpid();
+
+ set = ssm_flow_set_create(pid);
+ if (set == NULL) {
+ printf("Failed to create flow set.\n");
+ goto fail;
+ }
+
+ if (ssm_flow_set_add(set, idx, flow_id) < 0) {
+ printf("Failed to add flow to set.\n");
+ goto fail_destroy;
+ }
+
+ /* Test immediate timeout when no events */
+ clock_gettime(PTHREAD_COND_CLOCK, &timeout);
+ ret = ssm_flow_set_wait(set, idx, events, &timeout);
+ if (ret != -ETIMEDOUT) {
+ printf("Wait should timeout immediately when no events.\n");
+ goto fail_destroy;
+ }
+
+ /* Notify an event */
+ ssm_flow_set_notify(set, flow_id, FLOW_PKT);
+
+ /* Should be able to read the event immediately */
+ clock_gettime(PTHREAD_COND_CLOCK, &timeout);
+ ts_add(&timeout, &timeout, &((struct timespec) {1, 0}));
+
+ ret = ssm_flow_set_wait(set, idx, events, &timeout);
+ if (ret != 1) {
+ printf("Wait should return 1 event, got %zd.\n", ret);
+ goto fail_destroy;
+ }
+
+ if (events[0].flow_id != flow_id) {
+ printf("Event flow_id mismatch: expected %d, got %d.\n",
+ flow_id, events[0].flow_id);
+ goto fail_destroy;
+ }
+
+ if (events[0].event != FLOW_PKT) {
+ printf("Event type mismatch: expected %d, got %d.\n",
+ FLOW_PKT, events[0].event);
+ goto fail_destroy;
+ }
+
+ ssm_flow_set_destroy(set);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+fail_destroy:
+ ssm_flow_set_destroy(set);
+fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int flow_set_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_ssm_flow_set_create_destroy();
+ ret |= test_ssm_flow_set_add_del_has();
+ ret |= test_ssm_flow_set_zero();
+ ret |= test_ssm_flow_set_notify_wait();
+
+ return ret;
+}
diff --git a/src/lib/ssm/tests/pool_sharding_test.c b/src/lib/ssm/tests/pool_sharding_test.c
new file mode 100644
index 00000000..ec464a92
--- /dev/null
+++ b/src/lib/ssm/tests/pool_sharding_test.c
@@ -0,0 +1,474 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the SSM pool sharding with fallback
+ *
+ * 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"
+#include "ssm.h"
+
+#include <test/test.h>
+#include <ouroboros/ssm_pool.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <signal.h>
+
+#define TEST_SIZE 256
+
+/* Helper to get pool header for inspection */
+static struct _ssm_pool_hdr * get_pool_hdr(struct ssm_pool * pool)
+{
+ /* ssm_pool is opaque, but we know its layout:
+ * uint8_t * shm_base
+ * struct _ssm_pool_hdr * hdr
+ * void * pool_base
+ */
+ struct _ssm_pool_hdr ** hdr_ptr =
+ (struct _ssm_pool_hdr **)((uint8_t *)pool + sizeof(void *));
+
+ return *hdr_ptr;
+}
+
+static int test_lazy_distribution(void)
+{
+ struct ssm_pool * pool;
+ struct _ssm_pool_hdr * hdr;
+ struct _ssm_size_class * sc;
+ int i;
+ int sc_idx;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail;
+ }
+
+ hdr = get_pool_hdr(pool);
+ if (hdr == NULL) {
+ printf("Failed to get pool header.\n");
+ goto fail_pool;
+ }
+
+ /* Inspect the class that TEST_SIZE allocations will use */
+ sc_idx = select_size_class(hdr, TEST_SIZE);
+ if (sc_idx < 0) {
+ printf("No size class fits TEST_SIZE=%d.\n", TEST_SIZE);
+ for (i = 0; i < SSM_POOL_MAX_CLASSES; i++) {
+ printf(" Class %d: object_size=%zu count=%zu\n", i,
+ hdr->size_classes[i].object_size,
+ hdr->size_classes[i].object_count);
+ }
+ goto fail_pool;
+ }
+
+ sc = &hdr->size_classes[sc_idx];
+
+ /* Verify all blocks start in shard 0 */
+ if (sc->shards[0].free_count == 0) {
+ printf("Shard 0 should have all blocks initially.\n");
+ goto fail_pool;
+ }
+
+ /* Verify other shards are empty */
+ for (i = 1; i < SSM_POOL_SHARDS; i++) {
+ if (sc->shards[i].free_count != 0) {
+ printf("Shard %d should be empty, has %zu.\n",
+ i, sc->shards[i].free_count);
+ goto fail_pool;
+ }
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_shard_migration(void)
+{
+ struct ssm_pool * pool;
+ struct _ssm_pool_hdr * hdr;
+ struct _ssm_size_class * sc;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t off;
+ int shard_idx;
+ int sc_idx;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail;
+ }
+
+ hdr = get_pool_hdr(pool);
+
+ /* Inspect the class that TEST_SIZE allocations will use */
+ sc_idx = select_size_class(hdr, TEST_SIZE);
+ if (sc_idx < 0) {
+ printf("No size class fits TEST_SIZE=%d.\n", TEST_SIZE);
+ goto fail_pool;
+ }
+
+ sc = &hdr->size_classes[sc_idx];
+
+ /* Allocate from this process */
+ off = ssm_pool_alloc(pool, TEST_SIZE, &ptr, &spb);
+ if (off < 0) {
+ printf("Allocation failed: %zd.\n", off);
+ goto fail_pool;
+ }
+
+ /* Free it - should go to this process's shard */
+ shard_idx = getpid() % SSM_POOL_SHARDS;
+ if (ssm_pool_remove(pool, off) != 0) {
+ printf("Remove failed.\n");
+ goto fail_pool;
+ }
+
+ /* Verify block migrated away from shard 0 or in allocator's shard */
+ if (sc->shards[shard_idx].free_count == 0 &&
+ sc->shards[0].free_count == 0) {
+ printf("Block should have been freed to a shard.\n");
+ goto fail_pool;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_fallback_stealing(void)
+{
+ struct ssm_pool * pool;
+ struct _ssm_pool_hdr * hdr;
+ struct _ssm_size_class * sc;
+ struct ssm_pk_buff ** spbs;
+ uint8_t ** ptrs;
+ size_t total_blocks;
+ size_t total_free;
+ size_t i;
+ int sc_idx;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail;
+ }
+
+ hdr = get_pool_hdr(pool);
+
+ /* Inspect the class that TEST_SIZE allocations will use */
+ sc_idx = select_size_class(hdr, TEST_SIZE);
+ if (sc_idx < 0) {
+ printf("No size class fits TEST_SIZE=%d.\n", TEST_SIZE);
+ goto fail_pool;
+ }
+
+ sc = &hdr->size_classes[sc_idx];
+ total_blocks = sc->object_count;
+
+ spbs = malloc(total_blocks * sizeof(struct ssm_pk_buff *));
+ ptrs = malloc(total_blocks * sizeof(uint8_t *));
+ if (spbs == NULL || ptrs == NULL) {
+ printf("Failed to allocate test arrays.\n");
+ free(spbs);
+ free(ptrs);
+ goto fail_pool;
+ }
+
+ /* Allocate half the blocks from single process */
+ for (i = 0; i < total_blocks / 2; i++) {
+ ssize_t off = ssm_pool_alloc(pool, TEST_SIZE,
+ &ptrs[i], &spbs[i]);
+ if (off < 0) {
+ printf("Allocation %zu failed: %zd.\n", i, off);
+ free(spbs);
+ free(ptrs);
+ goto fail_pool;
+ }
+ }
+
+ /* Free them all - they go to local_shard */
+ for (i = 0; i < total_blocks / 2; i++) {
+ size_t off = ssm_pk_buff_get_off(spbs[i]);
+ if (ssm_pool_remove(pool, off) != 0) {
+ printf("Remove %zu failed.\n", i);
+ free(spbs);
+ free(ptrs);
+ goto fail_pool;
+ }
+ }
+
+ /* Freed blocks should be in shards (all blocks free again) */
+ total_free = 0;
+ for (i = 0; i < SSM_POOL_SHARDS; i++) {
+ total_free += sc->shards[i].free_count;
+ }
+
+ if (total_free != total_blocks) {
+ printf("Expected %zu free blocks total, got %zu.\n",
+ total_blocks, total_free);
+ free(spbs);
+ free(ptrs);
+ goto fail_pool;
+ }
+
+ /* Allocate again - should succeed by taking from shards */
+ for (i = 0; i < total_blocks / 2; i++) {
+ ssize_t off = ssm_pool_alloc(pool, TEST_SIZE,
+ &ptrs[i], &spbs[i]);
+ if (off < 0) {
+ printf("Fallback alloc %zu failed: %zd.\n", i, off);
+ free(spbs);
+ free(ptrs);
+ goto fail_pool;
+ }
+ }
+
+ /* Now all allocated blocks are in use again */
+ /* Cleanup - free all allocated blocks */
+ for (i = 0; i < total_blocks / 2; i++) {
+ size_t off = ssm_pk_buff_get_off(spbs[i]);
+ ssm_pool_remove(pool, off);
+ }
+
+ free(spbs);
+ free(ptrs);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_multiprocess_sharding(void)
+{
+ struct ssm_pool * pool;
+ struct _ssm_pool_hdr * hdr;
+ struct _ssm_size_class * sc;
+ pid_t children[SSM_POOL_SHARDS];
+ int i;
+ int status;
+
+ TEST_START();
+
+ for (i = 0; i < SSM_POOL_SHARDS; i++)
+ children[i] = -1;
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail;
+ }
+
+ /* Fork processes to test different shards */
+ for (i = 0; i < SSM_POOL_SHARDS; i++) {
+ children[i] = fork();
+ if (children[i] == -1) {
+ printf("Fork %d failed.\n", i);
+ goto fail_children;
+ }
+
+ if (children[i] == 0) {
+ /* Child process */
+ struct ssm_pool * child_pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t off;
+ int my_shard;
+
+ child_pool = ssm_pool_open(getuid());
+ if (child_pool == NULL)
+ exit(EXIT_FAILURE);
+
+ my_shard = getpid() % SSM_POOL_SHARDS;
+ (void) my_shard; /* Reserved for future use */
+
+ /* Each child allocates and frees a block */
+ off = ssm_pool_alloc(child_pool, TEST_SIZE,
+ &ptr, &spb);
+ if (off < 0) {
+ ssm_pool_close(child_pool);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Small delay to ensure allocation visible */
+ usleep(10000);
+
+ if (ssm_pool_remove(child_pool, off) != 0) {
+ ssm_pool_close(child_pool);
+ exit(EXIT_FAILURE);
+ }
+
+ ssm_pool_close(child_pool);
+ exit(EXIT_SUCCESS);
+ }
+ }
+
+ /* Wait for all children */
+ for (i = 0; i < SSM_POOL_SHARDS; i++) {
+ if (waitpid(children[i], &status, 0) == -1) {
+ printf("Waitpid %d failed.\n", i);
+ goto fail_children;
+ }
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ printf("Child %d failed.\n", i);
+ goto fail_pool;
+ }
+ }
+
+ /* Verify blocks distributed across shards */
+ hdr = get_pool_hdr(pool);
+
+ /* Inspect the class that TEST_SIZE allocations used */
+ i = select_size_class(hdr, TEST_SIZE);
+ if (i < 0) {
+ printf("No size class fits TEST_SIZE=%d.\n", TEST_SIZE);
+ goto fail_pool;
+ }
+
+ sc = &hdr->size_classes[i];
+
+ /* After children allocate and free, blocks should be in shards
+ * (though exact distribution depends on PID values)
+ */
+ for (i = 0; i < SSM_POOL_SHARDS; i++) {
+ /* At least some shards should have blocks */
+ if (sc->shards[i].free_count > 0) {
+ break;
+ }
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_children:
+ /* Kill any remaining children */
+ for (i = 0; i < SSM_POOL_SHARDS; i++) {
+ if (children[i] > 0)
+ kill(children[i], SIGKILL);
+ }
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_exhaustion_with_fallback(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t off;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail;
+ }
+
+ /* Allocate until exhausted across all shards */
+ while (true) {
+ off = ssm_pool_alloc(pool, TEST_SIZE, &ptr, &spb);
+ if (off < 0) {
+ if (off == -EAGAIN)
+ break;
+ printf("Unexpected error: %zd.\n", off);
+ goto fail_pool;
+ }
+ }
+
+ /* Should fail with -EAGAIN when truly exhausted */
+ off = ssm_pool_alloc(pool, TEST_SIZE, &ptr, &spb);
+ if (off != -EAGAIN) {
+ printf("Expected -EAGAIN, got %zd.\n", off);
+ goto fail_pool;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int pool_sharding_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_lazy_distribution();
+ ret |= test_shard_migration();
+ ret |= test_fallback_stealing();
+ ret |= test_multiprocess_sharding();
+ ret |= test_exhaustion_with_fallback();
+
+ return ret;
+}
diff --git a/src/lib/ssm/tests/pool_test.c b/src/lib/ssm/tests/pool_test.c
new file mode 100644
index 00000000..f86fbd9e
--- /dev/null
+++ b/src/lib/ssm/tests/pool_test.c
@@ -0,0 +1,1068 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the Secure Shared Memory (SSM) system
+ *
+ * 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 "ssm.h"
+
+#include <test/test.h>
+#include <ouroboros/ssm_pool.h>
+#include <ouroboros/ssm_rbuff.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <stdatomic.h>
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <signal.h>
+#include <time.h>
+
+#define POOL_256 256
+#define POOL_512 512
+#define POOL_1K 1024
+#define POOL_2K 2048
+#define POOL_4K 4096
+#define POOL_16K 16384
+#define POOL_64K 65536
+#define POOL_256K 262144
+#define POOL_1M 1048576
+#define POOL_2M (2 * 1024 * 1024)
+
+static int test_ssm_pool_basic_allocation(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr;
+ struct ssm_pk_buff * spb;
+ ssize_t ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Alloc failed: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ if (spb == NULL) {
+ printf("Spb is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ptr == NULL) {
+ printf("Ptr is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ssm_pk_buff_len(spb) != POOL_256) {
+ printf("Bad length: %zu.\n", ssm_pk_buff_len(spb));
+ goto fail_alloc;
+ }
+
+ ret = ssm_pool_remove(pool, ret);
+ if (ret != 0) {
+ printf("Remove failed: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_multiple_allocations(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr1;
+ uint8_t * ptr2;
+ uint8_t * ptr3;
+ struct ssm_pk_buff * spb1;
+ struct ssm_pk_buff * spb2;
+ struct ssm_pk_buff * spb3;
+ ssize_t ret1;
+ ssize_t ret2;
+ ssize_t ret3;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret1 = ssm_pool_alloc(pool, POOL_256, &ptr1, &spb1);
+ ret2 = ssm_pool_alloc(pool, POOL_256, &ptr2, &spb2);
+ ret3 = ssm_pool_alloc(pool, POOL_256, &ptr3, &spb3);
+ if (ret1 < 0 || ret2 < 0 || ret3 < 0) {
+ printf("Allocs failed: %zd, %zd, %zd.\n", ret1, ret2, ret3);
+ goto fail_alloc;
+ }
+
+ if (spb1 == NULL) {
+ printf("Spb1 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ptr1 == NULL) {
+ printf("Ptr1 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (spb2 == NULL) {
+ printf("Spb2 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ptr2 == NULL) {
+ printf("Ptr2 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (spb3 == NULL) {
+ printf("Spb3 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ptr3 == NULL) {
+ printf("Ptr3 is NULL.\n");
+ goto fail_alloc;
+ }
+
+ if (ssm_pk_buff_len(spb1) != POOL_256) {
+ printf("Bad length spb1: %zu.\n", ssm_pk_buff_len(spb1));
+ goto fail_alloc;
+ }
+
+ if (ssm_pk_buff_len(spb2) != POOL_256) {
+ printf("Bad length spb2: %zu.\n", ssm_pk_buff_len(spb2));
+ goto fail_alloc;
+ }
+
+ if (ssm_pk_buff_len(spb3) != POOL_256) {
+ printf("Bad length spb3: %zu.\n", ssm_pk_buff_len(spb3));
+ goto fail_alloc;
+ }
+
+ if (ssm_pool_remove(pool, ret2) != 0) {
+ printf("Remove ret2 failed.\n");
+ goto fail_alloc;
+ }
+
+ if (ssm_pool_remove(pool, ret1) != 0) {
+ printf("Remove ret1 failed.\n");
+ goto fail_alloc;
+ }
+
+ if (ssm_pool_remove(pool, ret3) != 0) {
+ printf("Remove ret3 failed.\n");
+ goto fail_alloc;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_no_fallback_for_large(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr;
+ struct ssm_pk_buff * spb;
+ ssize_t ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret = ssm_pool_alloc(pool, POOL_2M, &ptr, &spb);
+ if (ret >= 0) {
+ printf("Oversized alloc succeeded: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ if (ret != -EMSGSIZE) {
+ printf("Wrong error: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_blocking_vs_nonblocking(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr;
+ struct ssm_pk_buff * spb;
+ ssize_t ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret = ssm_pool_alloc(pool, POOL_2M, &ptr, &spb);
+ if (ret != -EMSGSIZE) {
+ printf("Nonblocking oversized: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ret = ssm_pool_alloc_b(pool, POOL_2M, &ptr, &spb, NULL);
+ if (ret != -EMSGSIZE) {
+ printf("Blocking oversized: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Valid alloc failed: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ssm_pool_remove(pool, ret);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_stress_test(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr;
+ struct ssm_pk_buff * spb;
+ ssize_t * indices = NULL;
+ ssize_t ret;
+ size_t count = 0;
+ size_t i;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ indices = malloc(100 * sizeof(*indices));
+ if (indices == NULL) {
+ printf("Malloc failed.\n");
+ goto fail_alloc;
+ }
+
+ for (i = 0; i < 50; i++) {
+ size_t j;
+ size_t num;
+ size_t size;
+
+ num = (i % 50) + 1;
+
+ for (j = 0; j < num && count < 50; j++) {
+ switch (i % 4) {
+ case 0:
+ /* FALLTHRU */
+ case 1:
+ size = POOL_256;
+ break;
+ case 2:
+ /* FALLTHRU */
+ case 3:
+ size = POOL_1K;
+ break;
+ default:
+ size = POOL_256;
+ break;
+ }
+
+ ret = ssm_pool_alloc(pool, size, &ptr, &spb);
+ if (ret < 0) {
+ printf("Alloc at iter %zu: %zd.\n", i, ret);
+ goto fail_test;
+ }
+ indices[count++] = ret;
+ }
+
+ for (j = 0; j < count / 2; j++) {
+ size_t idx = j * 2;
+ if (idx < count) {
+ ret = ssm_pool_remove(pool, indices[idx]);
+ if (ret != 0) {
+ printf("Remove at iter %zu: %zd.\n",
+ i, ret);
+ goto fail_test;
+ }
+ memmove(&indices[idx], &indices[idx + 1],
+ (count - idx - 1) * sizeof(*indices));
+ count--;
+ }
+ }
+
+ if (i % 10 == 0) {
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Periodic alloc at %zu: %zd.\n", i, ret);
+ goto fail_test;
+ }
+ ssm_pool_remove(pool, ret);
+ }
+ }
+
+ for (i = 0; i < count; i++)
+ ssm_pool_remove(pool, indices[i]);
+
+ free(indices);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_test:
+ for (i = 0; i < count; i++)
+ ssm_pool_remove(pool, indices[i]);
+ free(indices);
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_open_initializes_ssm(void)
+{
+ struct ssm_pool * creator;
+ struct ssm_pool * opener;
+ uint8_t * ptr;
+ struct ssm_pk_buff * spb;
+ ssize_t ret;
+
+ TEST_START();
+
+ creator = ssm_pool_create(getuid(), getgid());
+ if (creator == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret = ssm_pool_alloc(creator, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Creator alloc failed: %zd.\n", ret);
+ goto fail_creator;
+ }
+ ssm_pool_remove(creator, ret);
+
+ opener = ssm_pool_open(getuid());
+ if (opener == NULL) {
+ printf("Open failed.\n");
+ goto fail_creator;
+ }
+
+ ret = ssm_pool_alloc(opener, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Opener alloc failed: %zd.\n", ret);
+ goto fail_opener;
+ }
+
+ ssm_pool_remove(opener, ret);
+ ssm_pool_close(opener);
+ ssm_pool_destroy(creator);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_opener:
+ ssm_pool_close(opener);
+ fail_creator:
+ ssm_pool_destroy(creator);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_bounds_checking(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ ssize_t ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret = ssm_pool_alloc(pool, POOL_256, NULL, &spb);
+ if (ret < 0) {
+ printf("alloc failed: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ spb = ssm_pool_get(pool, 0);
+ if (spb != NULL) {
+ printf("Get at offset 0.\n");
+ goto fail_alloc;
+ }
+
+ spb = ssm_pool_get(pool, 100000000UL);
+ if (spb != NULL) {
+ printf("Get beyond pool.\n");
+ goto fail_alloc;
+ }
+
+ ret = ssm_pool_remove(pool, 0);
+ if (ret != -EINVAL) {
+ printf("Remove at offset 0: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ret = ssm_pool_remove(pool, 100000000UL);
+ if (ret != -EINVAL) {
+ printf("Remove beyond pool: %zd.\n", ret);
+ goto fail_alloc;
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_inter_process_communication(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_rbuff * rb;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ uint8_t * data;
+ const char * msg = "inter-process test";
+ size_t len;
+ ssize_t idx;
+ pid_t pid;
+ int status;
+
+ TEST_START();
+
+ len = strlen(msg) + 1;
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ rb = ssm_rbuff_create(getpid(), 1);
+ if (rb == NULL) {
+ printf("Rbuff create failed.\n");
+ goto fail_pool;
+ }
+
+ pid = fork();
+ if (pid < 0) {
+ printf("Fork failed.\n");
+ goto fail_rbuff;
+ }
+
+ if (pid == 0) {
+ idx = ssm_rbuff_read_b(rb, NULL);
+ if (idx < 0) {
+ printf("Child: rbuff read: %zd.\n", idx);
+ exit(1);
+ }
+
+ spb = ssm_pool_get(pool, idx);
+ if (spb == NULL) {
+ printf("Child: pool get failed.\n");
+ exit(1);
+ }
+
+ data = ssm_pk_buff_head(spb);
+ if (data == NULL) {
+ printf("Child: data is NULL.\n");
+ ssm_pool_remove(pool, idx);
+ exit(1);
+ }
+
+ if (strcmp((char *)data, msg) != 0) {
+ printf("Child: data mismatch.\n");
+ ssm_pool_remove(pool, idx);
+ exit(1);
+ }
+
+ ssm_pool_remove(pool, idx);
+ exit(0);
+ }
+
+ idx = ssm_pool_alloc(pool, len, &ptr, &spb);
+ if (idx < 0) {
+ printf("Parent: pool alloc: %zd.\n", idx);
+ goto fail_child;
+ }
+
+ memcpy(ptr, msg, len);
+
+ if (ssm_rbuff_write(rb, idx) < 0) {
+ printf("Parent: rbuff write failed.\n");
+ ssm_pool_remove(pool, idx);
+ goto fail_child;
+ }
+
+ if (waitpid(pid, &status, 0) < 0) {
+ printf("Parent: waitpid failed.\n");
+ ssm_pool_remove(pool, idx);
+ goto fail_rbuff;
+ }
+
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ printf("Child failed.\n");
+ ssm_pool_remove(pool, idx);
+ goto fail_rbuff;
+ }
+
+ ssm_rbuff_destroy(rb);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_child:
+ waitpid(pid, &status, 0);
+ fail_rbuff:
+ ssm_rbuff_destroy(rb);
+ fail_pool:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_read_operation(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * wptr;
+ uint8_t * rptr;
+ const char * data = "ssm_pool_read test";
+ size_t len;
+ ssize_t idx;
+ ssize_t ret;
+
+ TEST_START();
+
+ len = strlen(data) + 1;
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ idx = ssm_pool_alloc(pool, len, &wptr, &spb);
+ if (idx < 0) {
+ printf("alloc failed: %zd.\n", idx);
+ goto fail_alloc;
+ }
+
+ memcpy(wptr, data, len);
+
+ ret = ssm_pool_read(&rptr, pool, idx);
+ if (ret < 0) {
+ printf("Read failed: %zd.\n", ret);
+ goto fail_read;
+ }
+
+ if (rptr == NULL) {
+ printf("NULL pointer.\n");
+ goto fail_read;
+ }
+
+ if (strcmp((char *)rptr, data) != 0) {
+ printf("Data mismatch.\n");
+ goto fail_read;
+ }
+
+ ssm_pool_remove(pool, idx);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_read:
+ ssm_pool_remove(pool, idx);
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_mlock_operation(void)
+{
+ struct ssm_pool * pool;
+ int ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ ret = ssm_pool_mlock(pool);
+ if (ret < 0)
+ printf("Mlock failed: %d (may need privileges).\n", ret);
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pk_buff_operations(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ uint8_t * head;
+ uint8_t * tail;
+ const char * data = "packet buffer test";
+ size_t dlen;
+ size_t len;
+ ssize_t idx;
+
+ TEST_START();
+
+ dlen = strlen(data);
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ idx = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (idx < 0) {
+ printf("alloc failed: %zd.\n", idx);
+ goto fail_alloc;
+ }
+
+ head = ssm_pk_buff_head(spb);
+ if (head != ptr) {
+ printf("Head mismatch.\n");
+ goto fail_ops;
+ }
+
+ len = ssm_pk_buff_len(spb);
+ if (len != POOL_256) {
+ printf("Bad length: %zu.\n", len);
+ goto fail_ops;
+ }
+
+ tail = ssm_pk_buff_tail(spb);
+ if (tail != ptr + len) {
+ printf("Tail mismatch.\n");
+ goto fail_ops;
+ }
+
+ memcpy(head, data, dlen);
+
+ tail = ssm_pk_buff_push_tail(spb, 32);
+ if (tail == NULL) {
+ printf("push_tail failed.\n");
+ goto fail_ops;
+ }
+
+ if (ssm_pk_buff_len(spb) != POOL_256 + 32) {
+ printf("Length after push_tail: %zu.\n",
+ ssm_pk_buff_len(spb));
+ goto fail_ops;
+ }
+
+ if (memcmp(head, data, dlen) != 0) {
+ printf("Data corrupted.\n");
+ goto fail_ops;
+ }
+
+ tail = ssm_pk_buff_pop_tail(spb, 32);
+ if (tail == NULL) {
+ printf("pop_tail failed.\n");
+ goto fail_ops;
+ }
+
+ if (ssm_pk_buff_len(spb) != POOL_256) {
+ printf("Length after pop_tail: %zu.\n",
+ ssm_pk_buff_len(spb));
+ goto fail_ops;
+ }
+
+ ssm_pool_remove(pool, idx);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_ops:
+ ssm_pool_remove(pool, idx);
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+#define OVERHEAD (offsetof(struct ssm_pk_buff, data) + \
+ SSM_PK_BUFF_HEADSPACE + SSM_PK_BUFF_TAILSPACE)
+static int test_ssm_pool_size_class_boundaries(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ size_t sizes[] = {
+ POOL_512 - OVERHEAD,
+ POOL_512 - OVERHEAD + 1,
+ POOL_1K - OVERHEAD,
+ POOL_1K - OVERHEAD + 1,
+ POOL_2K - OVERHEAD,
+ POOL_2K - OVERHEAD + 1,
+ POOL_4K - OVERHEAD,
+ POOL_4K - OVERHEAD + 1,
+ POOL_16K - OVERHEAD,
+ POOL_16K - OVERHEAD + 1,
+ POOL_64K - OVERHEAD,
+ POOL_64K - OVERHEAD + 1,
+ POOL_256K - OVERHEAD,
+ };
+ size_t expected_classes[] = {
+ 512, 1024, 1024, 2048, 2048, 4096, 4096, 16384,
+ 16384, 65536, 65536, 262144, 262144
+ };
+ size_t i;
+ ssize_t idx;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) {
+ struct ssm_pk_buff * hdr;
+ size_t actual_class;
+
+ idx = ssm_pool_alloc(pool, sizes[i], &ptr, &spb);
+ if (idx < 0) {
+ printf("Alloc at %zu failed: %zd.\n", sizes[i], idx);
+ goto fail_alloc;
+ }
+
+ if (ssm_pk_buff_len(spb) != sizes[i]) {
+ printf("Length mismatch at %zu: %zu.\n",
+ sizes[i], ssm_pk_buff_len(spb));
+ ssm_pool_remove(pool, idx);
+ goto fail_alloc;
+ }
+
+ /* Verify correct size class was used
+ * hdr->size is the data array size (object_size - header) */
+ hdr = spb;
+ actual_class = hdr->size + offsetof(struct ssm_pk_buff, data);
+ if (actual_class != expected_classes[i]) {
+ printf("Wrong class for len=%zu: want %zu, got %zu.\n",
+ sizes[i], expected_classes[i], actual_class);
+ ssm_pool_remove(pool, idx);
+ goto fail_alloc;
+ }
+
+ memset(ptr, i & 0xFF, sizes[i]);
+
+ ssm_pool_remove(pool, idx);
+ }
+
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_exhaustion(void)
+{
+ struct ssm_pool * pool;
+ struct ssm_pk_buff * spb;
+ uint8_t * ptr;
+ ssize_t * indices;
+ size_t count = 0;
+ size_t i;
+ ssize_t ret;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ indices = malloc(2048 * sizeof(*indices));
+ if (indices == NULL) {
+ printf("Malloc failed.\n");
+ goto fail_alloc;
+ }
+
+ for (i = 0; i < 2048; i++) {
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ if (ret == -EAGAIN)
+ break;
+ printf("Alloc error: %zd.\n", ret);
+ goto fail_test;
+ }
+ indices[count++] = ret;
+ }
+
+ if (count == 0) {
+ printf("No allocs succeeded.\n");
+ goto fail_test;
+ }
+
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret >= 0) {
+ ssm_pool_remove(pool, ret);
+ } else if (ret != -EAGAIN) {
+ printf("Unexpected error: %zd.\n", ret);
+ goto fail_test;
+ }
+
+ for (i = 0; i < count; i++)
+ ssm_pool_remove(pool, indices[i]);
+
+ ret = ssm_pool_alloc(pool, POOL_256, &ptr, &spb);
+ if (ret < 0) {
+ printf("Alloc after free failed: %zd.\n", ret);
+ goto fail_test;
+ }
+ ssm_pool_remove(pool, ret);
+
+ free(indices);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_test:
+ for (i = 0; i < count; i++)
+ ssm_pool_remove(pool, indices[i]);
+ free(indices);
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_pool_reclaim_orphans(void)
+{
+ struct ssm_pool * pool;
+ uint8_t * ptr1;
+ uint8_t * ptr2;
+ uint8_t * ptr3;
+ struct ssm_pk_buff * spb1;
+ struct ssm_pk_buff * spb2;
+ struct ssm_pk_buff * spb3;
+ ssize_t ret1;
+ ssize_t ret2;
+ ssize_t ret3;
+ pid_t my_pid;
+ pid_t fake_pid = 99999;
+ struct timespec now;
+ uint64_t old_ts;
+
+ TEST_START();
+
+ pool = ssm_pool_create(getuid(), getgid());
+ if (pool == NULL) {
+ printf("Failed to create pool.\n");
+ goto fail_create;
+ }
+
+ my_pid = getpid();
+
+ /* Allocate some blocks */
+ ret1 = ssm_pool_alloc(pool, POOL_256, &ptr1, &spb1);
+ ret2 = ssm_pool_alloc(pool, POOL_512, &ptr2, &spb2);
+ ret3 = ssm_pool_alloc(pool, POOL_1K, &ptr3, &spb3);
+ if (ret1 < 0 || ret2 < 0 || ret3 < 0) {
+ printf("Allocs failed: %zd, %zd, %zd.\n", ret1, ret2, ret3);
+ goto fail_alloc;
+ }
+
+ /* 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 */
+ ssm_pool_reclaim_orphans(pool, fake_pid);
+
+ /* Verify spb1 and spb2 have refcount 0 (reclaimed) */
+ if (spb1->refcount != 0) {
+ printf("spb1 refcount should be 0, got %u.\n", spb1->refcount);
+ goto fail_test;
+ }
+
+ if (spb2->refcount != 0) {
+ printf("spb2 refcount should be 0, got %u.\n", spb2->refcount);
+ goto fail_test;
+ }
+
+ /* Verify spb3 still has refcount 1 (not reclaimed) */
+ if (spb3->refcount != 1) {
+ printf("spb3 refcount should be 1, got %u.\n", spb3->refcount);
+ goto fail_test;
+ }
+
+ /* Clean up */
+ ssm_pool_remove(pool, ret3);
+
+ /* Try allocating again - should get blocks from reclaimed pool */
+ ret1 = ssm_pool_alloc(pool, POOL_256, &ptr1, &spb1);
+ if (ret1 < 0) {
+ printf("Alloc after reclaim failed: %zd.\n", ret1);
+ goto fail_test;
+ }
+
+ /* Verify new allocation has our pid */
+ if (spb1->allocator_pid != my_pid) {
+ printf("New block has wrong pid: %d vs %d.\n",
+ spb1->allocator_pid, my_pid);
+ goto fail_test;
+ }
+
+ ssm_pool_remove(pool, ret1);
+ ssm_pool_destroy(pool);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_test:
+ ssm_pool_remove(pool, ret3);
+ fail_alloc:
+ ssm_pool_destroy(pool);
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int pool_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_ssm_pool_basic_allocation();
+ ret |= test_ssm_pool_multiple_allocations();
+ ret |= test_ssm_pool_no_fallback_for_large();
+ ret |= test_ssm_pool_blocking_vs_nonblocking();
+ ret |= test_ssm_pool_stress_test();
+ ret |= test_ssm_pool_open_initializes_ssm();
+ ret |= test_ssm_pool_bounds_checking();
+ ret |= test_ssm_pool_inter_process_communication();
+ ret |= test_ssm_pool_read_operation();
+ ret |= test_ssm_pool_mlock_operation();
+ ret |= test_ssm_pk_buff_operations();
+ ret |= test_ssm_pool_size_class_boundaries();
+ ret |= test_ssm_pool_exhaustion();
+ ret |= test_ssm_pool_reclaim_orphans();
+
+ return ret;
+}
diff --git a/src/lib/ssm/tests/rbuff_test.c b/src/lib/ssm/tests/rbuff_test.c
new file mode 100644
index 00000000..48e5a714
--- /dev/null
+++ b/src/lib/ssm/tests/rbuff_test.c
@@ -0,0 +1,675 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the SSM notification ring buffer
+ *
+ * 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"
+#include "ssm.h"
+
+#include <test/test.h>
+#include <ouroboros/ssm_rbuff.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/time.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <pthread.h>
+
+static int test_ssm_rbuff_create_destroy(void)
+{
+ struct ssm_rbuff * rb;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 1);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ ssm_rbuff_destroy(rb);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_rbuff_write_read(void)
+{
+ struct ssm_rbuff * rb;
+ ssize_t idx;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 2);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ if (ssm_rbuff_write(rb, 42) < 0) {
+ printf("Failed to write value.\n");
+ goto fail_rb;
+ }
+
+ if (ssm_rbuff_queued(rb) != 1) {
+ printf("Queue length should be 1, got %zu.\n",
+ ssm_rbuff_queued(rb));
+ goto fail_rb;
+ }
+
+ idx = ssm_rbuff_read(rb);
+ if (idx != 42) {
+ printf("Expected 42, got %zd.\n", idx);
+ goto fail_rb;
+ }
+
+ if (ssm_rbuff_queued(rb) != 0) {
+ printf("Queue should be empty, got %zu.\n",
+ ssm_rbuff_queued(rb));
+ goto fail_rb;
+ }
+
+ 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_read_empty(void)
+{
+ struct ssm_rbuff * rb;
+ ssize_t ret;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 3);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ ret = ssm_rbuff_read(rb);
+ if (ret != -EAGAIN) {
+ printf("Expected -EAGAIN, got %zd.\n", ret);
+ goto fail_rb;
+ }
+
+ 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_fill_drain(void)
+{
+ struct ssm_rbuff * rb;
+ size_t i;
+ ssize_t ret;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 4);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) {
+ if (ssm_rbuff_queued(rb) != i) {
+ printf("Expected %zu queued, got %zu.\n",
+ 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;
+ }
+ }
+
+ if (ssm_rbuff_queued(rb) != SSM_RBUFF_SIZE - 1) {
+ printf("Expected %d queued, got %zu.\n",
+ SSM_RBUFF_SIZE - 1, ssm_rbuff_queued(rb));
+ goto fail_rb;
+ }
+
+ ret = ssm_rbuff_write(rb, 999);
+ if (ret != -EAGAIN) {
+ printf("Expected -EAGAIN on full buffer, got %zd.\n", ret);
+ goto fail_rb;
+ }
+
+ for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) {
+ ret = ssm_rbuff_read(rb);
+ if (ret != (ssize_t) i) {
+ printf("Expected %zu, got %zd.\n", i, ret);
+ goto fail_rb;
+ }
+ }
+
+ if (ssm_rbuff_queued(rb) != 0) {
+ printf("Expected empty queue, got %zu.\n",
+ ssm_rbuff_queued(rb));
+ 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_flags(void)
+{
+ struct ssm_rbuff * rb;
+ uint32_t flags;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 5);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ flags = ssm_rbuff_get_flags(rb);
+ if (flags != RB_RDWR) {
+ printf("Expected RB_RDWR, got %u.\n", flags);
+ goto fail_rb;
+ }
+
+ ssm_rbuff_clr_bits(rb, RB_WR);
+ flags = ssm_rbuff_get_flags(rb);
+ if (flags != RB_RD) {
+ printf("Expected RB_RD, got %u.\n", flags);
+ goto fail_rb;
+ }
+
+ if (ssm_rbuff_write(rb, 1) != -ENOTALLOC) {
+ printf("Expected -ENOTALLOC on RDONLY.\n");
+ goto fail_rb;
+ }
+
+ ssm_rbuff_set_bits(rb, RB_FLOWDOWN);
+ if (ssm_rbuff_write(rb, 1) != -EFLOWDOWN) {
+ printf("Expected -EFLOWDOWN on FLOWDOWN.\n");
+ goto fail_rb;
+ }
+
+ if (ssm_rbuff_read(rb) != -EFLOWDOWN) {
+ printf("Expected -EFLOWDOWN on read with FLOWDOWN.\n");
+ goto fail_rb;
+ }
+
+ 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_open_close(void)
+{
+ struct ssm_rbuff * rb1;
+ struct ssm_rbuff * rb2;
+ pid_t pid;
+
+ TEST_START();
+
+ pid = getpid();
+
+ rb1 = ssm_rbuff_create(pid, 6);
+ if (rb1 == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ if (ssm_rbuff_write(rb1, 123) < 0) {
+ printf("Failed to write value.\n");
+ goto fail_rb1;
+ }
+
+ rb2 = ssm_rbuff_open(pid, 6);
+ if (rb2 == NULL) {
+ printf("Failed to open existing rbuff.\n");
+ goto fail_rb1;
+ }
+
+ if (ssm_rbuff_queued(rb2) != 1) {
+ printf("Expected 1 queued in opened rbuff, got %zu.\n",
+ ssm_rbuff_queued(rb2));
+ goto fail_rb2;
+ }
+
+ if (ssm_rbuff_read(rb2) != 123) {
+ printf("Failed to read from opened rbuff.\n");
+ goto fail_rb2;
+ }
+
+ ssm_rbuff_close(rb2);
+ ssm_rbuff_destroy(rb1);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_rb2:
+ ssm_rbuff_close(rb2);
+ fail_rb1:
+ ssm_rbuff_destroy(rb1);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+struct thread_args {
+ struct ssm_rbuff * rb;
+ int iterations;
+ int delay_us;
+};
+
+static void * writer_thread(void * arg)
+{
+ struct thread_args * args = (struct thread_args *) arg;
+ struct timespec delay = {0, 0};
+ int i;
+
+ delay.tv_nsec = args->delay_us * 1000L;
+
+ for (i = 0; i < args->iterations; ++i) {
+ while (ssm_rbuff_write(args->rb, i) < 0)
+ nanosleep(&delay, NULL);
+ }
+
+ return NULL;
+}
+
+static void * reader_thread(void * arg)
+{
+ struct thread_args * args = (struct thread_args *) arg;
+ struct timespec delay = {0, 0};
+ int i;
+ ssize_t val;
+
+ delay.tv_nsec = args->delay_us * 1000L;
+
+ for (i = 0; i < args->iterations; ++i) {
+ 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;
+ }
+ }
+
+ return NULL;
+}
+
+static void * blocking_writer_thread(void * arg)
+{
+ struct thread_args * args = (struct thread_args *) arg;
+ int i;
+
+ for (i = 0; i < args->iterations; ++i) {
+ if (ssm_rbuff_write_b(args->rb, i, NULL) < 0)
+ return (void *) -1;
+ }
+
+ return NULL;
+}
+
+static void * blocking_reader_thread(void * arg)
+{
+ struct thread_args * args = (struct thread_args *) arg;
+ int i;
+ ssize_t val;
+
+ for (i = 0; i < args->iterations; ++i) {
+ val = ssm_rbuff_read_b(args->rb, NULL);
+ if (val < 0 || val != i) {
+ printf("Expected %d, got %zd.\n", i, val);
+ return (void *) -1;
+ }
+ }
+
+ return NULL;
+}
+
+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;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 8);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ args.rb = rb;
+ args.iterations = 50;
+ args.delay_us = 0;
+
+ if (pthread_create(&rthread, NULL, blocking_reader_thread, &args)) {
+ printf("Failed to create reader thread.\n");
+ goto fail_rthread;
+ }
+
+ nanosleep(&delay, NULL);
+
+ if (pthread_create(&wthread, NULL, blocking_writer_thread, &args)) {
+ printf("Failed to create writer thread.\n");
+ pthread_cancel(rthread);
+ goto fail_wthread;
+ }
+
+ pthread_join(wthread, &ret_w);
+ pthread_join(rthread, &ret_r);
+
+ if (ret_w != NULL || ret_r != NULL) {
+ printf("Thread returned error.\n");
+ goto fail_ret;
+ }
+
+ ssm_rbuff_destroy(rb);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_ret:
+ fail_wthread:
+ pthread_join(rthread, NULL);
+ fail_rthread:
+ ssm_rbuff_destroy(rb);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ssm_rbuff_blocking_timeout(void)
+{
+ struct ssm_rbuff * rb;
+ struct timespec abs_timeout;
+ struct timespec interval = {0, 100 * MILLION};
+ struct timespec start;
+ struct timespec end;
+ ssize_t ret;
+ long elapsed_ms;
+ size_t i;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 9);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &start);
+ ts_add(&start, &interval, &abs_timeout);
+
+ ret = ssm_rbuff_read_b(rb, &abs_timeout);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &end);
+
+ if (ret != -ETIMEDOUT) {
+ printf("Expected -ETIMEDOUT, got %zd.\n", ret);
+ goto fail_rb;
+ }
+
+ elapsed_ms = (end.tv_sec - start.tv_sec) * 1000L +
+ (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);
+ goto fail_rb;
+ }
+
+ for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) {
+ if (ssm_rbuff_write(rb, i) < 0) {
+ printf("Failed to fill buffer.\n");
+ goto fail_rb;
+ }
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &start);
+ ts_add(&start, &interval, &abs_timeout);
+
+ ret = ssm_rbuff_write_b(rb, 999, &abs_timeout);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &end);
+
+ if (ret != -ETIMEDOUT) {
+ printf("Expected -ETIMEDOUT on full buffer, got %zd.\n",
+ ret);
+ goto fail_rb;
+ }
+
+ elapsed_ms = (end.tv_sec - start.tv_sec) * 1000L +
+ (end.tv_nsec - start.tv_nsec) / 1000000L;
+
+ if (elapsed_ms < 90 || elapsed_ms > 200) {
+ printf("Write timeout took %ld ms, expected ~100 ms.\n",
+ elapsed_ms);
+ 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_blocking_flowdown(void)
+{
+ struct ssm_rbuff * rb;
+ struct timespec abs_timeout;
+ struct timespec now;
+ struct timespec interval = {5, 0};
+ ssize_t ret;
+ size_t i;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 10);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ ts_add(&now, &interval, &abs_timeout);
+
+ ssm_rbuff_set_bits(rb, RB_FLOWDOWN);
+
+ ret = ssm_rbuff_read_b(rb, &abs_timeout);
+ if (ret != -EFLOWDOWN) {
+ printf("Expected -EFLOWDOWN, got %zd.\n", ret);
+ goto fail_rb;
+ }
+
+ ssm_rbuff_clr_bits(rb, RB_FLOWDOWN);
+
+ for (i = 0; i < SSM_RBUFF_SIZE - 1; ++i) {
+ if (ssm_rbuff_write(rb, i) < 0) {
+ printf("Failed to fill buffer.\n");
+ goto fail_rb;
+ }
+ }
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ ts_add(&now, &interval, &abs_timeout);
+
+ ssm_rbuff_set_bits(rb, RB_FLOWDOWN);
+
+ ret = ssm_rbuff_write_b(rb, 999, &abs_timeout);
+ if (ret != -EFLOWDOWN) {
+ printf("Expected -EFLOWDOWN on write, got %zd.\n", ret);
+ goto fail_rb;
+ }
+
+ ssm_rbuff_clr_bits(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();
+ return TEST_RC_FAIL;
+}
+
+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;
+
+ TEST_START();
+
+ rb = ssm_rbuff_create(getpid(), 7);
+ if (rb == NULL) {
+ printf("Failed to create rbuff.\n");
+ goto fail;
+ }
+
+ args.rb = rb;
+ args.iterations = 100;
+ args.delay_us = 100;
+
+ if (pthread_create(&wthread, NULL, writer_thread, &args)) {
+ printf("Failed to create writer thread.\n");
+ goto fail_rb;
+ }
+
+ if (pthread_create(&rthread, NULL, reader_thread, &args)) {
+ printf("Failed to create reader thread.\n");
+ pthread_cancel(wthread);
+ pthread_join(wthread, NULL);
+ goto fail_rb;
+ }
+
+ pthread_join(wthread, &ret_w);
+ pthread_join(rthread, &ret_r);
+
+ if (ret_w != NULL || ret_r != NULL) {
+ printf("Thread returned error.\n");
+ goto fail_rb;
+ }
+
+ ssm_rbuff_destroy(rb);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_rb:
+ ssm_rbuff_destroy(rb);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int rbuff_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_ssm_rbuff_create_destroy();
+ ret |= test_ssm_rbuff_write_read();
+ ret |= test_ssm_rbuff_read_empty();
+ ret |= test_ssm_rbuff_fill_drain();
+ 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();
+
+ return ret;
+}
diff --git a/src/lib/tests/CMakeLists.txt b/src/lib/tests/CMakeLists.txt
index c795c1ac..002d94af 100644
--- a/src/lib/tests/CMakeLists.txt
+++ b/src/lib/tests/CMakeLists.txt
@@ -1,39 +1,36 @@
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
auth_test.c
+ auth_test_ml_dsa.c
+ auth_test_slh_dsa.c
bitmap_test.c
btree_test.c
- crc32_test.c
crypt_test.c
hash_test.c
+ kex_test.c
+ kex_test_ml_kem.c
+ keyrot_test.c
md5_test.c
sha3_test.c
- shm_rbuff_test.c
sockets_test.c
time_test.c
tpm_test.c
+ tw_test.c
)
add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
-target_link_libraries(${PARENT_DIR}_test ouroboros-common)
-
-add_dependencies(check ${PARENT_DIR}_test)
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_SOURCE_DIR}/src/lib)
-set(tests_to_run ${${PARENT_DIR}_tests})
-if(CMAKE_VERSION VERSION_LESS "3.29.0")
- remove(tests_to_run test_suite.c)
-else ()
- list(POP_FRONT tests_to_run)
-endif()
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test ouroboros-common)
-foreach (test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(${test_name} ${C_TEST_PATH}/${PARENT_DIR}_test ${test_name})
-endforeach (test)
+add_dependencies(build_tests ${PARENT_DIR}_test)
-set_property(TEST auth_test PROPERTY SKIP_RETURN_CODE 1)
-set_property(TEST crypt_test PROPERTY SKIP_RETURN_CODE 1)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/lib/tests/auth_test.c b/src/lib/tests/auth_test.c
index ede294b8..61f97683 100644
--- a/src/lib/tests/auth_test.c
+++ b/src/lib/tests/auth_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the authentication functions
*
@@ -22,110 +22,17 @@
#include "config.h"
-#include <ouroboros/test.h>
+#include <test/test.h>
#include <ouroboros/crypt.h>
+#include <ouroboros/name.h>
#include <ouroboros/random.h>
#include <ouroboros/utils.h>
-#define TEST_MSG_SIZE 1500
+#include <test/certs/ecdsa.h>
-/*
-* Certificates created following the guide
-* Building an openssl certificate authority
-* on
-* https://community.f5.com/kb/technicalarticles/
-*/
-
-/* Root certificate for CA ca.unittest.o7s */
-static const char * root_ca_crt = \
-"-----BEGIN CERTIFICATE-----\n"
-"MIICXTCCAgOgAwIBAgIURlENlCOy1OsA/AXFscPUQ2li8OYwCgYIKoZIzj0EAwIw\n"
-"fDELMAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAK\n"
-"BgNVBAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51\n"
-"bml0dGVzdC5vN3MxEDAOBgkqhkiG9w0BCQEWASAwHhcNMjUwODAzMTg1MzE1WhcN\n"
-"NDUwNzI5MTg1MzE1WjB8MQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYD\n"
-"VQQHDAVHaGVudDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n"
-"GDAWBgNVBAMMD2NhLnVuaXR0ZXN0Lm83czEQMA4GCSqGSIb3DQEJARYBIDBZMBMG\n"
-"ByqGSM49AgEGCCqGSM49AwEHA0IABEPMseCScbd/d5TlHmyYVszn/YGVeNdUCnFR\n"
-"naOr95WlTNo3MyKKBuoiEFwHhjPASgXr/VDVjJLSyM3JUPebAcGjYzBhMB0GA1Ud\n"
-"DgQWBBQkxjMILHH6lZ+rnCMnD/63GO3y1zAfBgNVHSMEGDAWgBQkxjMILHH6lZ+r\n"
-"nCMnD/63GO3y1zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAKBggq\n"
-"hkjOPQQDAgNIADBFAiEA1jVJWW4idkCgAYv0m2LT9C33Dq42aLyRkJ+9YdzDqLwC\n"
-"IHT6MS4I0k52YP/hxoqWVBbpOW79PKYMRLyXTk1r7+Fa\n"
-"-----END CERTIFICATE-----\n";
-
-
-/* Certificate for intermediary im.unittest.o7s used for signing */
-static const char * intermediate_ca_crt = \
-"-----BEGIN CERTIFICATE-----\n"
-"MIICbTCCAhOgAwIBAgICEAMwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCQkUxDDAK\n"
-"BgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAKBgNVBAoMA283czEVMBMGA1UE\n"
-"CwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51bml0dGVzdC5vN3MxEDAOBgkq\n"
-"hkiG9w0BCQEWASAwHhcNMjUwODAzMTkwMjU3WhcNNDUwNzI5MTkwMjU3WjBaMQsw\n"
-"CQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQwwCgYDVQQKDANvN3MxFTATBgNVBAsM\n"
-"DHVuaXR0ZXN0Lm83czEYMBYGA1UEAwwPaW0udW5pdHRlc3QubzdzMFkwEwYHKoZI\n"
-"zj0CAQYIKoZIzj0DAQcDQgAEdlra08XItIPtVl5veaq4UF6LIcBXj2mZFqKNEXFh\n"
-"l9uAz6UAbIc+FUPNfom6dwKbg/AjQ82a100eh6K/jCY7eKOBpjCBozAdBgNVHQ4E\n"
-"FgQUy8Go8BIO6i0lJ+mgBr9lvh2L0eswHwYDVR0jBBgwFoAUJMYzCCxx+pWfq5wj\n"
-"Jw/+txjt8tcwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEQYD\n"
-"VR0fBAowCDAGoASgAoYAMCoGCCsGAQUFBwEBBB4wHDAMBggrBgEFBQcwAoYAMAwG\n"
-"CCsGAQUFBzABhgAwCgYIKoZIzj0EAwIDSAAwRQIhAN3ZYhqu6mVLGidmONsbANk5\n"
-"rzT6aHJcmvj19OxMusaXAiBKy0gBFCri/GLizi4wZo09wf31yZMqfr8IrApvPaLw\n"
-"qA==\n"
-"-----END CERTIFICATE-----\n";
-
-/* Server test-1.unittest.o7s private-public key pair */
-static const char * server_ec_pkp = \
-"-----BEGIN EC PRIVATE KEY-----\n"
-"MHcCAQEEIA4/bcmquVvGrY4+TtfnFSy1SpXs896r5xJjGuD6NmGRoAoGCCqGSM49\n"
-"AwEHoUQDQgAE4BSOhv36q4bCMLSkJaCvzwZ3pPy2M0YzRKFKeV48tG5eD+MBaTrT\n"
-"eoHUcRfpz0EO/inq3FVDzEoAQ2NWpnz0kA==\n"
-"-----END EC PRIVATE KEY-----\n";
-
-/* Public key for the Private key */
-static const char * server_ec_pk = \
-"-----BEGIN PUBLIC KEY-----\n"
-"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4BSOhv36q4bCMLSkJaCvzwZ3pPy2\n"
-"M0YzRKFKeV48tG5eD+MBaTrTeoHUcRfpz0EO/inq3FVDzEoAQ2NWpnz0kA==\n"
-"-----END PUBLIC KEY-----\n";
-
-/* Valid signed server certificate for test-1.unittest.o7s */
-static const char * signed_server_crt = \
-"-----BEGIN CERTIFICATE-----\n"
-"MIIDiTCCAy+gAwIBAgICEAUwCgYIKoZIzj0EAwIwWjELMAkGA1UEBhMCQkUxDDAK\n"
-"BgNVBAgMA09WTDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n"
-"GDAWBgNVBAMMD2ltLnVuaXR0ZXN0Lm83czAeFw0yNTA4MDgxODQ4NTNaFw00NTA4\n"
-"MDMxODQ4NTNaMG4xCzAJBgNVBAYTAkJFMQwwCgYDVQQIDANPVkwxDjAMBgNVBAcM\n"
-"BUdoZW50MQwwCgYDVQQKDANvN3MxFTATBgNVBAsMDHVuaXR0ZXN0Lm83czEcMBoG\n"
-"A1UEAwwTdGVzdC0xLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49AwEH\n"
-"A0IABOAUjob9+quGwjC0pCWgr88Gd6T8tjNGM0ShSnlePLRuXg/jAWk603qB1HEX\n"
-"6c9BDv4p6txVQ8xKAENjVqZ89JCjggHPMIIByzAJBgNVHRMEAjAAMBEGCWCGSAGG\n"
-"+EIBAQQEAwIGQDA4BglghkgBhvhCAQ0EKxYpbzdzIHVuaXR0ZXN0IEdlbmVyYXRl\n"
-"ZCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFI+htsK0xxy6e1CqCyxn7mqi\n"
-"wRrpMIGoBgNVHSMEgaAwgZ2AFMvBqPASDuotJSfpoAa/Zb4di9HroYGApH4wfDEL\n"
-"MAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAKBgNV\n"
-"BAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMRgwFgYDVQQDDA9jYS51bml0\n"
-"dGVzdC5vN3MxEDAOBgkqhkiG9w0BCQEWASCCAhADMA4GA1UdDwEB/wQEAwIFoDAT\n"
-"BgNVHSUEDDAKBggrBgEFBQcDATAoBgNVHR8EITAfMB2gG6AZhhdodHRwczovL291\n"
-"cm9ib3Jvcy5yb2NrczBYBggrBgEFBQcBAQRMMEowIwYIKwYBBQUHMAKGF2h0dHBz\n"
-"Oi8vb3Vyb2Jvcm9zLnJvY2tzMCMGCCsGAQUFBzABhhdodHRwczovL291cm9ib3Jv\n"
-"cy5yb2NrczAKBggqhkjOPQQDAgNIADBFAiBZuw/Yb2pq925H7pEiOXr4fMo0wknz\n"
-"ktkxoHAFbjQEPQIhAMInHI7lvRmS0IMw1wBF/WlUZWKvhyU/TeMIZfk/JGCS\n"
-"-----END CERTIFICATE-----\n";
-
-/* Self-signed by server test-1.unittest.o7s using its key */
-static const char * server_crt = \
-"-----BEGIN CERTIFICATE-----\n"
-"MIIBfjCCASWgAwIBAgIUB5VYxp7i+sgYjvLiwfpf0W5NfqQwCgYIKoZIzj0EAwIw\n"
-"HjEcMBoGA1UEAwwTdGVzdC0xLnVuaXR0ZXN0Lm83czAeFw0yNTA4MDMxOTI4MzVa\n"
-"Fw00NTA3MjkxOTI4MzVaMB4xHDAaBgNVBAMME3Rlc3QtMS51bml0dGVzdC5vN3Mw\n"
-"WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATgFI6G/fqrhsIwtKQloK/PBnek/LYz\n"
-"RjNEoUp5Xjy0bl4P4wFpOtN6gdRxF+nPQQ7+KercVUPMSgBDY1amfPSQo0EwPzAe\n"
-"BgNVHREEFzAVghN0ZXN0LTEudW5pdHRlc3QubzdzMB0GA1UdDgQWBBSPobbCtMcc\n"
-"untQqgssZ+5qosEa6TAKBggqhkjOPQQDAgNHADBEAiAoFC/rqgrRXmMUx4y5cPbv\n"
-"jOKpoL3FpehRgGkPatmL/QIgMRHc2TSGo6q1SG22Xt1dHAIBsaN2AlSfhjKULMH5\n"
-"gRo=\n"
-"-----END CERTIFICATE-----\n";
+#include <string.h>
+
+#define TEST_MSG_SIZE 1500
static int test_auth_create_destroy_ctx(void)
{
@@ -155,7 +62,7 @@ static int test_load_free_crt(void)
TEST_START();
- if (crypt_load_crt_str(root_ca_crt, &crt) < 0) {
+ if (crypt_load_crt_str(root_ca_crt_ec, &crt) < 0) {
printf("Failed to load certificate string.\n");
goto fail_load;
}
@@ -177,7 +84,7 @@ static int test_crypt_get_pubkey_crt(void)
TEST_START();
- if (crypt_load_crt_str(signed_server_crt, &crt) < 0) {
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
printf("Failed to load server certificate from string.\n");
goto fail_load;
}
@@ -207,7 +114,7 @@ static int test_check_crt_name(void)
TEST_START();
- if (crypt_load_crt_str(signed_server_crt, &crt) < 0) {
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
printf("Failed to load certificate from string.\n");
goto fail_load;
}
@@ -234,13 +141,54 @@ 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;
TEST_START();
- if (crypt_load_privkey_str(server_ec_pkp, &key) < 0) {
+ if (crypt_load_privkey_str(server_pkp_ec, &key) < 0) {
printf("Failed to load server key pair from string.\n");
goto fail_load;
}
@@ -261,7 +209,7 @@ static int test_load_free_pubkey(void)
TEST_START();
- if (crypt_load_pubkey_str(server_ec_pk, &key) < 0) {
+ if (crypt_load_pubkey_str(server_pk_ec, &key) < 0) {
printf("Failed to load server public key from string.\n");
goto fail_load;
}
@@ -284,12 +232,12 @@ static int test_crypt_check_pubkey_crt(void)
TEST_START();
- if (crypt_load_crt_str(signed_server_crt, &crt) < 0) {
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
printf("Failed to load public certificate from string.\n");
goto fail_crt;
}
- if (crypt_load_pubkey_str(server_ec_pk, &pk) < 0) {
+ if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) {
printf("Failed to load public key from string.\n");
goto fail_pubkey;
}
@@ -336,7 +284,7 @@ static int test_store_add(void)
goto fail_create;
}
- if (crypt_load_crt_str(root_ca_crt, &_root_ca_crt) < 0) {
+ 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;
}
@@ -356,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;
@@ -368,7 +316,7 @@ static int test_verify_crt(void)
void * _server_crt;
void * _signed_server_crt;
void * _root_ca_crt;
- void * _intermediate_ca_crt;
+ void * _im_ca_crt;
TEST_START();
@@ -378,24 +326,24 @@ static int test_verify_crt(void)
goto fail_create_ctx;
}
- if (crypt_load_crt_str(server_crt, &_server_crt) < 0) {
+ if (crypt_load_crt_str(server_crt_ec, &_server_crt) < 0) {
printf("Failed to load self-signed crt from string.\n");
goto fail_load_server_crt;
}
- if (crypt_load_crt_str(signed_server_crt, &_signed_server_crt) < 0) {
+ 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_server_crt;
}
- if (crypt_load_crt_str(root_ca_crt, &_root_ca_crt) < 0) {
+ 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_crt;
}
- if (crypt_load_crt_str(intermediate_ca_crt, &_intermediate_ca_crt) < 0) {
+ 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_intermediate_ca_crt;
+ goto fail_load_im_ca_crt;
}
if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) {
@@ -403,7 +351,7 @@ static int test_verify_crt(void)
goto fail_verify;
}
- if (auth_add_crt_to_store(auth, _intermediate_ca_crt) < 0) {
+ 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;
}
@@ -418,7 +366,7 @@ static int test_verify_crt(void)
goto fail_verify;
}
- crypt_free_crt(_intermediate_ca_crt);
+ crypt_free_crt(_im_ca_crt);
crypt_free_crt(_root_ca_crt);
crypt_free_crt(_signed_server_crt);
crypt_free_crt(_server_crt);
@@ -429,8 +377,8 @@ static int test_verify_crt(void)
return TEST_RC_SUCCESS;
fail_verify:
- crypt_free_crt(_intermediate_ca_crt);
- fail_load_intermediate_ca_crt:
+ crypt_free_crt(_im_ca_crt);
+ fail_load_im_ca_crt:
crypt_free_crt(_root_ca_crt);
fail_load_root_ca_crt:
crypt_free_crt(_signed_server_crt);
@@ -443,6 +391,151 @@ static int test_verify_crt(void)
return TEST_RC_FAIL;
}
+static int test_verify_crt_missing_root_ca(void)
+{
+ struct auth_ctx * auth;
+ void * _signed_server_crt;
+ void * _im_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(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(im_ca_crt_ec, &_im_ca_crt) < 0) {
+ printf("Failed to load intermediate crt from string.\n");
+ goto fail_load_im_ca;
+ }
+
+ /* Add only the intermediate CA - root CA is missing */
+ if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) {
+ printf("Failed to add intermediate ca crt to auth store.\n");
+ goto fail_add;
+ }
+
+ if (auth_verify_crt(auth, _signed_server_crt) == 0) {
+ printf("Verification should fail without root CA.\n");
+ goto fail_add;
+ }
+
+ crypt_free_crt(_im_ca_crt);
+ crypt_free_crt(_signed_server_crt);
+ auth_destroy_ctx(auth);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_add:
+ crypt_free_crt(_im_ca_crt);
+ fail_load_im_ca:
+ crypt_free_crt(_signed_server_crt);
+ fail_load_signed:
+ auth_destroy_ctx(auth);
+ fail_create_ctx:
+ TEST_FAIL();
+ 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];
@@ -461,22 +554,22 @@ int test_auth_sign(void)
goto fail_init;
}
- if (crypt_load_privkey_str(server_ec_pkp, &pkp) < 0) {
+ if (crypt_load_privkey_str(server_pkp_ec, &pkp) < 0) {
printf("Failed to load server key pair from string.\n");
goto fail_init;
}
- if (crypt_load_pubkey_str(server_ec_pk, &pk) < 0) {
+ if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) {
printf("Failed to load public key.\n");
goto fail_pubkey;
}
- if (auth_sign(pkp, msg, &sig) < 0) {
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
printf("Failed to sign message.\n");
goto fail_sign;
}
- if (auth_verify_sig(pk, msg, sig) < 0) {
+ if (auth_verify_sig(pk, 0, msg, sig) < 0) {
printf("Failed to verify signature.\n");
goto fail_verify;
}
@@ -518,17 +611,17 @@ int test_auth_bad_signature(void)
goto fail_init;
}
- if (crypt_load_privkey_str(server_ec_pkp, &pkp) < 0) {
+ if (crypt_load_privkey_str(server_pkp_ec, &pkp) < 0) {
printf("Failed to load server key pair from string.\n");
goto fail_init;
}
- if (crypt_load_pubkey_str(server_ec_pk, &pk) < 0) {
+ if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) {
printf("Failed to load public key.\n");
goto fail_pubkey;
}
- if (auth_sign(pkp, msg, &sig) < 0) {
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
printf("Failed to sign message.\n");
goto fail_sign;
}
@@ -545,7 +638,7 @@ int test_auth_bad_signature(void)
goto fail_malloc;
}
- if (auth_verify_sig(pk, msg, fake_sig) == 0) {
+ if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) {
printf("Failed to detect bad signature.\n");
goto fail_verify;
}
@@ -571,14 +664,15 @@ int test_auth_bad_signature(void)
return TEST_RC_FAIL;
}
+#define SSC_BUF_SIZE 4096 /* OpenSSL version my return different lengths */
int test_crt_str(void)
{
- char str[2295];
+ char str[SSC_BUF_SIZE];
void * crt;
TEST_START();
- if (crypt_load_crt_str(signed_server_crt, &crt) < 0) {
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
printf("Failed to load certificate from string.\n");
goto fail_load;
}
@@ -615,29 +709,36 @@ 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();
ret |= test_crypt_check_pubkey_crt();
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;
(void) test_crypt_check_pubkey_crt;
(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
new file mode 100644
index 00000000..e324c32d
--- /dev/null
+++ b/src/lib/tests/auth_test_ml_dsa.c
@@ -0,0 +1,356 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the ML-DSA-65 authentication functions
+ *
+ * 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 <test/test.h>
+#include <ouroboros/crypt.h>
+#include <ouroboros/random.h>
+#include <ouroboros/utils.h>
+
+#include <test/certs/ml_dsa.h>
+
+#define TEST_MSG_SIZE 1500
+
+static int test_auth_create_destroy_ctx(void)
+{
+ struct auth_ctx * ctx;
+
+ TEST_START();
+
+ ctx = auth_create_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create auth context.\n");
+ goto fail_create;
+ }
+
+ auth_destroy_ctx(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_crt(void)
+{
+ void * crt;
+
+ TEST_START();
+
+ if (crypt_load_crt_str(root_ca_crt_ml, &crt) < 0) {
+ printf("Failed to load root crt from string.\n");
+ goto fail_load;
+ }
+
+ crypt_free_crt(crt);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_privkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_privkey_str(server_pkp_ml, &key) < 0) {
+ printf("Failed to load server key pair from string.\n");
+ goto fail_load;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_pubkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_pubkey_str(server_pk_ml, &key) < 0) {
+ printf("Failed to load server public key from string.\n");
+ goto fail_load;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_verify_crt(void)
+{
+ struct auth_ctx * auth;
+ void * _server_crt;
+ void * _signed_server_crt;
+ void * _root_ca_crt;
+ void * _im_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(server_crt_ml, &_server_crt) < 0) {
+ printf("Failed to load self-signed crt from string.\n");
+ goto fail_load_server_crt;
+ }
+
+ if (crypt_load_crt_str(signed_server_crt_ml, &_signed_server_crt) < 0) {
+ printf("Failed to load signed crt from string.\n");
+ goto fail_load_signed_server_crt;
+ }
+
+ if (crypt_load_crt_str(root_ca_crt_ml, &_root_ca_crt) < 0) {
+ printf("Failed to load root crt from string.\n");
+ goto fail_load_root_ca_crt;
+ }
+
+ if (crypt_load_crt_str(im_ca_crt_ml, &_im_ca_crt) < 0) {
+ printf("Failed to load intermediate crt from string.\n");
+ goto fail_load_im_ca_crt;
+ }
+
+ 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(auth, _signed_server_crt) < 0) {
+ printf("Failed to verify signed crt with ca crt.\n");
+ goto fail_verify;
+ }
+
+ if (auth_verify_crt(auth, _server_crt) == 0) {
+ printf("Failed to detect untrusted crt.\n");
+ goto fail_verify;
+ }
+
+ crypt_free_crt(_im_ca_crt);
+ crypt_free_crt(_root_ca_crt);
+ crypt_free_crt(_signed_server_crt);
+ crypt_free_crt(_server_crt);
+
+ auth_destroy_ctx(auth);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ crypt_free_crt(_im_ca_crt);
+ fail_load_im_ca_crt:
+ crypt_free_crt(_root_ca_crt);
+ fail_load_root_ca_crt:
+ crypt_free_crt(_signed_server_crt);
+ fail_load_signed_server_crt:
+ crypt_free_crt(_server_crt);
+ fail_load_server_crt:
+ auth_destroy_ctx(auth);
+ fail_create_ctx:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_auth_sign(void)
+{
+ uint8_t buf[TEST_MSG_SIZE];
+ void * pkp;
+ void * pk;
+ buffer_t msg;
+ buffer_t sig;
+
+ TEST_START();
+
+ msg.data = buf;
+ msg.len = sizeof(buf);
+
+ if (random_buffer(msg.data, msg.len) < 0) {
+ printf("Failed to generate random message.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_privkey_str(server_pkp_ml, &pkp) < 0) {
+ printf("Failed to load server key pair from string.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_pubkey_str(server_pk_ml, &pk) < 0) {
+ printf("Failed to load public key from string.\n");
+ goto fail_pubkey;
+ }
+
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
+ printf("Failed to sign message.\n");
+ goto fail_sign;
+ }
+
+ if (auth_verify_sig(pk, 0, msg, sig) < 0) {
+ printf("Failed to verify signature.\n");
+ goto fail_verify;
+ }
+
+ freebuf(sig);
+
+ crypt_free_key(pk);
+ crypt_free_key(pkp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ freebuf(sig);
+ fail_sign:
+ crypt_free_key(pk);
+ fail_pubkey:
+ crypt_free_key(pkp);
+ fail_init:
+ return TEST_RC_FAIL;
+}
+
+static int test_auth_bad_signature(void)
+{
+ uint8_t buf[TEST_MSG_SIZE];
+ void * pkp;
+ void * pk;
+ buffer_t msg;
+ buffer_t sig;
+ buffer_t fake_sig;
+
+ TEST_START();
+
+ msg.data = buf;
+ msg.len = sizeof(buf);
+
+ if (random_buffer(msg.data, msg.len) < 0) {
+ printf("Failed to generate random message.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_privkey_str(server_pkp_ml, &pkp) < 0) {
+ printf("Failed to load server key pair from string.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_pubkey_str(server_pk_ml, &pk) < 0) {
+ printf("Failed to load public key from string.\n");
+ goto fail_pubkey;
+ }
+
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
+ printf("Failed to sign message.\n");
+ goto fail_sign;
+ }
+
+ fake_sig.data = malloc(sig.len);
+ if (fake_sig.data == NULL) {
+ printf("Failed to allocate memory for fake signature.\n");
+ goto fail_malloc;
+ }
+
+ fake_sig.len = sig.len;
+ if (random_buffer(fake_sig.data, fake_sig.len) < 0) {
+ printf("Failed to generate random fake signature.\n");
+ goto fail_malloc;
+ }
+
+ if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) {
+ printf("Failed to detect bad ML-DSA-65 signature.\n");
+ goto fail_verify;
+ }
+
+ freebuf(fake_sig);
+ freebuf(sig);
+
+ crypt_free_key(pk);
+ crypt_free_key(pkp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ freebuf(fake_sig);
+ fail_malloc:
+ freebuf(sig);
+ fail_sign:
+ crypt_free_key(pk);
+ fail_pubkey:
+ crypt_free_key(pkp);
+ fail_init:
+ return TEST_RC_FAIL;
+}
+
+int auth_test_ml_dsa(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#ifdef HAVE_ML
+ ret |= test_auth_create_destroy_ctx();
+ ret |= test_load_free_crt();
+ ret |= test_load_free_privkey();
+ ret |= test_load_free_pubkey();
+ ret |= test_verify_crt();
+ ret |= test_auth_sign();
+ ret |= test_auth_bad_signature();
+#else
+ (void) test_auth_create_destroy_ctx;
+ (void) test_load_free_crt;
+ (void) test_load_free_privkey;
+ (void) test_load_free_pubkey;
+ (void) test_verify_crt;
+ (void) test_auth_sign;
+ (void) test_auth_bad_signature;
+
+ ret = TEST_RC_SKIP;
+#endif
+ return ret;
+}
diff --git a/src/lib/tests/auth_test_slh_dsa.c b/src/lib/tests/auth_test_slh_dsa.c
new file mode 100644
index 00000000..e9af8da8
--- /dev/null
+++ b/src/lib/tests/auth_test_slh_dsa.c
@@ -0,0 +1,367 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the SLH-DSA-SHA2-128s authentication functions
+ *
+ * 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 <test/test.h>
+#include <ouroboros/crypt.h>
+#include <ouroboros/random.h>
+#include <ouroboros/utils.h>
+
+#include <test/certs/slh_dsa.h>
+
+#define TEST_MSG_SIZE 1500
+
+static int test_auth_create_destroy_ctx(void)
+{
+ struct auth_ctx * ctx;
+
+ TEST_START();
+
+ ctx = auth_create_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create auth context.\n");
+ goto fail_create;
+ }
+
+ auth_destroy_ctx(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_create:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_crt(void)
+{
+ void * crt;
+
+ TEST_START();
+
+ if (crypt_load_crt_str(root_ca_crt_slh, &crt) < 0) {
+ printf("Failed to load root crt.\n");
+ goto fail_load;
+ }
+
+ crypt_free_crt(crt);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_privkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_privkey_str(server_pkp_slh, &key) < 0) {
+ printf("Failed to load server key pair.\n");
+ goto fail_load;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_load_free_pubkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_pubkey_str(server_pk_slh, &key) < 0) {
+ printf("Failed to load server public key.\n");
+ goto fail_load;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_load:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_verify_crt(void)
+{
+ struct auth_ctx * auth;
+ void * _server_crt;
+ void * _signed_server_crt;
+ void * _root_ca_crt;
+ void * _im_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(server_crt_slh,
+ &_server_crt) < 0) {
+ printf("Failed to load self-signed crt.\n");
+ goto fail_load_server_crt;
+ }
+
+ if (crypt_load_crt_str(signed_server_crt_slh,
+ &_signed_server_crt) < 0) {
+ printf("Failed to load signed crt.\n");
+ goto fail_load_signed_server_crt;
+ }
+
+ if (crypt_load_crt_str(root_ca_crt_slh,
+ &_root_ca_crt) < 0) {
+ printf("Failed to load root crt.\n");
+ goto fail_load_root_ca_crt;
+ }
+
+ if (crypt_load_crt_str(im_ca_crt_slh,
+ &_im_ca_crt) < 0) {
+ printf("Failed to load im crt.\n");
+ goto fail_load_im_ca_crt;
+ }
+
+ if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) {
+ printf("Failed to add root ca crt.\n");
+ goto fail_verify;
+ }
+
+ if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) {
+ printf("Failed to add im ca crt.\n");
+ goto fail_verify;
+ }
+
+ if (auth_verify_crt(auth, _signed_server_crt) < 0) {
+ printf("Failed to verify signed crt.\n");
+ goto fail_verify;
+ }
+
+ if (auth_verify_crt(auth, _server_crt) == 0) {
+ printf("Failed to detect untrusted crt.\n");
+ goto fail_verify;
+ }
+
+ crypt_free_crt(_im_ca_crt);
+ crypt_free_crt(_root_ca_crt);
+ crypt_free_crt(_signed_server_crt);
+ crypt_free_crt(_server_crt);
+
+ auth_destroy_ctx(auth);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ crypt_free_crt(_im_ca_crt);
+ fail_load_im_ca_crt:
+ crypt_free_crt(_root_ca_crt);
+ fail_load_root_ca_crt:
+ crypt_free_crt(_signed_server_crt);
+ fail_load_signed_server_crt:
+ crypt_free_crt(_server_crt);
+ fail_load_server_crt:
+ auth_destroy_ctx(auth);
+ fail_create_ctx:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_auth_sign(void)
+{
+ uint8_t buf[TEST_MSG_SIZE];
+ void * pkp;
+ void * pk;
+ buffer_t msg;
+ buffer_t sig;
+
+ TEST_START();
+
+ msg.data = buf;
+ msg.len = sizeof(buf);
+
+ if (random_buffer(msg.data, msg.len) < 0) {
+ printf("Failed to gen random message.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_privkey_str(server_pkp_slh,
+ &pkp) < 0) {
+ printf("Failed to load server key pair.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_pubkey_str(server_pk_slh,
+ &pk) < 0) {
+ printf("Failed to load public key.\n");
+ goto fail_pubkey;
+ }
+
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
+ printf("Failed to sign message.\n");
+ goto fail_sign;
+ }
+
+ if (auth_verify_sig(pk, 0, msg, sig) < 0) {
+ printf("Failed to verify signature.\n");
+ goto fail_verify;
+ }
+
+ freebuf(sig);
+
+ crypt_free_key(pk);
+ crypt_free_key(pkp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ freebuf(sig);
+ fail_sign:
+ crypt_free_key(pk);
+ fail_pubkey:
+ crypt_free_key(pkp);
+ fail_init:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_auth_bad_signature(void)
+{
+ uint8_t buf[TEST_MSG_SIZE];
+ void * pkp;
+ void * pk;
+ buffer_t msg;
+ buffer_t sig;
+ buffer_t fake_sig;
+
+ TEST_START();
+
+ msg.data = buf;
+ msg.len = sizeof(buf);
+
+ if (random_buffer(msg.data, msg.len) < 0) {
+ printf("Failed to gen random message.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_privkey_str(server_pkp_slh,
+ &pkp) < 0) {
+ printf("Failed to load server key pair.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_pubkey_str(server_pk_slh,
+ &pk) < 0) {
+ printf("Failed to load public key.\n");
+ goto fail_pubkey;
+ }
+
+ if (auth_sign(pkp, 0, msg, &sig) < 0) {
+ printf("Failed to sign message.\n");
+ goto fail_sign;
+ }
+
+ fake_sig.data = malloc(sig.len);
+ if (fake_sig.data == NULL) {
+ printf("Failed to alloc fake sig buf.\n");
+ goto fail_malloc;
+ }
+
+ fake_sig.len = sig.len;
+ if (random_buffer(fake_sig.data,
+ fake_sig.len) < 0) {
+ printf("Failed to gen random fake sig.\n");
+ goto fail_malloc;
+ }
+
+ if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) {
+ printf("Failed to detect bad sig.\n");
+ goto fail_verify;
+ }
+
+ freebuf(fake_sig);
+ freebuf(sig);
+
+ crypt_free_key(pk);
+ crypt_free_key(pkp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_verify:
+ freebuf(fake_sig);
+ fail_malloc:
+ freebuf(sig);
+ fail_sign:
+ crypt_free_key(pk);
+ fail_pubkey:
+ crypt_free_key(pkp);
+ fail_init:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int auth_test_slh_dsa(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#ifdef HAVE_SLH
+ ret |= test_auth_create_destroy_ctx();
+ ret |= test_load_free_crt();
+ ret |= test_load_free_privkey();
+ ret |= test_load_free_pubkey();
+ ret |= test_verify_crt();
+ ret |= test_auth_sign();
+ ret |= test_auth_bad_signature();
+#else
+ (void) test_auth_create_destroy_ctx;
+ (void) test_load_free_crt;
+ (void) test_load_free_privkey;
+ (void) test_load_free_pubkey;
+ (void) test_verify_crt;
+ (void) test_auth_sign;
+ (void) test_auth_bad_signature;
+
+ ret = TEST_RC_SKIP;
+#endif
+ return ret;
+}
diff --git a/src/lib/tests/bitmap_test.c b/src/lib/tests/bitmap_test.c
index 4dbd6653..81735a41 100644
--- a/src/lib/tests/bitmap_test.c
+++ b/src/lib/tests/bitmap_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the bitmap
*
diff --git a/src/lib/tests/btree_test.c b/src/lib/tests/btree_test.c
index 8bd30370..d1a72af8 100644
--- a/src/lib/tests/btree_test.c
+++ b/src/lib/tests/btree_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the B-tree implementation
*
diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c
index e7a09e8f..50b7268a 100644
--- a/src/lib/tests/crypt_test.c
+++ b/src/lib/tests/crypt_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the cryptography functions
*
@@ -22,26 +22,49 @@
#include "config.h"
-#include <ouroboros/test.h>
-#include <ouroboros/crypt.h>
+#include <test/test.h>
#include <ouroboros/random.h>
+#include <ouroboros/crypt.h>
#include <ouroboros/utils.h>
+#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[];
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,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
+ };
TEST_START();
- ctx = crypt_create_ctx(NULL);
+ 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();
@@ -51,125 +74,245 @@ static int test_crypt_create_destroy(void)
return TEST_RC_FAIL;
}
-static int test_crypt_create_destroy_with_key(void)
+static int test_crypt_encrypt_decrypt(int nid)
{
- struct crypt_ctx * ctx;
+ 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 out;
+ buffer_t out2;
+ const char * cipher;
- TEST_START();
+ sk_tx.nid = nid;
+ sk_rx.nid = nid;
- memset(key, 0, sizeof(key));
+ cipher = crypt_nid_to_str(nid);
+ TEST_START("(%s)", cipher);
- ctx = crypt_create_ctx(key);
- if (ctx == NULL) {
- printf("Failed to initialize cryptography.\n");
- goto fail;
+ if (random_buffer(key, sizeof(key)) < 0) {
+ printf("Failed to generate random key.\n");
+ goto fail_init;
}
- crypt_destroy_ctx(ctx);
+ if (random_buffer(pkt, sizeof(pkt)) < 0) {
+ printf("Failed to generate random data.\n");
+ goto fail_init;
+ }
- TEST_SUCCESS();
+ tx = crypt_create_ctx(&sk_tx);
+ if (tx == NULL) {
+ printf("Failed to initialize TX cryptography.\n");
+ goto fail_init;
+ }
- return TEST_RC_SUCCESS;
- fail:
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
+ rx = crypt_create_ctx(&sk_rx);
+ if (rx == NULL) {
+ printf("Failed to initialize RX cryptography.\n");
+ goto fail_tx;
+ }
-static int test_crypt_dh_pkp_create_destroy(void)
-{
- void * pkp;
- uint8_t buf[MSGBUFSZ];
+ in.len = sizeof(pkt);
+ in.data = pkt;
- TEST_START();
+ if (crypt_encrypt(tx, in, &out) < 0) {
+ printf("Encryption failed.\n");
+ goto fail_encrypt;
+ }
- if (crypt_dh_pkp_create(&pkp, buf) < 0) {
- printf("Failed to create DH PKP.");
- goto fail;
+ if (out.len < in.len) {
+ printf("Encryption returned too little data.\n");
+ goto fail_chk;
}
- crypt_dh_pkp_destroy(pkp);
+ if (crypt_decrypt(rx, out, &out2) < 0) {
+ printf("Decryption failed.\n");
+ goto fail_decrypt;
+ }
- TEST_SUCCESS();
+ if (out2.len != in.len) {
+ printf("Decrypted data length does not match original.\n");
+ goto fail_chk2;
+ }
+
+ if (memcmp(in.data, out2.data, in.len) != 0) {
+ printf("Decrypted data does not match original.\n");
+ goto fail_chk2;
+ }
+
+ freebuf(out2);
+ freebuf(out);
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
+
+ TEST_SUCCESS("(%s)", cipher);
return TEST_RC_SUCCESS;
- fail:
- TEST_FAIL();
+ fail_chk2:
+ freebuf(out2);
+ fail_decrypt:
+ fail_chk:
+ freebuf(out);
+ fail_encrypt:
+ crypt_destroy_ctx(rx);
+ fail_tx:
+ crypt_destroy_ctx(tx);
+ fail_init:
+ TEST_FAIL("(%s)", cipher);
return TEST_RC_FAIL;
}
-static int test_crypt_dh_derive(void)
+static int test_encrypt_decrypt_all(void)
{
- void * pkp1;
- void * pkp2;
- buffer_t pk1;
- buffer_t pk2;
- ssize_t len;
- uint8_t buf1[MSGBUFSZ];
- uint8_t buf2[MSGBUFSZ];
- uint8_t s1[SYMMKEYSZ];
- uint8_t s2[SYMMKEYSZ];
+ int ret = 0;
+ int i;
- TEST_START();
+ for (i = 0; crypt_supported_nids[i] != NID_undef; i++)
+ ret |= test_crypt_encrypt_decrypt(crypt_supported_nids[i]);
- len = crypt_dh_pkp_create(&pkp1, buf1);
- if (len < 0) {
- printf("Failed to create first key pair.");
- goto fail_pkp1;
- }
+ 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;
- pk1.len = (size_t) len;
- pk1.data = buf1;
+ sk_tx.nid = nid;
+ sk_rx.nid = nid;
- len = crypt_dh_pkp_create(&pkp2, buf2);
- if (len < 0) {
- printf("Failed to create second key pair.");
- goto fail_pkp2;
+ 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;
}
- pk2.len = (size_t) len;
- pk2.data = buf2;
+ if (random_buffer(pkt, sizeof(pkt)) < 0) {
+ printf("Failed to generate random data.\n");
+ goto fail_init;
+ }
- if (crypt_dh_derive(pkp1, pk2, s1) < 0) {
- printf("Failed to derive first key.");
- goto fail;
+ tx = crypt_create_ctx(&sk_tx);
+ if (tx == NULL) {
+ printf("Failed to create TX context.\n");
+ goto fail_init;
}
- if (crypt_dh_derive(pkp2, pk1, s2) < 0) {
- printf("Failed to derive second key.");
- goto fail;
+ rx = crypt_create_ctx(&sk_rx);
+ if (rx == NULL) {
+ printf("Failed to create RX context.\n");
+ goto fail_tx;
}
- if (memcmp(s1, s2, SYMMKEYSZ) != 0) {
- printf("Derived keys do not match.");
- goto fail;
+ 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_dh_pkp_destroy(pkp2);
- crypt_dh_pkp_destroy(pkp1);
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
- TEST_SUCCESS();
+ TEST_SUCCESS("(%s)", cipher);
return TEST_RC_SUCCESS;
- fail:
- crypt_dh_pkp_destroy(pkp2);
- fail_pkp2:
- crypt_dh_pkp_destroy(pkp1);
- fail_pkp1:
- TEST_FAIL();
+ fail_rx:
+ crypt_destroy_ctx(rx);
+ fail_tx:
+ crypt_destroy_ctx(tx);
+ fail_init:
+ TEST_FAIL("(%s)", cipher);
return TEST_RC_FAIL;
}
-int test_crypt_encrypt_decrypt(void)
+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_ctx * ctx;
+ 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;
+ buffer_t enc;
+ buffer_t dec;
+ const char * cipher;
- TEST_START();
+ 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");
@@ -181,54 +324,255 @@ int test_crypt_encrypt_decrypt(void)
goto fail_init;
}
- ctx = crypt_create_ctx(key);
- if (ctx == NULL) {
- printf("Failed to initialize cryptography.\n");
+ 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(ctx, in, &out) < 0) {
+ if (crypt_encrypt(tx, in, &enc) < 0) {
printf("Encryption failed.\n");
- goto fail_encrypt;
+ goto fail_rx;
}
- if (out.len < in.len) {
- printf("Encryption returned too little data.\n");
- goto fail_encrypt;
+ /* 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;
}
- if (crypt_decrypt(ctx, out, &out2) < 0) {
- printf("Decryption failed.\n");
- goto fail_decrypt;
+ 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>
+
+static int test_cipher_nid_values(void)
+{
+ int i;
+
+ TEST_START();
+
+ /* Loop over all supported ciphers and verify NIDs match OpenSSL's */
+ for (i = 0; crypt_supported_nids[i] != NID_undef; i++) {
+ uint16_t our_nid = crypt_supported_nids[i];
+ const char * str = crypt_nid_to_str(our_nid);
+ const EVP_CIPHER * cipher;
+ int openssl_nid;
+
+ if (str == NULL) {
+ printf("crypt_nid_to_str failed for NID %u\n", our_nid);
+ goto fail;
+ }
+
+ cipher = EVP_get_cipherbyname(str);
+ if (cipher == NULL) {
+ printf("OpenSSL doesn't recognize cipher '%s'\n", str);
+ goto fail;
+ }
+
+ openssl_nid = EVP_CIPHER_nid(cipher);
+
+ if (our_nid != openssl_nid) {
+ printf("NID mismatch for '%s': ours=%u, OpenSSL=%d\n",
+ str, our_nid, openssl_nid);
+ goto fail;
+ }
+
+ /* Test reverse conversion */
+ if (crypt_str_to_nid(str) != our_nid) {
+ printf("crypt_str_to_nid failed for '%s'\n", str);
+ goto fail;
+ }
}
- if (out2.len != in.len) {
- printf("Decrypted data length does not match original.\n");
- goto fail_chk;
+ /* Test error cases */
+ if (crypt_str_to_nid("invalid") != NID_undef) {
+ printf("crypt_str_to_nid: no NID_undef for invalid.\n");
+ goto fail;
}
- if (memcmp(in.data, out2.data, in.len) != 0) {
- printf("Decrypted data does not match original.\n");
- goto fail_chk;
+ if (crypt_nid_to_str(9999) != NULL) {
+ printf("crypt_nid_to_str should return NULL for invalid NID\n");
+ goto fail;
+ }
+
+ if (crypt_str_to_nid(NULL) != NID_undef) {
+ printf("crypt_str_to_nid should return NID_undef for NULL\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_md_nid_values(void)
+{
+ int i;
+
+ TEST_START();
+
+ for (i = 0; md_supported_nids[i] != NID_undef; i++) {
+ uint16_t our_nid = md_supported_nids[i];
+ const EVP_MD * md;
+ int openssl_nid;
+
+ md = EVP_get_digestbynid(our_nid);
+ if (md == NULL) {
+ printf("OpenSSL doesn't recognize NID %u\n", our_nid);
+ goto fail;
+ }
+
+ openssl_nid = EVP_MD_nid(md);
+ if (our_nid != openssl_nid) {
+ printf("NID mismatch: ours=%u, OpenSSL=%d\n",
+ our_nid, openssl_nid);
+ goto fail;
+ }
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+#endif
+
+static int test_crypt_headsz(void)
+{
+ 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
+ };
+ int headsz;
+
+ TEST_START();
+
+ if (random_buffer(key, sizeof(key)) < 0) {
+ printf("Failed to generate random key.\n");
+ goto fail;
+ }
+
+ ctx = crypt_create_ctx(&sk);
+ if (ctx == NULL) {
+ printf("Failed to initialize cryptography.\n");
+ goto fail;
+ }
+
+ headsz = crypt_get_headsz(ctx);
+ if (headsz != 6) {
+ printf("Unexpected header size: %d (expected 6).\n", headsz);
+ goto fail_ctx;
}
crypt_destroy_ctx(ctx);
- freebuf(out2);
- freebuf(out);
TEST_SUCCESS();
return TEST_RC_SUCCESS;
- fail_chk:
- freebuf(out2);
- fail_decrypt:
- freebuf(out);
- fail_encrypt:
+ fail_ctx:
crypt_destroy_ctx(ctx);
- fail_init:
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_crypt_ct_cmp(void)
+{
+ uint8_t a[64];
+ uint8_t b[64];
+ size_t i;
+
+ TEST_START();
+
+ for (i = 0; i < sizeof(a); i++)
+ a[i] = (uint8_t) i;
+
+ memcpy(b, a, sizeof(a));
+
+ if (crypt_ct_cmp(a, b, sizeof(a)) != 0) {
+ printf("Equal buffers should compare equal.\n");
+ goto fail;
+ }
+
+ if (crypt_ct_cmp(a, b, 0) != 0) {
+ printf("Zero length should compare equal.\n");
+ goto fail;
+ }
+
+ 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;
+ }
+
+ b[i] ^= 0x01;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
TEST_FAIL();
return TEST_RC_FAIL;
}
@@ -242,17 +586,21 @@ int crypt_test(int argc,
(void) argv;
ret |= test_crypt_create_destroy();
- ret |= test_crypt_create_destroy_with_key();
+ ret |= test_encrypt_decrypt_all();
+ ret |= test_crypt_ct_cmp();
#ifdef HAVE_OPENSSL
- ret |= test_crypt_dh_pkp_create_destroy();
- ret |= test_crypt_dh_derive();
- ret |= test_crypt_encrypt_decrypt();
+ ret |= test_cipher_nid_values();
+ ret |= test_md_nid_values();
+ ret |= test_multi_packet_all();
+ ret |= test_aad_tamper_all();
+ ret |= test_crypt_headsz();
#else
- (void) test_crypt_dh_pkp_create_destroy;
- (void) test_crypt_dh_derive;
- (void) test_crypt_encrypt_decrypt;
+ (void) test_multi_packet_all;
+ (void) test_aad_tamper_all;
+ (void) test_crypt_headsz;
- ret = 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 970d9185..a2ba62cc 100644
--- a/src/lib/tests/hash_test.c
+++ b/src/lib/tests/hash_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the hashing functions
*
@@ -21,7 +21,7 @@
*/
#include <ouroboros/hash.h>
-#include <ouroboros/test.h>
+#include <test/test.h>
#include <stdlib.h>
#include <stdint.h>
@@ -39,6 +39,79 @@ struct vec_entry {
char * out;
};
+struct mix_entry {
+ uint64_t in;
+ uint64_t out;
+};
+
+static int test_crc8(void)
+{
+ int ret = 0;
+
+ struct vec_entry vec [] = {
+ { "", "00" },
+ { "123456789", "df" },
+ { NULL, NULL }
+ };
+
+ struct vec_entry * cur = vec;
+
+ TEST_START();
+
+ while (cur->in != NULL) {
+ uint8_t crc;
+ char res[3];
+
+ str_hash(HASH_CRC8, &crc, cur->in);
+
+ sprintf(res, "%02x", crc);
+ if (strcmp(res, cur->out) != 0) {
+ printf("Hash failed %s != %s.\n", res, cur->out);
+ ret |= -1;
+ }
+
+ ++cur;
+ }
+
+ TEST_END(ret);
+
+ return ret;
+}
+
+static int test_crc16(void)
+{
+ int ret = 0;
+
+ struct vec_entry vec [] = {
+ { "", "ffff" },
+ { "123456789", "29b1" },
+ { NULL, NULL }
+ };
+
+ struct vec_entry * cur = vec;
+
+ TEST_START();
+
+ while (cur->in != NULL) {
+ uint8_t crc[2];
+ char res[5];
+
+ str_hash(HASH_CRC16, crc, cur->in);
+
+ sprintf(res, "%02x%02x", crc[0], crc[1]);
+ if (strcmp(res, cur->out) != 0) {
+ printf("Hash failed %s != %s.\n", res, cur->out);
+ ret |= -1;
+ }
+
+ ++cur;
+ }
+
+ TEST_END(ret);
+
+ return ret;
+}
+
static int test_crc32(void)
{
int ret = 0;
@@ -74,6 +147,42 @@ static int test_crc32(void)
return ret;
}
+static int test_crc64(void)
+{
+ int ret = 0;
+
+ struct vec_entry vec [] = {
+ { "", "0000000000000000" },
+ { "123456789", "ae8b14860a799888" },
+ { "0123456789abcdef",
+ "091485ca7018730e" },
+ { NULL, NULL }
+ };
+
+ struct vec_entry * cur = vec;
+
+ TEST_START();
+
+ while (cur->in != NULL) {
+ uint8_t crc[8];
+ char res[17];
+
+ str_hash(HASH_CRC64, crc, cur->in);
+
+ sprintf(res, HASH_FMT64, HASH_VAL64(crc));
+ if (strcmp(res, cur->out) != 0) {
+ printf("Hash failed %s != %s.\n", res, cur->out);
+ ret |= -1;
+ }
+
+ ++cur;
+ }
+
+ TEST_END(ret);
+
+ return ret;
+}
+
static int test_md5(void)
{
int ret = 0;
@@ -184,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)
{
@@ -192,11 +331,19 @@ int hash_test(int argc,
(void) argc;
(void) argv;
+ ret |= test_crc8();
+
+ ret |= test_crc16();
+
ret |= test_crc32();
+ ret |= test_crc64();
+
ret |= test_md5();
ret |= test_sha3();
+ ret |= test_mix64();
+
return ret;
}
diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c
new file mode 100644
index 00000000..5b2ccfc2
--- /dev/null
+++ b/src/lib/tests/kex_test.c
@@ -0,0 +1,1248 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the key exchange functions
+ *
+ * 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>
+#include <ouroboros/utils.h>
+#include <ouroboros/crypt.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifdef HAVE_OPENSSL
+#include <openssl/evp.h>
+#include <openssl/x509.h>
+#endif
+
+/* Test configuration strings */
+#define KEX_CONFIG_CUSTOM \
+ "kex=X25519\n"
+
+#define KEX_CONFIG_NONE \
+ "none\n"
+
+#define KEX_CONFIG_NO_ENC \
+ "encryption=none\n"
+
+#define KEX_CONFIG_WHITESPACE \
+ "# Comment line\n" \
+ "kex = X448" \
+ "\n" \
+ "# Another comment\n"
+
+#define KEX_CONFIG_CIPHER \
+ "kex=X25519\n" \
+ "cipher=chacha20-poly1305\n"
+
+#define KEX_CONFIG_DIGEST \
+ "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" \
+ "MC4CAQAwBQYDK2VuBCIEIJDd3+/0k2IZlaH5sZ9Z2e5J8dV2U0nsXaSUm70ZaMhL\n" \
+ "-----END PRIVATE KEY-----\n"
+
+#define X25519_PUBKEY_PEM \
+ "-----BEGIN PUBLIC KEY-----\n" \
+ "MCowBQYDK2VuAyEAKYLIycSZtLFlwAX07YWWgBAYhEnRxHfgK1TVw9+mtBs=\n" \
+ "-----END PUBLIC KEY-----\n"
+
+/* Helper macro to open string constant as FILE stream */
+#define FMEMOPEN_STR(str) fmemopen((void *) (str), strlen(str), "r")
+
+extern const uint16_t kex_supported_nids[];
+
+int parse_sec_config(struct sec_config * cfg,
+ FILE * fp);
+
+static int test_kex_create_destroy(void)
+{
+ struct sec_config cfg;
+
+ TEST_START();
+
+ memset(&cfg, 0, sizeof(cfg));
+ cfg.x.nid = NID_X9_62_prime256v1;
+ cfg.x.str = kex_nid_to_str(cfg.x.nid);
+ cfg.c.nid = NID_aes_256_gcm;
+ cfg.c.str = crypt_nid_to_str(cfg.c.nid);
+
+ if (cfg.x.nid == NID_undef || cfg.c.nid == NID_undef) {
+ printf("Failed to initialize kex config.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_dh_pkp_create_destroy(void)
+{
+ struct sec_config kex;
+ void * pkp;
+ uint8_t buf[CRYPT_KEY_BUFSZ];
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, "prime256v1");
+
+ if (kex_pkp_create(&kex, &pkp, buf) < 0) {
+ printf("Failed to create DH PKP.\n");
+ goto fail;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_get_algo_from_pk(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp;
+ buffer_t pk;
+ ssize_t len;
+ uint8_t buf[CRYPT_KEY_BUFSZ];
+ char extracted_algo[256];
+
+ TEST_START("(%s)", algo);
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, algo);
+
+ len = kex_pkp_create(&kex, &pkp, buf);
+ if (len < 0) {
+ printf("Failed to create key pair.\n");
+ goto fail;
+ }
+
+ 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;
+ }
+ }
+
+ /* All algorithms should now return the specific group name */
+ if (strcmp(extracted_algo, algo) != 0) {
+ printf("Algo mismatch: expected %s, got %s.\n",
+ algo, extracted_algo);
+ goto fail_pkp;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp:
+ kex_pkp_destroy(pkp);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_get_algo_from_pk_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]);
+ ret |= test_kex_get_algo_from_pk(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_dhe_derive(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp1;
+ void * pkp2;
+ buffer_t pk1;
+ buffer_t pk2;
+ ssize_t len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t s1[SYMMKEYSZ];
+ uint8_t s2[SYMMKEYSZ];
+
+ TEST_START("(%s)", 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) {
+ printf("Failed to create first key pair for %s.\n", algo);
+ goto fail;
+ }
+
+ pk1.len = (size_t) len;
+ pk1.data = buf1;
+
+ len = kex_pkp_create(&kex, &pkp2, buf2);
+ if (len < 0) {
+ printf("Failed to create second key pair for %s.\n", algo);
+ goto fail_pkp1;
+ }
+
+ pk2.len = (size_t) len;
+ pk2.data = buf2;
+
+ if (kex_dhe_derive(&kex, pkp1, pk2, s1) < 0) {
+ printf("Failed to derive first key for %s.\n", algo);
+ goto fail_pkp2;
+ }
+
+ if (kex_dhe_derive(&kex, pkp2, pk1, s2) < 0) {
+ printf("Failed to derive second key for %s.\n", algo);
+ goto fail_pkp2;
+ }
+
+ if (memcmp(s1, s2, SYMMKEYSZ) != 0) {
+ printf("Derived keys do not match for %s.\n", algo);
+ goto fail_pkp2;
+ }
+
+ kex_pkp_destroy(pkp2);
+ kex_pkp_destroy(pkp1);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp2:
+ kex_pkp_destroy(pkp2);
+ fail_pkp1:
+ kex_pkp_destroy(pkp1);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_validate_algo(void)
+{
+ TEST_START();
+
+ if (kex_validate_algo("prime256v1") != 0) {
+ printf("prime256v1 should be valid.\n");
+ goto fail;
+ }
+
+ if (kex_validate_algo("X25519") != 0) {
+ printf("X25519 should be valid.\n");
+ goto fail;
+ }
+
+#ifdef HAVE_ML
+ if (kex_validate_algo("ML-KEM-768") != 0) {
+ printf("ML-KEM-768 should be valid.\n");
+ goto fail;
+ }
+#endif
+
+ if (kex_validate_algo("ffdhe2048") != 0) {
+ printf("ffdhe2048 should be valid.\n");
+ goto fail;
+ }
+
+ if (kex_validate_algo("invalid_algo") == 0) {
+ printf("invalid_algo should be rejected.\n");
+ goto fail;
+ }
+
+ if (kex_validate_algo("rsa2048") == 0) {
+ printf("rsa2048 should be rejected.\n");
+ goto fail;
+ }
+
+ if (kex_validate_algo(NULL) == 0) {
+ printf("NULL should be rejected.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_dhe_corrupted_pubkey(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp;
+ buffer_t pk;
+ ssize_t len;
+ uint8_t buf[CRYPT_KEY_BUFSZ];
+ uint8_t s[SYMMKEYSZ];
+
+ TEST_START("(%s)", 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) {
+ printf("Failed to create key pair.\n");
+ goto fail;
+ }
+
+ pk.len = (size_t) len;
+ pk.data = buf;
+
+ /* Corrupt the public key */
+ buf[0] ^= 0xFF;
+ buf[len - 1] ^= 0xFF;
+
+ if (kex_dhe_derive(&kex, pkp, pk, s) == 0) {
+ printf("Should fail with corrupted public key.\n");
+ goto fail_pkp;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp:
+ kex_pkp_destroy(pkp);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_dhe_wrong_algo(void)
+{
+ struct sec_config kex1;
+ struct sec_config kex2;
+ 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];
+ const char * algo1 = "X25519";
+ const char * algo2 = "X448";
+
+ TEST_START("(%s vs %s)", algo1, algo2);
+
+ memset(&kex1, 0, sizeof(kex1));
+ 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");
+ goto fail;
+ }
+
+ len = kex_pkp_create(&kex2, &pkp2, buf2);
+ if (len < 0) {
+ printf("Failed to create second key pair.\n");
+ goto fail_pkp1;
+ }
+
+ pk2.len = (size_t) len;
+ pk2.data = buf2;
+
+ /* Try to derive with mismatched algorithms */
+ if (kex_dhe_derive(&kex1, pkp1, pk2, s) == 0) {
+ printf("Should fail with mismatched algorithms.\n");
+ goto fail_pkp2;
+ }
+
+ kex_pkp_destroy(pkp2);
+ kex_pkp_destroy(pkp1);
+
+ TEST_SUCCESS("(%s vs %s)", algo1, algo2);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp2:
+ kex_pkp_destroy(pkp2);
+ fail_pkp1:
+ kex_pkp_destroy(pkp1);
+ fail:
+ TEST_FAIL("(%s vs %s)", algo1, algo2);
+ 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;
+
+ TEST_START();
+
+ if (crypt_load_privkey_str(X25519_PRIVKEY_PEM, &key) < 0) {
+ printf("Failed to load X25519 private key.\n");
+ goto fail;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_load_dhe_pubkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_pubkey_str(X25519_PUBKEY_PEM, &key) < 0) {
+ printf("Failed to load X25519 public key.\n");
+ goto fail;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+#ifdef HAVE_OPENSSL
+#include <openssl/obj_mac.h>
+
+static int test_kex_nid_values(void)
+{
+ int i;
+
+ TEST_START();
+
+ /* Verify all KEX algorithm NIDs match OpenSSL's */
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ uint16_t our_nid = kex_supported_nids[i];
+ const char * kex_name;
+ int openssl_nid;
+
+ kex_name = kex_nid_to_str(our_nid);
+ if (kex_name == NULL) {
+ printf("kex_nid_to_str failed for NID %u\n", our_nid);
+ goto fail;
+ }
+
+ /* Test reverse conversion */
+ if (kex_str_to_nid(kex_name) != our_nid) {
+ printf("kex_str_to_nid failed for '%s'\n", kex_name);
+ goto fail;
+ }
+
+ /* Get OpenSSL's NID for this name */
+ openssl_nid = OBJ_txt2nid(kex_name);
+ if (openssl_nid != NID_undef) {
+ /* OpenSSL recognizes this algorithm */
+ if (our_nid != openssl_nid) {
+ printf("NID mismatch for '%s': "
+ "ours=%d, OpenSSL=%d\n",
+ kex_name, our_nid, openssl_nid);
+ goto fail;
+ }
+ } else {
+ /* Verify no NID collision with different algorithm */
+ const char * ossl_name = OBJ_nid2sn(our_nid);
+ if (ossl_name != NULL &&
+ strcmp(ossl_name, kex_name) != 0) {
+ printf("NID collision for '%d': "
+ "ours=%s, OpenSSL=%s\n",
+ our_nid, kex_name, ossl_name);
+ goto fail;
+ }
+ }
+ }
+
+ /* Test error cases */
+ if (kex_str_to_nid("invalid") != NID_undef) {
+ printf("kex_str_to_nid should return NID_undef for invalid\n");
+ goto fail;
+ }
+
+ if (kex_nid_to_str(9999) != NULL) {
+ printf("kex_nid_to_str should return NULL for invalid NID\n");
+ goto fail;
+ }
+
+ if (kex_str_to_nid(NULL) != NID_undef) {
+ printf("kex_str_to_nid should return NID_undef for NULL\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+#endif
+
+static int test_kex_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]);
+
+ /* KEM tests are in kex_test_ml_kem.c */
+ if (IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_kex_dhe_derive(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_dhe_corrupted_pubkey_all(void)
+{
+ int ret = 0;
+ int i;
+
+ /* Test corruption for all DHE algorithms */
+ /* KEM error injection tests are in kex_test_ml_kem.c */
+ 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_kex_dhe_corrupted_pubkey(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_parse_config_empty(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR("\n");
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ goto fail;
+ }
+
+ if (parse_sec_config(&kex, fp) < 0) {
+ printf("Failed to parse empty config.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (strcmp(kex.x.str, "prime256v1") != 0) {
+ printf("Empty config should use prime256v1.\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_custom(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR(KEX_CONFIG_CUSTOM);
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ goto fail;
+ }
+
+ if (parse_sec_config(&kex, fp) < 0) {
+ printf("Failed to parse custom config.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (strcmp(kex.x.str, "X25519") != 0) {
+ printf("Algorithm not set correctly.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ fclose(fp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The old bare 'none' keyword must be rejected loudly */
+static int test_kex_parse_config_none_rejected(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR(KEX_CONFIG_NONE);
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ 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 encryption=none config.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ 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;
+ }
+
+ fclose(fp);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_parse_config_whitespace(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR(KEX_CONFIG_WHITESPACE);
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ goto fail;
+ }
+
+ if (parse_sec_config(&kex, fp) < 0) {
+ printf("Failed to parse config with comments.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (strcmp(kex.x.str, "X448") != 0) {
+ printf("Algorithm with whitespace 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_cipher(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR(KEX_CONFIG_CIPHER);
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ goto fail;
+ }
+
+ if (parse_sec_config(&kex, fp) < 0) {
+ printf("Failed to parse cipher config.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (strcmp(kex.x.str, "X25519") != 0) {
+ printf("Algorithm not set correctly.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (kex.c.nid != NID_chacha20_poly1305) {
+ printf("Cipher not set 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_digest(void)
+{
+ struct sec_config kex;
+ FILE * fp;
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+
+ fp = FMEMOPEN_STR(KEX_CONFIG_DIGEST);
+ if (fp == NULL) {
+ printf("Failed to open memory stream.\n");
+ goto fail;
+ }
+
+ if (parse_sec_config(&kex, fp) < 0) {
+ printf("Failed to parse digest config.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (strcmp(kex.x.str, "X25519") != 0) {
+ printf("Algorithm not set correctly.\n");
+ fclose(fp);
+ goto fail;
+ }
+
+ if (kex.d.nid != NID_sha384) {
+ printf("Digest not set 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(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;
+}
+
+int kex_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_kex_create_destroy();
+ 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();
+#ifdef HAVE_OPENSSL
+ ret |= test_kex_parse_config_custom();
+ ret |= test_kex_parse_config_whitespace();
+ ret |= test_kex_parse_config_cipher();
+ ret |= test_kex_parse_config_digest();
+ ret |= test_kex_nid_values();
+ ret |= test_kex_dh_pkp_create_destroy();
+ ret |= test_kex_all();
+ 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
+ (void) test_kex_parse_config_custom;
+ (void) test_kex_parse_config_whitespace;
+ (void) test_kex_parse_config_cipher;
+ (void) test_kex_parse_config_digest;
+ (void) test_kex_dh_pkp_create_destroy;
+ (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_no_kdf;
+ (void) test_kex_dhe_corrupted_pubkey_all;
+ (void) test_kex_load_dhe_privkey;
+ (void) test_kex_load_dhe_pubkey;
+
+ if (ret == 0)
+ 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
new file mode 100644
index 00000000..1f7d7531
--- /dev/null
+++ b/src/lib/tests/kex_test_ml_kem.c
@@ -0,0 +1,549 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the post-quantum key exchange functions
+ *
+ * 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>
+#include <ouroboros/utils.h>
+#include <ouroboros/crypt.h>
+#include <ouroboros/random.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifdef HAVE_OPENSSL
+#include <openssl/evp.h>
+#include <openssl/x509.h>
+#endif
+
+extern const uint16_t kex_supported_nids[];
+extern const uint16_t md_supported_nids[];
+
+static int get_random_kdf(void)
+{
+ static int idx = 0;
+ int count;
+
+ if (md_supported_nids[0] == NID_undef)
+ return NID_undef;
+
+ for (count = 0; md_supported_nids[count] != NID_undef; count++)
+ ;
+
+ return md_supported_nids[(idx++) % count];
+}
+
+/* ML-KEM-768 test key material */
+
+#define MLKEM768_PRIVKEY_PEM \
+ "-----BEGIN PRIVATE KEY-----\n" \
+ "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\n" \
+ "108=\n" \
+ "-----END PRIVATE KEY-----\n"
+
+#define MLKEM768_PUBKEY_PEM \
+ "-----BEGIN PUBLIC KEY-----\n" \
+ "MIIEsjALBglghkgBZQMEBAIDggShAMPIO3U2cWpYVzqVh5Vrp5FyZjN4BtdqGvHC\n" \
+ "KJosh0dcP3lpn/RLr60oEaBDPCZmxvB1eGX6f7KoM0TpKjDZvC56qEPBlVagwK/l\n" \
+ "Ir4cAY+1xntRT2hauqmIvTzmbsXkbj0olQRlp1W2Phn3OuR5CSJ1r7gju/djczOx\n" \
+ "awYnDDtiFMMmmzHGZKbzlsfDH3yFldKpyQlMvhzacHOCsGW6QhP1aX6oi0y2FW8A\n" \
+ "E/OnjAZmF2AqsQhGXfIKeGFSCkuVTFh6DBYMwSNnEqVJsjKXmn2UJLzDaWwHOxDY\n" \
+ "UmIDL8xYtrnpoD7AWzcLyyZnoJ+nk1ZrxGb6Tz3wZYHFmK+EXTcIE5njZ27RhRl0\n" \
+ "I+gDlXUoLpuHgnJYUjj1UVtAUwaDACpzQSsAfeIxqaYoL3vjpZZqCdQgbejwvKBm\n" \
+ "u6y4Z5OiFcmFVRMkPDazSvM2FvDkjKiiSRjkFYQ0oUpMiB0HG9h6U4rBsYTxnfoU\n" \
+ "RbQAGWbHtJEIpaU1WNIQAheYyJBUt1AXfdBVgP1FHlyGNrbnhW4kfimWj7knCIpb\n" \
+ "PwuUIgGbsJWSJ9kwuh+YyTezcvixe0T4GF+bk+WTdGuAN2Ucf6EruCTrVcZ3KcaM\n" \
+ "Njtou0IpnRIyxWyRIuibWlshHkwotixDU+x7iaF8M4UKRYUsjlNKEYy4iijWTtd4\n" \
+ "gTqDxChSVXkRfJYceEhVoTjJYY8BWASnRSzAoV56jdKgIB4AMDdTzzKbGkEmnvfI\n" \
+ "MiqiTl0zdKCSN89ZkOSCQAjxYewIop6qtI6zWSQTcle0TuRxMdPpcejXrZOaAaSo\n" \
+ "xXA5DcSZHoVGljEmFm+5VuMmxLFiNbdJydHhmK+iLur3nKlyjffWkiUhmmpXQ3J1\n" \
+ "SkzJBrPVyeg8pgOHXFrouFyoQorxrryJeNCDOCpAEMAGidXJs0O2UnmWdNO4qFqT\n" \
+ "w8EUCaaGPUl2s+qgi9BEKYtpquQnv+r1Eu2TqRM4nSq0t7aqrALEnDKgx3ZDkXj7\n" \
+ "FNZke73SWNF1ydh7BqupwrOgvbu1ylM6ZXPzwjWFhVQkRXtoHfpAyFNLV2q5Oolk\n" \
+ "IPSEE9YAxOgSgPg4W8paZsWbNpbGsxzgQJh4MAApizZiqhdIt8CLEGZmNvpQMHrW\n" \
+ "XYYDnykUlbRnA3xLm0CkDY1GL7ugL/OAyAEYd+3oTT45RVvwi3z2ljW3vJurn5+Q\n" \
+ "G8SLEMh3d/WkreCpxpiob9iGgwkwlaSpjEI0aZ1nyAWjjpXor7jFlRuVBYrKk3r5\n" \
+ "Fg7iQOHSSimlYYRQuOAGgIf5XwTsZygTxJF1BjUEgyhFVZvxSvxlghVTyWqBpk+V\n" \
+ "YzehEKJaTSRHF7EUXSLlaUgsgr0ZPyxpf5uZF30pLLcmuk+FCfn0KkkFHHPxYp0n\n" \
+ "fDC2oT/ZNvFCEuigRYdhWtkQbkRKQig6RQAlRH4LMssUMx1QTzDcJqwTjKz6Fyjs\n" \
+ "ADlMCazbgDUVdzEyA1vpUcDRuLlzys8Jm+O5stdHgDT8DZRqWjhzLdQCv2TSttk4\n" \
+ "meOqTx4TGR5rFvKHjZwmlTvmLg40tixFkmSD1uQKk+BkpvvTuw9k7vYJfeH+VB1d\n" \
+ "WAaRntOp\n" \
+ "-----END PUBLIC KEY-----\n"
+
+/* Helper macro to open string constant as FILE stream */
+#define FMEMOPEN_STR(str) fmemopen((void *) (str), strlen(str), "r")
+
+static int test_kex_load_kem_privkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_privkey_str(MLKEM768_PRIVKEY_PEM, &key) < 0) {
+ printf("Failed to load ML-KEM-768 private key.\n");
+ goto fail;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_load_kem_pubkey(void)
+{
+ void * key;
+
+ TEST_START();
+
+ if (crypt_load_pubkey_str(MLKEM768_PUBKEY_PEM, &key) < 0) {
+ printf("Failed to load ML-KEM-768 public key.\n");
+ goto fail;
+ }
+
+ crypt_free_key(key);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_kem(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp;
+ buffer_t pk;
+ buffer_t ct;
+ ssize_t len;
+ ssize_t ct_len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t s1[SYMMKEYSZ];
+ uint8_t s2[SYMMKEYSZ];
+ int kdf;
+
+ TEST_START("(%s)", algo);
+
+ kdf = get_random_kdf();
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, algo);
+
+ len = kex_pkp_create(&kex, &pkp, buf1);
+ if (len < 0) {
+ printf("Failed to create key pair for %s.\n", algo);
+ goto fail;
+ }
+
+ pk.len = (size_t) len;
+ pk.data = buf1;
+
+ if (IS_HYBRID_KEM(algo))
+ ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1);
+ else
+ ct_len = kex_kem_encap(pk, buf2, kdf, s1);
+
+ if (ct_len < 0) {
+ printf("Failed to encapsulate for %s.\n", algo);
+ goto fail_pkp;
+ }
+
+ ct.len = (size_t) ct_len;
+ ct.data = buf2;
+
+ if (kex_kem_decap(pkp, ct, kdf, s2) < 0) {
+ printf("Failed to decapsulate for %s.\n", algo);
+ goto fail_pkp;
+ }
+
+ if (memcmp(s1, s2, SYMMKEYSZ) != 0) {
+ printf("Shared secrets don't match for %s.\n", algo);
+ goto fail_pkp;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp:
+ kex_pkp_destroy(pkp);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_kem_corrupted_ciphertext(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp;
+ buffer_t pk;
+ buffer_t ct;
+ ssize_t len;
+ ssize_t ct_len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t s1[SYMMKEYSZ];
+ uint8_t s2[SYMMKEYSZ];
+ int kdf;
+
+ TEST_START("(%s)", algo);
+
+ kdf = get_random_kdf();
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, algo);
+
+ len = kex_pkp_create(&kex, &pkp, buf1);
+ if (len < 0) {
+ printf("Failed to create key pair.\n");
+ goto fail;
+ }
+
+ pk.len = (size_t) len;
+ pk.data = buf1;
+
+ if (IS_HYBRID_KEM(algo))
+ ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1);
+ else
+ ct_len = kex_kem_encap(pk, buf2, kdf, s1);
+
+ if (ct_len < 0) {
+ printf("Failed to encapsulate.\n");
+ goto fail_pkp;
+ }
+
+ ct.len = (size_t) ct_len;
+ ct.data = buf2;
+
+ /* Corrupt the ciphertext */
+ buf2[0] ^= 0xFF;
+ buf2[ct_len - 1] ^= 0xFF;
+
+ /* ML-KEM uses implicit rejection */
+ if (kex_kem_decap(pkp, ct, kdf, s2) < 0) {
+ printf("Decapsulation failed unexpectedly.\n");
+ goto fail_pkp;
+ }
+
+ /* The shared secrets should NOT match with corrupted CT */
+ if (memcmp(s1, s2, SYMMKEYSZ) == 0) {
+ printf("Corrupted ciphertext produced same secret.\n");
+ goto fail_pkp;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp:
+ kex_pkp_destroy(pkp);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_kem_wrong_keypair(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp1;
+ void * pkp2;
+ buffer_t pk1;
+ buffer_t ct;
+ ssize_t len;
+ ssize_t ct_len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t buf3[CRYPT_KEY_BUFSZ];
+ uint8_t s1[SYMMKEYSZ];
+ uint8_t s2[SYMMKEYSZ];
+
+ TEST_START("(%s)", algo);
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, algo);
+
+ len = kex_pkp_create(&kex, &pkp1, buf1);
+ if (len < 0) {
+ printf("Failed to create first key pair.\n");
+ goto fail;
+ }
+
+ pk1.len = (size_t) len;
+ pk1.data = buf1;
+
+ if (kex_pkp_create(&kex, &pkp2, buf2) < 0) {
+ printf("Failed to create second key pair.\n");
+ goto fail_pkp1;
+ }
+
+ if (IS_HYBRID_KEM(algo))
+ ct_len = kex_kem_encap_raw(pk1, buf3, NID_sha256, s1);
+ else
+ ct_len = kex_kem_encap(pk1, buf3, NID_sha256, s1);
+
+ if (ct_len < 0) {
+ printf("Failed to encapsulate.\n");
+ goto fail_pkp2;
+ }
+
+ ct.len = (size_t) ct_len;
+ ct.data = buf3;
+
+ if (kex_kem_decap(pkp2, ct, NID_sha256, s2) == 0) {
+ if (memcmp(s1, s2, SYMMKEYSZ) == 0) {
+ printf("Wrong keypair produced same secret.\n");
+ goto fail_pkp2;
+ }
+ }
+
+ kex_pkp_destroy(pkp2);
+ kex_pkp_destroy(pkp1);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp2:
+ kex_pkp_destroy(pkp2);
+ fail_pkp1:
+ kex_pkp_destroy(pkp1);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_kem_truncated_ciphertext(const char * algo)
+{
+ struct sec_config kex;
+ void * pkp;
+ buffer_t pk;
+ buffer_t ct;
+ ssize_t len;
+ ssize_t ct_len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t s1[SYMMKEYSZ];
+ uint8_t s2[SYMMKEYSZ];
+
+ TEST_START("(%s)", algo);
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, algo);
+
+ len = kex_pkp_create(&kex, &pkp, buf1);
+ if (len < 0) {
+ printf("Failed to create key pair.\n");
+ goto fail;
+ }
+
+ pk.len = (size_t) len;
+ pk.data = buf1;
+
+ if (IS_HYBRID_KEM(algo))
+ ct_len = kex_kem_encap_raw(pk, buf2, NID_sha256, s1);
+ else
+ ct_len = kex_kem_encap(pk, buf2, NID_sha256, s1);
+
+ if (ct_len < 0) {
+ printf("Failed to encapsulate.\n");
+ goto fail_pkp;
+ }
+
+ /* Truncate the ciphertext */
+ ct.len = (size_t) ct_len / 2;
+ ct.data = buf2;
+
+ if (kex_kem_decap(pkp, ct, NID_sha256, s2) == 0) {
+ printf("Should fail with truncated ciphertext.\n");
+ goto fail_pkp;
+ }
+
+ kex_pkp_destroy(pkp);
+
+ TEST_SUCCESS("(%s)", algo);
+
+ return TEST_RC_SUCCESS;
+ fail_pkp:
+ kex_pkp_destroy(pkp);
+ fail:
+ TEST_FAIL("(%s)", algo);
+ return TEST_RC_FAIL;
+}
+
+static int test_kex_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_kex_kem(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_kem_corrupted_ciphertext_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_kex_kem_corrupted_ciphertext(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_kem_wrong_keypair_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_kex_kem_wrong_keypair(algo);
+ }
+
+ return ret;
+}
+
+static int test_kex_kem_truncated_ciphertext_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_kex_kem_truncated_ciphertext(algo);
+ }
+
+ return ret;
+}
+
+int kex_test_ml_kem(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#ifdef HAVE_ML
+ ret |= test_kex_load_kem_privkey();
+ ret |= test_kex_load_kem_pubkey();
+ ret |= test_kex_kem_all();
+ ret |= test_kex_kem_corrupted_ciphertext_all();
+ ret |= test_kex_kem_wrong_keypair_all();
+ ret |= test_kex_kem_truncated_ciphertext_all();
+#else
+ (void) test_kex_load_kem_privkey;
+ (void) test_kex_load_kem_pubkey;
+ (void) test_kex_kem_all;
+ (void) test_kex_kem_corrupted_ciphertext_all;
+ (void) test_kex_kem_wrong_keypair_all;
+ (void) test_kex_kem_truncated_ciphertext_all;
+
+ ret = TEST_RC_SKIP;
+#endif
+ return ret;
+}
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/md5_test.c b/src/lib/tests/md5_test.c
index 28e8f42f..ea3e12b3 100644
--- a/src/lib/tests/md5_test.c
+++ b/src/lib/tests/md5_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the MD5 function
*
diff --git a/src/lib/tests/sha3_test.c b/src/lib/tests/sha3_test.c
index 82b4ef0d..ccd4e12a 100644
--- a/src/lib/tests/sha3_test.c
+++ b/src/lib/tests/sha3_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the SHA3 function
*
diff --git a/src/lib/tests/shm_rbuff_test.c b/src/lib/tests/shm_rbuff_test.c
deleted file mode 100644
index e36c3229..00000000
--- a/src/lib/tests/shm_rbuff_test.c
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Test of the shm_rbuff
- *
- * 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 200112L
-
-#include "config.h"
-
-#include <ouroboros/shm_rbuff.h>
-
-#include <errno.h>
-#include <stdio.h>
-#include <unistd.h>
-
-int shm_rbuff_test(int argc,
- char ** argv)
-{
- struct shm_rbuff * rb;
- size_t i;
-
- (void) argc;
- (void) argv;
-
- printf("Test: create rbuff...");
-
- rb = shm_rbuff_create(getpid(), 1);
- if (rb == NULL)
- goto err;
-
- printf("success.\n\n");
- printf("Test: write a value...");
-
- if (shm_rbuff_write(rb, 1) < 0)
- goto error;
-
- printf("success.\n\n");
- printf("Test: check queue length is 1...");
-
- if (shm_rbuff_queued(rb) != 1)
- goto error;
-
- printf("success.\n\n");
- printf("Test: read a value...");
-
- if (shm_rbuff_read(rb) != 1)
- goto error;
-
- printf("success.\n\n");
- printf("Test: check queue is empty...");
-
- if (shm_rbuff_read(rb) != -EAGAIN)
- goto error;
-
- printf("success.\n\n");
- printf("Test: fill the queue...");
-
- for (i = 0; i < SHM_RBUFF_SIZE - 1; ++i) {
- if (shm_rbuff_queued(rb) != i)
- goto error;
- if (shm_rbuff_write(rb, 1) < 0)
- goto error;
- }
-
- printf("success.\n\n");
- printf("Test: check queue is full...");
-
- if (shm_rbuff_queued(rb) != SHM_RBUFF_SIZE - 1)
- goto error;
-
- printf("success [%zd entries].\n\n", shm_rbuff_queued(rb));
-
- printf("Test: check queue is full by writing value...");
- if (!(shm_rbuff_write(rb, 1) < 0))
- goto error;
-
- printf("success [%zd entries].\n\n", shm_rbuff_queued(rb));
-
- /* empty the rbuff */
- while (shm_rbuff_read(rb) >= 0)
- ;
-
- shm_rbuff_destroy(rb);
-
- return 0;
-
- error:
- /* empty the rbuff */
- while (shm_rbuff_read(rb) >= 0)
- ;
-
- shm_rbuff_destroy(rb);
- err:
- printf("failed.\n\n");
- return -1;
-}
diff --git a/src/lib/tests/sockets_test.c b/src/lib/tests/sockets_test.c
index bbf2323b..c00bfdc1 100644
--- a/src/lib/tests/sockets_test.c
+++ b/src/lib/tests/sockets_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Tests for socket.c
*
@@ -20,10 +20,14 @@
* 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/sockets.h>
-#include <ouroboros/test.h>
+#include <test/test.h>
#include <assert.h>
#include <stdio.h>
diff --git a/src/lib/tests/time_test.c b/src/lib/tests/time_test.c
index 2b75b873..919cf075 100644
--- a/src/lib/tests/time_test.c
+++ b/src/lib/tests/time_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Test of the time utilities
*
@@ -22,7 +22,7 @@
#define _POSIX_C_SOURCE 200809L
-#include <ouroboros/test.h>
+#include <test/test.h>
#include <ouroboros/time.h>
#include <stdio.h>
diff --git a/src/lib/tests/tpm_test.c b/src/lib/tests/tpm_test.c
index 98d4fab3..7cc049cd 100644
--- a/src/lib/tests/tpm_test.c
+++ b/src/lib/tests/tpm_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Tests for the threadpool manager
*
@@ -21,9 +21,9 @@
*/
-#include "tpm.c"
+#include <ouroboros/tpm.h>
-#include <ouroboros/test.h>
+#include <test/test.h>
static void * test_func(void * o)
{
diff --git a/src/lib/tests/tw_test.c b/src/lib/tests/tw_test.c
new file mode 100644
index 00000000..32c302c4
--- /dev/null
+++ b/src/lib/tests/tw_test.c
@@ -0,0 +1,663 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Generic timing-wheel 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 "config.h"
+
+#include <test/test.h>
+
+#include <ouroboros/time.h>
+#include <ouroboros/tw.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <time.h>
+
+struct payload {
+ struct tw_entry tw;
+ int fired;
+};
+
+struct cancel_payload {
+ struct tw_entry tw;
+ int fired;
+ struct tw_entry * sibling;
+};
+
+struct repost_payload {
+ struct tw_entry tw;
+ int fired;
+ struct payload * sibling;
+ uint64_t repost_at;
+};
+
+static void cb_count(void * arg)
+{
+ struct payload * p = arg;
+ p->fired++;
+}
+
+static void cb_cancel_sibling(void * arg)
+{
+ struct cancel_payload * p = arg;
+ p->fired++;
+ tw_cancel(p->sibling);
+}
+
+static void cb_repost_sibling(void * arg)
+{
+ struct repost_payload * p = arg;
+ p->fired++;
+ tw_post(&p->sibling->tw, p->repost_at, cb_count, p->sibling);
+}
+
+static uint64_t now_ns(void)
+{
+ struct timespec ts;
+ clock_gettime(PTHREAD_COND_CLOCK, &ts);
+ return TS_TO_UINT64(ts);
+}
+
+static void sleep_ns(uint64_t ns)
+{
+ struct timespec ts;
+ UINT64_TO_TS(ns, &ts);
+ nanosleep(&ts, NULL);
+}
+
+static int test_tw_init_fini(void)
+{
+ TEST_START();
+
+ if (tw_init() < 0) {
+ printf("tw_init failed.\n");
+ goto fail;
+ }
+
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_post_fires_after_deadline(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 5 * MILLION, cb_count, &p);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+
+ if (p.fired != 1) {
+ printf("expected 1 fire, got %d\n", p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_no_fire_before_deadline(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 100 * MILLION, cb_count, &p);
+
+ sleep_ns(2 * MILLION);
+ tw_move();
+
+ if (p.fired != 0) {
+ printf("expected 0 fires, got %d\n", p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_cancel_prevents_fire(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 5 * MILLION, cb_count, &p);
+ tw_cancel(&p.tw);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+
+ if (p.fired != 0) {
+ printf("cancelled entry fired %d times\n", p.fired);
+ goto fail_init;
+ }
+
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_cancel_unposted_is_noop(void)
+{
+ struct tw_entry e;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&e);
+ tw_cancel(&e);
+ tw_cancel(&e);
+
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_fire_only_once(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+ tw_move();
+ tw_move();
+
+ if (p.fired != 1) {
+ printf("expected 1 fire, got %d after 3 moves\n", p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Multi-level: post a level-1 (>= 256ms) deadline; should still fire. */
+static int test_tw_post_level1_fires(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 300 * MILLION, cb_count, &p);
+
+ if (p.tw.lvl != 1) {
+ printf("expected level 1 placement, got %zu\n", p.tw.lvl);
+ goto fail_post;
+ }
+
+ sleep_ns(320 * MILLION);
+ tw_move();
+
+ if (p.fired != 1) {
+ printf("level-1 entry didn't fire (got %d)\n", p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tw_many_entries_all_fire(void)
+{
+ struct payload pl[16];
+ size_t i;
+ size_t total = 0;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ for (i = 0; i < 16; ++i) {
+ tw_init_entry(&pl[i].tw);
+ pl[i].fired = 0;
+ tw_post(&pl[i].tw, now_ns() + (1 + i) * MILLION,
+ cb_count, &pl[i]);
+ }
+
+ sleep_ns(40 * MILLION);
+ tw_move();
+
+ for (i = 0; i < 16; ++i)
+ total += pl[i].fired;
+
+ if (total != 16) {
+ printf("expected 16 fires, got %zu\n", total);
+ goto fail_post;
+ }
+
+ for (i = 0; i < 16; ++i)
+ tw_cancel(&pl[i].tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ for (i = 0; i < 16; ++i)
+ tw_cancel(&pl[i].tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* tw_next_expiry signals empty wheel via tv_nsec == -1. */
+static int test_tw_next_expiry_empty(void)
+{
+ struct timespec out;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_next_expiry(&out);
+ if (out.tv_nsec != -1) {
+ printf("expected tv_nsec=-1, got %ld\n", (long) out.tv_nsec);
+ goto fail_init;
+ }
+
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* tw_next_expiry returns a deadline within the right ballpark. */
+static int test_tw_next_expiry_returns_deadline(void)
+{
+ struct payload p;
+ struct timespec out;
+ uint64_t target;
+ uint64_t out_ns;
+ int64_t skew;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ target = now_ns() + 50 * MILLION;
+ tw_post(&p.tw, target, cb_count, &p);
+
+ tw_next_expiry(&out);
+ out_ns = TS_TO_UINT64(out);
+
+ /* Level-0 quantization gives ±1 slot of skew. */
+ skew = (int64_t)(out_ns) - (int64_t)(target);
+ if (skew < -2 * MILLION || skew > 4 * MILLION) {
+ printf("deadline not in -2..+4 ms, skew=%ld ns\n", (long) skew);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Repost: fire, then post again. */
+static int test_tw_repost_after_fire(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p);
+ sleep_ns(20 * MILLION);
+ tw_move();
+ if (p.fired != 1) {
+ printf("first fire missed\n");
+ goto fail_post;
+ }
+
+ tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p);
+ sleep_ns(20 * MILLION);
+ tw_move();
+ if (p.fired != 2) {
+ printf("second fire missed (fired=%d)\n", p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Double-post replaces the schedule; only the second fires. */
+static int test_tw_double_post_replaces(void)
+{
+ struct payload p;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&p.tw);
+ p.fired = 0;
+
+ tw_post(&p.tw, now_ns() + 30 * MILLION, cb_count, &p);
+ tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+
+ if (p.fired != 1) {
+ printf("expected 1 fire after replace, got %d\n", p.fired);
+ goto fail_post;
+ }
+
+ sleep_ns(40 * MILLION);
+ tw_move();
+
+ if (p.fired != 1) {
+ printf("first schedule fired after replace (got %d)\n",
+ p.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&p.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&p.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Fire callback may safely cancel a sibling in the same slot. */
+static int test_tw_fire_cancels_sibling(void)
+{
+ struct cancel_payload a;
+ struct payload b;
+ uint64_t deadline;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&a.tw);
+ tw_init_entry(&b.tw);
+ a.fired = 0;
+ a.sibling = &b.tw;
+ b.fired = 0;
+
+ deadline = now_ns() + 3 * MILLION;
+ tw_post(&a.tw, deadline, cb_cancel_sibling, &a);
+ tw_post(&b.tw, deadline, cb_count, &b);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+
+ if (a.fired != 1) {
+ printf("a expected 1 fire, got %d\n", a.fired);
+ goto fail_post;
+ }
+ if (b.fired != 0) {
+ printf("b should not have fired (got %d)\n", b.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&a.tw);
+ tw_cancel(&b.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&a.tw);
+ tw_cancel(&b.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Fire callback may safely repost a sibling to a future slot. */
+static int test_tw_fire_posts_sibling(void)
+{
+ struct repost_payload a;
+ struct payload b;
+ uint64_t deadline;
+
+ TEST_START();
+
+ if (tw_init() < 0)
+ goto fail;
+
+ tw_init_entry(&a.tw);
+ tw_init_entry(&b.tw);
+ a.fired = 0;
+ a.sibling = &b;
+ a.repost_at = now_ns() + 30 * MILLION;
+ b.fired = 0;
+
+ deadline = now_ns() + 3 * MILLION;
+ tw_post(&a.tw, deadline, cb_repost_sibling, &a);
+ tw_post(&b.tw, deadline, cb_count, &b);
+
+ sleep_ns(20 * MILLION);
+ tw_move();
+
+ if (a.fired != 1) {
+ printf("a expected 1 fire, got %d\n", a.fired);
+ goto fail_post;
+ }
+ if (b.fired != 0) {
+ printf("b fired before reposted deadline (got %d)\n",
+ b.fired);
+ goto fail_post;
+ }
+
+ sleep_ns(25 * MILLION);
+ tw_move();
+
+ if (b.fired != 1) {
+ printf("b expected 1 fire after repost, got %d\n",
+ b.fired);
+ goto fail_post;
+ }
+
+ tw_cancel(&a.tw);
+ tw_cancel(&b.tw);
+ tw_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_post:
+ tw_cancel(&a.tw);
+ tw_cancel(&b.tw);
+ tw_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int tw_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_tw_init_fini();
+ ret |= test_tw_post_fires_after_deadline();
+ ret |= test_tw_no_fire_before_deadline();
+ ret |= test_tw_cancel_prevents_fire();
+ ret |= test_tw_cancel_unposted_is_noop();
+ ret |= test_tw_fire_only_once();
+ ret |= test_tw_post_level1_fires();
+ ret |= test_tw_many_entries_all_fire();
+ ret |= test_tw_next_expiry_empty();
+ ret |= test_tw_next_expiry_returns_deadline();
+ ret |= test_tw_repost_after_fire();
+ ret |= test_tw_double_post_replaces();
+ ret |= test_tw_fire_cancels_sibling();
+ ret |= test_tw_fire_posts_sibling();
+
+ return ret;
+}
diff --git a/src/lib/timerwheel.c b/src/lib/timerwheel.c
deleted file mode 100644
index 96f4ac47..00000000
--- a/src/lib/timerwheel.c
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2024
- *
- * Timerwheel
- *
- * 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 <ouroboros/list.h>
-
-/* Overflow limits range to about 6 hours. */
-#define ts_to_ns(ts) (ts.tv_sec * BILLION + ts.tv_nsec)
-#define ts_to_rxm_slot(ts) (ts_to_ns(ts) >> RXMQ_RES)
-#define ts_to_ack_slot(ts) (ts_to_ns(ts) >> ACKQ_RES)
-
-struct rxm {
- struct list_head next;
- uint32_t seqno;
-#ifndef RXM_BUFFER_ON_HEAP
- struct shm_du_buff * sdb;
-#endif
- struct frct_pci * pkt;
- size_t len;
- time_t t0; /* Time when original was sent (us). */
- struct frcti * frcti;
- int fd;
- int flow_id; /* Prevent rtx when fd reused. */
-};
-
-struct ack {
- struct list_head next;
- struct frcti * frcti;
- int fd;
- int flow_id;
-};
-
-struct {
- /*
- * At a 1 ms min resolution, every level bumps the
- * resolution by a factor of 16.
- */
- struct list_head rxms[RXMQ_LVLS][RXMQ_SLOTS];
-
- struct list_head acks[ACKQ_SLOTS];
- bool map[ACKQ_SLOTS][PROG_MAX_FLOWS];
-
- size_t prv_rxm[RXMQ_LVLS]; /* Last processed rxm slots. */
- size_t prv_ack; /* Last processed ack slot. */
- pthread_mutex_t lock;
-} rw;
-
-static void timerwheel_fini(void)
-{
- size_t i;
- size_t j;
- struct list_head * p;
- struct list_head * h;
-
- pthread_mutex_lock(&rw.lock);
-
- for (i = 0; i < RXMQ_LVLS; ++i) {
- for (j = 0; j < RXMQ_SLOTS; j++) {
- list_for_each_safe(p, h, &rw.rxms[i][j]) {
- struct rxm * rxm;
- rxm = list_entry(p, struct rxm, next);
- list_del(&rxm->next);
-#ifdef RXM_BUFFER_ON_HEAP
- free(rxm->pkt);
-#else
- shm_du_buff_ack(rxm->sdb);
- ipcp_sdb_release(rxm->sdb);
-#endif
- free(rxm);
- }
- }
- }
-
- for (i = 0; i < ACKQ_SLOTS; ++i) {
- list_for_each_safe(p, h, &rw.acks[i]) {
- struct ack * a = list_entry(p, struct ack, next);
- list_del(&a->next);
- free(a);
- }
- }
-
- pthread_mutex_unlock(&rw.lock);
-
- pthread_mutex_destroy(&rw.lock);
-}
-
-static int timerwheel_init(void)
-{
- struct timespec now;
- size_t i;
- size_t j;
-
- if (pthread_mutex_init(&rw.lock, NULL))
- return -1;
-
- clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- for (i = 0; i < RXMQ_LVLS; ++i) {
- rw.prv_rxm[i] = (ts_to_rxm_slot(now) - 1);
- rw.prv_rxm[i] >>= (RXMQ_BUMP * i);
- rw.prv_rxm[i] &= (RXMQ_SLOTS - 1);
- for (j = 0; j < RXMQ_SLOTS; ++j)
- list_head_init(&rw.rxms[i][j]);
- }
-
- rw.prv_ack = (ts_to_ack_slot(now) - 1) & (ACKQ_SLOTS - 1);
- for (i = 0; i < ACKQ_SLOTS; ++i)
- list_head_init(&rw.acks[i]);
-
- return 0;
-}
-
-static void timerwheel_move(void)
-{
- struct timespec now;
- struct list_head * p;
- struct list_head * h;
- size_t rxm_slot;
- size_t ack_slot;
- size_t i;
- size_t j;
-
- pthread_mutex_lock(&rw.lock);
-
- pthread_cleanup_push(__cleanup_mutex_unlock, &rw.lock);
-
- clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- rxm_slot = ts_to_rxm_slot(now);
-
- for (i = 0; i < RXMQ_LVLS; ++i) {
- size_t j_max_slot = rxm_slot & (RXMQ_SLOTS - 1);
- j = rw.prv_rxm[i];
- if (j_max_slot < j)
- j_max_slot += RXMQ_SLOTS;
- while (j++ < j_max_slot) {
- list_for_each_safe(p, h,
- &rw.rxms[i][j & (RXMQ_SLOTS - 1)]) {
- struct rxm * r;
- struct frct_cr * snd_cr;
- struct frct_cr * rcv_cr;
- size_t slot;
- size_t rslot;
- ssize_t idx;
- struct shm_du_buff * sdb;
- struct frct_pci * pci;
- struct flow * f;
- uint32_t snd_lwe;
- uint32_t rcv_lwe;
- size_t lvl = 0;
-
- r = list_entry(p, struct rxm, next);
-
- list_del(&r->next);
-
- snd_cr = &r->frcti->snd_cr;
- rcv_cr = &r->frcti->rcv_cr;
- f = &ai.flows[r->fd];
-#ifndef RXM_BUFFER_ON_HEAP
- shm_du_buff_ack(r->sdb);
-#endif
- if (f->frcti == NULL
- || f->info.id != r->flow_id)
- goto cleanup;
-
- pthread_rwlock_rdlock(&r->frcti->lock);
-
- snd_lwe = snd_cr->lwe;
- rcv_lwe = rcv_cr->lwe;
-
- pthread_rwlock_unlock(&r->frcti->lock);
-
- /* Has been ack'd, remove. */
- if (before(r->seqno, snd_lwe))
- goto cleanup;
-
- /* Check for r-timer expiry. */
- if (ts_to_ns(now) - r->t0 > r->frcti->r)
- goto flow_down;
-
- pthread_rwlock_wrlock(&r->frcti->lock);
-
- if (r->seqno == r->frcti->rttseq) {
- r->frcti->rto +=
- r->frcti->rto >> RTO_DIV;
- r->frcti->probe = false;
- }
-#ifdef PROC_FLOW_STATS
- r->frcti->n_rtx++;
-#endif
- rslot = r->frcti->rto >> RXMQ_RES;
-
- pthread_rwlock_unlock(&r->frcti->lock);
-
- /* Schedule at least in the next time slot. */
- slot = ts_to_ns(now) >> RXMQ_RES;
-
- while (rslot >= RXMQ_SLOTS) {
- ++lvl;
- rslot >>= RXMQ_BUMP;
- slot >>= RXMQ_BUMP;
- }
-
- if (lvl >= RXMQ_LVLS) /* Can't reschedule */
- goto flow_down;
-
- rslot = (rslot + slot + 1) & (RXMQ_SLOTS - 1);
-#ifdef RXM_BLOCKING
- if (ipcp_sdb_reserve(&sdb, r->len) < 0)
-#else
- if (shm_rdrbuff_alloc(ai.rdrb, r->len, NULL,
- &sdb) < 0)
-#endif
- goto reschedule; /* rdrbuff full */
-
- pci = (struct frct_pci *) shm_du_buff_head(sdb);
- memcpy(pci, r->pkt, r->len);
-#ifndef RXM_BUFFER_ON_HEAP
- ipcp_sdb_release(r->sdb);
- r->sdb = sdb;
- r->pkt = pci;
- shm_du_buff_wait_ack(sdb);
-#endif
- idx = shm_du_buff_get_idx(sdb);
-
- /* Retransmit the copy. */
- pci->ackno = hton32(rcv_lwe);
-#ifdef RXM_BLOCKING
- if (shm_rbuff_write_b(f->tx_rb, idx, NULL) < 0)
-#else
- if (shm_rbuff_write(f->tx_rb, idx) < 0)
-#endif
- goto flow_down;
- shm_flow_set_notify(f->set, f->info.id,
- FLOW_PKT);
- reschedule:
- list_add(&r->next, &rw.rxms[lvl][rslot]);
- continue;
-
- flow_down:
- shm_rbuff_set_acl(f->tx_rb, ACL_FLOWDOWN);
- shm_rbuff_set_acl(f->rx_rb, ACL_FLOWDOWN);
- cleanup:
-#ifdef RXM_BUFFER_ON_HEAP
- free(r->pkt);
-#else
- ipcp_sdb_release(r->sdb);
-#endif
- free(r);
- }
- }
- rw.prv_rxm[i] = rxm_slot & (RXMQ_SLOTS - 1);
- /* Move up a level in the wheel. */
- rxm_slot >>= RXMQ_BUMP;
- }
-
- ack_slot = ts_to_ack_slot(now) & (ACKQ_SLOTS - 1) ;
-
- j = rw.prv_ack;
-
- if (ack_slot < j)
- ack_slot += ACKQ_SLOTS;
-
- while (j++ < ack_slot) {
- list_for_each_safe(p, h, &rw.acks[j & (ACKQ_SLOTS - 1)]) {
- struct ack * a;
- struct flow * f;
-
- a = list_entry(p, struct ack, next);
-
- list_del(&a->next);
-
- f = &ai.flows[a->fd];
-
- rw.map[j & (ACKQ_SLOTS - 1)][a->fd] = false;
-
- if (f->info.id == a->flow_id && f->frcti != NULL)
- send_frct_pkt(a->frcti);
-
- free(a);
- }
- }
-
- rw.prv_ack = ack_slot & (ACKQ_SLOTS - 1);
-
- pthread_cleanup_pop(true);
-}
-
-static int timerwheel_rxm(struct frcti * frcti,
- uint32_t seqno,
- struct shm_du_buff * sdb)
-{
- struct timespec now;
- struct rxm * r;
- size_t slot;
- size_t lvl = 0;
- time_t rto_slot;
-
- r = malloc(sizeof(*r));
- if (r == NULL)
- return -ENOMEM;
-
- clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- r->t0 = ts_to_ns(now);
- r->seqno = seqno;
- r->frcti = frcti;
- r->len = shm_du_buff_len(sdb);
-#ifdef RXM_BUFFER_ON_HEAP
- r->pkt = malloc(r->len);
- if (r->pkt == NULL) {
- free(r);
- return -ENOMEM;
- }
- memcpy(r->pkt, shm_du_buff_head(sdb), r->len);
-#else
- r->sdb = sdb;
- r->pkt = (struct frct_pci *) shm_du_buff_head(sdb);
-#endif
- pthread_rwlock_rdlock(&r->frcti->lock);
-
- rto_slot = frcti->rto >> RXMQ_RES;
- slot = r->t0 >> RXMQ_RES;
-
- r->fd = frcti->fd;
- r->flow_id = ai.flows[r->fd].info.id;
-
- pthread_rwlock_unlock(&r->frcti->lock);
-
- while (rto_slot >= RXMQ_SLOTS) {
- ++lvl;
- rto_slot >>= RXMQ_BUMP;
- slot >>= RXMQ_BUMP;
- }
-
- if (lvl >= RXMQ_LVLS) { /* Out of timerwheel range. */
-#ifdef RXM_BUFFER_ON_HEAP
- free(r->pkt);
-#endif
- free(r);
- return -EPERM;
- }
-
- slot = (slot + rto_slot + 1) & (RXMQ_SLOTS - 1);
-
- pthread_mutex_lock(&rw.lock);
-
- list_add_tail(&r->next, &rw.rxms[lvl][slot]);
-#ifndef RXM_BUFFER_ON_HEAP
- shm_du_buff_wait_ack(sdb);
-#endif
- pthread_mutex_unlock(&rw.lock);
-
- return 0;
-}
-
-static int timerwheel_delayed_ack(int fd,
- struct frcti * frcti)
-{
- struct timespec now;
- struct ack * a;
- size_t slot;
-
- a = malloc(sizeof(*a));
- if (a == NULL)
- return -ENOMEM;
-
- clock_gettime(PTHREAD_COND_CLOCK, &now);
-
- pthread_rwlock_rdlock(&frcti->lock);
-
- slot = (((ts_to_ns(now) + (TICTIME << 1)) >> ACKQ_RES) + 1)
- & (ACKQ_SLOTS - 1);
-
- pthread_rwlock_unlock(&frcti->lock);
-
- a->fd = fd;
- a->frcti = frcti;
- a->flow_id = ai.flows[fd].info.id;
-
- pthread_mutex_lock(&rw.lock);
-
- if (rw.map[slot][fd]) {
- pthread_mutex_unlock(&rw.lock);
- free(a);
- return 0;
- }
-
- rw.map[slot][fd] = true;
-
- list_add_tail(&a->next, &rw.acks[slot]);
-
- pthread_mutex_unlock(&rw.lock);
-
- return 0;
-}
diff --git a/src/lib/tpm.c b/src/lib/tpm.c
index 7a17ef6b..9229ea1a 100644
--- a/src/lib/tpm.c
+++ b/src/lib/tpm.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Threadpool management
*
@@ -100,7 +100,7 @@ static void tpm_debug_thread(struct pthr_el * e)
if (BETWEEN(TPM_DEBUG_REPORT_INTERVAL, 0, intv)) {
log_dbg("Thread %d:%lx running for %ld s.\n",
- getpid(),e->thr, diff);
+ getpid(), (unsigned long) e->thr, diff);
e->last = now;
}
diff --git a/src/lib/tw.c b/src/lib/tw.c
new file mode 100644
index 00000000..ccde7dd1
--- /dev/null
+++ b/src/lib/tw.c
@@ -0,0 +1,307 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Generic deadline-ordered callback queue (timing wheel)
+ *
+ * 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__)
+#define _DEFAULT_SOURCE
+#else
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include "config.h"
+
+#include <ouroboros/list.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/time.h>
+#include <ouroboros/tw.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+/* 3 levels × 256 slots, 1 ms / 16 ms / 256 ms per-slot resolution. */
+#define TW_LVLS 3
+#define TW_SLOTS 256
+#define TW_BUMP 4
+#define TW_RES 20 /* 2^20 ns ≈ 1 ms per slot at level 0. */
+
+#define TW_SLOT(x) ((x) & (TW_SLOTS - 1))
+
+static struct {
+ struct list_head levels[TW_LVLS][TW_SLOTS];
+ size_t prv[TW_LVLS];
+ pthread_mutex_t mtx;
+ pthread_mutex_t move_mtx;
+ bool initialised;
+} tw;
+
+static size_t tw_lvl_res(size_t lvl)
+{
+ return TW_RES + TW_BUMP * lvl;
+}
+
+/* Smallest level whose slot range covers the deadline. */
+static size_t tw_pick_lvl(uint64_t now_ns,
+ uint64_t deadline_ns)
+{
+ uint64_t delta;
+ size_t lvl;
+
+ delta = deadline_ns > now_ns ? deadline_ns - now_ns : 0;
+ lvl = 0;
+
+ while (lvl < TW_LVLS - 1 && (delta >> tw_lvl_res(lvl)) >= TW_SLOTS)
+ ++lvl;
+
+ return lvl;
+}
+
+static size_t tw_slot(uint64_t ns,
+ size_t lvl)
+{
+ return TW_SLOT(ns >> tw_lvl_res(lvl));
+}
+
+int tw_init(void)
+{
+ struct timespec now;
+ size_t i;
+ size_t j;
+
+ assert(!tw.initialised);
+
+ if (pthread_mutex_init(&tw.mtx, NULL))
+ goto fail_mtx;
+
+ if (pthread_mutex_init(&tw.move_mtx, NULL))
+ goto fail_move_mtx;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ for (i = 0; i < TW_LVLS; ++i) {
+ tw.prv[i] = TW_SLOT(tw_slot(TS_TO_UINT64(now), i) - 1);
+ for (j = 0; j < TW_SLOTS; ++j)
+ list_head_init(&tw.levels[i][j]);
+ }
+
+ tw.initialised = true;
+
+ return 0;
+
+ fail_move_mtx:
+ pthread_mutex_destroy(&tw.mtx);
+ fail_mtx:
+ return -1;
+}
+
+void tw_fini(void)
+{
+ size_t i;
+ size_t j;
+
+ assert(tw.initialised);
+
+ for (i = 0; i < TW_LVLS; ++i) {
+ for (j = 0; j < TW_SLOTS; ++j)
+ assert(list_is_empty(&tw.levels[i][j]));
+ }
+
+ pthread_mutex_destroy(&tw.move_mtx);
+ pthread_mutex_destroy(&tw.mtx);
+
+ tw.initialised = false;
+}
+
+void tw_init_entry(struct tw_entry * e)
+{
+ list_head_init(&e->next);
+
+ e->deadline_ns = 0;
+ e->fire = NULL;
+ e->arg = NULL;
+ e->lvl = 0;
+}
+
+void tw_post(struct tw_entry * e,
+ uint64_t deadline_ns,
+ tw_fire_fn_t fire,
+ void * arg)
+{
+ struct timespec now;
+ size_t lvl;
+ size_t slot;
+
+ assert(tw.initialised);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ lvl = tw_pick_lvl(TS_TO_UINT64(now), deadline_ns);
+ /* +1 so deadline <= slot_start; lands later in slot. */
+ slot = TW_SLOT(tw_slot(deadline_ns, lvl) + 1);
+
+ e->deadline_ns = deadline_ns;
+ e->fire = fire;
+ e->arg = arg;
+ e->lvl = lvl;
+
+ pthread_mutex_lock(&tw.mtx);
+
+ if (!list_is_empty(&e->next))
+ list_del(&e->next);
+
+ list_add_tail(&e->next, &tw.levels[lvl][slot]);
+
+ pthread_mutex_unlock(&tw.mtx);
+}
+
+void tw_cancel(struct tw_entry * e)
+{
+ if (e == NULL)
+ return;
+
+ assert(tw.initialised);
+
+ pthread_mutex_lock(&tw.mtx);
+
+ if (!list_is_empty(&e->next)) {
+ list_del(&e->next);
+ list_head_init(&e->next);
+ }
+
+ pthread_mutex_unlock(&tw.mtx);
+}
+
+void tw_move(void)
+{
+ struct timespec now;
+ struct list_head deferred;
+ struct list_head * p;
+ uint64_t now_ns;
+ size_t i;
+ size_t j;
+ size_t cur;
+
+ assert(tw.initialised);
+
+ if (pthread_mutex_trylock(&tw.move_mtx) != 0)
+ return;
+
+ pthread_cleanup_push(__cleanup_mutex_unlock, &tw.move_mtx);
+
+ pthread_mutex_lock(&tw.mtx);
+
+ pthread_cleanup_push(__cleanup_mutex_unlock, &tw.mtx);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ for (i = 0; i < TW_LVLS; ++i) {
+ cur = tw_slot(now_ns, i);
+
+ j = tw.prv[i];
+ if (cur < j)
+ cur += TW_SLOTS;
+
+ while (j++ < cur) {
+ size_t s = TW_SLOT(j);
+
+ /* Pop-front so fire may mutate any entry. */
+ list_head_init(&deferred);
+
+ while (!list_is_empty(&tw.levels[i][s])) {
+ struct tw_entry * e;
+ p = tw.levels[i][s].nxt;
+ e = list_entry(p, struct tw_entry, next);
+ list_del(&e->next);
+
+ if (e->deadline_ns > now_ns) {
+ list_add_tail(&e->next, &deferred);
+ continue;
+ }
+
+ pthread_mutex_unlock(&tw.mtx);
+ e->fire(e->arg);
+ pthread_mutex_lock(&tw.mtx);
+ }
+
+ while (!list_is_empty(&deferred)) {
+ p = deferred.nxt;
+ list_del(p);
+ list_add_tail(p, &tw.levels[i][s]);
+ }
+ }
+
+ tw.prv[i] = TW_SLOT(cur);
+ }
+
+ pthread_cleanup_pop(true); /* tw.mtx */
+ pthread_cleanup_pop(true); /* tw.move_mtx */
+}
+
+/* Earliest pending deadline at level lvl, INT64_MAX if level is empty. */
+static int64_t tw_lvl_earliest(size_t lvl,
+ uint64_t now_ns)
+{
+ size_t cur = tw_slot(now_ns, lvl);
+ size_t j;
+
+ for (j = 1; j <= TW_SLOTS; ++j) {
+ size_t s = TW_SLOT(cur + j);
+
+ if (list_is_empty(&tw.levels[lvl][s]))
+ continue;
+
+ return (int64_t)(now_ns + ((uint64_t) j << tw_lvl_res(lvl)));
+ }
+
+ return INT64_MAX;
+}
+
+void tw_next_expiry(struct timespec * out)
+{
+ struct timespec now;
+ uint64_t now_ns;
+ int64_t earliest = INT64_MAX;
+ size_t i;
+
+ assert(tw.initialised);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+ now_ns = TS_TO_UINT64(now);
+
+ pthread_mutex_lock(&tw.mtx);
+
+ for (i = 0; i < TW_LVLS; ++i) {
+ int64_t dl = tw_lvl_earliest(i, now_ns);
+ if (dl < earliest)
+ earliest = dl;
+ }
+
+ pthread_mutex_unlock(&tw.mtx);
+
+ if (earliest == INT64_MAX) {
+ /* Empty wheel: tv_nsec=-1 is an invalid normalised value. */
+ out->tv_sec = 0;
+ out->tv_nsec = -1;
+ } else {
+ UINT64_TO_TS((uint64_t) earliest, out);
+ }
+}
diff --git a/src/lib/utils.c b/src/lib/utils.c
index fd275f63..6b49cc00 100644
--- a/src/lib/utils.c
+++ b/src/lib/utils.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handy utilities
*
@@ -20,10 +20,15 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
-#define _POSIX_C_SOURCE 200809L
+#define _DEFAULT_SOURCE
+
+#include "config.h"
#include <ouroboros/utils.h>
+#include <ctype.h>
+#include <grp.h>
+#include <pwd.h>
#include <stdlib.h>
#include <string.h>
@@ -67,6 +72,24 @@ char * path_strip(const char * src)
return dst;
}
+char * trim_whitespace(char * str)
+{
+ char * end;
+
+ while (isspace((unsigned char) *str))
+ str++;
+
+ if (*str == '\0')
+ return str;
+
+ /* Trim trailing space */
+ end = str + strlen(str) - 1;
+ while (end > str && isspace((unsigned char)*end))
+ *end-- = '\0';
+
+ return str;
+}
+
size_t argvlen(const char ** argv)
{
size_t argc = 0;
@@ -119,5 +142,72 @@ char ** argvdup(char ** argv)
}
argv_dup[argc] = NULL;
+
return argv_dup;
}
+
+bool is_ouroboros_member_uid(uid_t uid)
+{
+ struct group * grp;
+ struct passwd * pw;
+#ifdef __APPLE__
+ unsigned int gid;
+ int * groups = NULL;
+#else
+ gid_t gid;
+ gid_t * groups = NULL;
+#endif
+ int ngroups;
+ int i;
+
+ /* Root is always privileged */
+ if (uid == 0)
+ return true;
+
+ grp = getgrnam("ouroboros");
+ if (grp == NULL)
+ return false;
+
+ gid = grp->gr_gid;
+
+ pw = getpwuid(uid);
+ if (pw == NULL)
+ return false;
+
+ if (pw->pw_gid == gid)
+ return true;
+
+ ngroups = 0;
+ getgrouplist(pw->pw_name, pw->pw_gid, NULL, &ngroups);
+ if (ngroups <= 0)
+ return false;
+
+ groups = malloc(ngroups * sizeof(*groups));
+ if (groups == NULL)
+ return false;
+
+ if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) < 0) {
+ free(groups);
+ return false;
+ }
+
+ for (i = 0; i < ngroups; i++) {
+#ifdef __APPLE__
+ if (groups[i] == (int) gid) {
+#else
+ if (groups[i] == gid) {
+#endif
+ free(groups);
+ return true;
+ }
+ }
+
+ free(groups);
+
+ return false;
+}
+
+bool is_ouroboros_member(void)
+{
+ return is_ouroboros_member_uid(getuid());
+}
diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt
index 7c40d9ae..6b418838 100644
--- a/src/tools/CMakeLists.txt
+++ b/src/tools/CMakeLists.txt
@@ -1,9 +1,76 @@
-add_subdirectory(irm)
-add_subdirectory(ocbr)
-add_subdirectory(oecho)
-add_subdirectory(obc)
-add_subdirectory(oping)
-add_subdirectory(operf)
-if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
- add_subdirectory(ovpn)
-endif ()
+# Tools build configuration
+
+set(TOOLS_INCLUDE_DIRS
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+)
+
+set(IRM_SOURCES
+ irm/irm.c
+ irm/irm_bind_program.c
+ irm/irm_bind_process.c
+ irm/irm_bind_ipcp.c
+ irm/irm_ipcp_create.c
+ irm/irm_ipcp_destroy.c
+ irm/irm_ipcp_bootstrap.c
+ irm/irm_ipcp_enroll.c
+ irm/irm_ipcp_list.c
+ irm/irm_ipcp_connect.c
+ irm/irm_ipcp_disconnect.c
+ irm/irm_unbind_program.c
+ irm/irm_unbind_process.c
+ irm/irm_unbind_ipcp.c
+ irm/irm_unbind.c
+ irm/irm_bind.c
+ irm/irm_ipcp.c
+ irm/irm_name.c
+ irm/irm_name_create.c
+ irm/irm_name_destroy.c
+ irm/irm_name_reg.c
+ irm/irm_name_unreg.c
+ irm/irm_name_list.c
+ irm/irm_utils.c
+)
+
+add_executable(irm ${IRM_SOURCES})
+target_include_directories(irm PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(irm PRIVATE ouroboros-irm)
+install(TARGETS irm RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
+
+add_executable(oping oping/oping.c)
+target_include_directories(oping PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(oping PRIVATE ${LIBM_LIBRARIES} ouroboros-dev)
+install(TARGETS oping RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(oecho oecho/oecho.c)
+target_include_directories(oecho PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(oecho PRIVATE ouroboros-dev)
+install(TARGETS oecho RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(ocbr ocbr/ocbr.c)
+target_include_directories(ocbr PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(ocbr PRIVATE ouroboros-dev)
+install(TARGETS ocbr RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(obc obc/obc.c)
+target_include_directories(obc PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(obc PRIVATE ouroboros-dev)
+install(TARGETS obc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(operf operf/operf.c)
+target_include_directories(operf PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(operf PRIVATE ouroboros-dev)
+install(TARGETS operf RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(oftp oftp/oftp.c)
+target_include_directories(oftp PRIVATE ${TOOLS_INCLUDE_DIRS})
+target_link_libraries(oftp PRIVATE ouroboros-dev)
+install(TARGETS oftp RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ add_executable(ovpn ovpn/ovpn.c)
+ target_include_directories(ovpn PRIVATE ${TOOLS_INCLUDE_DIRS})
+ target_link_libraries(ovpn PRIVATE ouroboros-dev)
+ install(TARGETS ovpn RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+endif()
diff --git a/src/tools/irm/CMakeLists.txt b/src/tools/irm/CMakeLists.txt
deleted file mode 100644
index 3c599300..00000000
--- a/src/tools/irm/CMakeLists.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(TOOLS_IRM_SOURCE_FILES
- # Add source files here
- irm.c
- irm_bind_program.c
- irm_bind_process.c
- irm_bind_ipcp.c
- irm_ipcp_create.c
- irm_ipcp_destroy.c
- irm_ipcp_bootstrap.c
- irm_ipcp_enroll.c
- irm_ipcp_list.c
- irm_ipcp_connect.c
- irm_ipcp_disconnect.c
- irm_unbind_program.c
- irm_unbind_process.c
- irm_unbind_ipcp.c
- irm_unbind.c
- irm_bind.c
- irm_ipcp.c
- irm_name.c
- irm_name_create.c
- irm_name_destroy.c
- irm_name_reg.c
- irm_name_unreg.c
- irm_name_list.c
- irm_utils.c
- )
-
-add_executable(irm ${TOOLS_IRM_SOURCE_FILES})
-
-target_link_libraries(irm LINK_PUBLIC ouroboros-irm)
-
-install(TARGETS irm RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
-
-# enable when we have tests
-# if(BUILD_TESTS)
-# add_subdirectory(tests)
-# endif ()
diff --git a/src/tools/irm/irm.c b/src/tools/irm/irm.c
index ba0f4713..6c2719d0 100644
--- a/src/tools/irm/irm.c
+++ b/src/tools/irm/irm.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A tool to instruct the IRM daemon
*
diff --git a/src/tools/irm/irm_bind.c b/src/tools/irm/irm_bind.c
index 2e8b14ef..3107837a 100644
--- a/src/tools/irm/irm_bind.c
+++ b/src/tools/irm/irm_bind.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bind names in the processing system
*
diff --git a/src/tools/irm/irm_bind_ipcp.c b/src/tools/irm/irm_bind_ipcp.c
index 7d5dd636..4c183534 100644
--- a/src/tools/irm/irm_bind_ipcp.c
+++ b/src/tools/irm/irm_bind_ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bind IPCP Instance to a name
*
diff --git a/src/tools/irm/irm_bind_process.c b/src/tools/irm/irm_bind_process.c
index fffd5fe9..fee0c46b 100644
--- a/src/tools/irm/irm_bind_process.c
+++ b/src/tools/irm/irm_bind_process.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bind a process to a name
*
diff --git a/src/tools/irm/irm_bind_program.c b/src/tools/irm/irm_bind_program.c
index 8a0dc33c..14d09db7 100644
--- a/src/tools/irm/irm_bind_program.c
+++ b/src/tools/irm/irm_bind_program.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bind programs to a name
*
diff --git a/src/tools/irm/irm_ipcp.c b/src/tools/irm/irm_ipcp.c
index 63e617d9..34458a20 100644
--- a/src/tools/irm/irm_ipcp.c
+++ b/src/tools/irm/irm_ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A tool to instruct the IRM daemon
*
diff --git a/src/tools/irm/irm_ipcp_bootstrap.c b/src/tools/irm/irm_ipcp_bootstrap.c
index 3fabc3cc..de73b076 100644
--- a/src/tools/irm/irm_ipcp_bootstrap.c
+++ b/src/tools/irm/irm_ipcp_bootstrap.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Bootstrap IPC Processes
*
diff --git a/src/tools/irm/irm_ipcp_connect.c b/src/tools/irm/irm_ipcp_connect.c
index 68e13bd0..fb21faec 100644
--- a/src/tools/irm/irm_ipcp_connect.c
+++ b/src/tools/irm/irm_ipcp_connect.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Connect components of unicast or broadcast IPC processes
*
@@ -100,16 +100,18 @@ int do_connect_ipcp(int argc,
}
if (qos != NULL) {
- if (strcmp(qos, "best") == 0)
- qs = qos_best_effort;
- else if (strcmp(qos, "raw") == 0)
+ if (strcmp(qos, "raw") == 0)
qs = qos_raw;
- else if (strcmp(qos, "video") == 0)
- qs = qos_video;
- else if (strcmp(qos, "voice") == 0)
- qs = qos_voice;
- else if (strcmp(qos, "data") == 0)
- qs = qos_data;
+ else if (strcmp(qos, "safe") == 0)
+ qs = qos_raw_safe;
+ else if (strcmp(qos, "rt") == 0)
+ qs = qos_rt;
+ else if (strcmp(qos, "rt-safe") == 0)
+ qs = qos_rt_safe;
+ else if (strcmp(qos, "msg") == 0)
+ qs = qos_msg;
+ else if (strcmp(qos, "stream") == 0)
+ qs = qos_stream;
else
printf("Unknown QoS cube, defaulting to raw.\n");
}
@@ -126,7 +128,7 @@ int do_connect_ipcp(int argc,
if (wildcard_match(comp, MGMT) == 0) {
component = MGMT_COMP;
- /* FIXME: move to qos_data when stable */
+ /* FIXME: move to qos_msg when stable */
if (irm_connect_ipcp(pid, dst, component, qos_raw))
return -1;
}
diff --git a/src/tools/irm/irm_ipcp_create.c b/src/tools/irm/irm_ipcp_create.c
index e2a5c488..c6b2074b 100644
--- a/src/tools/irm/irm_ipcp_create.c
+++ b/src/tools/irm/irm_ipcp_create.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Create IPC Processes
*
diff --git a/src/tools/irm/irm_ipcp_destroy.c b/src/tools/irm/irm_ipcp_destroy.c
index 1a5e564e..523836af 100644
--- a/src/tools/irm/irm_ipcp_destroy.c
+++ b/src/tools/irm/irm_ipcp_destroy.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Destroy IPC Processes
*
diff --git a/src/tools/irm/irm_ipcp_disconnect.c b/src/tools/irm/irm_ipcp_disconnect.c
index 7ce724e1..0f37ec91 100644
--- a/src/tools/irm/irm_ipcp_disconnect.c
+++ b/src/tools/irm/irm_ipcp_disconnect.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Connect components of unicast or broadcast IPC processes
*
diff --git a/src/tools/irm/irm_ipcp_enroll.c b/src/tools/irm/irm_ipcp_enroll.c
index 86a22a71..350b536e 100644
--- a/src/tools/irm/irm_ipcp_enroll.c
+++ b/src/tools/irm/irm_ipcp_enroll.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Enroll IPC Processes
*
diff --git a/src/tools/irm/irm_ipcp_list.c b/src/tools/irm/irm_ipcp_list.c
index 54985eb4..a211a02b 100644
--- a/src/tools/irm/irm_ipcp_list.c
+++ b/src/tools/irm/irm_ipcp_list.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* List IPC Processes
*
diff --git a/src/tools/irm/irm_name.c b/src/tools/irm/irm_name.c
index d60b6c78..830ae305 100644
--- a/src/tools/irm/irm_name.c
+++ b/src/tools/irm/irm_name.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A tool to instruct the IRM daemon
*
diff --git a/src/tools/irm/irm_name_create.c b/src/tools/irm/irm_name_create.c
index 22341d2e..40a51193 100644
--- a/src/tools/irm/irm_name_create.c
+++ b/src/tools/irm/irm_name_create.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Create IPC Processes
*
@@ -51,10 +51,10 @@
#define RR "round-robin"
#define SPILL "spillover"
-#define SENC "<security_dir>/server/<name>/enc.cfg"
+#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.cfg"
+#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;
@@ -119,14 +119,14 @@ int do_create_name(int argc,
while (argc > 0) {
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 +151,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 +160,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_destroy.c b/src/tools/irm/irm_name_destroy.c
index d4bd6c82..d5ed05d5 100644
--- a/src/tools/irm/irm_name_destroy.c
+++ b/src/tools/irm/irm_name_destroy.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Create IPC Processes
*
diff --git a/src/tools/irm/irm_name_list.c b/src/tools/irm/irm_name_list.c
index a807008c..37e1f023 100644
--- a/src/tools/irm/irm_name_list.c
+++ b/src/tools/irm/irm_name_list.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* List names
*
diff --git a/src/tools/irm/irm_name_reg.c b/src/tools/irm/irm_name_reg.c
index 7689119a..860f4a70 100644
--- a/src/tools/irm/irm_name_reg.c
+++ b/src/tools/irm/irm_name_reg.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Register names with IPCPs
*
diff --git a/src/tools/irm/irm_name_unreg.c b/src/tools/irm/irm_name_unreg.c
index 6e579f04..abf08548 100644
--- a/src/tools/irm/irm_name_unreg.c
+++ b/src/tools/irm/irm_name_unreg.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unregister names from IPCPs
*
diff --git a/src/tools/irm/irm_ops.h b/src/tools/irm/irm_ops.h
index e04ffc02..195c5cbc 100644
--- a/src/tools/irm/irm_ops.h
+++ b/src/tools/irm/irm_ops.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Functions of the IRM tool that are one level deep
*
diff --git a/src/tools/irm/irm_unbind.c b/src/tools/irm/irm_unbind.c
index d6594d01..4e5914a9 100644
--- a/src/tools/irm/irm_unbind.c
+++ b/src/tools/irm/irm_unbind.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unbind names in the processing system
*
diff --git a/src/tools/irm/irm_unbind_ipcp.c b/src/tools/irm/irm_unbind_ipcp.c
index 53a2d16c..23e25057 100644
--- a/src/tools/irm/irm_unbind_ipcp.c
+++ b/src/tools/irm/irm_unbind_ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unbind name from IPCP Instance
*
diff --git a/src/tools/irm/irm_unbind_process.c b/src/tools/irm/irm_unbind_process.c
index 264ed538..bc7e545c 100644
--- a/src/tools/irm/irm_unbind_process.c
+++ b/src/tools/irm/irm_unbind_process.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unbind process names
*
diff --git a/src/tools/irm/irm_unbind_program.c b/src/tools/irm/irm_unbind_program.c
index 0c751e80..031b9909 100644
--- a/src/tools/irm/irm_unbind_program.c
+++ b/src/tools/irm/irm_unbind_program.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Unbind programs
*
diff --git a/src/tools/irm/irm_utils.c b/src/tools/irm/irm_utils.c
index 9694d647..69873097 100644
--- a/src/tools/irm/irm_utils.c
+++ b/src/tools/irm/irm_utils.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handy helper functions for the IRM tool
*
diff --git a/src/tools/irm/irm_utils.h b/src/tools/irm/irm_utils.h
index 27a0b941..c6d4bf18 100644
--- a/src/tools/irm/irm_utils.h
+++ b/src/tools/irm/irm_utils.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Handy helper functions for the IRM tool
*
diff --git a/src/tools/obc/CMakeLists.txt b/src/tools/obc/CMakeLists.txt
deleted file mode 100644
index db5e999b..00000000
--- a/src/tools/obc/CMakeLists.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(SOURCE_FILES
- # Add source files here
- obc.c
- )
-
-add_executable(obc ${SOURCE_FILES})
-
-target_link_libraries(obc LINK_PUBLIC ouroboros-dev)
-
-install(TARGETS obc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/obc/obc.c b/src/tools/obc/obc.c
index 778eb8a8..5b8470f0 100644
--- a/src/tools/obc/obc.c
+++ b/src/tools/obc/obc.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A simple broadcast application
*
diff --git a/src/tools/ocbr/CMakeLists.txt b/src/tools/ocbr/CMakeLists.txt
deleted file mode 100644
index f7ba66cd..00000000
--- a/src/tools/ocbr/CMakeLists.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-
-set(SOURCE_FILES
- # Add source files here
- ocbr.c
- )
-
-add_executable(ocbr ${SOURCE_FILES})
-
-target_link_libraries(ocbr LINK_PUBLIC ouroboros-dev)
-
-install(TARGETS ocbr RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/ocbr/ocbr.c b/src/tools/ocbr/ocbr.c
index 775bcaac..c92ba0e0 100644
--- a/src/tools/ocbr/ocbr.c
+++ b/src/tools/ocbr/ocbr.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* CBR traffic generator
*
diff --git a/src/tools/ocbr/ocbr_client.c b/src/tools/ocbr/ocbr_client.c
index eada6e60..36c07d43 100644
--- a/src/tools/ocbr/ocbr_client.c
+++ b/src/tools/ocbr/ocbr_client.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A simple CBR generator
*
@@ -37,8 +37,11 @@
*/
#include <ouroboros/dev.h>
+#include <ouroboros/qos.h>
#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
volatile bool stop;
@@ -86,6 +89,11 @@ int client_main(char * server,
struct timespec end;
struct timespec intv = {(gap / BILLION), gap % BILLION};
int ms;
+ const char * qenv;
+ qosspec_t qs;
+ qosspec_t * qsp;
+
+ qsp = NULL;
stop = false;
@@ -98,16 +106,38 @@ int client_main(char * server,
sigaction(SIGHUP, &sig_act, NULL) ||
sigaction(SIGPIPE, &sig_act, NULL)) {
printf("Failed to install sighandler.\n");
- return -1;
+ return 2;
}
printf("Client started, duration %d, rate %lu b/s, size %d B.\n",
duration, rate, size);
- fd = flow_alloc(server, NULL, NULL);
+ qenv = getenv("OCBR_QOS");
+ if (qenv != NULL) {
+ if (strcmp(qenv, "raw") == 0)
+ qs = qos_raw;
+ else if (strcmp(qenv, "safe") == 0)
+ qs = qos_raw_safe;
+ else if (strcmp(qenv, "rt") == 0)
+ qs = qos_rt;
+ else if (strcmp(qenv, "rt_safe") == 0)
+ qs = qos_rt_safe;
+ else if (strcmp(qenv, "msg") == 0)
+ qs = qos_msg;
+ else if (strcmp(qenv, "stream") == 0)
+ qs = qos_stream;
+ else {
+ fprintf(stderr,
+ "Unknown OCBR_QOS='%s', using raw.\n", qenv);
+ qs = qos_raw;
+ }
+ qsp = &qs;
+ printf("OCBR_QOS=%s\n", qenv);
+ }
+ fd = flow_alloc(server, qsp, NULL);
if (fd < 0) {
printf("Failed to allocate flow.\n");
- return -1;
+ return 2;
}
clock_gettime(CLOCK_REALTIME, &start);
diff --git a/src/tools/ocbr/ocbr_server.c b/src/tools/ocbr/ocbr_server.c
index 34c4fa94..f9f7e3c8 100644
--- a/src/tools/ocbr/ocbr_server.c
+++ b/src/tools/ocbr/ocbr_server.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A simple CBR generator
*
@@ -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/oecho/CMakeLists.txt b/src/tools/oecho/CMakeLists.txt
deleted file mode 100644
index 50a66138..00000000
--- a/src/tools/oecho/CMakeLists.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-set(SOURCE_FILES
- # Add source files here
- oecho.c
- )
-
-add_executable(oecho ${SOURCE_FILES})
-
-target_link_libraries(oecho LINK_PUBLIC ouroboros-dev)
-
-install(TARGETS oecho RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/oecho/oecho.c b/src/tools/oecho/oecho.c
index d5d03027..ef0a168f 100644
--- a/src/tools/oecho/oecho.c
+++ b/src/tools/oecho/oecho.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* A simple echo application
*
@@ -101,20 +101,20 @@ static int client_main(void)
fd = flow_alloc("oecho", NULL, NULL);
if (fd < 0) {
printf("Failed to allocate flow.\n");
- return -1;
+ return 2;
}
if (flow_write(fd, message, strlen(message) + 1) < 0) {
printf("Failed to write packet.\n");
flow_dealloc(fd);
- return -1;
+ return 1;
}
count = flow_read(fd, buf, BUF_SIZE);
if (count < 0) {
printf("Failed to read packet.\n");
flow_dealloc(fd);
- return -1;
+ return 1;
}
printf("Server replied with %.*s\n", (int) count, buf);
@@ -126,7 +126,7 @@ static int client_main(void)
int main(int argc, char ** argv)
{
- int ret = -1;
+ int ret = 0;
bool server = false;
argc--;
diff --git a/src/tools/oftp/oftp.c b/src/tools/oftp/oftp.c
new file mode 100644
index 00000000..2504393a
--- /dev/null
+++ b/src/tools/oftp/oftp.c
@@ -0,0 +1,445 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * A minimal file-transfer tool over an FRCT stream flow
+ *
+ * Dimitri Staessens <dimitri@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.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <ouroboros/crc64.h>
+#include <ouroboros/dev.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/fccntl.h>
+#include <ouroboros/qos.h>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define BUF_SIZE 16384
+
+static volatile sig_atomic_t stop = 0;
+
+static void apply_rto_min_env(int fd)
+{
+ const char * env;
+ long v;
+
+ 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);
+}
+
+static void apply_stream_ring_sz_env(int fd)
+{
+ const char * env;
+ long v;
+
+ 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);
+}
+
+static void on_signal(int signo)
+{
+ (void) signo;
+ stop = 1;
+}
+
+static void usage(void)
+{
+ printf("Usage: oftp [OPTION]...\n"
+ "Stream-mode file transfer over an Ouroboros flow.\n\n"
+ " -l, --listen Run as the receiver (server)\n"
+ " -n, --name NAME Destination service name (client)\n"
+ " -i, --in FILE Read input from FILE (default stdin)\n"
+ " -o, --out FILE Write output to FILE (default stdout)\n"
+ " -N, --bytes SIZE Stop after SIZE bytes "
+ "(K/M/G suffix; client only)\n"
+ " --help Display this help text and exit\n");
+}
+
+static int parse_size(const char * s, size_t * out)
+{
+ char * end;
+ unsigned long v;
+ size_t mul;
+
+ v = strtoul(s, &end, 0);
+ if (end == s)
+ return -1;
+
+ mul = 1;
+ if (*end == 'k' || *end == 'K')
+ mul = 1024UL;
+ else if (*end == 'm' || *end == 'M')
+ mul = 1024UL * 1024UL;
+ else if (*end == 'g' || *end == 'G')
+ mul = 1024UL * 1024UL * 1024UL;
+ else if (*end != '\0')
+ return -1;
+
+ *out = (size_t) v * mul;
+ return 0;
+}
+
+static void report_xfer(const char * tag,
+ size_t total,
+ uint64_t crc,
+ const struct timespec * t0,
+ const struct timespec * t1)
+{
+ double elapsed_s;
+ double mib_per_s;
+
+ elapsed_s = (t1->tv_sec - t0->tv_sec)
+ + (t1->tv_nsec - t0->tv_nsec) / 1e9;
+ if (elapsed_s <= 0.0)
+ elapsed_s = 1e-9;
+
+ mib_per_s = ((double) total / (1024.0 * 1024.0)) / elapsed_s;
+
+ fprintf(stderr,
+ "oftp: %s %zu bytes in %.3f s (%.2f MiB/s) "
+ "crc64=%016" PRIx64 "\n",
+ tag, total, elapsed_s, mib_per_s, crc);
+}
+
+static int xfer_to_flow(int fd, FILE * in, size_t max_bytes)
+{
+ char buf[BUF_SIZE];
+ size_t n;
+ size_t total;
+ size_t want;
+ size_t off;
+ ssize_t w;
+ uint64_t crc;
+ struct timespec t0;
+ struct timespec t1;
+
+ total = 0;
+ crc = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+
+ while (!stop) {
+ want = sizeof(buf);
+ if (max_bytes > 0 && max_bytes - total < want)
+ want = max_bytes - total;
+ if (want == 0)
+ break;
+
+ n = fread(buf, 1, want, in);
+ if (n == 0)
+ break;
+
+ crc64_nvme(&crc, buf, n);
+
+ off = 0;
+ while (off < n) {
+ w = flow_write(fd, buf + off, n - off);
+ if (w < 0) {
+ fprintf(stderr,
+ "flow_write failed: %zd\n", w);
+ return 1;
+ }
+ off += (size_t) w;
+ total += (size_t) w;
+ }
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &t1);
+
+ if (ferror(in)) {
+ fprintf(stderr, "Input read error.\n");
+ return 1;
+ }
+
+ report_xfer("sent", total, crc, &t0, &t1);
+ return 0;
+}
+
+static int xfer_from_flow(int fd, FILE * out)
+{
+ char buf[BUF_SIZE];
+ size_t total;
+ ssize_t n;
+ uint64_t crc;
+ struct timespec timeout;
+ struct timespec t0;
+ struct timespec t1;
+ bool started;
+
+ total = 0;
+ crc = 0;
+ started = false;
+ timeout.tv_sec = 1;
+ timeout.tv_nsec = 0;
+
+ /* Short timeout so SIGTERM/SIGINT 'stop' is observed promptly. */
+ fccntl(fd, FLOWSRCVTIMEO, &timeout);
+
+ while (!stop) {
+ n = flow_read(fd, buf, sizeof(buf));
+ if (n == 0) {
+ /* Clean EOF: peer sent EOS and we drained it. */
+ clock_gettime(CLOCK_MONOTONIC, &t1);
+ fflush(out);
+ if (!started)
+ t0 = t1;
+ report_xfer("received", total, crc, &t0, &t1);
+ return 0;
+ }
+ if (n == -ETIMEDOUT)
+ continue;
+ if (n < 0) {
+ /* Peer aborted before EOS: partial transfer. */
+ if (n == -EFLOWDOWN || n == -EFLOWPEER) {
+ fprintf(stderr,
+ "oftp: peer aborted at %zu B\n",
+ total);
+ return 2;
+ }
+ fprintf(stderr,
+ "flow_read failed: %zd\n", n);
+ return 1;
+ }
+ if (!started) {
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+ started = true;
+ }
+ crc64_nvme(&crc, buf, (size_t) n);
+ if (fwrite(buf, 1, (size_t) n, out) != (size_t) n) {
+ fprintf(stderr, "Output write error.\n");
+ return 1;
+ }
+ total += (size_t) n;
+ }
+
+ /* Receiver was signalled (SIGINT/SIGTERM) before EOF. */
+ fflush(out);
+ fprintf(stderr, "oftp: interrupted at %zu B\n", total);
+ return 2;
+}
+
+static int server_main(const char * outpath)
+{
+ FILE * out = stdout;
+ int fd;
+ int ofd;
+ int rc;
+ qosspec_t qs;
+
+ if (outpath != NULL) {
+ ofd = open(outpath,
+ O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW,
+ 0600);
+ if (ofd < 0) {
+ perror("open");
+ return 1;
+ }
+ out = fdopen(ofd, "wb");
+ if (out == NULL) {
+ perror("fdopen");
+ close(ofd);
+ unlink(outpath);
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "oftp: listening...\n");
+
+ fd = flow_accept(&qs, NULL);
+ if (fd < 0) {
+ fprintf(stderr, "flow_accept failed: %d\n", fd);
+ if (outpath != NULL)
+ fclose(out);
+ return 1;
+ }
+
+ if (qs.service != SVC_STREAM) {
+ fprintf(stderr,
+ "oftp: rejecting non-stream flow (service=%u)\n",
+ qs.service);
+ flow_dealloc(fd);
+ if (outpath != NULL) {
+ fclose(out);
+ unlink(outpath);
+ }
+ return 1;
+ }
+
+ apply_rto_min_env(fd);
+ apply_stream_ring_sz_env(fd);
+
+ rc = xfer_from_flow(fd, out);
+
+ flow_dealloc(fd);
+
+ if (outpath != NULL) {
+ fclose(out);
+ /* Drop the half-written file on abort/interrupt. */
+ if (rc != 0)
+ unlink(outpath);
+ }
+
+ return rc;
+}
+
+static int client_main(const char * name,
+ const char * inpath,
+ size_t max_bytes)
+{
+ FILE * in;
+ int fd;
+ int rc;
+ qosspec_t qs;
+
+ in = stdin;
+ qs = qos_stream;
+
+ if (inpath != NULL) {
+ in = fopen(inpath, "rb");
+ if (in == NULL) {
+ perror("fopen");
+ return 1;
+ }
+ }
+
+ fd = flow_alloc(name, &qs, NULL);
+ if (fd < 0) {
+ fprintf(stderr, "flow_alloc failed: %d\n", fd);
+ if (inpath != NULL)
+ fclose(in);
+ return 2;
+ }
+
+ apply_rto_min_env(fd);
+ apply_stream_ring_sz_env(fd);
+
+ rc = xfer_to_flow(fd, in, max_bytes);
+
+ flow_dealloc(fd);
+
+ if (inpath != NULL)
+ fclose(in);
+
+ return rc;
+}
+
+int main(int argc, char ** argv)
+{
+ bool server;
+ const char * name;
+ const char * inpath;
+ const char * outpath;
+ size_t max_bytes;
+ struct sigaction sa;
+
+ server = false;
+ name = NULL;
+ inpath = NULL;
+ outpath = NULL;
+ max_bytes = 0;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = on_signal;
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+ signal(SIGPIPE, SIG_IGN);
+
+ argc--; argv++;
+ while (argc > 0) {
+ if (strcmp(*argv, "-l") == 0 ||
+ strcmp(*argv, "--listen") == 0) {
+ server = true;
+ } else if ((strcmp(*argv, "-n") == 0 ||
+ strcmp(*argv, "--name") == 0) && argc > 1) {
+ name = *(++argv); argc--;
+ } else if ((strcmp(*argv, "-i") == 0 ||
+ strcmp(*argv, "--in") == 0) && argc > 1) {
+ inpath = *(++argv); argc--;
+ } else if ((strcmp(*argv, "-o") == 0 ||
+ strcmp(*argv, "--out") == 0) && argc > 1) {
+ outpath = *(++argv); argc--;
+ } else if ((strcmp(*argv, "-N") == 0 ||
+ strcmp(*argv, "--bytes") == 0) && argc > 1) {
+ if (parse_size(*(++argv), &max_bytes) < 0) {
+ fprintf(stderr,
+ "oftp: bad size '%s'\n", *argv);
+ return 1;
+ }
+ argc--;
+ } else if (strcmp(*argv, "--help") == 0) {
+ usage();
+ return 0;
+ } else {
+ usage();
+ return 1;
+ }
+ argc--; argv++;
+ }
+
+ if (server)
+ return server_main(outpath);
+
+ if (name == NULL) {
+ usage();
+ return 1;
+ }
+
+ return client_main(name, inpath, max_bytes);
+}
diff --git a/src/tools/operf/CMakeLists.txt b/src/tools/operf/CMakeLists.txt
deleted file mode 100644
index b6faf04e..00000000
--- a/src/tools/operf/CMakeLists.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-
-find_library(LIBM_LIBRARIES m)
-if(NOT LIBM_LIBRARIES)
- message(FATAL_ERROR "libm not found")
-endif()
-
-set(SOURCE_FILES
- # Add source files here
- operf.c
- )
-
-add_executable(operf ${SOURCE_FILES})
-
-target_link_libraries(operf LINK_PUBLIC ${LIBM_LIBRARIES} ouroboros-dev)
-
-install(TARGETS operf RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/operf/operf.c b/src/tools/operf/operf.c
index 10896bd5..0198e871 100644
--- a/src/tools/operf/operf.c
+++ b/src/tools/operf/operf.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros perf application
*
@@ -54,7 +54,6 @@
#include <stdlib.h>
#include <sys/time.h>
#include <arpa/inet.h>
-#include <math.h>
#include <errno.h>
#include <float.h>
@@ -249,5 +248,5 @@ int main(int argc, char ** argv)
if (ret < 0)
exit(EXIT_FAILURE);
- exit(EXIT_SUCCESS);
+ exit(ret);
}
diff --git a/src/tools/operf/operf_client.c b/src/tools/operf/operf_client.c
index 7060ce5b..e478aeff 100644
--- a/src/tools/operf/operf_client.c
+++ b/src/tools/operf/operf_client.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros ping application
*
@@ -185,7 +185,7 @@ int client_main(void)
sigaction(SIGHUP, &sig_act, NULL) ||
sigaction(SIGPIPE, &sig_act, NULL)) {
printf("Failed to install sighandler.\n");
- return -1;
+ return 2;
}
client.sent = 0;
@@ -196,7 +196,7 @@ int client_main(void)
fd = flow_alloc(client.server_name, NULL, NULL);
if (fd < 0) {
printf("Failed to allocate flow.\n");
- return -1;
+ return 2;
}
if (client.conf.test_type == TEST_TYPE_BI)
@@ -207,7 +207,7 @@ int client_main(void)
if (flow_write(fd, &client.conf, sizeof(client.conf)) < 0) {
printf("Failed to send configuration.\n");
flow_dealloc(fd);
- return -1;
+ return 1;
}
sleep(1);
diff --git a/src/tools/operf/operf_server.c b/src/tools/operf/operf_server.c
index a611f79c..00f780ba 100644
--- a/src/tools/operf/operf_server.c
+++ b/src/tools/operf/operf_server.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros perf application
*
diff --git a/src/tools/oping/CMakeLists.txt b/src/tools/oping/CMakeLists.txt
deleted file mode 100644
index 31a4f961..00000000
--- a/src/tools/oping/CMakeLists.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-
-find_library(LIBM_LIBRARIES m)
-if(NOT LIBM_LIBRARIES)
- message(FATAL_ERROR "libm not found")
-endif()
-
-mark_as_advanced(LIBM_LIBRARIES)
-
-set(SOURCE_FILES
- # Add source files here
- oping.c
- )
-
-add_executable(oping ${SOURCE_FILES})
-
-target_link_libraries(oping LINK_PUBLIC ${LIBM_LIBRARIES} ouroboros-dev)
-
-install(TARGETS oping RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/oping/oping.c b/src/tools/oping/oping.c
index 87c1ee18..10e1e23c 100644
--- a/src/tools/oping/oping.c
+++ b/src/tools/oping/oping.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros ping application
*
@@ -60,7 +60,7 @@
#include <errno.h>
#include <float.h>
-#define OPING_BUF_SIZE 1500
+#define OPING_BUF_SIZE 16384
#define ECHO_REQUEST 0
#define ECHO_REPLY 1
#define OPING_MAX_FLOWS 256
@@ -72,13 +72,18 @@
"and reports the Round Trip Time (RTT)\n" \
"\n" \
" -l, --listen Run in server mode\n" \
+" --poll Server uses polling (lower latency)\n" \
+" --busy Server uses busy-poll (single flow)\n" \
"\n" \
" -c, --count Number of packets\n" \
" -d, --duration Duration of the test (default 1s)\n" \
+" -f, --flood Send back-to-back without waiting\n" \
+" -F, --flood-busy Flood with busy-polling (lower latency)\n" \
" -i, --interval Interval (default 1000ms)\n" \
" -n, --server-name Name of the oping server\n" \
-" -q, --qos QoS (raw, best, video, voice, data)\n" \
+" -q, --qos QoS (raw, safe, rt, rt-safe, msg)\n" \
" -s, --size Payload size (B, default 64)\n" \
+" -W, --timeout Per-packet recv timeout, ms (default 2000)\n" \
" -Q, --quiet Only print final statistics\n" \
" -D, --timeofday Print time of day before each line\n" \
"\n" \
@@ -89,7 +94,11 @@ struct {
int interval;
uint32_t count;
int size;
+ int timeout; /* per-packet recv timeout, ms */
bool timestamp;
+ bool flood;
+ bool flood_busy;
+ long duration;
qosspec_t qs;
/* stats */
@@ -114,6 +123,8 @@ struct {
pthread_mutex_t lock;
bool quiet;
+ bool poll;
+ bool busy;
pthread_t cleaner_pt;
pthread_t accept_pt;
@@ -167,14 +178,20 @@ int main(int argc,
argc--;
argv++;
- client.s_apn = NULL;
- client.interval = 1000;
- client.size = 64;
- client.count = INT_MAX;
- client.timestamp = false;
- client.qs = qos_raw;
- client.quiet = false;
- server.quiet = false;
+ client.s_apn = NULL;
+ client.interval = 1000;
+ client.size = 64;
+ client.count = INT_MAX;
+ client.timeout = 2000;
+ client.timestamp = false;
+ client.flood = false;
+ client.flood_busy = false;
+ client.duration = 0;
+ client.qs = qos_raw;
+ client.quiet = false;
+ server.quiet = false;
+ server.poll = false;
+ server.busy = false;
while (argc > 0) {
if ((strcmp(*argv, "-i") == 0 ||
@@ -204,6 +221,12 @@ int main(int argc,
argc > 1) {
client.size = strtol(*(++argv), &rem, 10);
--argc;
+ } else if ((strcmp(*argv, "-W") == 0 ||
+ strcmp(*argv, "--timeout") == 0) &&
+ argc > 1) {
+ client.timeout = strtol(*(++argv), &rem, 10);
+ client.timeout *= time_mul(rem);
+ --argc;
} else if ((strcmp(*argv, "-q") == 0 ||
strcmp(*argv, "--qos") == 0) &&
argc > 1) {
@@ -212,6 +235,12 @@ int main(int argc,
} else if (strcmp(*argv, "-l") == 0 ||
strcmp(*argv, "--listen") == 0) {
serv = true;
+ } else if (strcmp(*argv, "-f") == 0 ||
+ strcmp(*argv, "--flood") == 0) {
+ client.flood = true;
+ } else if (strcmp(*argv, "-F") == 0 ||
+ strcmp(*argv, "--flood-busy") == 0) {
+ client.flood_busy = true;
} else if (strcmp(*argv, "-D") == 0 ||
strcmp(*argv, "--timeofday") == 0) {
client.timestamp = true;
@@ -219,6 +248,10 @@ int main(int argc,
strcmp(*argv, "--quiet") == 0) {
client.quiet = true;
server.quiet = true;
+ } else if (strcmp(*argv, "--poll") == 0) {
+ server.poll = true;
+ } else if (strcmp(*argv, "--busy") == 0) {
+ server.busy = true;
} else {
goto fail;
}
@@ -227,23 +260,25 @@ int main(int argc,
}
if (duration > 0) {
- if (client.interval == 0)
+ if (client.flood || client.flood_busy)
+ client.duration = duration;
+ else if (client.interval == 0)
client.count = duration * 10;
else
client.count = duration / client.interval;
}
if (qos != NULL) {
- if (strcmp(qos, "best") == 0)
- client.qs = qos_best_effort;
- else if (strcmp(qos, "raw") == 0)
+ if (strcmp(qos, "raw") == 0)
client.qs = qos_raw;
- else if (strcmp(qos, "video") == 0)
- client.qs = qos_video;
- else if (strcmp(qos, "voice") == 0)
- client.qs = qos_voice;
- else if (strcmp(qos, "data") == 0)
- client.qs = qos_data;
+ else if (strcmp(qos, "safe") == 0)
+ client.qs = qos_raw_safe;
+ else if (strcmp(qos, "rt") == 0)
+ client.qs = qos_rt;
+ else if (strcmp(qos, "rt-safe") == 0)
+ client.qs = qos_rt_safe;
+ else if (strcmp(qos, "msg") == 0)
+ client.qs = qos_msg;
else
printf("Unknown QoS cube, defaulting to raw.\n");
}
@@ -276,7 +311,7 @@ int main(int argc,
if (ret < 0)
exit(EXIT_FAILURE);
- exit(EXIT_SUCCESS);
+ exit(ret);
fail:
usage();
diff --git a/src/tools/oping/oping_client.c b/src/tools/oping/oping_client.c
index 5a9e03dc..4b01315d 100644
--- a/src/tools/oping/oping_client.c
+++ b/src/tools/oping/oping_client.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros ping application
*
@@ -47,15 +47,50 @@ void shutdown_client(int signo, siginfo_t * info, void * c)
case SIGINT:
case SIGTERM:
case SIGHUP:
+ case SIGALRM:
stop = true;
default:
return;
}
}
+static void update_rtt_stats(double ms)
+{
+ double d;
+
+ if (ms < client.rtt_min)
+ client.rtt_min = ms;
+ if (ms > client.rtt_max)
+ client.rtt_max = ms;
+
+ d = (ms - client.rtt_avg);
+ client.rtt_avg += d / client.rcvd;
+ client.rtt_m2 += d * (ms - client.rtt_avg);
+}
+
+static double rtt_val(double ms)
+{
+ return ms < 0.1 ? ms * 1000 : ms;
+}
+
+static const char * rtt_unit(double ms)
+{
+ return ms < 0.1 ? "µs" : "ms";
+}
+
+static void print_rtt(int len, int seq,
+ double ms, const char * suf)
+{
+ printf("%d bytes from %s: seq=%d "
+ "time=%.3f %s%s\n",
+ len, client.s_apn, seq,
+ rtt_val(ms), rtt_unit(ms),
+ suf != NULL ? suf : "");
+}
+
void * reader(void * o)
{
- struct timespec timeout = {client.interval / 1000 + 2, 0};
+ struct timespec timeout;
struct timespec now = {0, 0};
struct timespec sent;
@@ -64,9 +99,11 @@ void * reader(void * o)
int fd = *((int *) o);
int msg_len = 0;
double ms = 0;
- double d = 0;
uint32_t exp_id = 0;
+ timeout.tv_sec = client.timeout / 1000;
+ timeout.tv_nsec = (client.timeout % 1000) * MILLION;
+
fccntl(fd, FLOWSRCVTIMEO, &timeout);
while (!stop && client.rcvd != client.count) {
@@ -114,22 +151,12 @@ void * reader(void * o)
(size_t) rtc.tv_nsec / 1000);
}
- printf("%d bytes from %s: seq=%d time=%.3f ms%s\n",
- msg_len,
- client.s_apn,
- ntohl(msg->id),
- ms,
- id < exp_id ? " [out-of-order]" : "");
+ print_rtt(msg_len, ntohl(msg->id), ms,
+ id < exp_id ?
+ " [out-of-order]" : NULL);
}
- if (ms < client.rtt_min)
- client.rtt_min = ms;
- if (ms > client.rtt_max)
- client.rtt_max = ms;
-
- d = (ms - client.rtt_avg);
- client.rtt_avg += d / client.rcvd;
- client.rtt_m2 += d * (ms - client.rtt_avg);
+ update_rtt_stats(ms);
if (id >= exp_id)
exp_id = id + 1;
@@ -204,13 +231,167 @@ static void client_fini(void)
return;
}
+static void print_stats(struct timespec * tic,
+ struct timespec * toc)
+{
+ printf("\n");
+ printf("--- %s ping statistics ---\n", client.s_apn);
+ printf("%d packets transmitted, ", client.sent);
+ printf("%d received, ", client.rcvd);
+ printf("%zd out-of-order, ", client.ooo);
+ printf("%.0lf%% packet loss, ", client.sent == 0 ? 0 :
+ ceil(100 - (100 * (client.rcvd / (float) client.sent))));
+ printf("time: %.3f ms\n", ts_diff_us(toc, tic) / 1000.0);
+
+ if (client.rcvd > 0) {
+ double a = client.rtt_avg;
+ double f = a < 0.1 ? 1000 : 1;
+ printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/",
+ client.rtt_min * f, client.rtt_avg * f,
+ client.rtt_max * f);
+ if (client.rcvd > 1)
+ printf("%.3f %s\n",
+ sqrt(client.rtt_m2 /
+ (client.rcvd - 1)) * f,
+ rtt_unit(a));
+ else
+ printf("NaN %s\n", rtt_unit(a));
+ }
+}
+
+static int flood_busy_ping(int fd)
+{
+ char buf[OPING_BUF_SIZE];
+ struct oping_msg * msg = (struct oping_msg *) buf;
+ struct timespec sent;
+ struct timespec rcvd;
+ double ms;
+ int n;
+
+ memset(buf, 0, client.size);
+
+ fccntl(fd, FLOWSFLAGS,
+ FLOWFRDWR | FLOWFRNOPART | FLOWFRNOBLOCK);
+
+ if (!client.quiet)
+ printf("Pinging %s with %d bytes"
+ " of data (%u packets,"
+ " busy-poll):\n\n",
+ client.s_apn, client.size,
+ client.count);
+
+ while (!stop && client.sent < client.count) {
+ clock_gettime(CLOCK_MONOTONIC, &sent);
+
+ msg->type = htonl(ECHO_REQUEST);
+ msg->id = htonl(client.sent);
+ msg->tv_sec = sent.tv_sec;
+ msg->tv_nsec = sent.tv_nsec;
+
+ if (flow_write(fd, buf, client.size) < 0) {
+ printf("Failed to send packet.\n");
+ break;
+ }
+
+ ++client.sent;
+
+ do {
+ n = flow_read(fd, buf, OPING_BUF_SIZE);
+ } while (n == -EAGAIN && !stop);
+
+ if (n < 0)
+ break;
+
+ clock_gettime(CLOCK_MONOTONIC, &rcvd);
+
+ if (ntohl(msg->type) != ECHO_REPLY)
+ continue;
+
+ ++client.rcvd;
+
+ sent.tv_sec = msg->tv_sec;
+ sent.tv_nsec = msg->tv_nsec;
+ ms = ts_diff_us(&rcvd, &sent) / 1000.0;
+
+ update_rtt_stats(ms);
+
+ if (!client.quiet)
+ print_rtt(client.size, ntohl(msg->id), ms, NULL);
+ }
+
+ return 0;
+}
+
+static int flood_ping(int fd)
+{
+ char buf[OPING_BUF_SIZE];
+ struct oping_msg * msg = (struct oping_msg *) buf;
+ struct timespec sent;
+ struct timespec rcvd;
+ double ms;
+
+ memset(buf, 0, client.size);
+
+ if (!client.quiet)
+ printf("Pinging %s with %d bytes of data (%u packets):\n\n",
+ client.s_apn, client.size, client.count);
+
+ while (!stop && client.sent < client.count) {
+ clock_gettime(CLOCK_MONOTONIC, &sent);
+
+ msg->type = htonl(ECHO_REQUEST);
+ msg->id = htonl(client.sent);
+ msg->tv_sec = sent.tv_sec;
+ msg->tv_nsec = sent.tv_nsec;
+
+ if (flow_write(fd, buf, client.size) < 0) {
+ printf("Failed to send packet.\n");
+ break;
+ }
+
+ ++client.sent;
+
+ if (flow_read(fd, buf, OPING_BUF_SIZE) < 0) {
+ printf("Failed to read packet.\n");
+ break;
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &rcvd);
+
+ if (ntohl(msg->type) != ECHO_REPLY)
+ continue;
+
+ ++client.rcvd;
+
+ sent.tv_sec = msg->tv_sec;
+ sent.tv_nsec = msg->tv_nsec;
+ ms = ts_diff_us(&rcvd, &sent) / 1000.0;
+
+ update_rtt_stats(ms);
+
+ if (!client.quiet)
+ print_rtt(client.size, ntohl(msg->id), ms, NULL);
+ }
+
+ return 0;
+}
+
+static int threaded_ping(int fd)
+{
+ pthread_create(&client.reader_pt, NULL, reader, &fd);
+ pthread_create(&client.writer_pt, NULL, writer, &fd);
+
+ pthread_join(client.writer_pt, NULL);
+ pthread_join(client.reader_pt, NULL);
+
+ return 0;
+}
+
static int client_main(void)
{
struct sigaction sig_act;
-
struct timespec tic;
struct timespec toc;
-
int fd;
memset(&sig_act, 0, sizeof sig_act);
@@ -220,59 +401,49 @@ static int client_main(void)
if (sigaction(SIGINT, &sig_act, NULL) ||
sigaction(SIGTERM, &sig_act, NULL) ||
sigaction(SIGHUP, &sig_act, NULL) ||
- sigaction(SIGPIPE, &sig_act, NULL)) {
+ sigaction(SIGPIPE, &sig_act, NULL) ||
+ sigaction(SIGALRM, &sig_act, NULL)) {
printf("Failed to install sighandler.\n");
- return -1;
+ return 2;
}
if (client_init()) {
printf("Failed to initialize client.\n");
- return -1;
+ return 2;
}
fd = flow_alloc(client.s_apn, &client.qs, NULL);
if (fd < 0) {
printf("Failed to allocate flow: %d.\n", fd);
client_fini();
- return -1;
+ return 2;
}
fccntl(fd, FLOWSFLAGS, FLOWFRDWR | FLOWFRNOPART);
- clock_gettime(CLOCK_REALTIME, &tic);
+ if (client.duration > 0) {
+ struct itimerval it;
+ memset(&it, 0, sizeof(it));
+ it.it_value.tv_sec = client.duration / 1000;
+ it.it_value.tv_usec = (client.duration % 1000) * 1000;
+ setitimer(ITIMER_REAL, &it, NULL);
+ }
- pthread_create(&client.reader_pt, NULL, reader, &fd);
- pthread_create(&client.writer_pt, NULL, writer, &fd);
+ clock_gettime(CLOCK_REALTIME, &tic);
- pthread_join(client.writer_pt, NULL);
- pthread_join(client.reader_pt, NULL);
+ if (client.flood_busy)
+ flood_busy_ping(fd);
+ else if (client.flood)
+ flood_ping(fd);
+ else
+ threaded_ping(fd);
clock_gettime(CLOCK_REALTIME, &toc);
- printf("\n");
- printf("--- %s ping statistics ---\n", client.s_apn);
- printf("%d packets transmitted, ", client.sent);
- printf("%d received, ", client.rcvd);
- printf("%zd out-of-order, ", client.ooo);
- printf("%.0lf%% packet loss, ", client.sent == 0 ? 0 :
- ceil(100 - (100 * (client.rcvd / (float) client.sent))));
- printf("time: %.3f ms\n", ts_diff_us(&toc, &tic) / 1000.0);
-
- if (client.rcvd > 0) {
- printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/",
- client.rtt_min,
- client.rtt_avg,
- client.rtt_max);
- if (client.rcvd > 1)
- printf("%.3f ms\n",
- sqrt(client.rtt_m2 / (client.rcvd - 1)));
- else
- printf("NaN ms\n");
- }
+ print_stats(&tic, &toc);
flow_dealloc(fd);
-
client_fini();
- return 0;
+ return client.rcvd == client.sent ? 0 : 1;
}
diff --git a/src/tools/oping/oping_server.c b/src/tools/oping/oping_server.c
index c1d5e6e5..e98ca040 100644
--- a/src/tools/oping/oping_server.c
+++ b/src/tools/oping/oping_server.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros ping application
*
@@ -89,12 +89,15 @@ void * server_thread(void *o)
struct oping_msg * msg = (struct oping_msg *) buf;
struct timespec now = {0, 0};
struct timespec timeout = {0, 100 * MILLION};
+ struct timespec poll_timeout = {0, 0};
int fd;
(void) o;
while (true) {
- if (fevent(server.flows, server.fq, &timeout) == -ETIMEDOUT)
+ if (fevent(server.flows, server.fq,
+ server.poll ? &poll_timeout : &timeout)
+ == -ETIMEDOUT)
continue;
while ((fd = fqueue_next(server.fq)) >= 0) {
@@ -135,7 +138,10 @@ void * accept_thread(void * o)
(void) o;
- printf("Ouroboros ping server started.\n");
+ printf("Ouroboros ping server started.");
+ if (server.busy)
+ printf(" [busy-poll]");
+ printf("\n");
while (true) {
fd = flow_accept(&qs, NULL);
@@ -155,12 +161,56 @@ void * accept_thread(void * o)
pthread_mutex_unlock(&server.lock);
fccntl(fd, FLOWSFLAGS,
- FLOWFRNOBLOCK | FLOWFRDWR | FLOWFRNOPART);
+ FLOWFRNOBLOCK | FLOWFRDWR
+ | FLOWFRNOPART);
}
return (void *) 0;
}
+void * busy_thread(void * o)
+{
+ char buf[OPING_BUF_SIZE];
+ struct oping_msg * msg = (struct oping_msg *) buf;
+ int fd;
+ int msg_len;
+
+ (void) o;
+
+ /* Accept a single flow. */
+ fd = flow_accept(NULL, NULL);
+ if (fd < 0) {
+ printf("Failed to accept flow.\n");
+ return (void *) -1;
+ }
+
+ printf("New flow %d (busy-poll).\n", fd);
+
+ fccntl(fd, FLOWSFLAGS,
+ FLOWFRNOBLOCK | FLOWFRDWR
+ | FLOWFRNOPART);
+
+ while (true) {
+ msg_len = flow_read(fd, buf,
+ OPING_BUF_SIZE);
+ if (msg_len == -EAGAIN)
+ continue;
+ if (msg_len < 0)
+ break;
+
+ if (ntohl(msg->type) != ECHO_REQUEST)
+ continue;
+
+ msg->type = htonl(ECHO_REPLY);
+
+ flow_write(fd, buf, msg_len);
+ }
+
+ flow_dealloc(fd);
+
+ return (void *) 0;
+}
+
int server_main(void)
{
struct sigaction sig_act;
@@ -187,20 +237,39 @@ int server_main(void)
return -1;
}
+ if (pthread_mutex_init(&server.lock, NULL)) {
+ fqueue_destroy(server.fq);
+ fset_destroy(server.flows);
+ return -1;
+ }
+
+ memset(server.times, 0, sizeof(server.times));
+
pthread_create(&server.cleaner_pt, NULL, cleaner_thread, NULL);
- pthread_create(&server.accept_pt, NULL, accept_thread, NULL);
- pthread_create(&server.server_pt, NULL, server_thread, NULL);
- pthread_join(server.accept_pt, NULL);
+ if (server.busy) {
+ pthread_create(&server.server_pt, NULL,
+ busy_thread, NULL);
+ pthread_join(server.server_pt, NULL);
+ pthread_cancel(server.cleaner_pt);
+ } else {
+ pthread_create(&server.accept_pt, NULL,
+ accept_thread, NULL);
+ pthread_create(&server.server_pt, NULL,
+ server_thread, NULL);
+ pthread_join(server.accept_pt, NULL);
+ pthread_cancel(server.server_pt);
+ }
- pthread_cancel(server.server_pt);
pthread_cancel(server.cleaner_pt);
- fset_destroy(server.flows);
- fqueue_destroy(server.fq);
-
+ /* Join cancellable threads before tearing down their fset. */
pthread_join(server.server_pt, NULL);
pthread_join(server.cleaner_pt, NULL);
+ pthread_mutex_destroy(&server.lock);
+ fset_destroy(server.flows);
+ fqueue_destroy(server.fq);
+
return 0;
}
diff --git a/src/tools/ovpn/CMakeLists.txt b/src/tools/ovpn/CMakeLists.txt
deleted file mode 100644
index f3a2cac8..00000000
--- a/src/tools/ovpn/CMakeLists.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-get_filename_component(CURRENT_SOURCE_PARENT_DIR
- ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
-
-include_directories(${CURRENT_SOURCE_PARENT_DIR})
-
-set(SOURCE_FILES
- # Add source files here
- ovpn.c
- )
-
-add_executable(ovpn ${SOURCE_FILES})
-
-target_link_libraries(ovpn LINK_PUBLIC ouroboros-dev)
-
-install(TARGETS ovpn RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/tools/ovpn/ovpn.c b/src/tools/ovpn/ovpn.c
index b25e3ea2..95b4572d 100644
--- a/src/tools/ovpn/ovpn.c
+++ b/src/tools/ovpn/ovpn.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Ouroboros VPN
*
diff --git a/src/tools/time_utils.h b/src/tools/time_utils.h
index a4117f44..a0729074 100644
--- a/src/tools/time_utils.h
+++ b/src/tools/time_utils.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Time utilities
*