diff --git a/flake.nix b/flake.nix index 3f9fa72d8..62298fa28 100644 --- a/flake.nix +++ b/flake.nix @@ -40,7 +40,10 @@ system: import nixpkgs { inherit system; - overlays = with self.overlays; [ default ]; + overlays = with self.overlays; [ + default + patches + ]; }; # Initialize development nixpkgs for the specified `system` @@ -50,6 +53,7 @@ inherit system; overlays = with self.overlays; [ default + patches debug ]; }; @@ -111,10 +115,14 @@ # Standard flake attribute allowing you to add the villas packages to your nixpkgs overlays = { default = final: prev: packagesWith final; + + patches = import ./packaging/nix/patches.nix; + debug = final: prev: { jansson = addSeparateDebugInfo prev.jansson; libmodbus = addSeparateDebugInfo prev.libmodbus; }; + minimal = final: prev: { mosquitto = prev.mosquitto.override { systemd = final.systemdMinimal; }; rdma-core = prev.rdma-core.override { udev = final.systemdMinimal; }; @@ -193,7 +201,7 @@ villas = { imports = [ (nixDir + "/module.nix") ]; - nixpkgs.overlays = [ self.overlays.default ]; + nixpkgs.overlays = [ self.overlays.default self.overlays.patches ]; }; }; }; diff --git a/include/villas/nodes/iec61850_goose.hpp b/include/villas/nodes/iec61850_goose.hpp index efe56890a..783aafd62 100644 --- a/include/villas/nodes/iec61850_goose.hpp +++ b/include/villas/nodes/iec61850_goose.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -129,6 +130,13 @@ class GooseNode : public Node { ALWAYS, }; + struct SessionKey { + int id; + RSecurityAlgorithm security; + RSignatureAlgorithm signature; + std::vector data; + }; + struct InputMapping { std::string subscriber; unsigned int index; @@ -151,14 +159,17 @@ class GooseNode : public Node { }; struct Input { - enum { NONE, STOPPED, READY } state; - GooseReceiver receiver; + RSession session = nullptr; + GooseReceiver receiver = nullptr; CQueueSignalled queue; Pool pool; std::map contexts; std::vector mappings; std::string interface_id; + std::string local_address; + uint16_t local_port; + std::vector multicast_groups; bool with_timestamp; unsigned int queue_length; } input; @@ -184,13 +195,17 @@ class GooseNode : public Node { PublisherConfig config; std::vector values; - GoosePublisher publisher; + GoosePublisher publisher = nullptr; }; struct Output { - enum { NONE, STOPPED, READY } state; std::vector contexts; std::string interface_id; + std::string local_address; + uint16_t local_port; + std::string remote_address; + uint16_t remote_port; + int key_id; double resend_interval; std::mutex send_mutex; @@ -198,15 +213,18 @@ class GooseNode : public Node { bool resend_thread_stop; std::optional resend_thread; std::condition_variable resend_thread_cv; + RSession session = nullptr; } output; - void createReceiver() noexcept; + std::vector keys; + + void createReceiver() noexcept(false); void destroyReceiver() noexcept; void startReceiver() noexcept(false); void stopReceiver() noexcept; - void createPublishers() noexcept; + void createPublishers() noexcept(false); void destroyPublishers() noexcept; void startPublishers() noexcept(false); @@ -224,6 +242,7 @@ class GooseNode : public Node { static void resend_thread(GooseNode::Output *output) noexcept; void parseInput(json_t *json); + void parseSessionKey(json_t *json); void parseSubscriber(json_t *json, SubscriberConfig &sc); void parseSubscribers(json_t *json, std::map &ctx); diff --git a/lib/nodes/iec61850_goose.cpp b/lib/nodes/iec61850_goose.cpp index c7477c237..a2ab62879 100644 --- a/lib/nodes/iec61850_goose.cpp +++ b/lib/nodes/iec61850_goose.cpp @@ -7,16 +7,25 @@ #include #include +#include +#include + +#include +#include +#include +#include #include #include #include #include +#include #include #include using namespace std::literals::chrono_literals; using namespace std::literals::string_literals; +using namespace std::literals::string_view_literals; using namespace villas; using namespace villas::node; @@ -248,7 +257,8 @@ void GooseNode::onEvent(GooseSubscriber subscriber, ctx.values.clear(); - for (unsigned int i = 0; i < MmsValue_getArraySize(mms_values); i++) { + unsigned int array_size = MmsValue_getArraySize(mms_values); + for (unsigned int i = 0; i < array_size; i++) { auto mms_value = MmsValue_getElement(mms_values, i); auto goose_value = GooseSignal::fromMmsValue(mms_value); ctx.values.push_back(goose_value); @@ -325,116 +335,158 @@ void GooseNode::addSubscriber(GooseNode::InputEventContext &ctx) noexcept { GooseReceiver_addSubscriber(input.receiver, subscriber); } -void GooseNode::createReceiver() noexcept { - destroyReceiver(); +void GooseNode::createReceiver() noexcept(false) { + if (!input.interface_id.empty()) { + input.receiver = GooseReceiver_create(); + + GooseReceiver_setInterfaceId(input.receiver, input.interface_id.c_str()); + } else { + RSessionError err; + + input.session = RSession_create(); + if (!input.session) + throw RuntimeError{"failed to create R-GOOSE session"}; + + err = RSession_setLocalAddress(input.session, input.local_address.c_str(), + input.local_port); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to set local address for R-GOOSE session"); + + for (auto &key : keys) { + err = RSession_addKey(input.session, key.id, key.data.data(), + key.data.size(), key.security, key.signature); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to add key with id {} to R-GOOSE session", + key.id); + } - input.receiver = GooseReceiver_create(); + for (auto const &multicast_group : input.multicast_groups) { + err = RSession_addMulticastGroup(input.session, multicast_group.c_str()); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError( + "failed to add multicast group {} to R-GOOSE session", + multicast_group); + } - GooseReceiver_setInterfaceId(input.receiver, input.interface_id.c_str()); + input.receiver = GooseReceiver_createRemote(input.session); + } for (auto &pair_key_context : input.contexts) addSubscriber(pair_key_context.second); - - input.state = Input::READY; } void GooseNode::destroyReceiver() noexcept { - int err __attribute__((unused)); - - if (input.state == Input::NONE) - return; - - stopReceiver(); - - GooseReceiver_destroy(input.receiver); - - err = queue_signalled_destroy(&input.queue); + if (input.receiver) { + stopReceiver(); + GooseReceiver_destroy(input.receiver); + input.receiver = nullptr; + } - input.state = Input::NONE; + if (input.session) { + RSession_destroy(input.session); + input.session = nullptr; + } } void GooseNode::startReceiver() noexcept(false) { - if (input.state == Input::NONE) - createReceiver(); - else - stopReceiver(); - GooseReceiver_start(input.receiver); if (!GooseReceiver_isRunning(input.receiver)) throw RuntimeError{"iec61850-GOOSE receiver could not be started"}; - - input.state = Input::READY; } void GooseNode::stopReceiver() noexcept { - if (input.state == Input::NONE) - return; - - input.state = Input::STOPPED; - if (!GooseReceiver_isRunning(input.receiver)) return; GooseReceiver_stop(input.receiver); } -void GooseNode::createPublishers() noexcept { - destroyPublishers(); +void GooseNode::createPublishers() { + if (output.interface_id.empty()) { + RSessionError err; + + output.session = RSession_create(); + if (!output.session) + throw RuntimeError{"failed to create R-GOOSE session"}; + + err = RSession_setLocalAddress(output.session, output.local_address.c_str(), + output.local_port); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to set local address {} for R-GOOSE session", + output.local_address); + + err = RSession_setRemoteAddress( + output.session, output.remote_address.c_str(), output.remote_port); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to set remote address {} for R-GOOSE session", + output.remote_address); + + for (auto &key : keys) { + err = RSession_addKey(output.session, key.id, key.data.data(), + key.data.size(), key.security, key.signature); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to add key with id {} to R-GOOSE session", + key.id); + } - for (auto &ctx : output.contexts) { - auto dst_address = ctx.config.dst_address; - auto comm = CommParameters{/* vlanPriority */ 0, - /* vlanId */ 0, - ctx.config.app_id, - {}}; + RSession_setActiveKey(output.session, output.key_id); + } - memcpy(comm.dstAddress, dst_address.data(), dst_address.size()); + for (auto &ctx : output.contexts) { + if (!output.interface_id.empty()) { + auto comm = CommParameters{/* vlanPriority */ 0, + /* vlanId */ 0, + ctx.config.app_id, + {}}; + + memcpy(comm.dstAddress, ctx.config.dst_address.data(), + ctx.config.dst_address.size()); + + ctx.publisher = + GoosePublisher_createEx(&comm, output.interface_id.c_str(), false); + } else { + ctx.publisher = + GoosePublisher_createRemote(output.session, ctx.config.app_id); + } - ctx.publisher = - GoosePublisher_createEx(&comm, output.interface_id.c_str(), false); + if (!ctx.config.go_id.empty()) + GoosePublisher_setGoID(ctx.publisher, ctx.config.go_id.data()); - GoosePublisher_setGoID(ctx.publisher, ctx.config.go_id.data()); GoosePublisher_setGoCbRef(ctx.publisher, ctx.config.go_cb_ref.data()); GoosePublisher_setDataSetRef(ctx.publisher, ctx.config.data_set_ref.data()); GoosePublisher_setConfRev(ctx.publisher, ctx.config.conf_rev); GoosePublisher_setTimeAllowedToLive(ctx.publisher, ctx.config.time_allowed_to_live); } - - output.state = Output::READY; } void GooseNode::destroyPublishers() noexcept { - int err __attribute__((unused)); - - if (output.state == Output::NONE) - return; - - stopPublishers(); - - for (auto &ctx : output.contexts) - GoosePublisher_destroy(ctx.publisher); + for (auto &ctx : output.contexts) { + if (ctx.publisher) { + GoosePublisher_destroy(ctx.publisher); + ctx.publisher = nullptr; + } + } - output.state = Output::NONE; + if (output.session) { + RSession_destroy(output.session); + output.session = nullptr; + } } void GooseNode::startPublishers() noexcept(false) { - if (output.state == Output::NONE) - createPublishers(); - else - stopPublishers(); - output.resend_thread_stop = false; output.resend_thread = std::thread(resend_thread, &output); - output.state = Output::READY; + if (output.session) { + RSessionError err = RSession_start(output.session); + if (err != R_SESSION_ERROR_OK) + throw RuntimeError("failed to start R-GOOSE publisher session"); + } } void GooseNode::stopPublishers() noexcept { - if (output.state == Output::NONE) - return; - if (output.resend_thread) { auto lock = std::unique_lock{output.send_mutex}; output.resend_thread_stop = true; @@ -444,17 +496,12 @@ void GooseNode::stopPublishers() noexcept { output.resend_thread->join(); output.resend_thread = std::nullopt; } - - output.state = Output::STOPPED; } int GooseNode::_read(Sample *samples[], unsigned sample_count) { int available_samples; struct Sample *copies[sample_count]; - if (input.state != Input::READY) - return 0; - available_samples = queue_signalled_pull_many(&input.queue, (void **)copies, sample_count); sample_copy_many(samples, copies, available_samples); @@ -555,15 +602,9 @@ int GooseNode::_write(Sample *samples[], unsigned sample_count) { GooseNode::GooseNode(const uuid_t &id, const std::string &name) : Node(id, name) { - input.state = Input::NONE; - input.contexts = {}; input.mappings = {}; - input.interface_id = "lo"; input.queue_length = 1024; - - output.state = Output::NONE; - output.interface_id = "lo"; output.changed = false; output.resend_interval = 1.; output.resend_thread = std::nullopt; @@ -588,15 +629,33 @@ int GooseNode::parse(json_t *json) { if (ret) return ret; + json_t *json_keys = nullptr; json_t *json_in = nullptr; json_t *json_out = nullptr; - ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o }", "in", &json_in, "out", - &json_out); + ret = json_unpack_ex(json, &err, 0, // + "{ s:?o, s:?o, s:?o }", // + "keys", &json_keys, // + "in", &json_in, // + "out", &json_out); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); - parseInput(json_in); - parseOutput(json_out); + if (json_keys) { + size_t index; + json_t *json_key; + assert(json_is_array(json_keys)); + keys.reserve(json_array_size(json_keys)); + json_array_foreach(json_keys, index, json_key) { + assert(json_is_object(json_key)); + parseSessionKey(json_key); + } + } + + if (json_in) + parseInput(json_in); + + if (json_out) + parseOutput(json_out); return 0; } @@ -607,22 +666,126 @@ void GooseNode::parseInput(json_t *json) { json_t *json_subscribers = nullptr; json_t *json_signals = nullptr; - char const *interface_id = input.interface_id.c_str(); + int routed = false; + char const *local_address = "localhost"; + int local_port = 102; + json_t *json_multicast_groups = nullptr; + char const *interface_id = "lo"; int with_timestamp = true; - ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o, s?: s, s: b }", - "subscribers", &json_subscribers, "signals", - &json_signals, "interface", &interface_id, + ret = json_unpack_ex(json, &err, 0, // + "{ s:o, s:o, s:?b, s:?s, s:?i, s:?o, s:?s, s:?b }", // + "subscribers", &json_subscribers, // + "signals", &json_signals, // + "routed", &routed, // + "local_address", &local_address, // + "local_port", &local_port, // + "multicast_groups", &json_multicast_groups, // + "interface", &interface_id, // "with_timestamp", &with_timestamp); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); + if (routed) { + input.local_address = local_address; + input.local_port = local_port; + + if (json_multicast_groups) { + size_t index; + json_t *json_multicast_group; + assert(json_is_array(json_multicast_groups)); + input.multicast_groups.reserve(json_array_size(json_multicast_groups)); + json_array_foreach(json_multicast_groups, index, json_multicast_group) { + assert(json_is_string(json_multicast_group)); + input.multicast_groups.emplace_back( + json_string_value(json_multicast_group)); + } + } + + if (keys.empty()) + throw RuntimeError{"missing session keys for R-GOOSE"}; + } else { + input.interface_id = interface_id; + } + parseSubscribers(json_subscribers, input.contexts); parseInputSignals(json_signals, input.mappings); - input.interface_id = interface_id; input.with_timestamp = with_timestamp; } +void GooseNode::parseSessionKey(json_t *json) { + int ret; + json_error_t err; + + int id; + char const *security_str; + char const *signature_str; + char const *data_str = nullptr; + char const *data_base64 = nullptr; + ret = json_unpack_ex(json, &err, 0, // + "{ s:i, s:s, s:s, s:?s, s:?s }", // + "id", &id, // + "security", &security_str, // + "signature", &signature_str, // + "string", &data_str, // + "base64", &data_base64); + if (ret) + throw ConfigError(json, err, "node-config-node-iec61850-8-1"); + + RSecurityAlgorithm security; + if (security_str == "aes_128_gcm"sv) + security = R_SESSION_SEC_ALGO_AES_128_GCM; + else if (security_str == "aes_256_gcm"sv) + security = R_SESSION_SEC_ALGO_AES_256_GCM; + else if (signature_str == "none"sv) + security = R_SESSION_SEC_ALGO_NONE; + else + throw RuntimeError("unknown security algorithm {}", security_str); + + RSignatureAlgorithm signature; + if (signature_str == "aes_gmac_64"sv) + signature = R_SESSION_SIG_ALGO_AES_GMAC_64; + else if (signature_str == "aes_gmac_128"sv) + signature = R_SESSION_SIG_ALGO_AES_GMAC_128; + else if (signature_str == "hmac_sha256_80"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA256_80; + else if (signature_str == "hmac_sha256_128"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA256_128; + else if (signature_str == "hmac_sha256_256"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA256_256; + else if (signature_str == "hmac_sha3_80"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA3_80; + else if (signature_str == "hmac_sha3_128"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA3_128; + else if (signature_str == "hmac_sha3_256"sv) + signature = R_SESSION_SIG_ALGO_HMAC_SHA3_256; + else if (signature_str == "none"sv) + signature = R_SESSION_SIG_ALGO_NONE; + else + throw RuntimeError("unknown signature algorithm {}", signature_str); + + std::vector data; + if (data_str && data_base64) + throw RuntimeError( + "can't use both 'base64' and 'string' for R-GOOSE key with id {}", id); + else if (data_str) { + auto data_sv = std::string_view(data_str); + data = std::vector(begin(data_sv), end(data_sv)); + } else if (data_base64) { + data = base64::decode(data_base64); + } else { + throw RuntimeError( + "missing one of 'base64' or 'string' for R-GOOSE key with id {}", id); + } + + keys.emplace_back(SessionKey{ + .id = id, + .security = security, + .signature = signature, + .data = data, + }); +} + void GooseNode::parseSubscriber(json_t *json, GooseNode::SubscriberConfig &sc) { int ret; json_error_t err; @@ -631,9 +794,11 @@ void GooseNode::parseSubscriber(json_t *json, GooseNode::SubscriberConfig &sc) { char *dst_address_str = nullptr; char *trigger = nullptr; int app_id = 0; - ret = json_unpack_ex(json, &err, 0, "{ s: s, s?: s, s?: s, s?: i }", - "go_cb_ref", &go_cb_ref, "trigger", &trigger, - "dst_address", &dst_address_str, "app_id", &app_id); + ret = json_unpack_ex(json, &err, 0, "{ s:s, s:?s, s:?s, s:?i }", // + "go_cb_ref", &go_cb_ref, // + "trigger", &trigger, // + "dst_address", &dst_address_str, // + "app_id", &app_id); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); @@ -687,8 +852,10 @@ void GooseNode::parseInputSignals( char *mapping_subscriber; unsigned int mapping_index; char *mapping_type_name; - ret = json_unpack_ex(value, &err, 0, "{ s: s, s: i, s: s }", "subscriber", - &mapping_subscriber, "index", &mapping_index, + ret = json_unpack_ex(value, &err, 0, // + "{ s: s, s: i, s: s }", // + "subscriber", &mapping_subscriber, // + "index", &mapping_index, // "mms_type", &mapping_type_name); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); @@ -696,6 +863,13 @@ void GooseNode::parseInputSignals( auto mapping_type = GooseSignal::lookupMmsTypeName(mapping_type_name).value(); + auto const &config_type = in.signals->getByIndex(index)->type; + if (mapping_type->signal_type != config_type) + throw RuntimeError("configured 'type' {} for signal {} " + "does not match the 'mms_type' {}", + signalTypeToString(config_type), index, + mapping_type->name); + mappings.push_back(InputMapping{ mapping_subscriber, mapping_index, @@ -709,18 +883,52 @@ void GooseNode::parseOutput(json_t *json) { json_error_t err; json_t *json_publishers = nullptr; - json_t *json_signals = nullptr; - char const *interface_id = output.interface_id.c_str(); - ret = json_unpack_ex(json, &err, 0, "{ s: o, s: o, s?: s, s?: f }", - "publishers", &json_publishers, "signals", &json_signals, - "interface", &interface_id, "resend_interval", - &output.resend_interval); + int routed = false; + char const *local_address = "localhost"; + int local_port = 0; + char const *remote_address = "localhost"; + int remote_port = 102; + int key_id = -1; + char const *interface_id = "lo"; + ret = json_unpack_ex( + json, &err, 0, + "{ s:o, s:?b, s:?s, s:?i, s:?s, s:?i, s:?i, s:?s, s:?f }", // + "publishers", &json_publishers, // + "routed", &routed, // + "local_address", &local_address, // + "local_port", &local_port, // + "remote_address", &remote_address, // + "remote_port", &remote_port, // + "key_id", &key_id, // + "interface", &interface_id, // + "resend_interval", &output.resend_interval); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); - parsePublishers(json_publishers, output.contexts); + if (!routed) { + output.interface_id = interface_id; + } else { + output.local_address = local_address; + output.local_port = local_port; + output.remote_address = remote_address; + output.remote_port = remote_port; + + if (keys.empty()) + throw RuntimeError{"missing session 'keys' for R-GOOSE"}; + else if (keys.size() == 1 && key_id == -1) + key_id = keys[0].id; + else if (key_id == -1) + throw RuntimeError{"missing 'key_id' for R-GOOSE out"}; + else if (end(keys) != + std::find_if(begin(keys), end(keys), [key_id](auto const &key) { + return key.id == key_id; + })) + throw RuntimeError{"'key_id' does not correspond to any known key"}; + + output.key_id = key_id; + } - output.interface_id = interface_id; + parsePublishers(json_publishers, output.contexts); } void GooseNode::parsePublisherData(json_t *json, @@ -739,8 +947,10 @@ void GooseNode::parsePublisherData(json_t *json, json_t *json_value = nullptr; int bitstring_size = -1; ret = json_unpack_ex(json_signal_or_value, &err, 0, - "{ s: s, s?: s, s?: o, s?: i }", "mms_type", &mms_type, - "signal", &signal_str, "value", &json_value, + "{ s: s, s?: s, s?: o, s?: i }", // + "mms_type", &mms_type, // + "signal", &signal_str, // + "value", &json_value, // "mms_bitstring_size", &bitstring_size); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); @@ -785,24 +995,33 @@ void GooseNode::parsePublisher(json_t *json, PublisherConfig &pc) { int time_allowed_to_live = 0; int burst = 1; json_t *json_data = nullptr; - ret = json_unpack_ex( - json, &err, 0, - "{ s: s, s: s, s: s, s: s, s: i, s: i, s: i, s?: i, s: o }", "go_id", - &go_id, "go_cb_ref", &go_cb_ref, "data_set_ref", &data_set_ref, - "dst_address", &dst_address_str, "app_id", &app_id, "conf_rev", &conf_rev, - "time_allowed_to_live", &time_allowed_to_live, "burst", &burst, "data", - &json_data); + ret = json_unpack_ex(json, &err, 0, + "{ s:?s, s:s, s:s, s:?s, s:i, s:i, s:i, s?:i, s:o }", // + "go_id", &go_id, // + "go_cb_ref", &go_cb_ref, // + "data_set_ref", &data_set_ref, // + "dst_address", &dst_address_str, // + "app_id", &app_id, // + "conf_rev", &conf_rev, // + "time_allowed_to_live", &time_allowed_to_live, // + "burst", &burst, // + "data", &json_data); if (ret) throw ConfigError(json, err, "node-config-node-iec61850-8-1"); - std::optional dst_address = stringToMac(dst_address_str); - if (!dst_address) - throw RuntimeError("Invalid dst_address"); + if (!output.interface_id.empty()) { + std::optional dst_address = stringToMac(dst_address_str); + if (!dst_address) + throw RuntimeError("Invalid dst_address"); + + pc.dst_address = *dst_address; + } - pc.go_id = std::string{go_id}; - pc.go_cb_ref = std::string{go_cb_ref}; - pc.data_set_ref = std::string{data_set_ref}; - pc.dst_address = *dst_address; + if (go_id) + pc.go_id = go_id; + + pc.go_cb_ref = go_cb_ref; + pc.data_set_ref = data_set_ref; pc.app_id = app_id; pc.conf_rev = conf_rev; pc.time_allowed_to_live = time_allowed_to_live; @@ -840,6 +1059,11 @@ int GooseNode::prepare() { if (ret) return ret; + if (in.enabled) { + createReceiver(); + createPublishers(); + } + return Node::prepare(); } diff --git a/packaging/deps.sh b/packaging/deps.sh index d8f86f2a8..2e80aca02 100644 --- a/packaging/deps.sh +++ b/packaging/deps.sh @@ -132,7 +132,7 @@ if ! ( pkg-config "lua >= 5.1" || \ pkg-config "lua51" || \ { [[ -n "${RTLAB_ROOT:+x}" ]] && [[ -f "/usr/local/include/lua.h" ]]; } \ ) && should_build "lua" "for the lua hook"; then - wget http://www.lua.org/ftp/lua-5.4.7.tar.gz -O - | tar -xz + curl -L http://www.lua.org/ftp/lua-5.4.7.tar.gz | tar -xz pushd lua-5.4.4 make ${MAKE_OPTS} MYCFLAGS=-fPIC linux make ${MAKE_OPTS} MYCFLAGS=-fPIC INSTALL_TOP=${PREFIX} install @@ -194,6 +194,11 @@ fi if ! pkg-config "libiec61850 >= 1.6.0" && \ should_build "iec61850" "for the iec61850 node-type"; then git clone ${GIT_OPTS} --branch v1.6.0 https://github.com/mz-automation/libiec61850.git + + pushd libiec61850/third_party/mbedtls/ + curl -L https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v3.6.0.tar.gz | tar -xz + popd + mkdir -p libiec61850/build pushd libiec61850/build cmake -DBUILD_EXAMPLES=OFF \ @@ -436,7 +441,7 @@ if ! pkg-config "nice >= 0.1.16" && \ # Install ninja if ! command -v ninja; then - wget https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip + curl -L https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip > ninja-linux.zip unzip ninja-linux.zip export PATH=${PATH}:. fi diff --git a/packaging/nix/libiec61850_debug_r_session.patch b/packaging/nix/libiec61850_debug_r_session.patch new file mode 100644 index 000000000..a803f36c2 --- /dev/null +++ b/packaging/nix/libiec61850_debug_r_session.patch @@ -0,0 +1,31 @@ +diff --git a/src/r_session/r_session.c b/src/r_session/r_session.c +index 538ad8ec..c15d2f9c 100644 +--- a/src/r_session/r_session.c ++++ b/src/r_session/r_session.c +@@ -89,7 +89,7 @@ struct sRSession + int timeToNextKey; + }; + +-#ifdef DEBUG_RSESSION ++#if (DEBUG_RSESSION == 1) + static void + printBuffer(uint8_t* buffer, int bufSize) + { +@@ -1000,7 +1000,7 @@ encodePacket(RSession self, uint8_t payloadType, uint8_t* buffer, int bufPos, RS + int addPartSize = encryptedPartStartPos - startPos; + int encryptedPartSize = payloadEndPos - encryptedPartStartPos; + +-#ifdef DEBUG_RSESSION ++#if (DEBUG_RSESSION == 1) + printBuffer(buffer + startPos, bufPos - startPos); + #endif + +@@ -1053,7 +1053,7 @@ RSession_sendMessage(RSession self, RSessionProtocol_SPDU_ID spduId, bool simula + + int msgSize = encodePacket(self, (uint8_t) spduId, self->sendBuffer, 0, &element); + +-#ifdef DEBUG_RSESSION ++#if (DEBUG_RSESSION == 1) + printBuffer(self->sendBuffer, msgSize); + #endif + diff --git a/packaging/nix/patches.nix b/packaging/nix/patches.nix new file mode 100644 index 000000000..6df227b45 --- /dev/null +++ b/packaging/nix/patches.nix @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University# +# SPDX-License-Identifier: Apache-2.0 +# +# This overlay contains patches to dependencies of villas-node. +# It is only guaranteed to work for the locked version of nixpkgs, +# future updates to upstream nixpkgs may make these obsolete. +final: prev: let + inherit (final) lib; +in { + libiec61850 = prev.libiec61850.overrideAttrs { + patches = [ ./libiec61850_debug_r_session.patch ]; + cmakeFlags = (prev.cmakeFlags or []) ++ [ + "-DCONFIG_USE_EXTERNAL_MBEDTLS_DYNLIB=ON" + "-DCONFIG_EXTERNAL_MBEDTLS_DYNLIB_PATH=${final.mbedtls}/lib" + "-DCONFIG_EXTERNAL_MBEDTLS_INCLUDE_PATH=${final.mbedtls}/include" + ]; + nativeBuildInputs = (prev.nativeBuildInputs or []) ++ [ final.buildPackages.cmake ]; + buildInputs = [ final.mbedtls ]; + separateDebugInfo = true; + }; + + lib60870 = prev.lib60870.overrideAttrs { + buildInputs = [ final.mbedtls ]; + cmakeFlags = [ (lib.cmakeBool "WITH_MBEDTLS3" true) ]; + }; +} diff --git a/tests/integration/node-iec61850-8-1-rgoose-multicast.sh b/tests/integration/node-iec61850-8-1-rgoose-multicast.sh new file mode 100755 index 000000000..9fa12f556 --- /dev/null +++ b/tests/integration/node-iec61850-8-1-rgoose-multicast.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Integration test for Multicast Routed GOOSE messages. +# +# Author: Philipp Jungkamp +# SPDX-FileCopyrightText: 2025 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 + +set -e + +# Check if user is superuser. +if [[ "${EUID}" -ne 0 ]]; then + echo "Please run as root" + exit 99 +fi + +DIR=$(mktemp -d) +pushd ${DIR} + +function finish { + popd + rm -rf ${DIR} +} +trap finish EXIT + +NUM_SAMPLES=${NUM_SAMPLES:-10} + +cat > config.json << EOF +{ + "nodes": { + "infile": { + "type": "file", + "uri": "input.dat", + "signals": [{ "name": "counter" }] + }, + + "outfile": { + "type": "file", + "uri": "output.dat" + }, + + "goose": { + "type": "iec61850-8-1", + + "keys": [ + { + "id": 1, + "security": "aes_128_gcm", + "signature": "none", + "string": "0123456789ABCDEF" + } + ], + + "out": { + "routed": true, + "publishers": [ + { + "go_cb_ref": "simpleIOGenericIO/LLN0$GO$gcbAnalogValues", + "data_set_ref": "simpleIOGenericIO/LLN0$AnalogValues", + "conf_rev": 1, + "time_allowed_to_live": 500, + "app_id": 16385, + "data": [ + { + "mms_type": "int32", + "signal": "counter" + } + ] + } + ], + "signals": [ + { + "name": "counter", + "type": "integer" + } + ] + }, + + "in": { + "routed": true, + "subscribers": { + "sub": { + "go_cb_ref": "simpleIOGenericIO/LLN0$GO$gcbAnalogValues", + "app_id": 1000, + "trigger": "change" + } + }, + "signals": [ + { + "name": "counter", + "type": "integer", + "mms_type": "int32", + "subscriber": "sub", + "index": 0 + } + ] + } + } + }, + + "paths": [ + { + "in": "infile", + "hooks": [{ + "type": "cast", + "new_type": "integer", + "signals": ["counter"] + }], + "out": "goose" + }, + { + "in": "goose", + "hooks": [{ + "type": "cast", + "new_type": "float", + "signals": ["counter"] + }], + "out": "outfile" + } + ] +} +EOF + +villas signal -l ${NUM_SAMPLES} -n counter -r 10 > input.dat + +VILLAS_LOG_PREFIX="[node] " \ +villas node config.json & + +# Wait for node to complete +sleep $((NUM_SAMPLES / 10 + 2)) + +kill %% +wait %% + +# Send / Receive data to node +VILLAS_LOG_PREFIX="[compare] " \ +villas compare -T input.dat output.dat diff --git a/tools/integration-tests.sh b/tools/integration-tests.sh index d42925ae8..d1d531f1f 100755 --- a/tools/integration-tests.sh +++ b/tools/integration-tests.sh @@ -15,7 +15,9 @@ BUILDDIR=${BUILDDIR:-${SRCDIR}/build} LOGDIR=${BUILDDIR}/tests/integration PATH=${BUILDDIR}/src:${SRCDIR}/tools:${PATH} -export PATH SRCDIR BUILDDIR LOGDIR +LANG=C + +export PATH SRCDIR BUILDDIR LOGDIR LANG # Default values VERBOSE=${VERBOSE:-0}