diff --git a/include/ipfixprobe/ipfix-elements.hpp b/include/ipfixprobe/ipfix-elements.hpp index 76c15adf3..2bf35b337 100644 --- a/include/ipfixprobe/ipfix-elements.hpp +++ b/include/ipfixprobe/ipfix-elements.hpp @@ -306,6 +306,8 @@ namespace ipxp { #define MPLS_TOP_LABEL_STACK_SECTION F(0, 70, -1, nullptr) +#define TCP_RTT(F) F(8057, 904, 8, nullptr) + /** * IPFIX Templates - list of elements * @@ -461,6 +463,8 @@ namespace ipxp { F(MQTT_PUBLISH_FLAGS) \ F(MQTT_TOPICS) +#define IPFIX_TCP_RTT_TEMPLATE(F) F(TCP_RTT) + #define IPFIX_PSTATS_TEMPLATE(F) \ F(STATS_PCKT_SIZES) \ F(STATS_PCKT_TIMESTAMPS) \ diff --git a/pkg/rpm/ipfixprobe-msec.spec.in b/pkg/rpm/ipfixprobe-msec.spec.in index 85d41da55..81ee6a4f8 100644 --- a/pkg/rpm/ipfixprobe-msec.spec.in +++ b/pkg/rpm/ipfixprobe-msec.spec.in @@ -98,6 +98,7 @@ source /opt/rh/gcc-toolset-14/enable %{_libdir}/ipfixprobe/process/libipfixprobe-process-passivedns.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssadetector.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssdp.so +%{_libdir}/ipfixprobe/process/libipfixprobe-process-tcp-rtt.so %{_libdir}/ipfixprobe/storage/libipfixprobe-storage-cache.so diff --git a/pkg/rpm/ipfixprobe-nemea.spec.in b/pkg/rpm/ipfixprobe-nemea.spec.in index 9d097ad73..a4a9f5133 100644 --- a/pkg/rpm/ipfixprobe-nemea.spec.in +++ b/pkg/rpm/ipfixprobe-nemea.spec.in @@ -121,6 +121,7 @@ source /opt/rh/gcc-toolset-14/enable %{_libdir}/ipfixprobe/process/libipfixprobe-process-passivedns.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssadetector.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssdp.so +%{_libdir}/ipfixprobe/process/libipfixprobe-process-tcp-rtt.so %{_libdir}/ipfixprobe/storage/libipfixprobe-storage-cache.so diff --git a/pkg/rpm/ipfixprobe.spec.in b/pkg/rpm/ipfixprobe.spec.in index 7beca85c0..4cc6c4aae 100644 --- a/pkg/rpm/ipfixprobe.spec.in +++ b/pkg/rpm/ipfixprobe.spec.in @@ -142,6 +142,7 @@ source /opt/rh/gcc-toolset-14/enable %{_libdir}/ipfixprobe/process/libipfixprobe-process-passivedns.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssadetector.so %{_libdir}/ipfixprobe/process/libipfixprobe-process-ssdp.so +%{_libdir}/ipfixprobe/process/libipfixprobe-process-tcp-rtt.so %{_libdir}/ipfixprobe/storage/libipfixprobe-storage-cache.so diff --git a/src/plugins/process/CMakeLists.txt b/src/plugins/process/CMakeLists.txt index a47322075..25650a882 100644 --- a/src/plugins/process/CMakeLists.txt +++ b/src/plugins/process/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(smtp) add_subdirectory(quic) add_subdirectory(tls) add_subdirectory(http) +add_subdirectory(tcpRtt) if (ENABLE_PROCESS_EXPERIMENTAL) add_subdirectory(sip) diff --git a/src/plugins/process/tcpRtt/CMakeLists.txt b/src/plugins/process/tcpRtt/CMakeLists.txt new file mode 100644 index 000000000..d3e51f080 --- /dev/null +++ b/src/plugins/process/tcpRtt/CMakeLists.txt @@ -0,0 +1,31 @@ +project(ipfixprobe-process-tcp-rtt VERSION 1.0.0 DESCRIPTION "ipfixprobe-process-tcp-rtt plugin") + +add_library(ipfixprobe-process-tcp-rtt MODULE + src/tcpRtt.cpp + src/tcpRtt.hpp +) + +set_target_properties(ipfixprobe-process-tcp-rtt PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +target_include_directories(ipfixprobe-process-tcp-rtt PRIVATE + ${CMAKE_SOURCE_DIR}/include/ +) + +target_link_libraries(ipfixprobe-process-tcp-rtt PRIVATE + ipfixprobe-output-ipfix +) + +if(ENABLE_NEMEA) + target_link_libraries(ipfixprobe-process-tcp-rtt PRIVATE + -Wl,--whole-archive ipfixprobe-nemea-fields -Wl,--no-whole-archive + unirec::unirec + trap::trap + ) +endif() + +install(TARGETS ipfixprobe-process-tcp-rtt + LIBRARY DESTINATION "${INSTALL_DIR_LIB}/ipfixprobe/process/" +) diff --git a/src/plugins/process/tcpRtt/README.md b/src/plugins/process/tcpRtt/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/process/tcpRtt/src/tcpRtt.cpp b/src/plugins/process/tcpRtt/src/tcpRtt.cpp new file mode 100644 index 000000000..822daf093 --- /dev/null +++ b/src/plugins/process/tcpRtt/src/tcpRtt.cpp @@ -0,0 +1,107 @@ +/** + * @file + * @brief Plugin for accounting round trip time of tcp handshakes. + * @author Damir Zainullin + * + * Copyright (c) 2025 CESNET + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "tcpRtt.hpp" + +#include +#include + +namespace ipxp { + +static const PluginManifest tcpRttPluginManifest = { + .name = "tcprtt", + .description = "Process plugin to obtain round trip time of TCP connection.", + .pluginVersion = "1.0.0", + .apiVersion = "1.0.0", + .usage = + []() { + OptionsParser parser("tcprtt", "Calculate tcp rtt"); + parser.usage(std::cout); + }, +}; + +TCPRTTPlugin::TCPRTTPlugin(const std::string& params, int pluginID) + : ProcessPlugin(pluginID) +{ + init(params.c_str()); +} + +OptionsParser* TCPRTTPlugin::get_parser() const +{ + return new OptionsParser("tcprtt", "Calculate tcp rtt"); +} + +std::string TCPRTTPlugin::get_name() const +{ + return "tcprtt"; +} + +RecordExtTCPRTT* TCPRTTPlugin::get_ext() const +{ + return new RecordExtTCPRTT(m_pluginID); +} + +void TCPRTTPlugin::init([[maybe_unused]] const char* params) {} + +TCPRTTPlugin::TCPRTTPlugin(const TCPRTTPlugin& other) noexcept + : ProcessPlugin(other.m_pluginID) +{ +} + +ProcessPlugin* TCPRTTPlugin::copy() +{ + return new TCPRTTPlugin(*this); +} + +int TCPRTTPlugin::post_create(Flow& rec, const Packet& pkt) +{ + if (m_prealloced_extension == nullptr) { + m_prealloced_extension.reset(get_ext()); + } + + if (pkt.ip_proto == IPPROTO_TCP) { + rec.add_extension(m_prealloced_extension.release()); + } + + update_tcp_rtt_record(rec, pkt); + return 0; +} + +int TCPRTTPlugin::pre_update(Flow& rec, Packet& pkt) +{ + update_tcp_rtt_record(rec, pkt); + return 0; +} + +constexpr static inline bool is_tcp_syn(uint8_t tcp_flags) noexcept +{ + return tcp_flags & 0b10; +} + +constexpr static inline bool is_tcp_syn_ack(uint8_t tcp_flags) noexcept +{ + return (tcp_flags & 0b10) && (tcp_flags & 0b10000); +} + +void TCPRTTPlugin::update_tcp_rtt_record(Flow& rec, const Packet& pkt) noexcept +{ + auto* extension = static_cast(rec.get_extension(m_pluginID)); + + if (extension != nullptr && is_tcp_syn_ack(pkt.tcp_flags)) { + extension->tcp_synack_timestamp = pkt.ts; + } else if (extension != nullptr && is_tcp_syn(pkt.tcp_flags)) { + extension->tcp_syn_timestamp = pkt.ts; + } +} + +static const PluginRegistrar + tcpRttRegistrar(tcpRttPluginManifest); + +} // namespace ipxp diff --git a/src/plugins/process/tcpRtt/src/tcpRtt.hpp b/src/plugins/process/tcpRtt/src/tcpRtt.hpp new file mode 100644 index 000000000..ad24954d4 --- /dev/null +++ b/src/plugins/process/tcpRtt/src/tcpRtt.hpp @@ -0,0 +1,153 @@ +/** + * @file + * @brief Plugin for accounting round trip time of tcp handshakes. + * @author Damir Zainullin + * + * Copyright (c) 2025 CESNET + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef WITH_NEMEA +#include "fields.h" +#endif + +using namespace std::chrono_literals; + +namespace ipxp { + +#define TCPRTT_UNIREC_TEMPLATE "TCPRTT_TIME" +UR_FIELDS(uint64 TCPRTT_TIME) + +/** + * @brief Convert timeval struct to count of milliseconds since epoch + * @param timeval Timeval to convert + * @return Count of milliseconds since epoch + */ +constexpr static inline uint64_t timeval_to_msec(timeval timeval) noexcept +{ + constexpr std::size_t MSEC_IN_SEC = std::chrono::milliseconds(1s).count(); + constexpr std::size_t USEC_IN_MSEC = std::chrono::microseconds(1ms).count(); + return timeval.tv_sec * MSEC_IN_SEC + timeval.tv_usec / USEC_IN_MSEC; +} + +/** + * \brief Flow record extension header for storing observed handshake timestamps. + */ +struct RecordExtTCPRTT : public RecordExt { +private: + constexpr static timeval NO_TIMESTAMP = timeval {std::numeric_limits::min(), 0}; + + constexpr inline static bool has_no_value(timeval timeval) noexcept + { + return timeval.tv_sec == NO_TIMESTAMP.tv_sec && timeval.tv_usec == NO_TIMESTAMP.tv_usec; + } + +public: + timeval tcp_syn_timestamp {NO_TIMESTAMP}; ///< Timestamp of last observed TCP SYN packet + timeval tcp_synack_timestamp {NO_TIMESTAMP}; ///< Timestamp of last observed TCP SYNACK packet + + RecordExtTCPRTT(int pluginID) + : RecordExt(pluginID) + { + } + +#ifdef WITH_NEMEA + virtual void fill_unirec(ur_template_t* tmplt, void* record) + { + if (has_no_value(tcp_syn_timestamp) || has_no_value(tcp_synack_timestamp)) { + ur_set(tmplt, record, F_TCPRTT_TIME, std::numeric_limits::max()); + return; + } + + const ur_time_t round_trip_time = ur_timediff( + ur_time_from_sec_usec(tcp_synack_timestamp.tv_sec, tcp_synack_timestamp.tv_usec), + ur_time_from_sec_usec(tcp_syn_timestamp.tv_sec, tcp_syn_timestamp.tv_usec)); + ur_set(tmplt, record, F_TCPRTT_TIME, round_trip_time); + } + + const char* get_unirec_tmplt() const { return TCPRTT_UNIREC_TEMPLATE; } + +#endif // ifdef WITH_NEMEA + + int fill_ipfix(uint8_t* buffer, int size) override + { + if (size < static_cast(sizeof(uint64_t))) { + return -1; + } + + if (has_no_value(tcp_syn_timestamp) || has_no_value(tcp_synack_timestamp)) { + *reinterpret_cast(buffer) = std::numeric_limits::max(); + return static_cast(sizeof(uint64_t)); + } + + const uint64_t round_trip_time + = timeval_to_msec(tcp_synack_timestamp) - timeval_to_msec(tcp_syn_timestamp); + *reinterpret_cast(buffer) = round_trip_time; + return static_cast(sizeof(round_trip_time)); + } + + const char** get_ipfix_tmplt() const + { + static const char* ipfix_template[] = {IPFIX_TLS_TEMPLATE(IPFIX_FIELD_NAMES) nullptr}; + + return ipfix_template; + } + + std::string get_text() const override + { + std::ostringstream out; + + if (has_no_value(tcp_syn_timestamp) || has_no_value(tcp_synack_timestamp)) { + out << "tcprtt = UNKNOWN"; + } else { + out << "tcprtt = " + << timeval_to_msec(tcp_synack_timestamp) - timeval_to_msec(tcp_syn_timestamp); + } + + return out.str(); + } +}; + +class TCPRTTPlugin : public ProcessPlugin { +public: + TCPRTTPlugin(const std::string& params, int pluginID); + + TCPRTTPlugin(const TCPRTTPlugin&) noexcept; + + ~TCPRTTPlugin() override = default; + + void init(const char* params) override; + + OptionsParser* get_parser() const override; + + std::string get_name() const override; + + RecordExtTCPRTT* get_ext() const override; + + ProcessPlugin* copy(); + + int post_create(Flow& rec, const Packet& pkt) override; + + int pre_update(Flow& rec, Packet& pkt) override; + +private: + void update_tcp_rtt_record(Flow& rec, const Packet& pkt) noexcept; + + std::unique_ptr m_prealloced_extension {get_ext()}; +}; + +} // namespace ipxp diff --git a/tests/functional/CMakeLists.txt b/tests/functional/CMakeLists.txt index a3a476e7d..c7004ef90 100644 --- a/tests/functional/CMakeLists.txt +++ b/tests/functional/CMakeLists.txt @@ -24,6 +24,7 @@ add_process_plugin_test(QuicProcessPlugin quic quic_initial-sample.pcap) add_process_plugin_test(SmtpProcessPlugin smtp smtp.pcap) add_process_plugin_test(SsadetectorProcessPlugin ssadetector ovpn.pcap) add_process_plugin_test(SsdpProcessPlugin ssdp ssdp.pcap) +add_process_plugin_test(TcpRttProcessPlugin tcprtt rtsp.pcap) add_process_plugin_test(TlsProcessPlugin tls tls.pcap) add_process_plugin_test(VlanProcessPlugin vlan vlan.pcap) add_process_plugin_test(WgProcessPlugin wg wg.pcap) diff --git a/tests/functional/outputs/tcprtt b/tests/functional/outputs/tcprtt new file mode 100644 index 000000000..1cffc6696 --- /dev/null +++ b/tests/functional/outputs/tcprtt @@ -0,0 +1,3 @@ +82.211.92.253,81.131.231.67,1124,471,0,1652,2005-07-03T09:52:37.027000,2005-07-03T09:52:45.019000,bc:df:20:00:02:00,00:00:02:00:00:00,6,5,554,3925,0,6,27,27 +82.211.92.253,81.131.231.67,646,431,0,1238,2005-07-03T09:52:43.781000,2005-07-03T09:52:49.337000,bc:df:20:00:02:00,00:00:02:00:00:00,5,4,554,3937,0,6,27,27 +ipaddr DST_IP,ipaddr SRC_IP,uint64 BYTES,uint64 BYTES_REV,uint64 LINK_BIT_FIELD,uint64 TCPRTT_TIME,time TIME_FIRST,time TIME_LAST,macaddr DST_MAC,macaddr SRC_MAC,uint32 PACKETS,uint32 PACKETS_REV,uint16 DST_PORT,uint16 SRC_PORT,uint8 DIR_BIT_FIELD,uint8 PROTOCOL,uint8 TCP_FLAGS,uint8 TCP_FLAGS_REV