From 99d2738a84e6fba5891dc89ae5378f86c6ed3be2 Mon Sep 17 00:00:00 2001 From: Sid Faber Date: Thu, 5 Mar 2020 21:08:26 +0000 Subject: [PATCH 1/5] Enable use of Cyclone DDS security features Add utility function to insert security settings to the cyclone QOS object used to create nodes. Include a utility to find security files and properly format their location to use with DDS. Signed-off-by: Sid Faber --- README.md | 5 +- rmw_cyclonedds_cpp/src/rmw_node.cpp | 94 ++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9e0c15ca..7cef6aa1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,5 @@ DDS directly instead of via the ROS2 abstraction. ## Known limitations -Cyclone DDS doesn't yet implement the DDS Security standard, nor does it fully implement -the Lifespan, Deadline and some of the Liveliness QoS modes. Consequently these features -of ROS2 are also not yet supported when using Cyclone DDS. +Cyclone DDS doesn't yet fully implement the Lifespan, Deadline and some of the Liveliness QoS modes. +Consequently these features of ROS2 are also not yet supported when using Cyclone DDS. diff --git a/rmw_cyclonedds_cpp/src/rmw_node.cpp b/rmw_cyclonedds_cpp/src/rmw_node.cpp index 46414858..505892d8 100644 --- a/rmw_cyclonedds_cpp/src/rmw_node.cpp +++ b/rmw_cyclonedds_cpp/src/rmw_node.cpp @@ -26,9 +26,11 @@ #include #include +#include "rcutils/filesystem.h" #include "rcutils/get_env.h" #include "rcutils/logging_macros.h" #include "rcutils/strdup.h" +#include "rcutils/format_string.h" #include "rmw/allocators.h" #include "rmw/convert_rcutils_ret_to_rmw_ret.h" @@ -642,6 +644,80 @@ static std::string get_node_user_data(const char * node_name, const char * node_ std::string(";"); } +/* Returns the full URI of a security file properly formatted for DDS */ +char * get_security_file_URI( + const char * security_filename, const char * node_secure_root, + const rcutils_allocator_t allocator) +{ + char * ret; + + char * file_path = rcutils_join_path(node_secure_root, security_filename, allocator); + if (file_path == nullptr) { + ret = nullptr; + } else if (!rcutils_is_readable(file_path)) { + RCUTILS_LOG_ERROR_NAMED( + "rmw_cyclonedds_cpp", "get_security_file: %s not found", file_path); + ret = nullptr; + allocator.deallocate(file_path, allocator.state); + } else { + /* Cyclone also supports a "data:" URI */ + ret = rcutils_format_string(allocator, "file:%s", file_path); + allocator.deallocate(file_path, allocator.state); + } + return ret; +} + +void store_security_filepath_in_qos( + dds_qos_t * qos, const char * qos_property_name, const char * file_name, + const rmw_node_security_options_t * security_options) +{ + rcutils_allocator_t allocator = rcutils_get_default_allocator(); + + char * security_file_path = get_security_file_URI( + file_name, security_options->security_root_path, allocator); + if (security_file_path != nullptr) { + dds_qset_prop(qos, qos_property_name, security_file_path); + allocator.deallocate(security_file_path, allocator.state); + } +} + +/* Set all the qos properties needed to enable DDS security */ +void configure_qos_for_security( + dds_qos_t * qos, const rmw_node_security_options_t * security_options) +{ + /* File path is set to nullptr if file does not exist or is not readable */ + store_security_filepath_in_qos( + qos, "dds.sec.auth.identity_ca", "identity_ca.cert.pem", + security_options); + store_security_filepath_in_qos( + qos, "dds.sec.auth.identity_certificate", "cert.pem", + security_options); + store_security_filepath_in_qos( + qos, "dds.sec.auth.private_key", "key.pem", + security_options); + store_security_filepath_in_qos( + qos, "dds.sec.access.permissions_ca", "permissions_ca.cert.pem", + security_options); + store_security_filepath_in_qos( + qos, "dds.sec.access.governance", "governance.p7s", + security_options); + store_security_filepath_in_qos( + qos, "dds.sec.access.permissions", "permissions.p7s", + security_options); + + dds_qset_prop(qos, "dds.sec.auth.library.path", "libdds_security_auth.so"); + dds_qset_prop(qos, "dds.sec.auth.library.init", "init_authentication"); + dds_qset_prop(qos, "dds.sec.auth.library.finalize", "finalize_authentication"); + + dds_qset_prop(qos, "dds.sec.crypto.library.path", "libdds_security_crypto.so"); + dds_qset_prop(qos, "dds.sec.crypto.library.init", "init_crypto"); + dds_qset_prop(qos, "dds.sec.crypto.library.finalize", "finalize_crypto"); + + dds_qset_prop(qos, "dds.sec.access.library.path", "libdds_security_ac.so"); + dds_qset_prop(qos, "dds.sec.access.library.init", "init_access_control"); + dds_qset_prop(qos, "dds.sec.access.library.finalize", "finalize_access_control"); +} + extern "C" rmw_node_t * rmw_create_node( rmw_context_t * context, const char * name, const char * namespace_, size_t domain_id, @@ -666,7 +742,13 @@ extern "C" rmw_node_t * rmw_create_node( static_cast(domain_id); const dds_domainid_t did = DDS_DOMAIN_DEFAULT; #endif - (void) security_options; + + if (security_options == nullptr) { + RCUTILS_LOG_ERROR_NAMED( + "rmw_cyclonedds_cpp", "rmw_create_node: security options null"); + return nullptr; + } + rmw_ret_t ret; int dummy_validation_result; size_t dummy_invalid_index; @@ -688,8 +770,18 @@ extern "C" rmw_node_t * rmw_create_node( #endif dds_qos_t * qos = dds_create_qos(); + if (qos == nullptr) { + RCUTILS_LOG_ERROR_NAMED( + "rmw_cyclonedds_cpp", "rmw_create_node: Unable to create qos"); + return nullptr; + } std::string user_data = get_node_user_data(name, namespace_); dds_qset_userdata(qos, user_data.c_str(), user_data.size()); + + if (security_options->enforce_security) { + configure_qos_for_security(qos, security_options); + } + dds_entity_t pp = dds_create_participant(did, qos, nullptr); dds_delete_qos(qos); if (pp < 0) { From bca0852f502261484da318330f02a400473240b3 Mon Sep 17 00:00:00 2001 From: Sid Faber Date: Mon, 23 Mar 2020 21:13:19 +0000 Subject: [PATCH 2/5] Update conditional compile logic Add in conditional compile based on ENABLE_SECURITY make flag and Cyclone DDS feature availability. Also addressed review comments. Signed-off-by: Sid Faber --- rmw_cyclonedds_cpp/src/rmw_node.cpp | 50 ++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/rmw_cyclonedds_cpp/src/rmw_node.cpp b/rmw_cyclonedds_cpp/src/rmw_node.cpp index 505892d8..5dc4406c 100644 --- a/rmw_cyclonedds_cpp/src/rmw_node.cpp +++ b/rmw_cyclonedds_cpp/src/rmw_node.cpp @@ -27,10 +27,10 @@ #include #include "rcutils/filesystem.h" +#include "rcutils/format_string.h" #include "rcutils/get_env.h" #include "rcutils/logging_macros.h" #include "rcutils/strdup.h" -#include "rcutils/format_string.h" #include "rmw/allocators.h" #include "rmw/convert_rcutils_ret_to_rmw_ret.h" @@ -61,6 +61,7 @@ #include "namespace_prefix.hpp" #include "dds/dds.h" +#include "dds/ddsc/dds_public_qos.h" #include "dds/ddsi/ddsi_sertopic.h" #include "rmw_cyclonedds_cpp/serdes.hpp" #include "serdata.hpp" @@ -81,6 +82,13 @@ #define SUPPORT_LOCALHOST 0 #endif +/* Security must be enabled when compiling and requires cyclone to support QOS property lists */ +#if DDS_HAS_SECURITY && DDS_HAS_PROPERTY_LIST_QOS +#define RMW_SUPPORT_SECURITY 1 +#else +#define RMW_SUPPORT_SECURITY 0 +#endif + /* Set to > 0 for printing warnings to stderr for each messages that was taken more than this many ms after writing */ #define REPORT_LATE_MESSAGES 0 @@ -644,6 +652,7 @@ static std::string get_node_user_data(const char * node_name, const char * node_ std::string(";"); } +#if RMW_SUPPORT_SECURITY /* Returns the full URI of a security file properly formatted for DDS */ char * get_security_file_URI( const char * security_filename, const char * node_secure_root, @@ -680,11 +689,14 @@ void store_security_filepath_in_qos( allocator.deallocate(security_file_path, allocator.state); } } +#endif /* RMW_SUPPORT_SECURITY */ /* Set all the qos properties needed to enable DDS security */ -void configure_qos_for_security( +rmw_ret_t configure_qos_for_security( dds_qos_t * qos, const rmw_node_security_options_t * security_options) { + +#if RMW_SUPPORT_SECURITY /* File path is set to nullptr if file does not exist or is not readable */ store_security_filepath_in_qos( qos, "dds.sec.auth.identity_ca", "identity_ca.cert.pem", @@ -705,17 +717,28 @@ void configure_qos_for_security( qos, "dds.sec.access.permissions", "permissions.p7s", security_options); - dds_qset_prop(qos, "dds.sec.auth.library.path", "libdds_security_auth.so"); + dds_qset_prop(qos, "dds.sec.auth.library.path", "dds_security_auth"); dds_qset_prop(qos, "dds.sec.auth.library.init", "init_authentication"); dds_qset_prop(qos, "dds.sec.auth.library.finalize", "finalize_authentication"); - dds_qset_prop(qos, "dds.sec.crypto.library.path", "libdds_security_crypto.so"); + dds_qset_prop(qos, "dds.sec.crypto.library.path", "dds_security_crypto"); dds_qset_prop(qos, "dds.sec.crypto.library.init", "init_crypto"); dds_qset_prop(qos, "dds.sec.crypto.library.finalize", "finalize_crypto"); - dds_qset_prop(qos, "dds.sec.access.library.path", "libdds_security_ac.so"); + dds_qset_prop(qos, "dds.sec.access.library.path", "dds_security_ac"); dds_qset_prop(qos, "dds.sec.access.library.init", "init_access_control"); dds_qset_prop(qos, "dds.sec.access.library.finalize", "finalize_access_control"); + + return RMW_RET_OK; +#else + (void) qos; + (void) security_options; + RMW_SET_ERROR_MSG( + "Security was requested but the Cyclone DDS being used does not have security " + "support enabled. Recompile Cyclone DDS with the '-DENABLE_SECURITY=ON' " + "CMake option"); + return RMW_RET_UNSUPPORTED; +#endif } extern "C" rmw_node_t * rmw_create_node( @@ -743,11 +766,7 @@ extern "C" rmw_node_t * rmw_create_node( const dds_domainid_t did = DDS_DOMAIN_DEFAULT; #endif - if (security_options == nullptr) { - RCUTILS_LOG_ERROR_NAMED( - "rmw_cyclonedds_cpp", "rmw_create_node: security options null"); - return nullptr; - } + RCUTILS_CHECK_ARGUMENT_FOR_NULL(security_options, nullptr); rmw_ret_t ret; int dummy_validation_result; @@ -770,16 +789,15 @@ extern "C" rmw_node_t * rmw_create_node( #endif dds_qos_t * qos = dds_create_qos(); - if (qos == nullptr) { - RCUTILS_LOG_ERROR_NAMED( - "rmw_cyclonedds_cpp", "rmw_create_node: Unable to create qos"); - return nullptr; - } + RCUTILS_CHECK_FOR_NULL_WITH_MSG( + security_options, "rmw_create_node: Unable to create qos", return nullptr); std::string user_data = get_node_user_data(name, namespace_); dds_qset_userdata(qos, user_data.c_str(), user_data.size()); if (security_options->enforce_security) { - configure_qos_for_security(qos, security_options); + if (configure_qos_for_security(qos, security_options) != RMW_RET_OK) { + return nullptr; + } } dds_entity_t pp = dds_create_participant(did, qos, nullptr); From 5e934200cebbda70cf0028031349bf2b0e64a9ed Mon Sep 17 00:00:00 2001 From: Sid Faber <56845980+SidFaber@users.noreply.github.com> Date: Fri, 27 Mar 2020 11:54:55 -0400 Subject: [PATCH 3/5] Fix memory leaks Also remove superfluous include and blank line. Signed-off-by: Sid Faber --- rmw_cyclonedds_cpp/src/rmw_node.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rmw_cyclonedds_cpp/src/rmw_node.cpp b/rmw_cyclonedds_cpp/src/rmw_node.cpp index 5dc4406c..993758c7 100644 --- a/rmw_cyclonedds_cpp/src/rmw_node.cpp +++ b/rmw_cyclonedds_cpp/src/rmw_node.cpp @@ -61,7 +61,6 @@ #include "namespace_prefix.hpp" #include "dds/dds.h" -#include "dds/ddsc/dds_public_qos.h" #include "dds/ddsi/ddsi_sertopic.h" #include "rmw_cyclonedds_cpp/serdes.hpp" #include "serdata.hpp" @@ -695,7 +694,6 @@ void store_security_filepath_in_qos( rmw_ret_t configure_qos_for_security( dds_qos_t * qos, const rmw_node_security_options_t * security_options) { - #if RMW_SUPPORT_SECURITY /* File path is set to nullptr if file does not exist or is not readable */ store_security_filepath_in_qos( @@ -789,13 +787,18 @@ extern "C" rmw_node_t * rmw_create_node( #endif dds_qos_t * qos = dds_create_qos(); - RCUTILS_CHECK_FOR_NULL_WITH_MSG( - security_options, "rmw_create_node: Unable to create qos", return nullptr); + if (qos == nullptr) { + RCUTILS_LOG_ERROR_NAMED("rmw_cyclonedds_cpp", "rmw_create_node: Unable to create qos"); + node_gone_from_domain_locked(did); + return nullptr; + } std::string user_data = get_node_user_data(name, namespace_); dds_qset_userdata(qos, user_data.c_str(), user_data.size()); if (security_options->enforce_security) { if (configure_qos_for_security(qos, security_options) != RMW_RET_OK) { + dds_delete_qos(qos); + node_gone_from_domain_locked(did); return nullptr; } } From 1ca2269e6640881072eb3cc61d42666ea947adf2 Mon Sep 17 00:00:00 2001 From: Sid Faber <56845980+SidFaber@users.noreply.github.com> Date: Thu, 2 Apr 2020 12:36:31 -0400 Subject: [PATCH 4/5] Improve security logic and memory management Properly handle downstream effects of ROS_SECURITY_STRATEGY and ROS_SECURITY_ENABLE environment variables through security_options. Improve memory management and make sure to only set security qos properties when all files are sure to exist. --- rmw_cyclonedds_cpp/src/rmw_node.cpp | 164 +++++++++++++++++----------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/rmw_cyclonedds_cpp/src/rmw_node.cpp b/rmw_cyclonedds_cpp/src/rmw_node.cpp index 993758c7..8187cf29 100644 --- a/rmw_cyclonedds_cpp/src/rmw_node.cpp +++ b/rmw_cyclonedds_cpp/src/rmw_node.cpp @@ -147,6 +147,18 @@ struct builtin_readers dds_entity_t rds[sizeof(builtin_topics) / sizeof(builtin_topics[0])]; }; +#if RMW_SUPPORT_SECURITY +struct dds_security_files_t +{ + char * identity_ca_cert = nullptr; + char * cert = nullptr; + char * key = nullptr; + char * permissions_ca_cert = nullptr; + char * governance_p7s = nullptr; + char * permissions_p7s = nullptr; +}; +#endif + struct CddsEntity { dds_entity_t enth; @@ -653,81 +665,108 @@ static std::string get_node_user_data(const char * node_name, const char * node_ #if RMW_SUPPORT_SECURITY /* Returns the full URI of a security file properly formatted for DDS */ -char * get_security_file_URI( - const char * security_filename, const char * node_secure_root, +bool get_security_file_URI( + char ** security_file, const char * security_filename, const char * node_secure_root, const rcutils_allocator_t allocator) { - char * ret; - + *security_file = nullptr; char * file_path = rcutils_join_path(node_secure_root, security_filename, allocator); - if (file_path == nullptr) { - ret = nullptr; - } else if (!rcutils_is_readable(file_path)) { - RCUTILS_LOG_ERROR_NAMED( - "rmw_cyclonedds_cpp", "get_security_file: %s not found", file_path); - ret = nullptr; - allocator.deallocate(file_path, allocator.state); - } else { - /* Cyclone also supports a "data:" URI */ - ret = rcutils_format_string(allocator, "file:%s", file_path); - allocator.deallocate(file_path, allocator.state); + if (file_path != nullptr) { + if (rcutils_is_readable(file_path)) { + /* Cyclone also supports a "data:" URI */ + *security_file = rcutils_format_string(allocator, "file:%s", file_path); + allocator.deallocate(file_path, allocator.state); + } else { + RCUTILS_LOG_INFO_NAMED( + "rmw_cyclonedds_cpp", "get_security_file_URI: %s not found", file_path); + allocator.deallocate(file_path, allocator.state); + } + } + return *security_file != nullptr; +} + +bool get_security_file_URIs( + const rmw_node_security_options_t * security_options, + dds_security_files_t & dds_security_files, rcutils_allocator_t allocator) +{ + bool ret = false; + + if (security_options->security_root_path != nullptr) { + ret = ( + get_security_file_URI( + &dds_security_files.identity_ca_cert, "identity_ca.cert.pem", + security_options->security_root_path, allocator) && + get_security_file_URI( + &dds_security_files.cert, "cert.pem", + security_options->security_root_path, allocator) && + get_security_file_URI( + &dds_security_files.key, "key.pem", + security_options->security_root_path, allocator) && + get_security_file_URI( + &dds_security_files.permissions_ca_cert, "permissions_ca.cert.pem", + security_options->security_root_path, allocator) && + get_security_file_URI( + &dds_security_files.governance_p7s, "governance.p7s", + security_options->security_root_path, allocator) && + get_security_file_URI( + &dds_security_files.permissions_p7s, "permissions.p7s", + security_options->security_root_path, allocator)); } return ret; } -void store_security_filepath_in_qos( - dds_qos_t * qos, const char * qos_property_name, const char * file_name, - const rmw_node_security_options_t * security_options) +void finalize_security_file_URIs( + dds_security_files_t dds_security_files, const rcutils_allocator_t allocator) { - rcutils_allocator_t allocator = rcutils_get_default_allocator(); - - char * security_file_path = get_security_file_URI( - file_name, security_options->security_root_path, allocator); - if (security_file_path != nullptr) { - dds_qset_prop(qos, qos_property_name, security_file_path); - allocator.deallocate(security_file_path, allocator.state); - } + allocator.deallocate(dds_security_files.identity_ca_cert, allocator.state); + dds_security_files.identity_ca_cert = nullptr; + allocator.deallocate(dds_security_files.cert, allocator.state); + dds_security_files.cert = nullptr; + allocator.deallocate(dds_security_files.key, allocator.state); + dds_security_files.key = nullptr; + allocator.deallocate(dds_security_files.permissions_ca_cert, allocator.state); + dds_security_files.permissions_ca_cert = nullptr; + allocator.deallocate(dds_security_files.governance_p7s, allocator.state); + dds_security_files.governance_p7s = nullptr; + allocator.deallocate(dds_security_files.permissions_p7s, allocator.state); + dds_security_files.permissions_p7s = nullptr; } + #endif /* RMW_SUPPORT_SECURITY */ -/* Set all the qos properties needed to enable DDS security */ +/* Attempt to set all the qos properties needed to enable DDS security */ rmw_ret_t configure_qos_for_security( dds_qos_t * qos, const rmw_node_security_options_t * security_options) { #if RMW_SUPPORT_SECURITY - /* File path is set to nullptr if file does not exist or is not readable */ - store_security_filepath_in_qos( - qos, "dds.sec.auth.identity_ca", "identity_ca.cert.pem", - security_options); - store_security_filepath_in_qos( - qos, "dds.sec.auth.identity_certificate", "cert.pem", - security_options); - store_security_filepath_in_qos( - qos, "dds.sec.auth.private_key", "key.pem", - security_options); - store_security_filepath_in_qos( - qos, "dds.sec.access.permissions_ca", "permissions_ca.cert.pem", - security_options); - store_security_filepath_in_qos( - qos, "dds.sec.access.governance", "governance.p7s", - security_options); - store_security_filepath_in_qos( - qos, "dds.sec.access.permissions", "permissions.p7s", - security_options); - - dds_qset_prop(qos, "dds.sec.auth.library.path", "dds_security_auth"); - dds_qset_prop(qos, "dds.sec.auth.library.init", "init_authentication"); - dds_qset_prop(qos, "dds.sec.auth.library.finalize", "finalize_authentication"); - - dds_qset_prop(qos, "dds.sec.crypto.library.path", "dds_security_crypto"); - dds_qset_prop(qos, "dds.sec.crypto.library.init", "init_crypto"); - dds_qset_prop(qos, "dds.sec.crypto.library.finalize", "finalize_crypto"); - - dds_qset_prop(qos, "dds.sec.access.library.path", "dds_security_ac"); - dds_qset_prop(qos, "dds.sec.access.library.init", "init_access_control"); - dds_qset_prop(qos, "dds.sec.access.library.finalize", "finalize_access_control"); + rmw_ret_t ret = RMW_RET_UNSUPPORTED; + dds_security_files_t dds_security_files; + rcutils_allocator_t allocator = rcutils_get_default_allocator(); - return RMW_RET_OK; + if (get_security_file_URIs(security_options, dds_security_files, allocator)) { + dds_qset_prop(qos, "dds.sec.auth.identity_ca", dds_security_files.identity_ca_cert); + dds_qset_prop(qos, "dds.sec.auth.identity_certificate", dds_security_files.cert); + dds_qset_prop(qos, "dds.sec.auth.private_key", dds_security_files.key); + dds_qset_prop(qos, "dds.sec.access.permissions_ca", dds_security_files.permissions_ca_cert); + dds_qset_prop(qos, "dds.sec.access.governance", dds_security_files.governance_p7s); + dds_qset_prop(qos, "dds.sec.access.permissions", dds_security_files.permissions_p7s); + + dds_qset_prop(qos, "dds.sec.auth.library.path", "dds_security_auth"); + dds_qset_prop(qos, "dds.sec.auth.library.init", "init_authentication"); + dds_qset_prop(qos, "dds.sec.auth.library.finalize", "finalize_authentication"); + + dds_qset_prop(qos, "dds.sec.crypto.library.path", "dds_security_crypto"); + dds_qset_prop(qos, "dds.sec.crypto.library.init", "init_crypto"); + dds_qset_prop(qos, "dds.sec.crypto.library.finalize", "finalize_crypto"); + + dds_qset_prop(qos, "dds.sec.access.library.path", "dds_security_ac"); + dds_qset_prop(qos, "dds.sec.access.library.init", "init_access_control"); + dds_qset_prop(qos, "dds.sec.access.library.finalize", "finalize_access_control"); + + ret = RMW_RET_OK; + } + finalize_security_file_URIs(dds_security_files, allocator); + return ret; #else (void) qos; (void) security_options; @@ -795,11 +834,14 @@ extern "C" rmw_node_t * rmw_create_node( std::string user_data = get_node_user_data(name, namespace_); dds_qset_userdata(qos, user_data.c_str(), user_data.size()); - if (security_options->enforce_security) { - if (configure_qos_for_security(qos, security_options) != RMW_RET_OK) { + if (configure_qos_for_security(qos, security_options) != RMW_RET_OK) { + if (security_options->enforce_security == RMW_SECURITY_ENFORCEMENT_ENFORCE) { dds_delete_qos(qos); node_gone_from_domain_locked(did); return nullptr; + } else { + RCUTILS_LOG_INFO_NAMED( + "rmw_cyclonedds_cpp", "rmw_create_node: Unable to configure security"); } } From 68d8b523ef1205088e132ceaa783bf6239dabc0e Mon Sep 17 00:00:00 2001 From: Sid Faber <56845980+SidFaber@users.noreply.github.com> Date: Thu, 2 Apr 2020 13:15:06 -0400 Subject: [PATCH 5/5] Removed stray newline Fix residue from merge conflict in README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e5861682..1879246b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ Cyclone DDS is ready to use. It seeks to give the fastest, easiest, and most rob ## Building from source and contributing - Note the `master` branch maintains compatibility with ROS releases Dashing and later, including the not-yet-released [*Foxy*](https://index.ros.org/doc/ros2/Releases/Release-Foxy-Fitzroy/). If building ROS2 from source ([ros2.repos](https://github.com/ros2/ros2/blob/master/ros2.repos)), you already have this package and Cyclone DDS: