diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dee6c5a2..d4023749 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,12 +15,19 @@ jobs: strategy: fail-fast: false matrix: - image: ['ubuntu:18.04', 'ubuntu:20.04', 'debian:stretch', 'debian:buster', 'debian:bullseye', 'centos:7', 'centos:8'] + image: + - 'ubuntu:18.04' + - 'ubuntu:20.04' + - 'debian:stretch' + - 'debian:buster' + - 'debian:bullseye' + - 'centos:7' + - 'quay.io/centos/centos:stream8' + - 'oraclelinux:8' name: Build on ${{ matrix.image }} container: ${{ matrix.image }} steps: - - uses: actions/checkout@v1 # Dependencies --------------------------------------------------------------------------- - name: Install dependencies for libfds and IPFIXcol2 (Ubuntu/Debian) @@ -32,23 +39,33 @@ jobs: apt-get -y install librdkafka-dev env: DEBIAN_FRONTEND: noninteractive - - name: Enable additional repositories (CentOS 8) - if: startsWith(matrix.image, 'centos:8') + - name: Enable additional repositories (CentOS Steam) + if: contains(matrix.image, 'centos:stream') run: | dnf -y install 'dnf-command(config-manager)' dnf config-manager --set-enabled appstream powertools - - name: Install dependencies for libfds and IPFIXcol2 (CentOS) - if: startsWith(matrix.image, 'centos') + - name: Enable additional repositories (Oracle Linux) + if: contains(matrix.image, 'oraclelinux') + run: | + dnf -y install 'dnf-command(config-manager)' + dnf config-manager --set-enabled ol8_appstream ol8_codeready_builder + - name: Enable EPEL (CentOS) + if: contains(matrix.image, 'centos') run: | yum -y install epel-release + - name: Enable EPEL (OracleLinux) + if: contains(matrix.image, 'oraclelinux') + run: | + dnf -y install oracle-epel-release-el8 + - name: Install dependencies for libfds and IPFIXcol2 (CentOS, Oracle Linux) + if: contains(matrix.image, 'centos') || contains(matrix.image, 'oraclelinux') + run: | yum -y install git gcc gcc-c++ cmake make libxml2-devel lz4-devel libzstd-devel yum -y install zlib-devel pkgconfig librdkafka-devel yum -y install python3-docutils || yum -y install python-docutils - - name: Install dependencies for libfds and IPFIXcol2 (Fedora) - if: startsWith(matrix.image, 'fedora') - run: | - dnf -y install git gcc gcc-c++ cmake make libxml2-devel lz4-devel libzstd-devel - dnf -y install python3-docutils zlib-devel pkgconfig librdkafka-devel + + # Checkout repository -------------------------------------------------------------------- + - uses: actions/checkout@v2 # Build libfds library ------------------------------------------------------------------ # Note: Master against master branch. Otherwise against debug branch. diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 30170954..8fc28285 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -22,13 +22,19 @@ jobs: container: ${{ matrix.image }} steps: - - uses: actions/checkout@v1 - - name: Define global variables - run: echo "::set-output name=zip_file::ipfixcol2-${IMAGE//:/}-$GITHUB_SHA.zip" - shell: bash - env: - IMAGE: ${{ matrix.image }} - id: vars + - uses: actions/checkout@v2 + - name: Define variables + uses: actions/github-script@v5 + with: + script: | + const sha = context.sha.substring(0, 8); + const image = `${{ matrix.image }}` + const distro = image.split('/').pop().replace(/:/g,'_'); + const zip = `ipfixcol2-${distro}-${sha}`; + core.exportVariable('ZIP_FILE', zip); + - name: Prepare environment + run: | + mkdir -p build/libfds_repo # Dependencies --------------------------------------------------------------------------- - name: Install dependencies for libfds and IPFIXcol2 (Ubuntu/Debian) @@ -67,17 +73,16 @@ jobs: ipfixcol2 -V ipfixcol2 -h ipfixcol2 -L - - name: Pack IPFIXcol2 DEB packages - working-directory: 'build/pkg/deb/debbuild/' - run: zip "$GITHUB_WORKSPACE/$ZIP_FILE" *.deb *.ddeb *.tar.gz *.dsc - env: - ZIP_FILE: ${{ steps.vars.outputs.zip_file }} - name: Archive DEB packages if: github.event_name == 'push' - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: - name: ${{ steps.vars.outputs.zip_file }} - path: ${{ steps.vars.outputs.zip_file }} + name: ${{ env.ZIP_FILE }} + path: | + build/pkg/deb/debbuild/*.deb + build/pkg/deb/debbuild/*.ddeb + build/pkg/deb/debbuild/*.tar.gz + build/pkg/deb/debbuild/*.dsc rpm: # Try to build RPM packages @@ -85,39 +90,56 @@ jobs: strategy: fail-fast: false matrix: - image: ['centos:7', 'centos:8'] + image: + - 'centos:7' + - 'quay.io/centos/centos:stream8' + - 'oraclelinux:8' name: Build RPMs on ${{ matrix.image }} container: ${{ matrix.image }} steps: - - uses: actions/checkout@v1 - - name: Prepare environment and variables + - name: Define variables + uses: actions/github-script@v5 + with: + script: | + const sha = context.sha.substring(0, 8); + const image = `${{ matrix.image }}` + const distro = image.split('/').pop().replace(/:/g,'_'); + const zip = `ipfixcol2-${distro}-${sha}`; + core.exportVariable('ZIP_FILE', zip); + - name: Prepare environment run: | - echo "::set-output name=zip_file::ipfixcol2-${IMAGE//:/}-$GITHUB_SHA.zip" mkdir -p build/libfds_repo - env: - IMAGE: ${{ matrix.image }} - id: vars # Dependencies --------------------------------------------------------------------------- - - name: Enable additional repositories (CentOS 8) - if: startsWith(matrix.image, 'centos:8') + - name: Enable additional repositories (CentOS Stream) + if: contains(matrix.image, 'centos:stream') run: | dnf -y install 'dnf-command(config-manager)' dnf config-manager --set-enabled appstream powertools - - name: Install dependencies for libfds and IPFIXcol2 (CentOS) - if: startsWith(matrix.image, 'centos') + - name: Enable additional repositories (Oracle Linux) + if: contains(matrix.image, 'oraclelinux') + run: | + dnf -y install 'dnf-command(config-manager)' + dnf config-manager --set-enabled ol8_appstream ol8_codeready_builder + - name: Enable EPEL (CentOS) + if: contains(matrix.image, 'centos') run: | yum -y install epel-release + - name: Enable EPEL (OracleLinux) + if: contains(matrix.image, 'oraclelinux') + run: | + dnf -y install oracle-epel-release-el8 + - name: Install dependencies for libfds and IPFIXcol2 (CentOS, Oracle Linux) + if: contains(matrix.image, 'centos') || contains(matrix.image, 'oraclelinux') + run: | yum -y install git gcc gcc-c++ cmake make libxml2-devel lz4-devel libzstd-devel yum -y install zlib-devel pkgconfig rpm-build librdkafka-devel yum -y install python3-docutils || yum -y install python-docutils - - name: Install depedencies for libfds and IPFIXcol2 (Fedora) - if: startsWith(matrix.image, 'fedora') - run: | - dnf -y install git gcc gcc-c++ cmake make libxml2-devel lz4-devel libzstd-devel - dnf -y install python3-docutils zlib-devel pkgconfig rpm-build librdkafka-devel + + # Checkout repository -------------------------------------------------------------------- + - uses: actions/checkout@v2 # Build LIBFDS RPM package --------------------------------------------------------------- - name: Checkout libfds library - master branch @@ -146,14 +168,11 @@ jobs: ipfixcol2 -V ipfixcol2 -h ipfixcol2 -L -v - - name: Pack IPFIXcol2 RPM packages - working-directory: 'build/pkg/rpm/rpmbuild' - run: zip -r "$GITHUB_WORKSPACE/$ZIP_FILE" RPMS SRPMS - env: - ZIP_FILE: ${{ steps.vars.outputs.zip_file }} - name: Archive RPM packages if: github.event_name == 'push' - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: - name: ${{ steps.vars.outputs.zip_file }} - path: ${{ steps.vars.outputs.zip_file }} + name: ${{ env.ZIP_FILE }} + path: | + build/pkg/rpm/rpmbuild/RPMS/ + build/pkg/rpm/rpmbuild/SRPMS/ diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d5e5ea..8dd95de3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ endif() # Versions and other informations set(IPFIXCOL_VERSION_MAJOR 2) -set(IPFIXCOL_VERSION_MINOR 2) +set(IPFIXCOL_VERSION_MINOR 3) set(IPFIXCOL_VERSION_PATCH 0) set(IPFIXCOL_VERSION ${IPFIXCOL_VERSION_MAJOR}.${IPFIXCOL_VERSION_MINOR}.${IPFIXCOL_VERSION_PATCH}) diff --git a/README.rst b/README.rst index ef3b3b17..d3df01b1 100644 --- a/README.rst +++ b/README.rst @@ -86,15 +86,23 @@ Second, install build dependencies of the collector yum install gcc gcc-c++ cmake make python3-docutils zlib-devel librdkafka-devel # Optionally: doxygen pkgconfig -* Note: latest systems (e.g. Fedora/CentOS 8) use ``dnf`` instead of ``yum``. +* Note: latest systems (e.g. Fedora/CentOS Stream 8) use ``dnf`` instead of ``yum``. * Note: package ``python3-docutils`` may by also named as ``python-docutils`` or ``python2-docutils`` * Note: package ``pkgconfig`` may by also named as ``pkg-config`` -* Note: CentOS 8 requires additional system repositories (``appstream`` and ``powertools``) to be enabled: +* Note: CentOS Stream 8 usually requires additional system repositories to be enabled: .. code-block:: + dnf -y install epel-release dnf config-manager --set-enabled appstream powertools +* Note: Oracle Linux 8 usually requires additional system repositories to be enabled: + +.. code-block:: + + dnf -y install oracle-epel-release-el8 + dnf config-manager --set-enabled ol8_appstream ol8_codeready_builder + **Debian/Ubuntu:** .. code-block:: diff --git a/extra_plugins/output/unirec/README.rst b/extra_plugins/output/unirec/README.rst index 39d383ae..36cab628 100644 --- a/extra_plugins/output/unirec/README.rst +++ b/extra_plugins/output/unirec/README.rst @@ -123,6 +123,10 @@ Parameters Specification of interface type and its parameters. For more details, see section "Output interface types". +:``mappingFile``: + Path to configuration file with mapping IPFIX fields to UniRec fields. If the parameter is + not defined, the default configuration file is used. See section "UniRec configuration file". + Output interface types ---------------------- Exactly one of the following output type must be defined in the instance configuration of this diff --git a/extra_plugins/output/unirec/config/unirec-elements.txt b/extra_plugins/output/unirec/config/unirec-elements.txt index 74b18fdd..f6f97dcd 100644 --- a/extra_plugins/output/unirec/config/unirec-elements.txt +++ b/extra_plugins/output/unirec/config/unirec-elements.txt @@ -24,34 +24,42 @@ SRC_IP ipaddr e0id8,e0id27 # IPv4 or IPv6 source address DST_IP ipaddr e0id12,e0id28 # IPv4 or IPv6 destination address SRC_PORT uint16 e0id7 # Transport protocol source port -DST_PORT uint16 e0id11 # Transport protocol destination port +DST_PORT uint16 e0id11,e0id32 # Transport protocol destination port or ICMP type/code PROTOCOL uint8 e0id4 # Transport protocol TCP_FLAGS uint8 e0id6 # TCP flags BYTES uint64 e0id1 # Number of bytes in flow PACKETS uint32 e0id2 # Number of packets in flow TTL uint8 e0id192 # IP time to live +TTL_REV uint8 e29305id192 # IP time to live rev TOS uint8 e0id5 # IP type of service TIME_FIRST time e0id150,e0id152,e0id154,e0id156 # Time of the first packet of a flow TIME_LAST time e0id151,e0id153,e0id155,e0id157 # Time of the last packet of a flow DIR_BIT_FIELD uint8 _internal_dbf_ # Bit field used for determining incoming/outgoing flow (1 => Incoming, 0 => Outgoing) LINK_BIT_FIELD uint64 _internal_lbf_ # Bit field of links on which was flow seen + SRC_MAC macaddr e0id56 DST_MAC macaddr e0id80 +FLOW_END_REASON uint8 iana:flowEndReason # Reason of exporting the flow record, see https://www.iana.org/assignments/ipfix/ipfix.xhtml#ipfix-flow-end-reason + # --- Additional biflow fields --- BYTES_REV uint64 e29305id1 PACKETS_REV uint32 e29305id2 TCP_FLAGS_REV uint8 e29305id6 +# --- Additional TCP fields --- +TCP_SYN_SIZE uint8 flowmon:tcpSynSize +TCP_SYN_TTL uint8 flowmon:tcpSynTtl + # --- DNS specific fields --- DNS_ANSWERS uint16 cesnet:DNSAnswers # DNS answers DNS_RCODE uint8 cesnet:DNSRCode # DNS rcode -DNS_NAME string cesnet:DNSName # DNS name -DNS_QTYPE uint16 cesnet:DNSQType # DNS qtype -DNS_CLASS uint16 cesnet:DNSClass # DNS class +DNS_Q_NAME string cesnet:DNSName # DNS name +DNS_Q_TYPE uint16 cesnet:DNSQType # DNS qtype +DNS_Q_CLASS uint16 cesnet:DNSClass # DNS class DNS_RR_TTL uint32 cesnet:DNSRRTTL # DNS rr ttl -DNS_RLENGTH uint16 cesnet:DNSRDataLength # DNS rlenght -DNS_RDATA bytes cesnet:DNSRData # DNS rdata +DNS_RR_RLENGTH uint16 cesnet:DNSRDataLength # DNS rlenght +DNS_RR_RDATA bytes cesnet:DNSRData # DNS rdata DNS_PSIZE uint16 cesnet:DNSPSize # DNS payload size DNS_DO uint8 cesnet:DNSRDO # DNS DNSSEC OK bit DNS_ID uint16 cesnet:DNSTransactionID # DNS transaction id @@ -118,14 +126,7 @@ FME_SIP_CALLED_PARTY string flowmon:sipCalledParty FME_SIP_VIA string flowmon:sipVia # SIP VIA # --- HTTP elements --- -HTTP_REQUEST_METHOD_ID uint32 e16982id500 # HTTP request method id -HTTP_REQUEST_HOST string e16982id501 # HTTP(S) request host -HTTP_REQUEST_URL string e16982id502 # HTTP request url -HTTP_REQUEST_AGENT_ID uint32 e16982id503 # HTTP request agent id -HTTP_REQUEST_AGENT string e16982id504 # HTTP request agent -HTTP_REQUEST_REFERER string e16982id505 # HTTP referer -HTTP_RESPONSE_STATUS_CODE uint32 e16982id506 # HTTP response status code -HTTP_RESPONSE_CONTENT_TYPE string e16982id507 # HTTP response content type +HTTP_REQUEST_METHOD_ID string cesnet:httpMethod # HTTP request method FME_HTTP_UA_OS uint16 flowmon:httpUaOs FME_HTTP_UA_OS_MAJ uint16 flowmon:httpUaOsMaj @@ -135,11 +136,14 @@ FME_HTTP_UA_APP uint16 flowmon:httpUaApp FME_HTTP_UA_APP_MAJ uint16 flowmon:httpUaAppMaj FME_HTTP_UA_APP_MIN uint16 flowmon:httpUaAppMin FME_HTTP_UA_APP_BLD uint16 flowmon:httpUaAppBld +HTTP_REQUEST_REFERER string flowmon:httpReferer +HTTP_RESPONSE_CONTENT_TYPE string flowmon:httpContentType FME_HTTP_METHOD_MASK uint16 flowmon:httpMethodMask -FME_HTTP_REQUEST_HOST string flowmon:httpHost # HTTP(S) request host -FME_HTTP_REQUEST_URL string flowmon:httpUrl # HTTP request url -FME_HTTP_RESPONSE_STATUS_CODE uint32 flowmon:httpStatusCode # HTTP response status code -FME_HTTP_REQUEST_USER_AGENT string flowmon:httpUserAgent +HTTP_REQUEST_HOST string flowmon:httpHost # HTTP(S) request host +HTTP_REQUEST_URL string flowmon:httpUrl # HTTP request url +HTTP_RESPONSE_STATUS_CODE uint32 flowmon:httpStatusCode # HTTP response status code +HTTP_REQUEST_AGENT string flowmon:httpUserAgent + # --- Other fields --- IPV6_TUN_TYPE uint8 e16982id405 # IPv6 tunnel type @@ -173,10 +177,13 @@ FME_TLS_VALIDITY_NOTAFTER int64 flowmon:tlsValidityNotAfter FME_TLS_SIGNATURE_ALG uint16 flowmon:tlsSignatureAlg # tlsSignatureAlg FME_TLS_PUBLIC_KEYALG uint16 flowmon:tlsPublicKeyAlg # tlsPublicKeyAlg FME_TLS_PUBLIC_KEYLENGTH int32 flowmon:tlsPublicKeyLength # tlsPublicKeyLength -FME_TLS_JA_3FINGERPRINT bytes flowmon:tlsJa3Fingerprint # tlsJa3Fingerprint TLS_SNI string cesnet:TLSSNI # Server Name Indication https://en.wikipedia.org/wiki/Server_Name_Indication -TLS_JA_3FINGERPRINT bytes cesnet:tlsJa3Fingerprint # tlsJa3Fingerprint +TLS_JA3_FINGERPRINT bytes flowmon:tlsJa3Fingerprint # tlsJa3Fingerprint + +QUIC_SNI string cesnet:quicSNI # Server Name Indication from QUIC +QUIC_USER_AGENT string cesnet:quicUserAgent # User-Agent value extracted from decrypted QUIC header +QUIC_VERSION uint32 cesnet:quicVersion # Version of QUIC protocol extracted from decrypted QUIC header # --- Per-Packet Information elements --- PPI_PKT_LENGTHS uint16* e0id291/cesnet:packetLength # basicList of packet lengths @@ -185,15 +192,15 @@ PPI_PKT_FLAGS uint8* e0id291/cesnet:packetFlag PPI_PKT_DIRECTIONS int8* e0id291/cesnet:packetDirection # basicList of packet directions # --- SSDP Information elements --- -SSDP_LOCATION_PORT uint16 cesnet:SSDPLocationPort -SSDP_SERVER string cesnet:SSDPServer -SSDP_USER_AGENT string cesnet:SSDPUserAgent -SSDP_NT string cesnet:SSDPNT -SSDP_ST string cesnet:SSDPST +SSDP_LOCATION_PORT uint16 cesnet:SSDPLocationPort,flowmon:SSDPLocationPort +SSDP_SERVER string cesnet:SSDPServer,flowmon:SSDPServer +SSDP_USER_AGENT string cesnet:SSDPUserAgent,flowmon:SSDPUserAgent +SSDP_NT string cesnet:SSDPNT,flowmon:SSDPNT +SSDP_ST string cesnet:SSDPST,flowmon:SSDPST # --- DNSDD Information elements --- -DNSSD_QUERIES string cesnet:DNSSDQueries -DNSSD_RESPONSES string cesnet:DNSSDResponses +DNSSD_QUERIES string cesnet:DNSSDQueries,flowmon:DNSSDQuery +DNSSD_RESPONSES string cesnet:DNSSDResponses,flowmon:DNSSDResponse # --- OVPN Information elements --- OVPN_CONF_LEVEL uint8 cesnet:OVPNConfLevel @@ -229,3 +236,51 @@ NB_SUFFIX uint8 cesnet:NBSuffix # --- IDPContent Information elements --- IDP_CONTENT bytes cesnet:IDPContent IDP_CONTENT_REV bytes cesnet:IDPContentRev + +# --- Hists --- +S_PHISTS_SIZES uint32* e0id291/cesnet:phistSrcSizes +S_PHISTS_IPT uint32* e0id291/cesnet:phistSrcInterPacketTime +D_PHISTS_SIZES uint32* e0id291/cesnet:phistDstSizes +D_PHISTS_IPT uint32* e0id291/cesnet:phistDstInterPacketTime + +# --- Bursts --- + +SBI_BRST_BYTES uint32* e0id291/cesnet:burstSrcBytes +SBI_BRST_PACKETS uint32* e0id291/cesnet:burstSrcPackets +SBI_BRST_TIME_START time* e0id291/cesnet:burstSrcTimeStart +SBI_BRST_TIME_STOP time* e0id291/cesnet:burstSrcTimeStop +DBI_BRST_PACKETS uint32* e0id291/cesnet:burstDstPackets +DBI_BRST_BYTES uint32* e0id291/cesnet:burstDstBytes +DBI_BRST_TIME_START time* e0id291/cesnet:burstDstTimeStart +DBI_BRST_TIME_STOP time* e0id291/cesnet:burstDstTimeStop + +# --- BasicPlus --- +L4_TCP_MSS uint32 cesnet:tcpMss +L4_TCP_MSS_REV uint32 cesnet:tcpMssRev +L4_TCP_SYN_SIZE uint16 cesnet:tcpSynSize + +L4_TCP_WIN uint16 e0id186 +L4_TCP_WIN_REV uint16 e29305id186 +L4_TCP_OPTIONS uint64 e0id209 +L4_TCP_OPTIONS_REV uint64 e29305id209 +L3_FLAGS uint8 e0id197 +L3_FLAGS_REV uint8 e29305id197 + +# --- wireguard --- +WG_CONF_LEVEL uint8 cesnet:WGConfLevel +WG_SRC_PEER uint32 cesnet:WGSrcPeer +WG_DST_PEER uint32 cesnet:WGDstPeer + +# --- OSQuery --- +OSQUERY_PROGRAM_NAME string cesnet:OSQueryProgramName +OSQUERY_USERNAME string cesnet:OSQueryUsername +OSQUERY_OS_NAME string cesnet:OSQueryOSName +OSQUERY_OS_MAJOR uint16 cesnet:OSQueryOSMajor +OSQUERY_OS_MINOR uint16 cesnet:OSQueryOSMinor +OSQUERY_OS_BUILD string cesnet:OSQueryOSBuild +OSQUERY_OS_PLATFORM string cesnet:OSQueryOSPlatform +OSQUERY_OS_PLATFORM_LIKE string cesnet:OSQueryOSPlatformLike +OSQUERY_OS_ARCH string cesnet:OSQueryOSArch +OSQUERY_KERNEL_VERSION string cesnet:OSQueryKernelVersion +OSQUERY_SYSTEM_HOSTNAME string cesnet:OSQuerySystemHostname + diff --git a/extra_plugins/output/unirec/ipfixcol2-unirec-output.spec.in b/extra_plugins/output/unirec/ipfixcol2-unirec-output.spec.in index 71c348de..0f8bd1d0 100644 --- a/extra_plugins/output/unirec/ipfixcol2-unirec-output.spec.in +++ b/extra_plugins/output/unirec/ipfixcol2-unirec-output.spec.in @@ -12,7 +12,7 @@ Packager: BuildRoot: %{_tmppath}/%{name}-%{version}-%{release} BuildRequires: gcc >= 4.8, cmake >= 2.8.8, make -BuildRequires: ipfixcol2-devel, libfds-devel, python2-docutils +BuildRequires: ipfixcol2-devel, libfds-devel, /usr/bin/rst2man BuildRequires: libtrap-devel, unirec >= 2.3.0 Requires: libtrap >= 0.12.0, ipfixcol2 >= 2.0.0, libfds >= 0.1.0 diff --git a/extra_plugins/output/unirec/src/configuration.c b/extra_plugins/output/unirec/src/configuration.c index 5d8a3c02..41e6930c 100644 --- a/extra_plugins/output/unirec/src/configuration.c +++ b/extra_plugins/output/unirec/src/configuration.c @@ -63,6 +63,8 @@ enum cfg_timeout_mode { CFG_TIMEOUT_HALF_WAIT = -3 /**< Block only if some client is connected */ }; +/** Filename of IPFIX-to-UniRec */ +#define DEF_CONF_FILENAME "unirec-elements.txt" /** Default maximum number of connections over TCP/TCP-TLS/Unix */ #define DEF_MAX_CONNECTIONS 64 /** Default output interface timeout */ @@ -84,6 +86,7 @@ struct ifc_common { /* * + * /etc/ipfixcol2/unirec-elements.txt * DST_IP,SRC_IP,BYTES,DST_PORT,?TCP_FLAGS,SRC_PORT,PROTOCOL * true * @@ -123,6 +126,7 @@ enum params_xml_nodes { // Main parameters NODE_UNIREC_FMT = 1, NODE_BIFLOW_SPLIT, + NODE_MAPPING_FILE, NODE_TRAP_COMMON, NODE_TRAP_SPEC, // TRAP common parameters @@ -208,6 +212,7 @@ static const struct fds_xml_args args_params[] = { FDS_OPTS_ROOT("params"), FDS_OPTS_ELEM(NODE_UNIREC_FMT, "uniRecFormat", FDS_OPTS_T_STRING, 0), FDS_OPTS_ELEM(NODE_BIFLOW_SPLIT, "splitBiflow", FDS_OPTS_T_BOOL, FDS_OPTS_P_OPT), + FDS_OPTS_ELEM(NODE_MAPPING_FILE, "mappingFile", FDS_OPTS_T_STRING, FDS_OPTS_P_OPT), FDS_OPTS_NESTED(NODE_TRAP_COMMON, "trapIfcCommon", args_trap_common, FDS_OPTS_P_OPT), FDS_OPTS_NESTED(NODE_TRAP_SPEC, "trapIfcSpec", args_trap_spec, 0), FDS_OPTS_END @@ -480,9 +485,10 @@ cfg_parse_tcp(ipx_ctx_t *ctx, fds_xml_ctx_t *root, struct conf_params *cfg, enum // Prepare the TRAP interface specification char *res = NULL; const char *trap_ifc = (type == SPEC_TCP) ? "t" : "T"; - if (cfg_str_append(&res, "%s:%" PRIu16 ":%" PRIu64, trap_ifc, port, max_conn) != IPX_OK + if (cfg_str_append(&res, "%s:%" PRIu16, trap_ifc, port) != IPX_OK || (type == SPEC_TCP_TLS - && cfg_str_append(&res, ":%s:%s:%s", file_key, file_cert, file_ca) != IPX_OK)) { + && cfg_str_append(&res, ":%s:%s:%s", file_key, file_cert, file_ca) != IPX_OK) + || (cfg_str_append(&res, ":max_clients=%" PRIu64, max_conn) != IPX_OK)) { IPX_CTX_ERROR(ctx, "Unable to allocate memory (%s:%d)", __FILE__, __LINE__); free(file_ca); free(file_cert); free(file_key); free(res); @@ -544,7 +550,7 @@ cfg_parse_unix(ipx_ctx_t *ctx, fds_xml_ctx_t *root, struct conf_params *cfg) // Prepare the TRAP interface specification char *res = NULL; - if (cfg_str_append(&res, "u:%s:%" PRIu64, name, max_conn) != IPX_OK) { + if (cfg_str_append(&res, "u:%s:max_clients=%" PRIu64, name, max_conn) != IPX_OK) { IPX_CTX_ERROR(ctx, "Unable to allocate memory (%s:%d)", __FILE__, __LINE__); free(name); return IPX_ERR_NOMEM; @@ -780,6 +786,13 @@ cfg_parse_params(ipx_ctx_t *ctx, fds_xml_ctx_t *root, struct conf_params *cfg) // Set default values cfg->biflow_split = true; + cfg->mapping_file = NULL; + + rc = cfg_str_append(&cfg->mapping_file, "%s/%s", ipx_api_cfg_dir(), DEF_CONF_FILENAME); + if (rc != FDS_OK) { + IPX_CTX_ERROR(ctx, "Unable to allocate memory (%s:%d)", __FILE__, __LINE__); + return rc; + } // Set default TRAP common parameters struct ifc_common common; @@ -806,6 +819,16 @@ cfg_parse_params(ipx_ctx_t *ctx, fds_xml_ctx_t *root, struct conf_params *cfg) assert(content->type == FDS_OPTS_T_BOOL); cfg->biflow_split = content->val_bool; break; + case NODE_MAPPING_FILE: + // Mapping file + assert(content->type == FDS_OPTS_T_STRING); + free(cfg->mapping_file); + cfg->mapping_file = strdup(content->ptr_string); + if (cfg->mapping_file == NULL) { + IPX_CTX_ERROR(ctx, "Unable to allocate memory (%s:%d)", __FILE__, __LINE__); + return IPX_ERR_NOMEM; + } + break; case NODE_TRAP_SPEC: // TRAP output interface specifier assert(content->type == FDS_OPTS_T_CONTEXT); @@ -919,6 +942,7 @@ configuration_free(struct conf_params *cfg) return; } + free(cfg->mapping_file); free(cfg->trap_ifc_spec); free(cfg->unirec_fmt); free(cfg->unirec_spec); diff --git a/extra_plugins/output/unirec/src/configuration.h b/extra_plugins/output/unirec/src/configuration.h index c9a5eb91..8890bbd3 100644 --- a/extra_plugins/output/unirec/src/configuration.h +++ b/extra_plugins/output/unirec/src/configuration.h @@ -46,7 +46,9 @@ * \brief Structure for a configuration parsed from XML */ struct conf_params { - /** Prepared TRAP interface specification string */ + /** Path to IPFIX-to-UniRec mapping file */ + char *mapping_file; + /** Prepared TRAP interface specification string */ char *trap_ifc_spec; /** * TRAP interface UniRec template diff --git a/extra_plugins/output/unirec/src/map.c b/extra_plugins/output/unirec/src/map.c index 415facda..8c3b8b6f 100644 --- a/extra_plugins/output/unirec/src/map.c +++ b/extra_plugins/output/unirec/src/map.c @@ -480,9 +480,9 @@ map_sort_fn(const void *p1, const void *p2) ipfix2 = ipfix2->next; } - if (ipfix1->next) { + if (ipfix1 && ipfix1->next) { return -1; - } else if (ipfix2->next) { + } else if (ipfix2 && ipfix2->next) { return 1; } @@ -585,7 +585,7 @@ map_load(map_t *map, const char *file) // Collision detected! snprintf(map->err_buffer, ERR_SIZE, "The IPFIX IE '%s' (PEN %" PRIu32 ", ID %" PRIu16 ") " - "is mapped to multiple different UniRec fields ('%s' and '%s')", + "is mapped to multiple UniRec fields ('%s' and '%s')", rec_now->ipfix.def->name, rec_now->ipfix.en, rec_now->ipfix.id, rec_now->unirec.name, rec_prev->unirec.name); map_clear(map); diff --git a/extra_plugins/output/unirec/src/unirecplugin.c b/extra_plugins/output/unirec/src/unirecplugin.c index dca9f8b3..e336f786 100644 --- a/extra_plugins/output/unirec/src/unirecplugin.c +++ b/extra_plugins/output/unirec/src/unirecplugin.c @@ -48,8 +48,6 @@ #include "configuration.h" #include "map.h" -/** Filename of IPFIX-to-UniRec */ -#define CONF_FILENAME "unirec-elements.txt" /** Name of TRAP context that belongs to the plugin */ #define PLUGIN_TRAP_NAME "IPFIXcol2-UniRec" /** Description of the TRAP context that belongs to the plugin */ @@ -100,41 +98,26 @@ struct conf_unirec { /** * \brief Get the IPFIX-to-UniRec conversion database * \param ctx Plugin context + * \param file Path to a file with IPFIX-to-UniRec mapping * \return Conversion table or NULL (an error has occurred) */ static map_t * -ipfix2unirec_db(ipx_ctx_t *ctx) +ipfix2unirec_db(ipx_ctx_t *ctx, const char *file) { - const char *path = ipx_api_cfg_dir(); - const size_t full_size = strlen(path) + strlen(CONF_FILENAME) + 2; // 2 = '/' + '\0' - char *full_path = malloc(full_size * sizeof(char)); - if (!full_path) { - IPX_CTX_ERROR(ctx, "Unable to allocate memory (%s:%d)", __FILE__, __LINE__); - return NULL; - } - - int ret_val = snprintf(full_path, full_size, "%s/%s", path, CONF_FILENAME); - if (ret_val < 0 || ((size_t) ret_val) >= full_size) { - IPX_CTX_ERROR(ctx, "Failed to generate a configuration path (internal error)", '\0'); - free(full_path); - return NULL; - } - map_t *map = map_init(ipx_ctx_iemgr_get(ctx)); if (!map) { IPX_CTX_ERROR(ctx, "Failed to initialize conversion map! (%s:%d)", __FILE__, __LINE__); - free(full_path); return NULL; } - if (map_load(map, full_path) != IPX_OK) { + IPX_CTX_INFO(ctx, "Loading IPFIX-to-UniRec mapping file '%s'", file); + + if (map_load(map, file) != IPX_OK) { IPX_CTX_ERROR(ctx, "Failed to initialize conversion database: %s", map_last_error(map)); map_destroy(map); - free(full_path); return NULL; } - free(full_path); return map; } @@ -317,7 +300,7 @@ ipx_plugin_init(ipx_ctx_t *ctx, const char *params) conf->params = parsed_params; // Load IPFIX-to-UniRec conversion database - map_t *conv_db = ipfix2unirec_db(ctx); + map_t *conv_db = ipfix2unirec_db(ctx, parsed_params->mapping_file); if (!conv_db) { configuration_free(parsed_params); free(conf); diff --git a/include/ipfixcol2/message_ipfix.h b/include/ipfixcol2/message_ipfix.h index c45c1941..a0bb1f92 100644 --- a/include/ipfixcol2/message_ipfix.h +++ b/include/ipfixcol2/message_ipfix.h @@ -201,6 +201,30 @@ ipx_msg_ipfix2base(ipx_msg_ipfix_t *msg) return (ipx_msg_t *) msg; } +/** + * \brief Add a new IPFIX Set description. + * + * The record is uninitialized and user MUST fill it! The function is + * intended for annotation of newly created IPFIX Message. + * \param[in] msg IPFIX Message wrapper + * \return Pointer to the record or NULL (memory allocation error) + */ +IPX_API struct ipx_ipfix_set * +ipx_msg_ipfix_add_set_ref(struct ipx_msg_ipfix *msg); + +/** + * \brief Add a new IPFIX Data Record description. + * + * The record is uninitialized and user MUST fill it! The function is + * intended for annotation of newly created IPFIX Message. + * \warning The wrapper \p msg_ref can be reallocated and different pointer + * can be returned! + * \param[in,out] msg_ref IPFIX Message wrapper + * \return Pointer to the record or NULL (memory allocation error) + */ +IPX_API struct ipx_ipfix_record * +ipx_msg_ipfix_add_drec_ref(struct ipx_msg_ipfix **msg_ref); + /**@}*/ #ifdef __cplusplus } diff --git a/pkg/deb/CMakeLists.txt b/pkg/deb/CMakeLists.txt index 129af061..7e79bcf5 100644 --- a/pkg/deb/CMakeLists.txt +++ b/pkg/deb/CMakeLists.txt @@ -48,7 +48,6 @@ file(MAKE_DIRECTORY # Copy and create configuration files file(COPY - "templates/changelog" "templates/rules" "templates/compat" "templates/ipfixcol2.install" @@ -61,6 +60,12 @@ file(COPY DESTINATION "${DEB_CFG_DIR}/source" ) +configure_file( + "templates/changelog.in" + "${DEB_CFG_DIR}/changelog" + @ONLY +) + configure_file( "templates/control.in" "${DEB_CFG_DIR}/control" diff --git a/pkg/deb/templates/changelog b/pkg/deb/templates/changelog.in similarity index 53% rename from pkg/deb/templates/changelog rename to pkg/deb/templates/changelog.in index 603cd04b..2382e678 100644 --- a/pkg/deb/templates/changelog +++ b/pkg/deb/templates/changelog.in @@ -1,4 +1,4 @@ -ipfixcol2 (2.2.0-1) unstable; urgency=low +ipfixcol2 (@CPACK_PACKAGE_VERSION@-@CPACK_PACKAGE_RELEASE@) unstable; urgency=low * Initial release. diff --git a/src/core/message_ipfix.h b/src/core/message_ipfix.h index 3719a6cb..60fed909 100644 --- a/src/core/message_ipfix.h +++ b/src/core/message_ipfix.h @@ -116,26 +116,4 @@ struct ipx_msg_ipfix { size_t ipx_msg_ipfix_size(uint32_t rec_cnt, size_t rec_size); -/** - * \brief Add a new IPFIX Set - * - * The record is uninitialized and user MUST fill it! - * \param[in] msg IPFIX Message wrapper - * \return Pointer to the record or NULL (memory allocation error) - */ -struct ipx_ipfix_set * -ipx_msg_ipfix_add_set_ref(struct ipx_msg_ipfix *msg); - -/** - * \brief Add a new IPFIX Data Record - * - * The record is uninitialized and user MUST fill it! - * \warning The wrapper \p msg_ref can be reallocated and different pointer can be returned! - * \param[in,out] msg_ref IPFIX Message wrapper - * \return Pointer to the record or NULL (memory allocation error) - */ -struct ipx_ipfix_record * -ipx_msg_ipfix_add_drec_ref(struct ipx_msg_ipfix **msg_ref); - - #endif // IPFIXCOL_MESSAGE_IPFIX_INTERNAL_H diff --git a/src/plugins/input/tcp/tcp.c b/src/plugins/input/tcp/tcp.c index e501804f..84b40083 100644 --- a/src/plugins/input/tcp/tcp.c +++ b/src/plugins/input/tcp/tcp.c @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -62,10 +63,6 @@ #define GETTER_TIMEOUT (10) /** Max sockets events processed in the getter - i.e. epoll_wait array size */ #define GETTER_MAX_EVENTS (16) -/** Timeout to read whole IPFIX Message after at least part has been received (in microseconds) */ -#define GETTER_RECV_TIMEOUT (500000) -/** Default size of a buffer prepared for new IPFIX/NetFlow message (bytes) */ -#define DEF_MSG_SIZE (4096) /** Plugin description */ IPX_API struct ipx_plugin_info ipx_plugin_info = { @@ -91,6 +88,13 @@ struct tcp_pair { struct ipx_session *session; /** No message has been received from the Session yet */ bool new_connection; + + /** Partly received message that waits for completition */ + uint8_t *msg; + /** Allocated size of the partly received msg message */ + uint16_t msg_size; + /** Already receive part of the msg message */ + uint16_t msg_offset; }; /** Instance data */ @@ -143,7 +147,7 @@ static int active_session_add(struct tcp_data *data, int sd, struct ipx_session *session) { // Create a new pair - struct tcp_pair *pair = malloc(sizeof(*pair)); + struct tcp_pair *pair = calloc(1, sizeof(*pair)); if (!pair) { IPX_CTX_ERROR(data->ctx, "Memory allocation failed! (%s:%d)", __FILE__, __LINE__); return IPX_ERR_NOMEM; @@ -242,7 +246,11 @@ active_session_remove_aux(struct tcp_data *data, size_t idx) } } - // Close internal structures an remove it from the list (do NOT free SESSION) + // Free internal structures and remove the pair from the list (do NOT free SESSION) + if (pair->msg) { + free(pair->msg); + } + close(pair->fd); free(pair); @@ -309,14 +317,21 @@ listener_add_connection(struct tcp_data *data, int sd) assert(sd >= 0); const char *err_str; - // Set receive timeout (after data on the socket is ready) - struct timeval rcv_timeout; - rcv_timeout.tv_sec = GETTER_RECV_TIMEOUT / 1000000; - rcv_timeout.tv_usec = GETTER_RECV_TIMEOUT % 1000000; - if (setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, &rcv_timeout, sizeof(rcv_timeout)) == -1) { + // Set non-blocking mode on the socket + int flags = fcntl(sd, F_GETFL, 0); + if (flags == -1) { ipx_strerror(errno, err_str); - IPX_CTX_WARNING(data->ctx, "Listener: Failed to specify receiving timeout of a socket: %s", + IPX_CTX_WARNING(data->ctx, "Listener: Failed to set non-blocking mode: fcntl() failed: %s", err_str); + return IPX_ERR_DENIED; + } + + flags |= O_NONBLOCK; + if (fcntl(sd, F_SETFL, flags) == -1) { + ipx_strerror(errno, err_str); + IPX_CTX_WARNING(data->ctx, "Listener: Failed to set non-blocking mode: fcntl() failed: %s", + err_str); + return IPX_ERR_DENIED; } // Get the description of the remove address @@ -810,102 +825,267 @@ active_destroy(ipx_ctx_t *ctx, struct tcp_data *instance) } /** - * \brief Get an IPFIX message from a socket and pass it + * \brief Try to read an IPFIX Message header + * + * The whole header might not be available right now. In that case, the function + * will only read and store available part and it will successfully return. When + * this happens, the allocated message is smaller that IPFIX Message header. The + * next call of this function will tried to read the rest. + * + * If the whole header is available, the function will allocate message buffer + * sufficient for the rest of the message. * * \param[in] ctx Instance data (necessary for passing messages) * \param[in] pair Connection pair (socket descriptor and session) to receive from * \return #IPX_OK on success - * \return #IPX_ERR_EOF if the socket is closed + * \return #IPX_ERR_EOF if the socket has been closed * \return #IPX_ERR_FORMAT if the message (or stream) is malformed and the connection MUST be closed * \return #IPX_ERR_NOMEM on a memory allocation error and the connection MUST be closed */ static int -socket_process(ipx_ctx_t *ctx, struct tcp_pair *pair) +socket_process_receive_header(ipx_ctx_t *ctx, struct tcp_pair *pair) { - const char *err_str; - struct fds_ipfix_msg_hdr hdr; - static_assert(sizeof(hdr) == FDS_IPFIX_MSG_HDR_LEN, "Invalid size of IPFIX Message header"); + struct fds_ipfix_msg_hdr hdr = {0}; + uint8_t *hdr_raw = (uint8_t *) &hdr; - // Get the message header (do not move pointer) - ssize_t len = recv(pair->fd, &hdr, FDS_IPFIX_MSG_HDR_LEN, MSG_WAITALL | MSG_PEEK); + uint16_t remains = FDS_IPFIX_MSG_HDR_LEN; + uint16_t offset = 0; + ssize_t len; + + uint8_t *msg_buffer; + uint16_t msg_version; + uint16_t msg_size; + + assert(!pair->msg || pair->msg_offset < FDS_IPFIX_MSG_HDR_LEN); + + if (pair->msg) { + // Fill the header with previously received data + offset = pair->msg_offset; + remains = FDS_IPFIX_MSG_HDR_LEN - offset; + + memcpy(hdr_raw, pair->msg, offset); + } + + len = recv(pair->fd, &hdr_raw[offset], remains, 0); if (len == 0) { - // Connection terminated - IPX_CTX_INFO(ctx, "Connection from '%s' closed.", pair->session->ident); - return IPX_ERR_EOF; + // Connection has been closed + if (offset > 0) { + IPX_CTX_WARNING(ctx, "Connection with '%s' has been unexpectly closed", + pair->session->ident); + return IPX_ERR_FORMAT; + } else { + IPX_CTX_INFO(ctx, "Connection with '%s' closed.", pair->session->ident); + return IPX_ERR_EOF; + } } - if (len == -1 || len < FDS_IPFIX_MSG_HDR_LEN) { - // Failed to read header -> close - int error_code = (len == -1) ? errno : EINTR; - ipx_strerror(error_code, err_str); - IPX_CTX_WARNING(ctx, "Connection from '%s' closed due to failure to receive " - "an IPFIX Message header: %s", pair->session->ident, err_str); + if (len < 0) { + // Something went wrong + const char *err_str; + + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return IPX_OK; + } + + ipx_strerror(errno, err_str); + IPX_CTX_WARNING(ctx, "Connection with '%s' failed: %s", + pair->session->ident, err_str); return IPX_ERR_FORMAT; } - assert(len == FDS_IPFIX_MSG_HDR_LEN); - // Check the header (version, size) - uint16_t msg_version = ntohs(hdr.version); - uint16_t msg_size = ntohs(hdr.length); - uint32_t msg_odid = ntohl(hdr.odid); + offset += len; + + if (offset < FDS_IPFIX_MSG_HDR_LEN) { + // We don't have whole IPFIX Message header + uint8_t *ptr = realloc(pair->msg, offset); + if (!ptr) { + IPX_CTX_ERROR(ctx, + "Connection with '%s' closed due to memory allocation failure! (%s:%d).", + pair->session->ident, __FILE__, __LINE__); + return IPX_ERR_NOMEM; + } + + memcpy(ptr, hdr_raw, offset); + pair->msg = ptr; + pair->msg_offset = offset; + pair->msg_size = offset; + + return IPX_OK; + } + + // Check the IPFIX Message header + msg_version = ntohs(hdr.version); + msg_size = ntohs(hdr.length); if (msg_version != FDS_IPFIX_VERSION || msg_size < FDS_IPFIX_MSG_HDR_LEN) { - // Unsupported header version - IPX_CTX_WARNING(ctx, "Connection from '%s' closed due to the unsupported version of " - "IPFIX/NetFlow.", pair->session->ident); + // Invalid header version + IPX_CTX_WARNING(ctx, + "Connection with '%s' closed due to invalid IPFIX Message header.", + pair->session->ident); return IPX_ERR_FORMAT; } - // Read the whole message - uint8_t *buffer = malloc(msg_size * sizeof(uint8_t)); - if (!buffer) { - IPX_CTX_ERROR(ctx, "Connection from '%s' closed due to memory allocation failure! (%s:%d).", + // Preallocated buffer for the rest of the IPFIX Message body + msg_buffer = realloc(pair->msg, msg_size); + if (!msg_buffer) { + IPX_CTX_ERROR(ctx, + "Connection with '%s' closed due to memory allocation failure! (%s:%d).", pair->session->ident, __FILE__, __LINE__); return IPX_ERR_NOMEM; } - len = recv(pair->fd, buffer, msg_size, MSG_WAITALL); - if (len != msg_size) { - int error_code = (len == -1) ? errno : ETIMEDOUT; - ipx_strerror(error_code, err_str); - IPX_CTX_ERROR(ctx, "Connection from '%s' closed due to failure while reading from " - "its socket: %s.", pair->session->ident, err_str); - free(buffer); + memcpy(msg_buffer, hdr_raw, FDS_IPFIX_MSG_HDR_LEN); + pair->msg = msg_buffer; + pair->msg_offset = FDS_IPFIX_MSG_HDR_LEN; + pair->msg_size = msg_size; + + return IPX_OK; +} + +/** + * \brief Try to read an IPFIX Message body + * + * The whole body might not be available right now. In that case, the function + * will only read and store available part and it will successfully return. When + * this happens, the message offset is smaller that the message size. The next call + * of this function will tried to read the rest. + * + * \param[in] ctx Instance data (necessary for passing messages) + * \param[in] pair Connection pair (socket descriptor and session) to receive from + * \return #IPX_OK on success + * \return #IPX_ERR_FORMAT if the message (or stream) is malformed and the connection MUST be closed + * \return #IPX_ERR_NOMEM on a memory allocation error and the connection MUST be closed + */ +static int +socket_process_receive_body(ipx_ctx_t *ctx, struct tcp_pair *pair) +{ + const uint16_t remains = pair->msg_size - pair->msg_offset; + assert(pair->msg && pair->msg_offset >= FDS_IPFIX_MSG_HDR_LEN); + + if (remains == 0) { + // This is an IPFIX Message without body... + return IPX_OK; + } + + ssize_t len = recv(pair->fd, &pair->msg[pair->msg_offset], remains, 0); + if (len == 0) { + // Connection closed + IPX_CTX_WARNING(ctx, "Connection from '%s' has been unexpectly closed", + pair->session->ident); return IPX_ERR_FORMAT; } + if (len < 0) { + // Something went wrong + const char *err_str; + + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return IPX_OK; + } + + ipx_strerror(errno, err_str); + IPX_CTX_WARNING(ctx, "Connection with '%s' failed: %s", + pair->session->ident, err_str); + return IPX_ERR_FORMAT; + } + + pair->msg_offset += (uint16_t) len; + return IPX_OK; +} + +/** + * \brief Try to pass fully received IPFIX Message to the collector. + * + * \param[in] ctx Instance data (necessary for passing messages) + * \param[in] pair Connection pair (socket descriptor and session) + * \return #IPX_OK on success + * \return #IPX_ERR_NOMEM on a memory allocation error and the connection MUST be closed + */ +static int +socket_process_pass_msg(ipx_ctx_t *ctx, struct tcp_pair *pair) +{ + assert(pair->msg && pair->msg_offset == pair->msg_size && "Partly received message"); + if (pair->new_connection) { // Send information about the new Transport Session - pair->new_connection = false; ipx_msg_session_t *msg = ipx_msg_session_create(pair->session, IPX_MSG_SESSION_OPEN); if (!msg) { - IPX_CTX_ERROR(ctx, "Connection from '%s' closed due to memory allocation " - "failure! (%s:%d).", pair->session->ident, __FILE__, __LINE__); - free(buffer); + IPX_CTX_ERROR(ctx, + "Connection with '%s' closed due to memory allocation failure! (%s:%d).", + pair->session->ident, __FILE__, __LINE__); return IPX_ERR_NOMEM; } ipx_ctx_msg_pass(ctx, ipx_msg_session2base(msg)); + pair->new_connection = false; } // Create a message wrapper and pass the message struct ipx_msg_ctx msg_ctx; msg_ctx.session = pair->session; - msg_ctx.odid = msg_odid; + msg_ctx.odid = ntohl(((struct fds_ipfix_msg_hdr *) pair->msg)->odid); msg_ctx.stream = 0; // Streams are not supported over TCP - ipx_msg_ipfix_t *msg = ipx_msg_ipfix_create(ctx, &msg_ctx, buffer, msg_size); + ipx_msg_ipfix_t *msg = ipx_msg_ipfix_create(ctx, &msg_ctx, pair->msg, pair->msg_size); if (!msg) { - IPX_CTX_ERROR(ctx, "Connection from '%s' closed due to memory allocation " - "failure! (%s:%d).", pair->session->ident, __FILE__, __LINE__); - free(buffer); + IPX_CTX_ERROR(ctx, + "Connection with '%s' closed due to memory allocation failure! (%s:%d).", + pair->session->ident, __FILE__, __LINE__); return IPX_ERR_NOMEM; } ipx_ctx_msg_pass(ctx, ipx_msg_ipfix2base(msg)); + + pair->msg = NULL; + pair->msg_offset = 0; + pair->msg_size = 0; + return IPX_OK; } +/** + * \brief Get an IPFIX message from a socket and pass it + * + * \param[in] ctx Instance data (necessary for passing messages) + * \param[in] pair Connection pair (socket descriptor and session) to receive from + * \return #IPX_OK on success + * \return #IPX_ERR_EOF if the socket is closed + * \return #IPX_ERR_FORMAT if the message (or stream) is malformed and the connection MUST be closed + * \return #IPX_ERR_NOMEM on a memory allocation error and the connection MUST be closed + */ +static int +socket_process(ipx_ctx_t *ctx, struct tcp_pair *pair) +{ + int ret; + + if (!pair->msg || pair->msg_offset < FDS_IPFIX_MSG_HDR_LEN) { + // Try to receive IPFIX Message header first + ret = socket_process_receive_header(ctx, pair); + if (ret != IPX_OK) { + return ret; + } + + if (pair->msg_offset < FDS_IPFIX_MSG_HDR_LEN) { + // Incomplete IPFIX Message header, read the rest later... + return IPX_OK; + } + } + + // Receive rest of the message body + ret = socket_process_receive_body(ctx, pair); + if (ret != IPX_OK) { + return ret; + } + + if (pair->msg_offset < pair->msg_size) { + // Incomplete IPFIX Message body, read the rest later... + return IPX_OK; + } + + // Pass the message + return socket_process_pass_msg(ctx, pair); +} + // ------------------------------------------------------------------------------------------------- int diff --git a/src/plugins/output/dummy/dummy.c b/src/plugins/output/dummy/dummy.c index ed883dd5..f0079260 100644 --- a/src/plugins/output/dummy/dummy.c +++ b/src/plugins/output/dummy/dummy.c @@ -46,6 +46,11 @@ #include "config.h" +#define IANA_PEN 0 +#define IANA_PEN_REV 29305 +#define IE_ID_BYTES 1 +#define IE_ID_PKTS 2 + /** Plugin description */ IPX_API struct ipx_plugin_info ipx_plugin_info = { // Plugin type @@ -57,7 +62,7 @@ IPX_API struct ipx_plugin_info ipx_plugin_info = { // Configuration flags (reserved for future use) .flags = 0, // Plugin version string (like "1.2.3") - .version = "2.1.0", + .version = "2.2.0", // Minimal IPFIXcol version string (like "1.2.3") .ipx_min = "2.0.0" }; @@ -91,7 +96,8 @@ stats_update(struct instance_data *inst, ipx_msg_ipfix_t *msg) // For each IPFIX Data Record for (uint32_t i = 0; i < rec_cnt; ++i) { struct ipx_ipfix_record *rec_ptr = ipx_msg_ipfix_get_drec(msg, i); - enum fds_template_type ttype = rec_ptr->rec.tmplt->type; + const struct fds_template *tmplt = rec_ptr->rec.tmplt; + const enum fds_template_type ttype = tmplt->type; struct fds_drec_field field; uint64_t value; @@ -105,13 +111,30 @@ stats_update(struct instance_data *inst, ipx_msg_ipfix_t *msg) } // Get octetDeltaCount - if (fds_drec_find(&rec_ptr->rec, 0, 1, &field) != FDS_EOC + if (fds_drec_find(&rec_ptr->rec, IANA_PEN, IE_ID_BYTES, &field) != FDS_EOC && fds_get_uint_be(field.data, field.size, &value) == FDS_OK) { inst->cnt_bytes += value; } // Get packetDeltaCount - if (fds_drec_find(&rec_ptr->rec, 0, 2, &field) != FDS_EOC + if (fds_drec_find(&rec_ptr->rec, IANA_PEN, IE_ID_PKTS, &field) != FDS_EOC + && fds_get_uint_be(field.data, field.size, &value) == FDS_OK) { + inst->cnt_pkts += value; + } + + if ((tmplt->flags & FDS_TEMPLATE_BIFLOW) == 0) { + // Not a biflow record + continue; + } + + // Get octetDeltaCount (reverse) + if (fds_drec_find(&rec_ptr->rec, IANA_PEN_REV, IE_ID_BYTES, &field) != FDS_EOC + && fds_get_uint_be(field.data, field.size, &value) == FDS_OK) { + inst->cnt_bytes += value; + } + + // Get packetDeltaCount (reverse) + if (fds_drec_find(&rec_ptr->rec, IANA_PEN_REV, IE_ID_PKTS, &field) != FDS_EOC && fds_get_uint_be(field.data, field.size, &value) == FDS_OK) { inst->cnt_pkts += value; } diff --git a/src/plugins/output/fds/src/Storage.cpp b/src/plugins/output/fds/src/Storage.cpp index 30db3d25..5867c344 100644 --- a/src/plugins/output/fds/src/Storage.cpp +++ b/src/plugins/output/fds/src/Storage.cpp @@ -17,6 +17,8 @@ #include #include "Storage.hpp" +const std::string TMP_SUFFIX = ".tmp"; + Storage::Storage(ipx_ctx_t *ctx, const Config &cfg) : m_ctx(ctx), m_path(cfg.m_path) { // Check if the directory exists @@ -46,6 +48,11 @@ Storage::Storage(ipx_ctx_t *ctx, const Config &cfg) : m_ctx(ctx), m_path(cfg.m_p m_flags |= FDS_FILE_APPEND; } +Storage::~Storage() +{ + window_close(); +} + void Storage::window_new(time_t ts) { @@ -53,7 +60,8 @@ Storage::window_new(time_t ts) window_close(); // Open new file - const std::string new_file = filename_gen(ts); + const std::string new_file = filename_gen(ts) + TMP_SUFFIX; + m_file_name = new_file; std::unique_ptr new_file_cpy(strdup(new_file.c_str()), &free); char *dir2create; @@ -80,8 +88,14 @@ Storage::window_new(time_t ts) void Storage::window_close() { + bool file_opened = (m_file.get() != nullptr); m_file.reset(); m_session2params.clear(); + if (file_opened) { + std::string new_file_name(m_file_name.begin(), m_file_name.end() - TMP_SUFFIX.size()); + std::rename(m_file_name.c_str(), new_file_name.c_str()); + m_file_name.clear(); + } } void diff --git a/src/plugins/output/fds/src/Storage.hpp b/src/plugins/output/fds/src/Storage.hpp index 29b0528e..56bbcc14 100644 --- a/src/plugins/output/fds/src/Storage.hpp +++ b/src/plugins/output/fds/src/Storage.hpp @@ -36,7 +36,7 @@ class Storage { * @throw FDS_exception if @p path directory doesn't exist in the system */ Storage(ipx_ctx_t *ctx, const Config &cfg); - virtual ~Storage() = default; + virtual ~Storage(); // Disable copy constructors Storage(const Storage &other) = delete; @@ -105,6 +105,8 @@ class Storage { /// Output FDS file std::unique_ptr m_file = {nullptr, &fds_file_close}; + /// Output FDS file name + std::string m_file_name; /// Mapping of Transport Sessions to FDS specific parameters std::map m_session2params; diff --git a/src/plugins/output/forwarder/CMakeLists.txt b/src/plugins/output/forwarder/CMakeLists.txt index 2bec16c3..66ee28b6 100644 --- a/src/plugins/output/forwarder/CMakeLists.txt +++ b/src/plugins/output/forwarder/CMakeLists.txt @@ -1,19 +1,30 @@ # Create a linkable module add_library(forwarder-output MODULE src/main.cpp - src/config.h + src/Config.h + src/Config.cpp src/Forwarder.h - src/ConnectionManager.h - src/ConnectionManager.cpp - src/ConnectionParams.h + src/Forwarder.cpp src/Connection.h src/Connection.cpp - src/ConnectionBuffer.h - src/SyncPipe.h - src/IPFIXMessage.h - src/MessageBuilder.h + src/Host.cpp + src/Host.h + src/common.h + src/common.cpp + src/Message.h + src/Message.cpp + src/Sender.h + src/Sender.cpp + src/connector/Connector.h + src/connector/Connector.cpp + src/connector/FutureSocket.h + src/connector/FutureSocket.cpp + src/connector/Pipe.h + src/connector/Pipe.cpp ) +target_include_directories(forwarder-output PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) + install( TARGETS forwarder-output LIBRARY DESTINATION "${INSTALL_DIR_LIB}/ipfixcol2/" diff --git a/src/plugins/output/forwarder/README.rst b/src/plugins/output/forwarder/README.rst index 93fdde4c..427b0970 100644 --- a/src/plugins/output/forwarder/README.rst +++ b/src/plugins/output/forwarder/README.rst @@ -1,7 +1,7 @@ -Forwarder (output plugin) [Preview] -=================================== +Forwarder (output plugin) +========================= -This plugin allows forwarding incoming IPFIX messages to other collector in various modes. +This plugin allows forwarding incoming flow records in IPFIX form to other collector in various modes. It can be used to broadcast messages to multiple collectors (e.g. a main and a backup collector), or to distribute messages across multiple collectors (e.g. for load balancing). @@ -20,13 +20,18 @@ Example configuration Subcollector 1 -
127.0.0.1
- 4751 +
192.168.1.2
+ 4739
Subcollector 2 +
192.168.1.3
+ 4739 +
+ + Subcollector 3
localhost
- 4752 + 4739
@@ -36,42 +41,48 @@ Parameters ---------- :``mode``: - The forwarding mode; round robin (messages are sent to one host at time and hosts are cycled through) or all (messages are broadcasted to all hosts) + Flow distribution mode. **RoundRobin** (each record will be delivered to one of hosts) or **All** (each record will be delivered to all hosts). [values: RoundRobin/All] :``protocol``: - The transport protocol to use + The transport protocol to use. [values: TCP/UDP] -:``connectionBufferSize``: - Size of the buffer of each connection (Warning: number of connections = number of input exporters * number of hosts) - [value: number of bytes, default: 4194304] +:``templatesResendSecs``: + Send templates again every N seconds (UDP only). + [value: number of seconds, default: 600, 0 = never] -:``templateRefreshIntervalSecs``: - Send templates again every N seconds (UDP only) - [value: number of seconds, default: 600] +:``templatesResendPkts``: + Send templates again every N packets (UDP only). + [value: number of packets, default: 5000, 0 = never] -:``templateRefreshIntervalBytes``: - Send templates again every N bytes (UDP only) - [value: number of bytes, default: 5000000] +:``reconnectSecs``: + Attempt to reconnect every N seconds in case the connection drops (TCP only). + [value: number of seconds, default: 10, 0 = don't wait] -:``reconnectIntervalSecs``: - Attempt to reconnect every N seconds in case the connection drops (TCP only) - [value: number of seconds, default: 10] +:``premadeConnections``: + Keep N connections open with each host so there is no delay in connecting once a connection is needed. + [value: number of connections, default: 5] :``hosts``: - The receiving hosts + The receiving hosts. :``host``: :``name``: - Optional identification of the host + Optional identification of the host. [value: string, default:
:] :``address``: - The address of the host + The address of the host. [value: IPv4/IPv6 address or a hostname] :``port``: - The port to connect to + The port to connect to. [value: port number] +Known limitations +----------------- + +Export time of IPFIX messages is set to the current time when forwarding. This may cause issues with data fields using deltaTime relative to the export time! + + diff --git a/src/plugins/output/forwarder/TODO.txt b/src/plugins/output/forwarder/TODO.txt deleted file mode 100644 index 37631f98..00000000 --- a/src/plugins/output/forwarder/TODO.txt +++ /dev/null @@ -1,5 +0,0 @@ -* Template withdrawals -* More effective way of handling template changes - currently all the templates are being sent again every time any change in templates is detected -* Message MTU -* Possible bug: when testing, a small number of data records seems to be lost (something like 20 out of 1,000,000) -* Connection buffer size \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Config.cpp b/src/plugins/output/forwarder/src/Config.cpp new file mode 100644 index 00000000..1c2b93b6 --- /dev/null +++ b/src/plugins/output/forwarder/src/Config.cpp @@ -0,0 +1,307 @@ +/** + * \file src/plugins/output/forwarder/src/Config.cpp + * \author Michal Sedlak + * \brief Plugin configuration implementation + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "Config.h" + +#include +#include +#include +#include +#include + +/// +/// Config schema definition +/// + +enum { + MODE, + PROTOCOL, + RECONNECT_SECS, + TEMPLATES_RESEND_SECS, + TEMPLATES_RESEND_PKTS, + CONNECTION_BUFFER_SIZE, + HOSTS, + HOST, + NAME, + ADDRESS, + PORT, + PREMADE_CONNECTIONS +}; + +static fds_xml_args host_schema[] = { + FDS_OPTS_ELEM(NAME , "name" , FDS_OPTS_T_STRING, FDS_OPTS_P_OPT), + FDS_OPTS_ELEM(ADDRESS, "address", FDS_OPTS_T_STRING, 0 ), + FDS_OPTS_ELEM(PORT , "port" , FDS_OPTS_T_UINT , 0 ), + FDS_OPTS_END +}; + +static fds_xml_args hosts_schema[] = { + FDS_OPTS_NESTED(HOST, "host", host_schema, FDS_OPTS_P_MULTI), + FDS_OPTS_END +}; + +static fds_xml_args params_schema[] = { + FDS_OPTS_ROOT ("params"), + FDS_OPTS_ELEM (MODE , "mode" , FDS_OPTS_T_STRING, 0 ), + FDS_OPTS_ELEM (PROTOCOL , "protocol" , FDS_OPTS_T_STRING, 0 ), + FDS_OPTS_ELEM (TEMPLATES_RESEND_SECS, "templatesResendSecs", FDS_OPTS_T_UINT , FDS_OPTS_P_OPT), + FDS_OPTS_ELEM (TEMPLATES_RESEND_PKTS, "templatesResendPkts", FDS_OPTS_T_UINT , FDS_OPTS_P_OPT), + FDS_OPTS_ELEM (RECONNECT_SECS , "reconnectSecs" , FDS_OPTS_T_UINT , FDS_OPTS_P_OPT), + FDS_OPTS_ELEM (PREMADE_CONNECTIONS , "premadeConnections" , FDS_OPTS_T_UINT , FDS_OPTS_P_OPT), + FDS_OPTS_NESTED(HOSTS , "hosts" , hosts_schema , 0 ), + FDS_OPTS_END +}; + +/// +/// Config definition +/// + +/** + * \brief Create a new configuration + * \param[in] params XML configuration of JSON plugin + * \throw runtime_error in case of invalid configuration + */ +Config::Config(const char *xml_config) +{ + set_defaults(); + + auto parser = std::unique_ptr(fds_xml_create(), &fds_xml_destroy); + if (!parser) { + throw std::runtime_error("Failed to create an XML parser!"); + } + + if (fds_xml_set_args(parser.get(), params_schema) != FDS_OK) { + throw std::runtime_error("Failed to parse the description of an XML document!"); + } + + fds_xml_ctx_t *params_ctx = fds_xml_parse_mem(parser.get(), xml_config, true); + if (!params_ctx) { + std::string err = fds_xml_last_err(parser.get()); + throw std::runtime_error("Failed to parse the configuration: " + err); + } + + try { + parse_params(params_ctx); + ensure_valid(); + } catch (const std::invalid_argument &ex) { + throw std::runtime_error("Config params error: " + std::string(ex.what())); + } +} + +void +Config::parse_params(fds_xml_ctx_t *params_ctx) +{ + const fds_xml_cont *content; + + while (fds_xml_next(params_ctx, &content) != FDS_EOC) { + + switch (content->id) { + + case MODE: + if (strcasecmp(content->ptr_string, "roundrobin") == 0) { + this->forward_mode = ForwardMode::ROUNDROBIN; + + } else if (strcasecmp(content->ptr_string, "all") == 0) { + this->forward_mode = ForwardMode::SENDTOALL; + + } else { + throw std::invalid_argument("mode must be one of: 'RoundRobin', 'All'"); + + } + break; + + case PROTOCOL: + if (strcasecmp(content->ptr_string, "tcp") == 0) { + this->protocol = Protocol::TCP; + + } else if (strcasecmp(content->ptr_string, "udp") == 0) { + this->protocol = Protocol::UDP; + + } else { + throw std::invalid_argument("protocol must be one of: 'TCP', 'UDP'"); + + } + break; + + case HOSTS: + parse_hosts(content->ptr_ctx); + break; + + case TEMPLATES_RESEND_SECS: + this->tmplts_resend_secs = content->val_uint; + break; + + case TEMPLATES_RESEND_PKTS: + this->tmplts_resend_pkts = content->val_uint; + break; + + case RECONNECT_SECS: + this->reconnect_secs = content->val_uint; + break; + + case PREMADE_CONNECTIONS: + this->nb_premade_connections = content->val_uint; + break; + + default: assert(0); + } + } +} + +void +Config::parse_hosts(fds_xml_ctx_t *hosts_ctx) +{ + const fds_xml_cont *content; + + while (fds_xml_next(hosts_ctx, &content) != FDS_EOC) { + assert(content->id == HOST); + parse_host(content->ptr_ctx); + } +} + +void +Config::parse_host(fds_xml_ctx_t *host_ctx) +{ + HostConfig host; + + const fds_xml_cont *content; + + while (fds_xml_next(host_ctx, &content) != FDS_EOC) { + + switch (content->id) { + + case NAME: + host.name = std::string(content->ptr_string); + break; + + case ADDRESS: + host.address = std::string(content->ptr_string); + break; + + case PORT: + if (content->val_uint > UINT16_MAX) { + throw std::invalid_argument("invalid host port " + std::to_string(content->val_uint)); + } + + host.port = static_cast(content->val_uint); + break; + } + } + + if (host.name.empty()) { + host.name = host.address + ":" + std::to_string(host.port); + } + + if (host_exists(host)) { + throw std::invalid_argument("duplicate host " + host.address + ":" + std::to_string(host.port)); + } + + hosts.push_back(host); +} + +void +Config::set_defaults() +{ + this->tmplts_resend_secs = 10 * 60; + this->tmplts_resend_pkts = 5000; + this->reconnect_secs = 10; + this->nb_premade_connections = 5; +} + +void +Config::ensure_valid() +{ + for (auto &host : hosts) { + + if (!can_resolve_host(host)) { + throw std::invalid_argument("cannot resolve host address " + host.address); + } + + } +} + +bool +Config::host_exists(HostConfig host) +{ + for (auto &host_ : hosts) { + + if (host.address == host_.address && host.port == host_.port) { + return true; + } + + } + + return false; +} + +bool +Config::can_resolve_host(HostConfig host) +{ + addrinfo *info; + + addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + + switch (protocol) { + case Protocol::TCP: + hints.ai_protocol = IPPROTO_TCP; + hints.ai_socktype = SOCK_STREAM; + break; + + case Protocol::UDP: + hints.ai_protocol = IPPROTO_UDP; + hints.ai_socktype = SOCK_DGRAM; + break; + + default: assert(0); + } + + int ret = getaddrinfo(host.address.c_str(), std::to_string(host.port).c_str(), &hints, &info); + + if (ret == 0) { + freeaddrinfo(info); + return true; + + } else { + return false; + } +} \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Config.h b/src/plugins/output/forwarder/src/Config.h new file mode 100644 index 00000000..9eda1651 --- /dev/null +++ b/src/plugins/output/forwarder/src/Config.h @@ -0,0 +1,122 @@ +/** + * \file src/plugins/output/forwarder/src/Config.h + * \author Michal Sedlak + * \brief Plugin configuration header + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ +#pragma once + +#include + +#include +#include + +#include "common.h" + +/// The forwarding mode +enum class ForwardMode { + UNASSIGNED, + SENDTOALL, /// Every message is forwarded to all of the hosts + ROUNDROBIN /// Only one host receives each message, next host is selected every message +}; + +struct HostConfig { + /// The displayed name of the host, purely informational + std::string name; + /// The address of the host, IP address or a hostname + std::string address; + /// The port of the host + uint16_t port; +}; + +/// The config to be passed to the forwarder +class Config +{ +public: + /// The transport protocol to be used for connection to the hosts + Protocol protocol; + /// The mode of forwarding messages + ForwardMode forward_mode; + /// Connection parameters of the hosts the data will be forwarded to + std::vector hosts; + /// The number of packets sent between sending refresh of templates (whichever happens first, packets or secs) + unsigned int tmplts_resend_pkts; + /// The number of seconds elapsed between sending refresh of templates (whichever happens first, packets or secs) + unsigned int tmplts_resend_secs; + /// The number of seconds to wait before trying to reconnect when using a TCP connection + unsigned int reconnect_secs; + /// Number of premade connections to keep + unsigned int nb_premade_connections; + + Config() {}; + + /** + * \brief Create a new configuration + * \param[in] params XML configuration of the plugin + * \throw runtime_error in case of invalid configuration + */ + Config(const char *xml_config); + +private: + /// Parse the element + void + parse_params(fds_xml_ctx_t *params_ctx); + + /// Parse the element + void + parse_hosts(fds_xml_ctx_t *hosts_ctx); + + /// Parse the element + void + parse_host(fds_xml_ctx_t *host_ctx); + + /// Set default config values + void + set_defaults(); + + /// Ensure the configuration is valid or throw an exception + void + ensure_valid(); + + /// Check if a host already exists in the vector of hosts + bool + host_exists(HostConfig host); + + /// Check if the host address can be resolved + bool + can_resolve_host(HostConfig host); +}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Connection.cpp b/src/plugins/output/forwarder/src/Connection.cpp index b7d2c5b6..0bdb55f3 100644 --- a/src/plugins/output/forwarder/src/Connection.cpp +++ b/src/plugins/output/forwarder/src/Connection.cpp @@ -1,7 +1,7 @@ /** * \file src/plugins/output/forwarder/src/Connection.cpp * \author Michal Sedlak - * \brief Buffered socket connection + * \brief Connection class implementation * \date 2021 */ @@ -41,90 +41,222 @@ #include "Connection.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + #include -Connection::Connection(ConnectionManager &manager, ConnectionParams params, long buffer_size) -: manager(manager) -, params(params) -, buffer(buffer_size) +#include "Message.h" + +Connection::Connection(const std::string &ident, ConnectionParams con_params, ipx_ctx_t *log_ctx, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs, + Connector &connector) : + m_ident(ident), + m_con_params(con_params), + m_log_ctx(log_ctx), + m_tmplts_resend_pkts(tmplts_resend_pkts), + m_tmplts_resend_secs(tmplts_resend_secs), + m_connector(connector) { } -bool +void Connection::connect() { - if (sockfd >= 0) { - ::close(sockfd); + assert(m_sockfd.get() < 0); + m_future_socket = m_connector.get(m_con_params); +} + +void +Connection::forward_message(ipx_msg_ipfix_t *msg) +{ + assert(check_connected()); + + Sender &sender = get_or_create_sender(msg); + try { + sender.process_message(msg); + + } catch (const ConnectionError &err) { + // In case connection was lost, we have to resend templates when it reconnects + sender.clear_templates(); + throw err; } - sockfd = params.make_socket(); - return sockfd >= 0; } -std::unique_lock -Connection::begin_write() +void +Connection::lose_message(ipx_msg_ipfix_t *msg) { - return std::unique_lock(buffer_mutex); + Sender &sender = get_or_create_sender(msg); + sender.lose_message(msg); } -bool -Connection::write(void *data, long length) +void +Connection::advance_transfers() { - return buffer.write((uint8_t *)data, length); + assert(check_connected()); + + IPX_CTX_DEBUG(m_log_ctx, "Waiting transfers on connection %s: %zu", m_ident.c_str(), m_transfers.size()); + + for (auto it = m_transfers.begin(); it != m_transfers.end(); ) { + + Transfer &transfer = *it; + + assert(transfer.data.size() <= UINT16_MAX); // The transfer consists of one IPFIX message which cannot be larger + + ssize_t ret = send(m_sockfd.get(), &transfer.data[transfer.offset], + transfer.data.size() - transfer.offset, MSG_DONTWAIT | MSG_NOSIGNAL); + + check_socket_error(ret); + + size_t sent = std::max(0, ret); + IPX_CTX_DEBUG(m_log_ctx, "Sent %zu/%zu B to %s", sent, transfer.data.size(), m_ident.c_str()); + + // Is the transfer done? + if (transfer.offset + sent == transfer.data.size()) { + it = m_transfers.erase(it); + // Remove the transfer and continue with the next one + + } else { + transfer.offset += sent; + + // Finish, cannot advance next transfer before the one before it is fully sent + break; + } + } } -void -Connection::rollback_write() +bool +Connection::check_connected() { - buffer.rollback(); + if (m_sockfd.get() >= 0) { + return true; + } + + if (m_future_socket && m_future_socket->ready()) { + m_sockfd = m_future_socket->retrieve(); + m_future_socket = nullptr; + return true; + } + + return false; } -long -Connection::writeable() + +static Transfer +make_transfer(const std::vector &parts, uint16_t offset, uint16_t total_length) { - return buffer.writeable(); + uint16_t length = total_length - offset; + + // Find first unfinished part + size_t i = 0; + + while (offset >= parts[i].iov_len) { + offset -= parts[i].iov_len; + i++; + } + + // Copy the unfinished portion + std::vector buffer(length); //NOTE: We might want to do this more effectively... + uint16_t buffer_pos = 0; + + for (; i < parts.size(); i++) { + memcpy(buffer.data() + buffer_pos, &((uint8_t *) parts[i].iov_base)[offset], parts[i].iov_len - offset); + buffer_pos += parts[i].iov_len - offset; + offset = 0; + } + + // Create the transfer + Transfer transfer; + transfer.data = std::move(buffer); + transfer.offset = 0; + + return transfer; } -void -Connection::commit_write() +void +Connection::store_unfinished_transfer(Message &msg, uint16_t offset) { - buffer.commit(); - manager.pipe.notify(); - has_data_to_send = buffer.readable(); + Transfer transfer = make_transfer(msg.parts(), offset, msg.length()); + + IPX_CTX_DEBUG(m_log_ctx, "Storing unfinished transfer of %" PRIu16 " bytes in connection to %s", + msg.length() - offset, m_ident.c_str()); + + m_transfers.push_back(std::move(transfer)); } -bool -Connection::send_some() + +void +Connection::send_message(Message &msg) { - if (params.protocol == TransProto::Udp) { - while (1) { - fds_ipfix_msg_hdr ipfix_header; - if (!buffer.peek(ipfix_header)) { - return true; - } - auto message_length = ntohs(ipfix_header.length); - int ret = buffer.send_data(sockfd, message_length); - if (ret == 0 || !buffer.readable()) { - return true; - } else if (ret < 0) { - return false; - } - } - return true; - } else { - return buffer.send_data(sockfd) >= 0; + // All waiting transfers have to be sent first + if (!m_transfers.empty()) { + store_unfinished_transfer(msg, 0); + return; + } + + std::vector &parts = msg.parts(); + + msghdr hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.msg_iov = parts.data(); + hdr.msg_iovlen = parts.size(); + + ssize_t ret = sendmsg(m_sockfd.get(), &hdr, MSG_DONTWAIT | MSG_NOSIGNAL); + + check_socket_error(ret); + + size_t sent = std::max(0, ret); + + IPX_CTX_DEBUG(m_log_ctx, "Sent %zu/%" PRIu16 " B to %s", sent, msg.length(), m_ident.c_str()); + + if (sent < msg.length()) { + store_unfinished_transfer(msg, sent); } } -void -Connection::close() +Sender & +Connection::get_or_create_sender(ipx_msg_ipfix_t *msg) { - close_flag = true; - manager.pipe.notify(); + uint32_t odid = ipx_msg_ipfix_get_ctx(msg)->odid; + + if (m_senders.find(odid) == m_senders.end()) { + m_senders.emplace(odid, + std::unique_ptr(new Sender( + [&](Message &msg) { + send_message(msg); + }, + m_con_params.protocol == Protocol::TCP, + m_tmplts_resend_pkts, + m_tmplts_resend_secs))); + } + + Sender &sender = *m_senders[odid].get(); + + return sender; } -Connection::~Connection() +void +Connection::check_socket_error(ssize_t sock_ret) { - if (sockfd >= 0) { - ::close(sockfd); + if (sock_ret < 0 && errno != EWOULDBLOCK && errno != EAGAIN) { + char *errbuf; + ipx_strerror(errno, errbuf); + + IPX_CTX_ERROR(m_log_ctx, "A connection to %s lost! (%s)", m_ident.c_str(), errbuf); + m_sockfd.reset(); + + // All state from the previous connection is lost once new one is estabilished + m_transfers.clear(); + + throw ConnectionError(errbuf); } -} \ No newline at end of file +} diff --git a/src/plugins/output/forwarder/src/Connection.h b/src/plugins/output/forwarder/src/Connection.h index 09af5a29..02c76f1d 100644 --- a/src/plugins/output/forwarder/src/Connection.h +++ b/src/plugins/output/forwarder/src/Connection.h @@ -1,7 +1,7 @@ /** * \file src/plugins/output/forwarder/src/Connection.h * \author Michal Sedlak - * \brief Buffered socket connection + * \brief Connection class header * \date 2021 */ @@ -41,77 +41,144 @@ #pragma once -#include "ConnectionManager.h" -#include "ConnectionParams.h" -#include "ConnectionBuffer.h" - -#include -#include -#include -#include - +#include +#include +#include #include -#include -#include -class ConnectionManager; +#include + +#include "common.h" +#include "connector/Connector.h" +#include "Sender.h" -class Connection -{ -friend class ConnectionManager; +class Connection; +/// An error to be thrown on connection errors +class ConnectionError { public: - /// Flag indicating that the connection was lost and the forwarder needs to resend templates etc. - /// The flag won't be reset when the connection is reestablished! - std::atomic connection_lost_flag { false }; + ConnectionError(std::string message) + : m_message(message) {} + + ConnectionError(std::string message, std::shared_ptr &connection) + : m_message(message), m_connection(&connection) {} + + ConnectionError with_connection(std::shared_ptr &connection) const + { return ConnectionError(m_message, connection); } + + const char *what() const { return m_message.c_str(); } - Connection(ConnectionManager &manager, ConnectionParams params, long buffer_size); - - bool + std::shared_ptr *connection() const { return m_connection; } + +private: + std::string m_message; + std::shared_ptr *m_connection; +}; + +/// A transfer to be sent through the connection +struct Transfer { + /// The data to send + std::vector data; + /// The offset to send from, i.e. the amount of data that was already sent + uint16_t offset; +}; + +/// A class representing one of the connections to the subcollector +/// Each host opens one connection per session +class Connection { +public: + /** + * \brief The constructor + * \param ident The host identification + * \param con_params The connection parameters + * \param log_ctx The logging context + * \param tmplts_resend_pkts Interval in packets after which templates are resend (UDP only) + * \param tmplts_resend_secs Interval in seconds after which templates are resend (UDP only) + */ + Connection(const std::string &ident, ConnectionParams con_params, ipx_ctx_t *log_ctx, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs, + Connector &connector); + + /// Do not permit copying or moving as the connection holds a raw socket that is closed in the destructor + /// (we could instead implement proper moving and copying behavior, but we don't really need it at the moment) + Connection(const Connection &) = delete; + Connection(Connection &&) = delete; + + /** + * \brief Connect the connection socket + * \throw ConnectionError if the socket couldn't be connected + */ + void connect(); - std::unique_lock - begin_write(); + /** + * \brief Forward an IPFIX message + * \param msg The IPFIX message + */ + void + forward_message(ipx_msg_ipfix_t *msg); + + /** + * \brief Lose an IPFIX message, i.e. update the internal state as if it has been forwarded + * even though it is not being sent + * \param msg The IPFIX message + */ + void + lose_message(ipx_msg_ipfix_t *msg); + + /** + * \brief Advance the unfinished transfers + */ + void + advance_transfers(); + + /** + * \brief Check if the connection socket is currently connected + * \return true or false + */ + bool check_connected(); + + /** + * \brief Get number of transfers still waiting to be transmitted + * \return The number of waiting transfers + */ + size_t waiting_transfers_cnt() const { return m_transfers.size(); } + + /** + * \brief The identification of the connection + */ + const std::string &ident() const { return m_ident; } - bool - write(void *data, long length); +private: + const std::string &m_ident; - bool - send_some(); + ConnectionParams m_con_params; - void - commit_write(); + ipx_ctx_t *m_log_ctx; - void - rollback_write(); + unsigned int m_tmplts_resend_pkts; - long - writeable(); + unsigned int m_tmplts_resend_secs; - void - close(); + UniqueFd m_sockfd; - ~Connection(); + std::shared_ptr m_future_socket; -private: - /// The manager managing this connection - ConnectionManager &manager; + std::unordered_map> m_senders; + + std::vector m_transfers; - /// The parameters to estabilish the connection - ConnectionParams params; + Connector &m_connector; - /// The connection socket - int sockfd = -1; + void + store_unfinished_transfer(Message &msg, uint16_t offset); - /// Buffer for the data to send and a mutex guarding it - /// (buffer will be accessed from sender thread and writer thread) - std::mutex buffer_mutex; - ConnectionBuffer buffer; + void + send_message(Message &msg); - /// Flag indicating whether the buffer has any data to send so we don't have to lock the mutex every time - /// (doesn't need to be atomic because we only set it while holding the mutex) - bool has_data_to_send = false; + Sender & + get_or_create_sender(ipx_msg_ipfix_t *msg); - /// Flag indicating that the connection has been closed and can be disposed of after the data is sent - std::atomic close_flag { false }; + void + check_socket_error(ssize_t sock_ret); }; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/ConnectionBuffer.h b/src/plugins/output/forwarder/src/ConnectionBuffer.h deleted file mode 100644 index 9db928a0..00000000 --- a/src/plugins/output/forwarder/src/ConnectionBuffer.h +++ /dev/null @@ -1,221 +0,0 @@ -/** - * \file src/plugins/output/forwarder/src/ConnectionBuffer.h - * \author Michal Sedlak - * \brief Ring buffer used by connections - * \date 2021 - */ - -/* Copyright (C) 2021 CESNET, z.s.p.o. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * 3. Neither the name of the Company nor the names of its contributors - * may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * ALTERNATIVELY, provided that this notice is retained in full, this - * product may be distributed under the terms of the GNU General Public - * License (GPL) version 2 or later, in which case the provisions - * of the GPL apply INSTEAD OF those given above. - * - * This software is provided ``as is'', and any express or implied - * warranties, including, but not limited to, the implied warranties of - * merchantability and fitness for a particular purpose are disclaimed. - * In no event shall the company or contributors be liable for any - * direct, indirect, incidental, special, exemplary, or consequential - * damages (including, but not limited to, procurement of substitute - * goods or services; loss of use, data, or profits; or business - * interruption) however caused and on any theory of liability, whether - * in contract, strict liability, or tort (including negligence or - * otherwise) arising in any way out of the use of this software, even - * if advised of the possibility of such damage. - * - */ - -#pragma once - -#include - -#include -#include -#include -#include - -class ConnectionBuffer -{ -public: - ConnectionBuffer(long capacity) - : capacity(capacity) - , buffer(capacity) - {} - - void - rollback() - { - write_offset = read_end_offset; - } - - void - commit() - { - read_end_offset = write_offset; - } - - long - writeable() - { - return writeable_from(write_offset); - } - - bool - write(uint8_t *data, long length) - { - long pos = raw_write_at(write_offset, data, length); - if (pos == -1) { - return false; - } - write_offset = pos; - return true; - } - - template - bool - write(T data) - { - return write((uint8_t *)&data, sizeof(T)); - } - - long - readable() - { - return read_offset > read_end_offset - ? capacity - read_offset + read_end_offset - : read_end_offset - read_offset; - } - - bool - peek(uint8_t *data, long length) - { - if (readable() < length) { - return false; - } - raw_read_at(read_offset, data, length); - return true; - } - - template - bool - peek(T &item) - { - return peek((uint8_t *)&item, sizeof(item)); - } - - int - send_data(int sockfd, long length = -1) - { - if (length == -1) { - length = readable(); - } - iovec iov[2] = {}; - iov[0].iov_len = std::min(cont_readable_from(read_offset), length); - iov[0].iov_base = &buffer[read_offset]; - iov[1].iov_len = length - iov[0].iov_len; - iov[1].iov_base = &buffer[0]; - msghdr msg_hdr = {}; - msg_hdr.msg_iov = iov; - msg_hdr.msg_iovlen = 2; - int ret = sendmsg(sockfd, &msg_hdr, MSG_DONTWAIT | MSG_NOSIGNAL); - if (ret < 0) { - return (errno == EWOULDBLOCK || errno == EAGAIN) ? 0 : ret; - } - read_offset = advance(read_offset, ret); - return ret; - } - -private: - long capacity; - long read_offset = 0; - long read_end_offset = 0; - long write_offset = 0; - std::vector buffer; - - long - advance(long pos, long n) - { - return (pos + n) % capacity; - } - - long - readable_from(long pos) - { - return pos > read_end_offset - ? capacity - pos + read_end_offset - : read_end_offset - pos; - } - - long - cont_readable_from(long pos) - { - return pos > read_end_offset - ? capacity - pos - : read_end_offset - pos; - } - - long - raw_read_at(long pos, uint8_t *data, long length) - { - if (readable_from(pos) < length) { - return -1; - } - long read1 = std::min(cont_readable_from(pos), length); - long read2 = length - read1; - memcpy(&data[0], &buffer[pos], read1); - memcpy(&data[read1], &buffer[advance(pos, read1)], read2); - return advance(pos, length); - } - - long - cont_writeable_from(long pos) - { - return read_offset > pos - ? read_offset - pos - 1 - : (read_offset == 0 ? capacity - pos - 1 : capacity - pos); - } - - long - writeable_from(long pos) - { - return read_offset > pos - ? read_offset - pos - 1 - : capacity - pos + read_offset - 1; - } - - long - raw_write_at(long pos, uint8_t *data, long length) - { - /// WARNING: Does not advance the write offset - if (writeable_from(pos) < length) { - return -1; - } - long write1 = std::min(length, cont_writeable_from(pos)); - long write2 = length - write1; - memcpy(&buffer[pos], &data[0], write1); - memcpy(&buffer[advance(pos, write1)], &data[write1], write2); - return advance(pos, length); - } - - template - bool - raw_write_at(long pos, T data) - { - return raw_write_at(pos, (uint8_t *)&data, sizeof(T)); - } - -}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/ConnectionManager.cpp b/src/plugins/output/forwarder/src/ConnectionManager.cpp deleted file mode 100644 index f55dafaf..00000000 --- a/src/plugins/output/forwarder/src/ConnectionManager.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/** - * \file src/plugins/output/forwarder/src/ConnectionManager.cpp - * \author Michal Sedlak - * \brief Connection manager - * \date 2021 - */ - -/* Copyright (C) 2021 CESNET, z.s.p.o. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * 3. Neither the name of the Company nor the names of its contributors - * may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * ALTERNATIVELY, provided that this notice is retained in full, this - * product may be distributed under the terms of the GNU General Public - * License (GPL) version 2 or later, in which case the provisions - * of the GPL apply INSTEAD OF those given above. - * - * This software is provided ``as is'', and any express or implied - * warranties, including, but not limited to, the implied warranties of - * merchantability and fitness for a particular purpose are disclaimed. - * In no event shall the company or contributors be liable for any - * direct, indirect, incidental, special, exemplary, or consequential - * damages (including, but not limited to, procurement of substitute - * goods or services; loss of use, data, or profits; or business - * interruption) however caused and on any theory of liability, whether - * in contract, strict liability, or tort (including negligence or - * otherwise) arising in any way out of the use of this software, even - * if advised of the possibility of such damage. - * - */ - -#include "ConnectionManager.h" - -Connection & -ConnectionManager::add_client(ConnectionParams params) -{ - auto connection_ptr = std::unique_ptr(new Connection(*this, params, connection_buffer_size)); - auto &connection = *connection_ptr; - std::lock_guard guard(mutex); - if (connection.connect()) { - active_connections.push_back(std::move(connection_ptr)); - } else { - reconnect_connections.push_back(std::move(connection_ptr)); - } - return connection; -} - -void -ConnectionManager::send_loop() -{ - int max_fd; - - fd_set pipe_fds; - FD_ZERO(&pipe_fds); - FD_SET(pipe.get_readfd(), &pipe_fds); - - fd_set socket_fds; - FD_ZERO(&socket_fds); - - auto watch_sock = [&](int fd) { - FD_SET(fd, &pipe_fds); - max_fd = std::max(max_fd, fd); - }; - - while (!exit_flag) { - max_fd = pipe.get_readfd(); - FD_ZERO(&socket_fds); - - { - std::lock_guard guard(mutex); - pipe.clear(); - auto it = active_connections.begin(); - while (it != active_connections.end()) { - auto &connection = **it; - if (connection.has_data_to_send) { - std::lock_guard guard(connection.buffer_mutex); - if (connection.send_some()) { - if (connection.buffer.readable()) { - watch_sock(connection.sockfd); - } - connection.has_data_to_send = connection.buffer.readable(); - } else { - connection.connection_lost_flag = true; - reconnect_connections.push_back(std::move(*it)); - it = active_connections.erase(it); - reconnect_cv.notify_one(); - continue; - } - } else { - if (connection.close_flag) { - it = active_connections.erase(it); - continue; - } - } - it++; - } - } - - select(max_fd + 1, &pipe_fds, &socket_fds, NULL, NULL); - } -} - -void -ConnectionManager::reconnect_loop() -{ - while (!exit_flag) { - auto lock = std::unique_lock(mutex); - auto it = reconnect_connections.begin(); - while (it != reconnect_connections.end()) { - auto &connection = **it; - if (connection.connect()) { - active_connections.push_back(std::move(*it)); - it = reconnect_connections.erase(it); - pipe.notify(); - } else { - if (connection.close_flag) { - it = reconnect_connections.erase(it); - continue; - } - it++; - } - } - - if (reconnect_connections.empty()) { - reconnect_cv.wait(lock); - } else { - reconnect_cv.wait_for(lock, std::chrono::seconds(reconnect_interval_secs)); - } - } -} - -void -ConnectionManager::start() -{ - send_thread = std::thread([this]() { send_loop(); }); - reconnect_thread = std::thread([this]() { reconnect_loop(); }); -} - -void -ConnectionManager::stop() -{ - exit_flag = true; - pipe.notify(); - reconnect_cv.notify_one(); - send_thread.join(); - reconnect_thread.join(); -} - -void -ConnectionManager::set_reconnect_interval(int number_of_seconds) -{ - reconnect_interval_secs = number_of_seconds; -} - -void -ConnectionManager::set_connection_buffer_size(long number_of_bytes) -{ - connection_buffer_size = number_of_bytes; -} - diff --git a/src/plugins/output/forwarder/src/ConnectionParams.h b/src/plugins/output/forwarder/src/ConnectionParams.h deleted file mode 100644 index 5f9fbfa7..00000000 --- a/src/plugins/output/forwarder/src/ConnectionParams.h +++ /dev/null @@ -1,136 +0,0 @@ -/** - * \file src/plugins/output/forwarder/src/ConnectionParams.h - * \author Michal Sedlak - * \brief Parameters for estabilishing connection - * \date 2021 - */ - -/* Copyright (C) 2021 CESNET, z.s.p.o. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * 3. Neither the name of the Company nor the names of its contributors - * may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * ALTERNATIVELY, provided that this notice is retained in full, this - * product may be distributed under the terms of the GNU General Public - * License (GPL) version 2 or later, in which case the provisions - * of the GPL apply INSTEAD OF those given above. - * - * This software is provided ``as is'', and any express or implied - * warranties, including, but not limited to, the implied warranties of - * merchantability and fitness for a particular purpose are disclaimed. - * In no event shall the company or contributors be liable for any - * direct, indirect, incidental, special, exemplary, or consequential - * damages (including, but not limited to, procurement of substitute - * goods or services; loss of use, data, or profits; or business - * interruption) however caused and on any theory of liability, whether - * in contract, strict liability, or tort (including negligence or - * otherwise) arising in any way out of the use of this software, even - * if advised of the possibility of such damage. - * - */ - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -using unique_addrinfo = std::unique_ptr; - -enum class TransProto { Tcp, Udp }; - -struct ConnectionParams -{ - ConnectionParams(std::string address, std::string port, TransProto protocol) - : address(address) - , port(port) - , protocol(protocol) - { - } - - unique_addrinfo - resolve_address() - { - addrinfo *info; - addrinfo hints = {}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = (protocol == TransProto::Tcp ? SOCK_STREAM : SOCK_DGRAM); - hints.ai_protocol = (protocol == TransProto::Tcp ? IPPROTO_TCP : IPPROTO_UDP); - if (getaddrinfo(address.c_str(), port.c_str(), &hints, &info) == 0) { - return unique_addrinfo(info, &freeaddrinfo); - } else { - return unique_addrinfo(NULL, &freeaddrinfo); - } - } - - int - make_socket() - { - auto address_info = resolve_address(); - if (!address_info) { - return -1; - } - - int sockfd; - addrinfo *p; - - for (p = address_info.get(); p != NULL; p = p->ai_next) { - sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (sockfd < 0) { - continue; - } - - if (protocol == TransProto::Udp) { - sockaddr_in sa = {}; - sa.sin_family = AF_INET; - sa.sin_port = 0; - sa.sin_addr.s_addr = INADDR_ANY; - sa.sin_port = 0; - if (bind(sockfd, (sockaddr *)&sa, sizeof(sa)) != 0) { - close(sockfd); - continue; - } - } - - if (connect(sockfd, p->ai_addr, p->ai_addrlen) != 0) { - close(sockfd); - continue; - } - - break; - } - - if (!p) { - return -1; - } - - return sockfd; - } - - std::string - str() - { - return std::string(protocol == TransProto::Tcp ? "TCP" : "UDP") + ":" - + address + ":" - + port; - } - - std::string address; - std::string port; - TransProto protocol; -}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Forwarder.cpp b/src/plugins/output/forwarder/src/Forwarder.cpp new file mode 100644 index 00000000..da0e89f1 --- /dev/null +++ b/src/plugins/output/forwarder/src/Forwarder.cpp @@ -0,0 +1,133 @@ +/** + * \file src/plugins/output/forwarder/src/Forwarder.cpp + * \author Michal Sedlak + * \brief Forwarder class implementation + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "Forwarder.h" + +Forwarder::Forwarder(Config config, ipx_ctx_t *log_ctx) : + m_config(config), + m_log_ctx(log_ctx) +{ + // Set up connector + std::vector con_params; + for (const auto &host_config : m_config.hosts) { + con_params.push_back(ConnectionParams{host_config.address, host_config.port, m_config.protocol}); + } + + m_connector.reset(new Connector(con_params, m_config.nb_premade_connections, + m_config.reconnect_secs, m_log_ctx)); + + // Set up hosts + for (const auto &host_config : m_config.hosts) { + m_hosts.emplace_back( + new Host(host_config.name, + ConnectionParams{host_config.address, host_config.port, m_config.protocol}, + m_log_ctx, + m_config.tmplts_resend_pkts, + m_config.tmplts_resend_secs, + m_config.forward_mode == ForwardMode::SENDTOALL, + *m_connector.get())); + + } +} + +void Forwarder::handle_session_message(ipx_msg_session_t *msg) +{ + const ipx_session *session = ipx_msg_session_get_session(msg); + + switch (ipx_msg_session_get_event(msg)) { + case IPX_MSG_SESSION_OPEN: + IPX_CTX_DEBUG(m_log_ctx, "New session %s", session->ident); + for (auto &host : m_hosts) { + host->setup_connection(session); + } + break; + + case IPX_MSG_SESSION_CLOSE: + IPX_CTX_DEBUG(m_log_ctx, "Closing session %s", session->ident); + for (auto &host : m_hosts) { + host->finish_connection(session); + } + break; + } +} + +void Forwarder::handle_ipfix_message(ipx_msg_ipfix_t *msg) +{ + // Forward message + switch (m_config.forward_mode) { + case ForwardMode::SENDTOALL: + forward_to_all(msg); + break; + + case ForwardMode::ROUNDROBIN: + forward_round_robin(msg); + break; + + default: assert(0); + } +} + +void +Forwarder::forward_to_all(ipx_msg_ipfix_t *msg) +{ + for (auto &host : m_hosts) { + host->forward_message(msg); + } +} + +void +Forwarder::forward_round_robin(ipx_msg_ipfix_t *msg) +{ + bool ok = false; + + for (size_t i = 0; i < m_hosts.size(); i++) { + auto &host = m_hosts[m_rr_index]; + ok = host->forward_message(msg); + m_rr_index = (m_rr_index + 1) % m_hosts.size(); + if (ok) { + break; + } + } + + if (!ok) { + IPX_CTX_WARNING(m_log_ctx, "Couldn't forward to any of the hosts, dropping message!", 0); + } +} diff --git a/src/plugins/output/forwarder/src/Forwarder.h b/src/plugins/output/forwarder/src/Forwarder.h index d75e6182..2fc11ddd 100644 --- a/src/plugins/output/forwarder/src/Forwarder.h +++ b/src/plugins/output/forwarder/src/Forwarder.h @@ -1,7 +1,7 @@ /** * \file src/plugins/output/forwarder/src/Forwarder.h * \author Michal Sedlak - * \brief Forwarder logic + * \brief Forwarder class header * \date 2021 */ @@ -41,402 +41,64 @@ #pragma once -#include "ConnectionManager.h" -#include "IPFIXMessage.h" -#include "MessageBuilder.h" +#include "Config.h" -#include -#include - -#include +#include #include -enum class ForwardMode { SendToAll, RoundRobin }; - -struct Session; -struct Client; - -struct Odid -{ - Odid() {} // Default constructor needed because of std::map - - Odid(Session &session, uint32_t odid) - : session(&session) - , odid(odid) - {} - - Session *session; - uint32_t odid; - uint32_t seq_num = 0; - - const fds_tsnapshot_t *templates_snapshot = NULL; - std::time_t last_templates_send_time = 0; - unsigned bytes_since_templates_sent = 0; - - std::string - str(); - - void - reset_values() - { - seq_num = 0; - templates_snapshot = NULL; - last_templates_send_time = 0; - bytes_since_templates_sent = 0; - } -}; - -struct Session -{ - Session(Connection &connection, Client &client, std::string ident) - : connection(connection) - , client(client) - , ident(ident) - {} - - Connection &connection; - Client &client; - std::string ident; - - std::map odids; - - std::string - str(); -}; - -struct Client -{ - Client(ConnectionManager &connection_manager, ConnectionParams connection_params, std::string name = "") - : connection_manager(connection_manager) - , connection_params(connection_params) - , name(name) - {} - - ConnectionManager &connection_manager; - ConnectionParams connection_params; - std::string name; - std::map> sessions; - - std::string - str() - { - return name; - } -}; - -std::string -Odid::str() -{ - return session->ident + "(" + std::to_string(odid) + ") -> " + session->client.name; -} - -std::string -Session::str() -{ - return ident + " -> " + client.name; -} +#include "Host.h" +#include "common.h" +#include "connector/Connector.h" -class Forwarder -{ +/// A class representing the forwarder itself +class Forwarder { public: - Forwarder(ipx_ctx_t *log_ctx) - : log_ctx(log_ctx) - {} - - void - set_transport_protocol(TransProto transport_protocol) - { - this->transport_protocol = transport_protocol; - } - - void - set_forward_mode(ForwardMode forward_mode) - { - this->forward_mode = forward_mode; - } - - void - set_connection_buffer_size(long number_of_bytes) - { - connection_manager.set_connection_buffer_size(number_of_bytes); - } - - void - set_template_refresh_interval_secs(int number_of_seconds) - { - this->template_refresh_interval_secs = number_of_seconds; - } - - void - set_template_refresh_interval_bytes(int number_of_bytes) - { - this->template_refresh_interval_bytes = number_of_bytes; - } - - void - set_reconnect_interval(int secs) - { - connection_manager.set_reconnect_interval(secs); - } - - void - add_client(std::string address, std::string port, std::string name = "") - { - auto connection_params = ConnectionParams { address, port, transport_protocol }; - if (!connection_params.resolve_address()) { - throw "Cannot resolve address " + address; - } - if (name.empty()) { - name = connection_params.str(); - } - auto client = Client { connection_manager, connection_params, name }; - clients.push_back(std::move(client)); - IPX_CTX_INFO(log_ctx, "Added client %s @ %s\n", name.c_str(), connection_params.str().c_str()); - } - - void - on_session_message(ipx_msg_session_t *session_msg) - { - const ipx_session *session = ipx_msg_session_get_session(session_msg); - switch (ipx_msg_session_get_event(session_msg)) { - case IPX_MSG_SESSION_OPEN: - for (auto &client : clients) { - open_session(client, session); - } - break; - case IPX_MSG_SESSION_CLOSE: - for (auto &client : clients) { - close_session(client, session); - } - break; - } - } - - void - on_ipfix_message(ipx_msg_ipfix_t *ipfix_msg) - { - auto message = IPFIXMessage { ipfix_msg }; - switch (forward_mode) { - case ForwardMode::RoundRobin: - forward_round_robin(message); - break; - case ForwardMode::SendToAll: - forward_to_all(message); - break; - } - } - - void - start() - { - connection_manager.start(); - } - - void - stop() - { - connection_manager.stop(); - - IPX_CTX_INFO(log_ctx, "Total bytes forwarded: %ld", total_bytes); - IPX_CTX_INFO(log_ctx, "Dropped messages: %ld", dropped_messages); - IPX_CTX_INFO(log_ctx, "Dropped data records: %ld", dropped_data_records); - } - - ~Forwarder() - { - } + /** + * \brief The constructor + * \param config The forwarder configuration + * \param log_ctx The logging context + */ + Forwarder(Config config, ipx_ctx_t *log_ctx); + + /** + * Disable copy and move constructors + */ + Forwarder(const Forwarder &) = delete; + Forwarder(Forwarder &&) = delete; + + /** + * \brief Handle a session message + * \param msg The session message + */ + void + handle_session_message(ipx_msg_session_t *msg); + + /** + * \brief Handle an IPFIX message + * \param msg The IPFIX message + */ + void + handle_ipfix_message(ipx_msg_ipfix_t *msg); + + /** + * \brief The destructor - finalize the forwarder + */ + ~Forwarder() {} private: - /// Logging context - ipx_ctx_t *log_ctx; + Config m_config; - /// Configuration - TransProto transport_protocol = TransProto::Tcp; - ForwardMode forward_mode = ForwardMode::SendToAll; - int template_refresh_interval_secs = 0; - int template_refresh_interval_bytes = 0; + ipx_ctx_t *m_log_ctx; - /// Mutating state - ConnectionManager connection_manager; - std::vector clients; - int rr_next_client = 0; + std::vector> m_hosts; - /// Statistics - long dropped_messages = 0; - long dropped_data_records = 0; - long total_bytes = 0; + size_t m_rr_index = 0; - void - open_session(Client &client, const ipx_session *session_info) - { - auto &connection = connection_manager.add_client(client.connection_params); - auto session_ptr = std::unique_ptr(new Session { connection, client, session_info->ident }); - IPX_CTX_INFO(log_ctx, "Opened session %s", session_ptr->str().c_str()); - client.sessions[session_info] = std::move(session_ptr); - } + std::unique_ptr m_connector; void - close_session(Client &client, const ipx_session *session_info) - { - auto &session = *client.sessions[session_info]; - session.connection.close(); - IPX_CTX_INFO(log_ctx, "Closed session %s", session.str().c_str()); - client.sessions.erase(session_info); - } + forward_to_all(ipx_msg_ipfix_t *msg); void - forward_to_all(IPFIXMessage &message) - { - for (auto &client : clients) { - if (!forward_message(client, message)) { - dropped_messages += 1; - dropped_data_records += message.drec_count(); - } - } - } - - void - forward_round_robin(IPFIXMessage &message) - { - int i = 0; - while (1) { - auto &client = next_client(); - if (forward_message(client, message)) { - break; - } - - i++; - - // If we went through all the clients multiple times in a row and all the buffers are still full, - // let's just give up and move onto the next message. If we loop for too long we'll start losing messages! - if (i < (int)clients.size() * 10) { - dropped_messages += 1; - dropped_data_records += message.drec_count(); - break; - } - } - } - - /// Pick the next client in round-robin mode - Client & - next_client() - { - if (rr_next_client == (int)clients.size()) { - rr_next_client = 0; - } - return clients[rr_next_client++]; - } - - /// Send all templates from the templates snapshot obtained from the message - /// through the session connection and update the state accordingly - /// - /// \return true if there was enough space in the connection buffer, false otherwise - bool - send_templates(Session &session, Odid &odid, IPFIXMessage &message) - { - auto templates_snapshot = message.get_templates_snapshot(); - - auto header = *message.header(); - header.seq_num = htonl(odid.seq_num); - - MessageBuilder builder; - builder.begin_message(header); - - fds_tsnapshot_for(templates_snapshot, - [](const fds_template *tmplt, void *data) -> bool { - auto &builder = *(MessageBuilder *)data; - builder.write_template(tmplt); - return true; - }, &builder); - - builder.finalize_message(); - - auto lock = session.connection.begin_write(); - if (builder.message_length() > session.connection.writeable()) { - // IPX_CTX_WARNING(log_ctx, - // "[%s] Cannot send templates because buffer is full! (need %dB, have %ldB)", - // odid.str().c_str(), builder.message_length(), session.connection.writeable()); - return false; - } - session.connection.write(builder.message_data(), builder.message_length()); - session.connection.commit_write(); - - odid.templates_snapshot = templates_snapshot; - odid.bytes_since_templates_sent = 0; - odid.last_templates_send_time = std::time(NULL); - - total_bytes += builder.message_length(); - // IPX_CTX_INFO(log_ctx, "[%s] Sent templates", odid.str().c_str()); - - return true; - } - - bool - should_refresh_templates(Odid &odid) - { - if (transport_protocol != TransProto::Udp) { - return false; - } - auto time_since = (std::time(NULL) - odid.last_templates_send_time); - return (time_since > (unsigned)template_refresh_interval_secs) - || (odid.bytes_since_templates_sent > (unsigned)template_refresh_interval_bytes); - } - - bool - templates_changed(Odid &odid, IPFIXMessage &message) - { - auto templates_snapshot = message.get_templates_snapshot(); - return templates_snapshot && odid.templates_snapshot != templates_snapshot; - } - - /// Forward message to the client, including templates update if needed - /// - /// \return true if there was enough space in the connection buffer, false otherwise - bool - forward_message(Client &client, IPFIXMessage &message) - { - auto &session = *client.sessions[message.session()]; - - if (session.connection.connection_lost_flag) { - for (auto &p : session.odids) { - p.second.reset_values(); - } - session.connection.connection_lost_flag = false; - } - - if (session.odids.find(message.odid()) == session.odids.end()) { - session.odids[message.odid()] = Odid { session, message.odid() }; - IPX_CTX_INFO(log_ctx, "[%s] Seen new ODID %u", session.str().c_str(), message.odid()); - } - auto &odid = session.odids[message.odid()]; - - if (should_refresh_templates(odid) || templates_changed(odid, message)) { - if (message.get_templates_snapshot() != nullptr && !send_templates(session, odid, message)) { - return false; - } - } - - auto lock = session.connection.begin_write(); - if (message.length() > session.connection.writeable()) { - // IPX_CTX_WARNING(log_ctx, - // "[%s] Cannot forward message because buffer is full! (need %dB, have %ldB)", - // odid.str().c_str(), message.length(), session.connection.writeable()); - return false; - } - - auto header = *message.header(); - header.seq_num = htonl(odid.seq_num); - session.connection.write(&header, sizeof(header)); - session.connection.write(message.data() + sizeof(header), message.length() - sizeof(header)); - session.connection.commit_write(); - - // IPX_CTX_DEBUG(log_ctx, "[%s] Forwarded message", odid.str().c_str()); - - odid.bytes_since_templates_sent += message.length(); - odid.seq_num += message.drec_count(); - - total_bytes += message.length(); - - return true; - } + forward_round_robin(ipx_msg_ipfix_t *msg); }; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Host.cpp b/src/plugins/output/forwarder/src/Host.cpp new file mode 100644 index 00000000..26851e7e --- /dev/null +++ b/src/plugins/output/forwarder/src/Host.cpp @@ -0,0 +1,168 @@ +/** + * \file src/plugins/output/forwarder/src/Host.cpp + * \author Michal Sedlak + * \brief Host class implementation + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "Host.h" +#include + +Host::Host(const std::string &ident, ConnectionParams con_params, ipx_ctx_t *log_ctx, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs, bool indicate_lost_msgs, + Connector &connector) : + m_ident(ident), + m_con_params(con_params), + m_log_ctx(log_ctx), + m_tmplts_resend_pkts(tmplts_resend_pkts), + m_tmplts_resend_secs(tmplts_resend_secs), + m_indicate_lost_msgs(indicate_lost_msgs), + m_connector(connector) +{ +} + +void +Host::setup_connection(const ipx_session *session) +{ + // There shouldn't be a connection for the session yet + assert(m_session_to_connection.find(session) == m_session_to_connection.end()); + + // Create new connection + IPX_CTX_INFO(m_log_ctx, "Setting up new connection to %s", m_ident.c_str()); + + m_session_to_connection.emplace(session, + std::unique_ptr(new Connection( + m_ident, + m_con_params, + m_log_ctx, + m_tmplts_resend_pkts, + m_tmplts_resend_secs, + m_connector))); + m_session_to_connection[session]->connect(); +} + +void +Host::finish_connection(const ipx_session *session) +{ + IPX_CTX_INFO(m_log_ctx, "Finishing a connection to %s", m_ident.c_str()); + + // Get the corresponding connection and remove it from the session map + std::unique_ptr &connection = m_session_to_connection[session]; + assert(connection); + + // Try to send the waiting transfers if there are any, if the connection is dropped just finish anyway + if (connection->check_connected()) { + + try { + connection->advance_transfers(); + + } catch (const ConnectionError &) { + // Ignore... + } + } + + if (connection->waiting_transfers_cnt() > 0) { + IPX_CTX_WARNING(m_log_ctx, "Dropping %zu transfers when finishing connection", + connection->waiting_transfers_cnt()); + } + + IPX_CTX_INFO(m_log_ctx, "Connection to %s finished", m_ident.c_str()); + + m_session_to_connection.erase(session); +} + +bool +Host::forward_message(ipx_msg_ipfix_t *msg) +{ + const ipx_session *session = ipx_msg_ipfix_get_ctx(msg)->session; + Connection &connection = *m_session_to_connection[session].get(); + + if (!connection.check_connected()) { + if (m_indicate_lost_msgs) { + connection.lose_message(msg); + } + return false; + } + + try { + connection.advance_transfers(); + + if (connection.waiting_transfers_cnt() > 0) { + IPX_CTX_DEBUG(m_log_ctx, "Message to %s not forwarded because there are unsent transfers\n", m_ident.c_str()); + if (m_indicate_lost_msgs) { + connection.lose_message(msg); + } + return false; + } + + IPX_CTX_DEBUG(m_log_ctx, "Forwarding message to %s\n", m_ident.c_str()); + + connection.forward_message(msg); + + } catch (const ConnectionError &err) { + IPX_CTX_ERROR(m_log_ctx, "Lost connection while forwarding: %s", err.what()); + connection.connect(); + return false; + } + + return true; +} + +Host::~Host() +{ + for (auto &p : m_session_to_connection) { + std::unique_ptr &connection = p.second; + + // Try to send the waiting transfers if there are any, if the connection is dropped just finish anyway + if (connection->check_connected()) { + + try { + connection->advance_transfers(); + + } catch (const ConnectionError &) { + // Ignore... + } + } + + if (connection->waiting_transfers_cnt() > 0) { + IPX_CTX_WARNING(m_log_ctx, "Dropping %zu transfers when closing connection %s", + connection->waiting_transfers_cnt(), connection->ident().c_str()); + } + } + + IPX_CTX_INFO(m_log_ctx, "All connections to %s closed", m_ident.c_str()); +} diff --git a/src/plugins/output/forwarder/src/Host.h b/src/plugins/output/forwarder/src/Host.h new file mode 100644 index 00000000..4189f566 --- /dev/null +++ b/src/plugins/output/forwarder/src/Host.h @@ -0,0 +1,119 @@ +/** + * \file src/plugins/output/forwarder/src/Host.h + * \author Michal Sedlak + * \brief Host class header + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#pragma once + +#include +#include +#include +#include "common.h" +#include "Config.h" +#include "Connection.h" +#include "connector/Connector.h" + +/// A class representing one of the subcollectors messages are forwarded to +class Host { +public: + /** + * \brief The constructor + * \param ident The host identification + * \param con_params The connection parameters + * \param log_ctx The logging context + * \param tmplts_resend_pkts Interval in packets after which templates are resend (UDP only) + * \param tmplts_resend_secs Interval in seconds after which templates are resend (UDP only) + * \param indicate_lost_msgs Indicate that the message has been lost if it couldn't be forwarded + * by increasing the sequence numbers + */ + Host(const std::string &ident, ConnectionParams con_params, ipx_ctx_t *log_ctx, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs, bool indicate_lost_msgs, + Connector &connector); + + /** + * Disable copy and move constructors + */ + Host(const Host &) = delete; + Host(Host &&) = delete; + + /** + * \brief The destructor - finishes all the connections + */ + ~Host(); + + /** + * \brief Set up a new connection for the sesison + * \param session The session + */ + void + setup_connection(const ipx_session *session); + + /** + * \brief Finish a connection for the sesison + * \param session The session + */ + void + finish_connection(const ipx_session *session); + + /** + * \brief Forward an IPFIX message to this host + * \param msg The IPFIX message + * \return true on success, false on failure + * \throw ConnectionError when the connection fails + */ + bool + forward_message(ipx_msg_ipfix_t *msg); + +private: + const std::string &m_ident; + + ConnectionParams m_con_params; + + ipx_ctx_t *m_log_ctx; + + unsigned int m_tmplts_resend_pkts; + + unsigned int m_tmplts_resend_secs; + + bool m_indicate_lost_msgs; + + Connector &m_connector; + + std::unordered_map> m_session_to_connection; +}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Message.cpp b/src/plugins/output/forwarder/src/Message.cpp new file mode 100644 index 00000000..08229dd7 --- /dev/null +++ b/src/plugins/output/forwarder/src/Message.cpp @@ -0,0 +1,185 @@ +/** + * \file src/plugins/output/forwarder/src/Message.cpp + * \author Michal Sedlak + * \brief Message class implementation + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "Message.h" +#include + +static uint16_t +get_set_id(fds_template_type template_type) +{ + switch (template_type) { + case FDS_TYPE_TEMPLATE: + return FDS_IPFIX_SET_TMPLT; + + case FDS_TYPE_TEMPLATE_OPTS: + return FDS_IPFIX_SET_OPTS_TMPLT; + + default: assert(0); + } +} + +void +Message::add_set(const fds_ipfix_set_hdr *set) +{ + if (m_current_set_hdr) { + finalize_set(); + } + + uint16_t set_len = ntohs(set->length); + add_part((uint8_t *) set, set_len); + m_current_set_hdr = nullptr; +} + +void +Message::add_template(const fds_template *tmplt) +{ + require_set(get_set_id(tmplt->type)); + write(tmplt->raw.data, tmplt->raw.length); + m_current_set_hdr->length += tmplt->raw.length; +} + +void +Message::add_template_withdrawal_all() +{ + finalize_set(); + + require_set(FDS_IPFIX_SET_TMPLT); + uint16_t wdrl2[2] = { htons(FDS_IPFIX_SET_TMPLT), 0 }; + write(&wdrl2); + m_current_set_hdr->length += sizeof(wdrl2); + finalize_set(); + + require_set(FDS_IPFIX_SET_OPTS_TMPLT); + uint16_t wdrl3[2] = { htons(FDS_IPFIX_SET_OPTS_TMPLT), 0 }; + write(&wdrl3); + m_current_set_hdr->length += sizeof(wdrl3); + finalize_set(); +} + +void +Message::finalize() +{ + finalize_set(); + m_msg_hdr->length = htons(m_length); +} + +void +Message::start(const fds_ipfix_msg_hdr *msg_hdr) +{ + m_parts.clear(); + m_length = 0; + m_buffer_pos = 0; + m_last_part_from_buffer = false; + m_msg_hdr = nullptr; + m_current_set_hdr = nullptr; + + m_msg_hdr = write(msg_hdr); +} + +void +Message::add_part(uint8_t *data, uint16_t length) +{ + assert(data < m_buffer || data >= m_buffer + BUFFER_SIZE); + + iovec part; + part.iov_base = data; + part.iov_len = length; + m_parts.push_back(part); + + m_length += length; + + m_last_part_from_buffer = false; +} + +uint8_t * +Message::write(const uint8_t *data, uint16_t length) +{ + assert(BUFFER_SIZE - m_buffer_pos >= length); + + uint8_t *p = &m_buffer[m_buffer_pos]; + m_buffer_pos += length; + memcpy(p, data, length); + + // If the last part is also allocated from our buffer, we can just expand it rather than pushing a new part + if (m_last_part_from_buffer) { + m_parts.back().iov_len += length; + + } else { + iovec part; + part.iov_base = p; + part.iov_len = length; + m_parts.push_back(part); + + m_last_part_from_buffer = true; + } + + m_length += length; + + return p; +} + +void +Message::require_set(uint16_t set_id) +{ + if (!m_current_set_hdr || m_current_set_hdr->flowset_id != set_id) { + finalize_set(); + + fds_ipfix_set_hdr hdr; + hdr.flowset_id = set_id; + hdr.length = sizeof(fds_ipfix_set_hdr); + m_current_set_hdr = write(&hdr); + } +} + +void +Message::finalize_set() +{ + // If there is no set, do nothing + if (!m_current_set_hdr) { + return; + } + + assert(m_current_set_hdr->length > sizeof(fds_ipfix_set_hdr)); + + m_current_set_hdr->flowset_id = htons(m_current_set_hdr->flowset_id); + m_current_set_hdr->length = htons(m_current_set_hdr->length); + m_current_set_hdr = nullptr; +} \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Message.h b/src/plugins/output/forwarder/src/Message.h new file mode 100644 index 00000000..890808c6 --- /dev/null +++ b/src/plugins/output/forwarder/src/Message.h @@ -0,0 +1,169 @@ +/** + * \file src/plugins/output/forwarder/src/Message.h + * \author Michal Sedlak + * \brief Message class header + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#pragma once + +#include +#include +#include + +/// A class for building IPFIX messages +class Message { +public: + /** + * \brief The default constructor + */ + Message() {} + + /// Do not allow moving or copying as the parts vector holds addresses to the buffer within the instance + Message(const Message &) = delete; + Message(Message &&) = delete; + + /** + * \brief Start new message, any existing message data is cleared + * \param msg_hdr The new message header + */ + void + start(const fds_ipfix_msg_hdr *msg_hdr); + + /** + * \brief Add an IPFIX set + * \param set Pointer to the set + * \warning No data is copied, the pointer is stored directly + */ + void + add_set(const fds_ipfix_set_hdr *set); + + /** + * \brief Add a template + * \param tmplt The template + * \note Unlike add_set, the template data is copied and stored in an internal buffer + */ + void + add_template(const fds_template *tmplt); + + /** + * \brief Add a template withdrawal + * \param tmplt The template to withdraw + */ + void + add_template_withdrawal(const fds_template *tmplt); + + /** + * \brief Add a template withdrawal all + */ + void + add_template_withdrawal_all(); + + /** + * \brief Finalize the message + * \warning This HAS to be called AFTER everything was added to the message and BEFORE parts() are accessed. + */ + void + finalize(); + + /** + * \brief Access the message parts + * \return The message parts + */ + std::vector &parts() { return m_parts; } + + /** + * \brief Get the total length of the message + * \return The length + */ + uint16_t length() const { return m_length; } + + /** + * \brief Check if the message is empty or only contains headers but no content + * \return true or false + */ + bool empty() const { return length() <= sizeof(fds_ipfix_msg_hdr) + sizeof(fds_ipfix_set_hdr); } + + /** + * \brief Access the message header + * \return The message header + */ + const fds_ipfix_msg_hdr *header() const { return m_msg_hdr; } + +private: + static constexpr uint16_t BUFFER_SIZE = UINT16_MAX; + + std::vector m_parts; + + uint16_t m_length = 0; + + uint8_t m_buffer[BUFFER_SIZE]; //NOTE: Maybe this should be dynamically expanding instead? + + uint16_t m_buffer_pos = 0; + + fds_ipfix_msg_hdr *m_msg_hdr = nullptr; + + fds_ipfix_set_hdr *m_current_set_hdr = nullptr; + + bool m_last_part_from_buffer = false; + + void + add_part(uint8_t *data, uint16_t length); + + template + T * + write(const T *item); + + uint8_t * + write(const uint8_t *data, uint16_t length); + + void + require_set(uint16_t set_id); + + void + finalize_set(); +}; + + +template +T * +Message::write(const T *item) +{ + T *p = (T *) write((const uint8_t *) item, sizeof(T)); + return p; +} + diff --git a/src/plugins/output/forwarder/src/MessageBuilder.h b/src/plugins/output/forwarder/src/MessageBuilder.h deleted file mode 100644 index 941dfd9a..00000000 --- a/src/plugins/output/forwarder/src/MessageBuilder.h +++ /dev/null @@ -1,150 +0,0 @@ -/** - * \file src/plugins/output/forwarder/src/MessageBuilder.h - * \author Michal Sedlak - * \brief IPFIX message builder - * \date 2021 - */ - -/* Copyright (C) 2021 CESNET, z.s.p.o. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * 3. Neither the name of the Company nor the names of its contributors - * may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * ALTERNATIVELY, provided that this notice is retained in full, this - * product may be distributed under the terms of the GNU General Public - * License (GPL) version 2 or later, in which case the provisions - * of the GPL apply INSTEAD OF those given above. - * - * This software is provided ``as is'', and any express or implied - * warranties, including, but not limited to, the implied warranties of - * merchantability and fitness for a particular purpose are disclaimed. - * In no event shall the company or contributors be liable for any - * direct, indirect, incidental, special, exemplary, or consequential - * damages (including, but not limited to, procurement of substitute - * goods or services; loss of use, data, or profits; or business - * interruption) however caused and on any theory of liability, whether - * in contract, strict liability, or tort (including negligence or - * otherwise) arising in any way out of the use of this software, even - * if advised of the possibility of such damage. - * - */ - -#pragma once - -#include -#include - -#include -#include - -class MessageBuilder -{ -public: - void - begin_message(fds_ipfix_msg_hdr message_header) - { - write(&message_header, sizeof(message_header)); - } - - void - write_template(const fds_template *tmplt) - { - if (current_set_id != get_template_set_id(tmplt->type)) { - end_template_set(); - begin_template_set(tmplt->type); - } - - write(tmplt->raw.data, tmplt->raw.length); - current_set_length += tmplt->raw.length; - } - - void - finalize_message() - { - end_template_set(); - header()->length = htons(write_offset); - } - - uint8_t * - message_data() - { - return &buffer[0]; - } - - int - message_length() - { - return write_offset; - } - -private: - std::vector buffer; - int write_offset = 0; - int set_header_offset = -1; - int current_set_id = 0; - int current_set_length = 0; - - fds_ipfix_msg_hdr * - header() - { - return (fds_ipfix_msg_hdr *)&buffer[0]; - } - - fds_ipfix_set_hdr * - current_set_header() - { - return (fds_ipfix_set_hdr *)&buffer[set_header_offset]; - } - - void - write(void *data, int length) - { - if (((int)buffer.size() - write_offset) < length) { - buffer.resize(buffer.size() + 1024); - return write(data, length); - } - std::memcpy(&buffer[write_offset], data, length); - write_offset += length; - } - - uint16_t - get_template_set_id(fds_template_type template_type) - { - return (template_type == FDS_TYPE_TEMPLATE - ? FDS_IPFIX_SET_TMPLT : FDS_IPFIX_SET_OPTS_TMPLT); - } - - void - begin_template_set(fds_template_type template_type) - { - fds_ipfix_set_hdr set_header = {}; - set_header.flowset_id = htons(get_template_set_id(template_type)); - - set_header_offset = write_offset; - write(&set_header, sizeof(set_header)); - - current_set_length = sizeof(set_header); - current_set_id = get_template_set_id(template_type); - } - - void - end_template_set() - { - if (set_header_offset != -1) { - current_set_header()->length = htons(current_set_length); - } - current_set_id = 0; - current_set_length = 0; - set_header_offset = -1; - } -}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/Sender.cpp b/src/plugins/output/forwarder/src/Sender.cpp new file mode 100644 index 00000000..635da33c --- /dev/null +++ b/src/plugins/output/forwarder/src/Sender.cpp @@ -0,0 +1,253 @@ +/** + * \file src/plugins/output/forwarder/src/Sender.cpp + * \author Michal Sedlak + * \brief Sender class implementation + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "Sender.h" +#include + +class DrecsForwardIterator { +public: + DrecsForwardIterator(ipx_msg_ipfix_t *msg) : m_msg(msg) {} + + ipx_ipfix_record * + get_drec_after_set(fds_ipfix_set_hdr *set_hdr) + { + uint32_t drec_cnt = ipx_msg_ipfix_get_drec_cnt(m_msg); + uint8_t *set_end = (uint8_t *) set_hdr + ntohs(set_hdr->length); + + for (; m_idx < drec_cnt; m_idx++) { + ipx_ipfix_record *drec = ipx_msg_ipfix_get_drec(m_msg, m_idx); + if (drec->rec.data > set_end) { + return drec; + } + } + + return nullptr; + } + + ipx_ipfix_record * + get_drec_in_set(fds_ipfix_set_hdr *set_hdr) + { + uint32_t drec_cnt = ipx_msg_ipfix_get_drec_cnt(m_msg); + uint8_t *set_start = (uint8_t *) set_hdr; + uint8_t *set_end = (uint8_t *) set_hdr + ntohs(set_hdr->length); + + for (; m_idx < drec_cnt; m_idx++) { + ipx_ipfix_record *drec = ipx_msg_ipfix_get_drec(m_msg, m_idx); + if (drec->rec.data >= set_end) { + return nullptr; + } + if (drec->rec.data >= set_start) { + return drec; + } + } + + return nullptr; + } + + uint32_t idx() const { return m_idx; } + +private: + ipx_msg_ipfix_t *m_msg; + uint32_t m_idx = 0; +}; + +Sender::Sender(std::function emit_callback, bool do_withdrawals, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs) : + m_emit_callback(emit_callback), + m_do_withdrawals(do_withdrawals), + m_tmplts_resend_pkts(tmplts_resend_pkts), + m_tmplts_resend_secs(tmplts_resend_secs) +{ +} + +void +Sender::process_message(ipx_msg_ipfix_t *msg) +{ + // Get current real time + timespec realtime_ts; + if (clock_gettime(CLOCK_REALTIME_COARSE, &realtime_ts) != 0) { + throw errno_runtime_error(errno, "clock_gettime"); + } + + // Begin the message, use the original message header with the sequence number and time replaced + fds_ipfix_msg_hdr msg_hdr = *(fds_ipfix_msg_hdr *) ipx_msg_ipfix_get_packet(msg); + msg_hdr.seq_num = htonl(m_seq_num); + msg_hdr.export_time = htonl(realtime_ts.tv_sec); + + m_message.start(&msg_hdr); + + // Get current time + time_t now = get_monotonic_time(); + + // Send templates update if necessary and possible + ipx_ipfix_record *drec = ipx_msg_ipfix_get_drec(msg, 0); + + if (drec) { + + const fds_tsnapshot_t *tsnap = drec->rec.snap; + + // If templates changed or any of the resend intervals elapsed + if (m_tsnap != tsnap + || (m_tmplts_resend_pkts != 0 && m_pkts_since_tmplts_sent >= m_tmplts_resend_pkts) + || (m_tmplts_resend_secs != 0 && now - m_last_tmplts_sent_time >= m_tmplts_resend_secs)) { + + process_templates(tsnap, m_seq_num); + } + } + + // Get sets from the message + ipx_ipfix_set *sets; + size_t num_sets; + ipx_msg_ipfix_get_sets(msg, &sets, &num_sets); + + // For walking through the parsed ipfix records of the message to get a parsed ipfix record + // belonging to a set to retieve e.g. a template snapshot from it + DrecsForwardIterator drecs_iter(msg); + + for (size_t i = 0; i < num_sets; i++) { + + fds_ipfix_set_hdr *set_hdr = sets[i].ptr; + uint16_t set_id = ntohs(set_hdr->flowset_id); + + // If it's not a template set, add the set as is + if (set_id != FDS_IPFIX_SET_TMPLT && set_id != FDS_IPFIX_SET_OPTS_TMPLT) { + + ipx_ipfix_record *drec = drecs_iter.get_drec_in_set(set_hdr); + + if (!drec) { + // No parsed data record belonging to the set means we don't have a template + // Skip the data set + continue; + } + + m_message.add_set(set_hdr); + continue; + } + + // It is a template set... + + // Find first data record after the template set + ipx_ipfix_record *drec = drecs_iter.get_drec_after_set(set_hdr); + + // The template set is at the end, we'll have to wait for next message to grab the template snapshot + if (!drec) { + break; + } + + // Get template snapshot from the data record after template set + const fds_tsnapshot_t *tsnap = drec->rec.snap; + + // In case the template set is at the start of the message and we already sent the templates + if (m_tsnap == tsnap) { + continue; + } + + // The next sequence number in case we'll need to start another message + uint32_t next_seq_num = m_seq_num + drecs_iter.idx(); + + process_templates(tsnap, next_seq_num); + } + + + if (!m_message.empty()) { + m_message.finalize(); + emit_message(); + } + + m_seq_num += ipx_msg_ipfix_get_drec_cnt(msg); + m_pkts_since_tmplts_sent++; +} + + +void +Sender::lose_message(ipx_msg_ipfix_t *msg) +{ + m_seq_num += ipx_msg_ipfix_get_drec_cnt(msg); +} + +void +Sender::process_templates(const fds_tsnapshot_t *tsnap, uint32_t next_seq_num) +{ + if (m_do_withdrawals) { + m_message.add_template_withdrawal_all(); + } + + tsnapshot_for_each(tsnap, [&](const fds_template *tmplt) { + // Should we start another message? + if (m_message.length() + sizeof(fds_ipfix_set_hdr) + tmplt->raw.length > TMPLTMSG_MAX_LENGTH && !m_message.empty()) { + m_message.finalize(); + + emit_message(); + + fds_ipfix_msg_hdr msg_hdr = *m_message.header(); + msg_hdr.seq_num = htonl(next_seq_num); + m_message.start(&msg_hdr); + } + + m_message.add_template(tmplt); + }); + + + if (!m_message.empty()) { + m_message.finalize(); + emit_message(); + } + + m_tsnap = tsnap; + m_last_tmplts_sent_time = get_monotonic_time(); + m_pkts_since_tmplts_sent = 0; + + fds_ipfix_msg_hdr msg_hdr = *m_message.header(); + msg_hdr.seq_num = htonl(next_seq_num); + m_message.start(&msg_hdr); +} + +void +Sender::emit_message() +{ + m_emit_callback(m_message); +} + +void +Sender::clear_templates() +{ + m_tsnap = nullptr; +} diff --git a/src/plugins/output/forwarder/src/Sender.h b/src/plugins/output/forwarder/src/Sender.h new file mode 100644 index 00000000..9df08d33 --- /dev/null +++ b/src/plugins/output/forwarder/src/Sender.h @@ -0,0 +1,110 @@ +/** + * \file src/plugins/output/forwarder/src/Sender.h + * \author Michal Sedlak + * \brief Sender class header + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#pragma once + +#include +#include "Message.h" +#include "common.h" +#include + +constexpr size_t TMPLTMSG_MAX_LENGTH = 2500; //NOTE: Maybe this should be configurable from the XML? + +/// A class to emit the messages to be sent through the connection in the process of forwarding a message +/// Each connection contains one sender per ODID +class Sender { +public: + /** + * \brief The constructor + * \param emit_callback The callback to be called when the sender emits a message to be sent + * \param do_withdrawals Specifies whether template messages should include withdrawals + * \param tmplts_resend_pkts Interval in packets after which templates are resend (0 = never) + * \param tmplts_resend_secs Interval in seconds after which templates are resend (0 = never) + */ + Sender(std::function emit_callback, bool do_withdrawals, + unsigned int tmplts_resend_pkts, unsigned int tmplts_resend_secs); + + /** + * \brief Receive an IPFIX message and emit messages to be sent to the receiving host + * \param msg The IPFIX message + */ + void + process_message(ipx_msg_ipfix_t *msg); + + /** + * \brief Lose an IPFIX message, i.e. update the internal state as if it has been forwarded + * even though it is not being sent + * \param msg The IPFIX message + */ + void + lose_message(ipx_msg_ipfix_t *msg); + + /** + * \brief Clear the templates state, i.e. force the templates to resend the next round + */ + void + clear_templates(); + +private: + std::function m_emit_callback; + + bool m_do_withdrawals; + + unsigned int m_tmplts_resend_pkts; + + unsigned int m_tmplts_resend_secs; + + uint32_t m_seq_num = 0; + + const fds_tsnapshot_t *m_tsnap = nullptr; + + unsigned int m_pkts_since_tmplts_sent = 0; + + time_t m_last_tmplts_sent_time = 0; + + Message m_message; + + void + process_templates(const fds_tsnapshot_t *tsnap, uint32_t next_seq_num); + + void + emit_message(); +}; \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/common.cpp b/src/plugins/output/forwarder/src/common.cpp new file mode 100644 index 00000000..100c435e --- /dev/null +++ b/src/plugins/output/forwarder/src/common.cpp @@ -0,0 +1,98 @@ +/** + * \file src/plugins/output/forwarder/src/common.cpp + * \author Michal Sedlak + * \brief Common use functions and structures + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "common.h" +#include +#include +#include +#include + +#include + +void +tsnapshot_for_each(const fds_tsnapshot_t *tsnap, std::function callback) +{ + struct CallbackData { + std::function callback; + std::exception_ptr ex; + }; + + CallbackData cb_data; + cb_data.callback = callback; + cb_data.ex = nullptr; + + fds_tsnapshot_for(tsnap, + [](const fds_template *tmplt, void *data) -> bool { + CallbackData &cb_data = *(CallbackData *) data; + + try { + cb_data.callback(tmplt); + return true; + + } catch (...) { + cb_data.ex = std::current_exception(); + return false; + } + + }, &cb_data); + + if (cb_data.ex) { + std::rethrow_exception(cb_data.ex); + } +} + +time_t +get_monotonic_time() +{ + timespec ts; + if (clock_gettime(CLOCK_MONOTONIC_COARSE, &ts) != 0) { + throw errno_runtime_error(errno, "clock_gettime"); + } + return ts.tv_sec; +} + +std::runtime_error +errno_runtime_error(int errno_, const std::string &func_name) +{ + char *errbuf; + ipx_strerror(errno_, errbuf); + return std::runtime_error(func_name + "() failed: " + std::string(errbuf)); +} diff --git a/src/plugins/output/forwarder/src/common.h b/src/plugins/output/forwarder/src/common.h new file mode 100644 index 00000000..48b2a23a --- /dev/null +++ b/src/plugins/output/forwarder/src/common.h @@ -0,0 +1,157 @@ +/** + * \file src/plugins/output/forwarder/src/common.h + * \author Michal Sedlak + * \brief Common use functions and structures + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +enum class Protocol : uint8_t { + UNASSIGNED, + TCP, + UDP +}; + +/** + * \brief Connection parameters + */ +struct ConnectionParams { + /// The IP address or hostname + std::string address; + /// The port + uint16_t port; + /// The transport protocol + Protocol protocol; + + struct Hasher { + size_t + operator()(const ConnectionParams ¶m) const + { + return std::hash()(param.address) + ^ std::hash()(param.port) + ^ std::hash()(uint8_t(param.protocol)); + } + }; + + bool + operator==(const ConnectionParams &other) const + { + return address == other.address && port == other.port && protocol == other.protocol; + } +}; + +/** + * \brief RAII wrapper for file descriptor such as a socket fd + */ +class UniqueFd { +public: + UniqueFd() {} + + UniqueFd(int fd) : m_fd(fd) {} + + UniqueFd(const UniqueFd &) = delete; + + UniqueFd &operator=(const UniqueFd &) = delete; + + UniqueFd(UniqueFd &&other) { + close(); + std::swap(m_fd, other.m_fd); + } + + UniqueFd &operator=(UniqueFd &&other) { + if (this != &other) { + close(); + std::swap(m_fd, other.m_fd); + } + return *this; + } + + ~UniqueFd() { + close(); + } + + void reset(int fd = -1) { + close(); + m_fd = fd; + } + + int get() const { return m_fd; } + +private: + int m_fd = -1; + + void close() { + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } + } +}; + +/** + * \brief A C++ wrapper for the fds_tsnapshot_for function that can handle exceptions + * \param tsnap The template snapshot + * \param callback The callback to be called for each of the templates in the snapshot + * \throw Whatever the callback might throw + */ +void +tsnapshot_for_each(const fds_tsnapshot_t *tsnap, std::function callback); + +/** + * \brief Get monotonic time to be used e.g. for calculating elapsed seconds + * \return The monotonic time + * \throw std::runtime_error on failure + */ +time_t +get_monotonic_time(); + +/** + * \brief Create runtime error from errno + * \param errno_ The errno + * \param func_name The function name to use in the error message + * \return The runtime error + */ +std::runtime_error +errno_runtime_error(int errno_, const std::string &func_name); diff --git a/src/plugins/output/forwarder/src/config.h b/src/plugins/output/forwarder/src/config.h deleted file mode 100644 index 81298548..00000000 --- a/src/plugins/output/forwarder/src/config.h +++ /dev/null @@ -1,306 +0,0 @@ -/** - * \file src/plugins/output/forwarder/src/config.h - * \author Michal Sedlak - * \brief Plugin configuration - * \date 2021 - */ - -/* Copyright (C) 2021 CESNET, z.s.p.o. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * 3. Neither the name of the Company nor the names of its contributors - * may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * ALTERNATIVELY, provided that this notice is retained in full, this - * product may be distributed under the terms of the GNU General Public - * License (GPL) version 2 or later, in which case the provisions - * of the GPL apply INSTEAD OF those given above. - * - * This software is provided ``as is'', and any express or implied - * warranties, including, but not limited to, the implied warranties of - * merchantability and fitness for a particular purpose are disclaimed. - * In no event shall the company or contributors be liable for any - * direct, indirect, incidental, special, exemplary, or consequential - * damages (including, but not limited to, procurement of substitute - * goods or services; loss of use, data, or profits; or business - * interruption) however caused and on any theory of liability, whether - * in contract, strict liability, or tort (including negligence or - * otherwise) arising in any way out of the use of this software, even - * if advised of the possibility of such damage. - * - */ - -#pragma once - -#include "Forwarder.h" - -#include -#include - -#include -#include - -/// DIY std::optional -template -class Maybe -{ -public: - Maybe() - : has_value_(false) - {} - - Maybe(T value) - : has_value_(true) - , value_(value) - {} - - bool - has_value() - { - return has_value_; - } - - T - value() - { - return value_; - } - -private: - bool has_value_; - T value_; -}; - - -/// Parses the XML config string and configures forwarder accordingly -void -parse_and_configure(ipx_ctx_t *log_ctx, const char *xml_config, Forwarder &forwarder) -{ - /// - /// Config schema definition - /// - enum { - MODE, - PROTOCOL, - RECONNECT_INTERVAL_SECS, - TEMPLATE_REFRESH_INTERVAL_SECS, - TEMPLATE_REFRESH_INTERVAL_BYTES, - CONNECTION_BUFFER_SIZE, - HOSTS, - HOST, - NAME, - ADDRESS, - PORT - }; - - fds_xml_args host_schema[] = { - FDS_OPTS_ELEM(NAME , "name" , FDS_OPTS_T_STRING, FDS_OPTS_P_OPT), - FDS_OPTS_ELEM(ADDRESS, "address", FDS_OPTS_T_STRING, 0 ), - FDS_OPTS_ELEM(PORT , "port" , FDS_OPTS_T_STRING, 0 ), - FDS_OPTS_END - }; - - fds_xml_args hosts_schema[] = { - FDS_OPTS_NESTED(HOST, "host", host_schema, FDS_OPTS_P_MULTI), - FDS_OPTS_END - }; - - fds_xml_args params_schema[] = { - FDS_OPTS_ROOT ("params"), - FDS_OPTS_ELEM (MODE , "mode" , FDS_OPTS_T_STRING, 0 ), - FDS_OPTS_ELEM (PROTOCOL , "protocol" , FDS_OPTS_T_STRING, 0 ), - FDS_OPTS_ELEM (CONNECTION_BUFFER_SIZE , "connectionBufferSize" , FDS_OPTS_T_INT , FDS_OPTS_P_OPT), - FDS_OPTS_ELEM (TEMPLATE_REFRESH_INTERVAL_SECS , "templateRefreshIntervalSecs" , FDS_OPTS_T_INT , FDS_OPTS_P_OPT), - FDS_OPTS_ELEM (TEMPLATE_REFRESH_INTERVAL_BYTES, "templateRefreshIntervalBytes", FDS_OPTS_T_INT , FDS_OPTS_P_OPT), - FDS_OPTS_ELEM (RECONNECT_INTERVAL_SECS , "reconnectIntervalSecs" , FDS_OPTS_T_INT , FDS_OPTS_P_OPT), - FDS_OPTS_NESTED(HOSTS , "hosts" , hosts_schema , 0 ), - FDS_OPTS_END - }; - - /// - /// Default parameter values - /// - const int default_template_refresh_interval_secs = 10 * 60; - const int default_template_refresh_interval_bytes = 5 * 1024 * 1024; - const int default_reconnect_interval_secs = 10; - const int default_connection_buffer_size = 4 * 1024 * 1024; - - /// - /// Parsed parameters - /// - struct HostInfo { - std::string name; - std::string address; - std::string port; - }; - - std::string mode; - std::string protocol; - std::vector hosts; - Maybe connection_buffer_size; - Maybe template_refresh_interval_secs; - Maybe template_refresh_interval_bytes; - Maybe reconnect_interval_secs; - - /// - /// Parsing - /// - auto parser = std::unique_ptr(fds_xml_create(), &fds_xml_destroy); - if (!parser) { - throw std::string("Cannot create XML parser"); - } - - auto lower = [](std::string s) { - for (auto &c : s) { - c = std::tolower(c); - } - return s; - }; - - auto parser_error = [&]() { - throw std::string("XML parser error: ") + fds_xml_last_err(parser.get()); - }; - - if (fds_xml_set_args(parser.get(), params_schema) != FDS_OK) { - parser_error(); - } - - auto params_elem = fds_xml_parse_mem(parser.get(), xml_config, true); - if (!params_elem) { - parser_error(); - } - - auto process_host = [&](fds_xml_ctx_t *host_elem) { - HostInfo host; - const fds_xml_cont *content; - while (fds_xml_next(host_elem, &content) != FDS_EOC) { - switch (content->id) { - case NAME: - host.name = std::string(content->ptr_string); - break; - case ADDRESS: - host.address = std::string(content->ptr_string); - break; - case PORT: - host.port = std::string(content->ptr_string); - break; - } - } - hosts.push_back(host); - }; - - auto process_hosts = [&](fds_xml_ctx_t *hosts_elem) { - const fds_xml_cont *content; - while (fds_xml_next(hosts_elem, &content) != FDS_EOC) { - process_host(content->ptr_ctx); - } - }; - - const fds_xml_cont *content; - while (fds_xml_next(params_elem, &content) != FDS_EOC) { - switch (content->id) { - case MODE: - mode = std::string(content->ptr_string); - break; - case PROTOCOL: - protocol = std::string(content->ptr_string); - break; - case HOSTS: - process_hosts(content->ptr_ctx); - break; - case CONNECTION_BUFFER_SIZE: - connection_buffer_size = content->val_int; - break; - case TEMPLATE_REFRESH_INTERVAL_SECS: - template_refresh_interval_secs = content->val_int; - break; - case TEMPLATE_REFRESH_INTERVAL_BYTES: - template_refresh_interval_bytes = content->val_int; - break; - case RECONNECT_INTERVAL_SECS: - reconnect_interval_secs = content->val_int; - break; - } - } - - /// - /// Check parameters and configure - /// - if (lower(mode) == "all" || lower(mode) == "send to all" || lower(mode) == "send-to-all") { - forwarder.set_forward_mode(ForwardMode::SendToAll); - } else if (lower(mode) == "roundrobin" || lower(mode) == "round robin" || lower(mode) == "round-robin") { - forwarder.set_forward_mode(ForwardMode::RoundRobin); - } else { - throw "Invalid mode '" + mode + "', possible values are: 'roundrobin', 'all'"; - } - - if (lower(protocol) == "udp") { - forwarder.set_transport_protocol(TransProto::Udp); - } else if (lower(protocol) == "tcp") { - forwarder.set_transport_protocol(TransProto::Tcp); - } else { - throw "Invalid protocol '" + protocol + "', possible values are: 'tcp', 'udp'"; - } - - if (connection_buffer_size.has_value()) { - if (connection_buffer_size.value() > 0) { - forwarder.set_connection_buffer_size(connection_buffer_size.value()); - } else { - throw std::string("Invalid connection buffer size"); - } - } else { - forwarder.set_connection_buffer_size(default_connection_buffer_size); - } - - if (template_refresh_interval_secs.has_value()) { - if (template_refresh_interval_secs.value() >= 0) { - if (lower(protocol) == "tcp") { - IPX_CTX_WARNING(log_ctx, "Templates refresh interval is set but transport protocol is TCP"); - } - forwarder.set_template_refresh_interval_secs(template_refresh_interval_secs.value()); - } else { - throw std::string("Invalid template refresh secs interval"); - } - } else { - forwarder.set_template_refresh_interval_secs(default_template_refresh_interval_secs); - } - - if (template_refresh_interval_bytes.has_value()) { - if (template_refresh_interval_bytes.value() >= 0) { - if (lower(protocol) == "tcp") { - IPX_CTX_WARNING(log_ctx, "Templates refresh interval is set but transport protocol is TCP"); - } - forwarder.set_template_refresh_interval_bytes(template_refresh_interval_bytes.value()); - } else { - throw std::string("Invalid template refresh bytes interval"); - } - } else { - forwarder.set_template_refresh_interval_secs(default_template_refresh_interval_bytes); - } - - if (template_refresh_interval_bytes.has_value()) { - if (reconnect_interval_secs.value() >= 0) { - if (lower(protocol) == "udp") { - IPX_CTX_WARNING(log_ctx, "Reconnect interval is set but transport protocol is UDP"); - } - forwarder.set_reconnect_interval(reconnect_interval_secs.value()); - } else { - throw std::string("Invalid reconnect interval"); - } - } else { - forwarder.set_template_refresh_interval_secs(default_reconnect_interval_secs); - } - - for (auto &host : hosts) { - forwarder.add_client(host.address, host.port, host.name); - } -} \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/connector/Connector.cpp b/src/plugins/output/forwarder/src/connector/Connector.cpp new file mode 100644 index 00000000..38f7f2d1 --- /dev/null +++ b/src/plugins/output/forwarder/src/connector/Connector.cpp @@ -0,0 +1,583 @@ +/** + * \file src/plugins/output/forwarder/src/connector/Connector.cpp + * \author Michal Sedlak + * \brief Connector class + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "connector/Connector.h" + +#include +#include + +#include +#include +#include + +Connector::Connector(const std::vector &hosts, unsigned int nb_premade_connections, + unsigned int reconnect_secs, ipx_ctx_t *log_ctx) : + m_reconnect_secs(reconnect_secs), + m_log_ctx(log_ctx), + m_nb_premade_connections(nb_premade_connections) +{ + // Set up tasks for premade connections + for (auto &host : hosts) { + for (size_t i = 0; i < nb_premade_connections; i++) { + Task task = {}; + task.params = host; + // The worker thread isn't running yet, so we can access m_tasks from here. + m_tasks.emplace_back(std::move(task)); + } + } + + // Start the worker thread + m_thread = std::thread([this](){ this->run(); }); +} + +Connector::~Connector() +{ + // Let the worker thread know that we're stopping + m_stop_flag = true; + m_statpipe.poke(false); + m_thread.join(); +} + +std::shared_ptr +Connector::get(const ConnectionParams &host) +{ + std::unique_lock lock(m_mutex); + + std::shared_ptr future(std::make_shared()); + m_new_requests.emplace_back(Request{host, future}); + m_statpipe.poke(); + + return future; +} + +/** + * Advance execution of tasks where a poll event occured + */ +void +Connector::process_poll_events() +{ + if (m_pollfds.size() == 0) { + return; + } + + for (size_t i = 0; i < m_pollfds.size() - 1; i++) { // - 1 because the last one is the pipe + if (!m_pollfds[i].revents) { + continue; + } + /* + const char *statestr[] = {"NotStarted", "Connecting", "Connected", "ToBeDeleted"}; + IPX_CTX_INFO(m_log_ctx, "[%d] Poll event %u Task %s:%" PRIu16 " State %s", i, m_pollfds[i].revents, + m_tasks[i].params.address.c_str(), m_tasks[i].params.port, statestr[(int)m_tasks[i].state]); + */ + try { + on_task_poll_event(m_tasks[i], m_pollfds[i].revents); + + } catch (const std::runtime_error &err) { + IPX_CTX_INFO(m_log_ctx, "Connecting to %s:%" PRIu16 " failed - %s", + m_tasks[i].params.address.c_str(), m_tasks[i].params.port, err.what()); + on_task_failed(m_tasks[i]); + } + } +} + +/** + * Start tasks that hasn't been started yet and should be + */ +void +Connector::start_tasks() +{ + time_t now = get_monotonic_time(); + + for (auto &task : m_tasks) { + if (task.state != Task::State::NotStarted || task.start_time > now) { + continue; + } + + try { + on_task_start(task); + + } catch (const std::runtime_error &err) { + IPX_CTX_INFO(m_log_ctx, "Connecting to %s:%" PRIu16 " failed - %s", + task.params.address.c_str(), task.params.port, err.what()); + on_task_failed(task); + } + + } +} + +/** + * Populate the vector of pollfds to listen for events on sockets of tasks that are connecting or connected + */ +void +Connector::setup_pollfds() +{ + m_pollfds.resize(m_tasks.size() + 1); + + size_t i = 0; + for (const auto &task : m_tasks) { + struct pollfd &poll_fd = m_pollfds[i++]; + poll_fd.fd = 0; + poll_fd.events = 0; + poll_fd.revents = 0; + + if (task.state == Task::State::Connecting) { + poll_fd.fd = task.sockfd.get(); + poll_fd.events = POLLOUT; + + } else if (task.state == Task::State::Connected) { + poll_fd.fd = task.sockfd.get(); + } + } + + struct pollfd &poll_fd = m_pollfds[i++]; + poll_fd.fd = m_statpipe.readfd(); + poll_fd.events = POLLIN; +} + +/** + * Process new requests from the caller and delete dead requests + */ +void +Connector::process_requests() +{ + std::unique_lock lock(m_mutex); + + // Delete dead requests, e.g. if the caller requested a connection for a new session, but the session closed before + // the connection was estabilished + m_requests.erase(std::remove_if(m_requests.begin(), m_requests.end(), + [](const Request &request) { return request.future.use_count() == 1; } + ), m_requests.end()); + + // Collect new requests + for (auto &new_request : m_new_requests) { + bool found = false; + + // Check if there is premade connection that can be used to fulfil the request + for (auto &task : m_tasks) { + if (task.state == Task::State::Connected && task.params == new_request.params) { + new_request.future->set(std::move(task.sockfd)); + + task.start_time = 0; + task.state = Task::State::NotStarted; + + found = true; + break; + } + } + + // Create task for the request + if (!found) { + Task task = {}; + task.params = new_request.params; + m_tasks.emplace_back(std::move(task)); + + m_requests.emplace_back(std::move(new_request)); + } + } + + m_new_requests.clear(); +} + +/** + * Delete tasks that are marked for deletion + */ +void +Connector::cleanup_tasks() +{ + m_tasks.erase(std::remove_if(m_tasks.begin(), m_tasks.end(), + [](const Task &task) { return task.state == Task::State::ToBeDeleted; }), + m_tasks.end()); +} + +/** + * Wait for a poll event on any of the task sockets or the status pipe + */ +void +Connector::wait_for_poll_event() +{ + // Wait for one of the sockets or the status fd event + if (poll(m_pollfds.data(), m_pollfds.size(), 1000) < 0) { + char *errbuf; + ipx_strerror(errno, errbuf); + IPX_CTX_ERROR(m_log_ctx, "poll() failed: %s", errbuf); + } + + // Empty the pipe as it's only used to stop the poll + m_statpipe.clear(); +} + +/** + * The main loop + */ +void +Connector::main_loop() +{ + while (!m_stop_flag) { + process_poll_events(); + + process_requests(); + + start_tasks(); + + cleanup_tasks(); + + /* + IPX_CTX_INFO(m_log_ctx, "Tasks: %d, Requests: %d", m_tasks.size(), m_requests.size()); + int i = 0; + for (const auto &task : m_tasks) { + i++; + const char *statestr[] = {"NotStarted", "Connecting", "Connected", "ToBeDeleted"}; + IPX_CTX_INFO(m_log_ctx, "[%d] Task %s:%" PRIu16 " State %s StartTime %d", i, + task.params.address.c_str(), task.params.port, statestr[(int)task.state], task.start_time); + } + */ + setup_pollfds(); + + wait_for_poll_event(); + } +} + +/** + * The entry point of the worker thread + */ +void +Connector::run() +{ + try { + main_loop(); + + } catch (const std::bad_alloc &ex) { + IPX_CTX_ERROR(m_log_ctx, "Caught exception in connector thread: Memory error", 0); + IPX_CTX_ERROR(m_log_ctx, "Fatal error, connector stopped!", 0); + + } catch (const std::runtime_error &ex) { + IPX_CTX_ERROR(m_log_ctx, "Caught exception in connector thread: %s", ex.what()); + IPX_CTX_ERROR(m_log_ctx, "Fatal error, connector stopped!", 0); + + } catch (const std::exception &ex) { + IPX_CTX_ERROR(m_log_ctx, "Caught exception in connector thread: %s", ex.what()); + IPX_CTX_ERROR(m_log_ctx, "Fatal error, connector stopped!", 0); + + } catch (...) { + IPX_CTX_ERROR(m_log_ctx, "Caught exception in connector thread", 0); + IPX_CTX_ERROR(m_log_ctx, "Fatal error, connector stopped!", 0); + } +} + +/**********************************************************************************************************************/ + +/** + * Wrapper around getaddrinfo + */ +static std::unique_ptr +resolve_addrs(const ConnectionParams ¶ms) +{ + struct addrinfo *ai; + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + + switch (params.protocol) { + case Protocol::TCP: + hints.ai_protocol = IPPROTO_TCP; + hints.ai_socktype = SOCK_STREAM; + break; + + case Protocol::UDP: + hints.ai_protocol = IPPROTO_UDP; + hints.ai_socktype = SOCK_DGRAM; + break; + + default: assert(0); + } + + int ret = getaddrinfo(params.address.c_str(), std::to_string(params.port).c_str(), &hints, &ai); + if (ret != 0) { + throw std::runtime_error(std::string("getaddrinfo() failed: ") + gai_strerror(ret)); + } + + return {ai, &freeaddrinfo}; +} + +/** + * Create a socket and begin non-blocking connect to the provided address + */ +static UniqueFd +create_and_connect_socket(struct addrinfo *addr) +{ + UniqueFd sockfd; + int raw_sockfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); + if (raw_sockfd < 0) { + throw errno_runtime_error(errno, "socket"); + } + sockfd.reset(raw_sockfd); + + int flags; + if ((flags = fcntl(sockfd.get(), F_GETFL)) == -1) { + throw errno_runtime_error(errno, "fcntl"); + } + if (fcntl(sockfd.get(), F_SETFL, flags | O_NONBLOCK) == -1) { + throw errno_runtime_error(errno, "fcntl"); + } + + if (addr->ai_socktype == SOCK_STREAM) { + int optval = 1; + if (setsockopt(sockfd.get(), SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) != 0) { + throw errno_runtime_error(errno, "setsockopt"); + } + } + + if (connect(sockfd.get(), addr->ai_addr, addr->ai_addrlen) != 0 && errno != EINPROGRESS) { + throw errno_runtime_error(errno, "connect"); + } + + return sockfd; +} + +/** + * Check if socket error from a non-blocking operation occured and throw it as a std::runtime_error + */ +static void +throw_if_socket_error(int sockfd) +{ + int optval; + socklen_t optlen = sizeof(optval); + + if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) == -1) { + throw errno_runtime_error(errno, "getsockopt"); + } + + if (optval != 0) { // optval is the errno of the non-blocking operation + throw errno_runtime_error(errno, "connect"); + } +} + +/** + * Connect to the next address and advance the next address pointer + */ +static UniqueFd +connect_next(struct addrinfo *&next_addr) +{ + assert(next_addr != nullptr); + + while (next_addr) { + struct addrinfo *addr = next_addr; + next_addr = next_addr->ai_next; + try { + // This might fail right away even though it is non-blocking connect + return create_and_connect_socket(addr); + + } catch (const std::runtime_error &err) { + // Ignore unless this is the last address + if (!next_addr) { + throw err; + } + } + } + + return {}; +} + +/** + * Handle task start + */ +void +Connector::on_task_start(Task &task) +{ + assert(task.state == Task::State::NotStarted); + + task.addrs = resolve_addrs(task.params); + task.next_addr = task.addrs.get(); + task.sockfd = connect_next(task.next_addr); + task.state = Task::State::Connecting; +} + +/** + * Handle poll event on the task socket + */ +void +Connector::on_task_poll_event(Task &task, int events) +{ + // If the connection is connected and we got a poll event, the connection probably dropped + if (task.state == Task::State::Connected) { + throw_if_socket_error(task.sockfd.get()); + + if (events & POLLERR) { + // Just in case getsockopt(SO_ERROR) didn't return any error but we got an error from the poll event, + // not sure if this can happen, but just to be safe + throw std::runtime_error("socket error"); + } + + return; + } + + assert(task.state == Task::State::Connecting); + // Otherwise we're connecting and the poll event is either connection success or connection error + try { + throw_if_socket_error(task.sockfd.get()); + // No error -> connection successful + on_task_connected(task); + + } catch (const std::runtime_error &err) { + if (!task.next_addr) { + throw err; + } + task.sockfd = connect_next(task.next_addr); + } + +} + +/** + * Handle successful connection of the task socket + */ +void +Connector::on_task_connected(Task &task) +{ + IPX_CTX_INFO(m_log_ctx, "Connecting to %s:%" PRIu16 " successful", + task.params.address.c_str(), task.params.port); + + std::unique_lock lock(m_mutex); + + // If there is a request for this connection, complete it + for (auto it = m_requests.begin(); it != m_requests.end(); it++) { + Request &request = *it; + if (request.params == task.params && request.future.use_count() > 1) { + request.future->set(std::move(task.sockfd)); + m_requests.erase(it); + + // If this was a premade connection, restart the task + if (!should_restart(task)) { + task.state = Task::State::ToBeDeleted; + } else { + task.start_time = 0; + task.state = Task::State::NotStarted; + } + return; + } + } + + // Count how many extra connections for this host there currently are + unsigned int count = 0; + for (const auto &task_ : m_tasks) { + if (task_.state == Task::State::Connected && task.params == task_.params) { + count++; + } + } + + if (count > m_nb_premade_connections) { + // Too many - discard the connection + task.sockfd.reset(); + task.state = Task::State::ToBeDeleted; + return; + } + + // Keep the connection as extra + task.state = Task::State::Connected; +} + +/** + * Handle failure of the task + */ +void +Connector::on_task_failed(Task &task) +{ + std::unique_lock lock(m_mutex); +#if 0 + // Count how many connections are required to see if we want to restart the task or throw it away. + // This is needed because a connection request may be cancelled before it is resolved, in which case we might + // end up with too many extra connections. + long long required = m_nb_premade_connections; + + // Count requests for this host that are still waiting for a socket + for (const auto &request : m_requests) { + // If the use_count is 1 it's only the Connector holding a reference to it -> the future was cancelled + if (task.params == request.params && request.future.use_count() > 1) { + required++; + } + } + + // How many tasks that are connected or might still successfully connect are there + for (const auto &task_ : m_tasks) { + if (task_.state != Task::State::ToBeDeleted && task.params == task_.params) { + required--; + } + } +#endif + // Don't restart the task, there is no need + if (!should_restart(task)) { + task.state = Task::State::ToBeDeleted; + return; + } + + // Reschedule the failed task to be ran after reconnect interval elapses + time_t now = get_monotonic_time(); + task.start_time = now + m_reconnect_secs; + task.state = Task::State::NotStarted; + + IPX_CTX_INFO(m_log_ctx, "Retrying connection to %s:%" PRIu16 " in %u seconds", + task.params.address.c_str(), task.params.port, m_reconnect_secs); +} + +bool +Connector::should_restart(Task &task) +{ + // Count how many connections are required to see if we want to restart the task or throw it away. + // This is needed because a connection request may be cancelled before it is resolved, in which case we might + // end up with too many extra connections. + long long required = m_nb_premade_connections; + + // Count requests for this host that are still waiting for a socket + for (const auto &request : m_requests) { + // If the use_count is 1 it's only the Connector holding a reference to it -> the future was cancelled + if (task.params == request.params && request.future.use_count() > 1) { + required++; + } + } + + // How many tasks that are connected or might still successfully connect are there + for (const auto &task_ : m_tasks) { + if (&task != &task_ && task_.state != Task::State::ToBeDeleted && task.params == task_.params) { + required--; + } + } + + return required > 0; +} \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/connector/Connector.h b/src/plugins/output/forwarder/src/connector/Connector.h new file mode 100644 index 00000000..1dfe25f4 --- /dev/null +++ b/src/plugins/output/forwarder/src/connector/Connector.h @@ -0,0 +1,180 @@ +/** + * \file src/plugins/output/forwarder/src/connector/Connector.h + * \author Michal Sedlak + * \brief Connector class + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "common.h" +#include "connector/Pipe.h" +#include "connector/FutureSocket.h" + +/** + * \brief A connector class that handles socket connections on a separate thread + */ +class Connector { +public: + + /** + * \brief The constructor + * + * \param hosts The list of hosts connections will be made to + * \param nb_premade_connections Number of extra open connections to keep + * \param reconnect_secs The reconnect interval + * \param log_ctx The logging context + */ + Connector(const std::vector &hosts, unsigned int nb_premade_connections, + unsigned int reconnect_secs, ipx_ctx_t *log_ctx); + + // No copying or moving + Connector(const Connector&) = delete; + Connector(Connector&&) = delete; + + /** + * \brief Get a connected socket to the host + * + * \param host The host + * \return A socket that is connected or will be connected in the future + */ + std::shared_ptr + get(const ConnectionParams &host); + + /** + * \brief The destructor + */ + ~Connector(); + +private: + struct Request { + // The connection parameters + ConnectionParams params; + // The future also owned by the caller + std::shared_ptr future; + }; + + struct Task { + enum class State { NotStarted, Connecting, Connected, ToBeDeleted }; + // The connection parameters + ConnectionParams params; + // State of the task + State state = State::NotStarted; + // When should the task start + time_t start_time = 0; + // The socket + UniqueFd sockfd; + // The resolved addresses + std::unique_ptr addrs{nullptr, &freeaddrinfo}; + // The next address to try + struct addrinfo *next_addr = nullptr; + }; + + // Reconnect interval + unsigned int m_reconnect_secs; + // Mutex for shared state + std::mutex m_mutex; + // Requests to be retrieved by the worker thread + std::vector m_new_requests; + // The requests - exclusively handled by the worker thread! + std::vector m_requests; + // The tasks - exclusively handled by the worker thread! + std::vector m_tasks; + // Pipe to notify for status changes + Pipe m_statpipe; + // The worker thread + std::thread m_thread; + // Stop flag for the worker thread + std::atomic m_stop_flag{false}; + // The logging context + ipx_ctx_t *m_log_ctx; + // Number of premade connections to keep + unsigned int m_nb_premade_connections; + // The poll fds + std::vector m_pollfds; + + void + wait_for_poll_event(); + + void + cleanup_tasks(); + + void + process_requests(); + + void + setup_pollfds(); + + void + start_tasks(); + + void + process_poll_events(); + + void + main_loop(); + + void + run(); + + void + on_task_start(Task &task); + + void + on_task_poll_event(Task &task, int events); + + void + on_task_connected(Task &task); + + void + on_task_failed(Task &task); + + bool + should_restart(Task &task); +}; diff --git a/src/plugins/output/forwarder/src/connector/FutureSocket.cpp b/src/plugins/output/forwarder/src/connector/FutureSocket.cpp new file mode 100644 index 00000000..b572cdd5 --- /dev/null +++ b/src/plugins/output/forwarder/src/connector/FutureSocket.cpp @@ -0,0 +1,70 @@ +/** + * \file src/plugins/output/forwarder/src/connector/FutureSocket.cpp + * \author Michal Sedlak + * \date 2021 + */ + +/* Copyright (C) 2021 CESNET, z.s.p.o. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name of the Company nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * ALTERNATIVELY, provided that this notice is retained in full, this + * product may be distributed under the terms of the GNU General Public + * License (GPL) version 2 or later, in which case the provisions + * of the GPL apply INSTEAD OF those given above. + * + * This software is provided ``as is'', and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose are disclaimed. + * In no event shall the company or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + * + */ + +#include "FutureSocket.h" + +bool +FutureSocket::ready() +{ + std::lock_guard lock(m_mutex); + return m_ready; +} + +UniqueFd +FutureSocket::retrieve() +{ + std::lock_guard lock(m_mutex); + if (!m_ready) { + throw std::runtime_error("result is not ready to be retrieved"); + } + m_ready = false; + return std::move(m_result); +} + +void +FutureSocket::set(UniqueFd result) +{ + std::lock_guard lock(m_mutex); + if (m_ready) { + throw std::runtime_error("result is already set"); + } + m_result = std::move(result); + m_ready = true; +} diff --git a/src/plugins/output/forwarder/src/ConnectionManager.h b/src/plugins/output/forwarder/src/connector/FutureSocket.h similarity index 61% rename from src/plugins/output/forwarder/src/ConnectionManager.h rename to src/plugins/output/forwarder/src/connector/FutureSocket.h index 2fe73237..2ea7f938 100644 --- a/src/plugins/output/forwarder/src/ConnectionManager.h +++ b/src/plugins/output/forwarder/src/connector/FutureSocket.h @@ -1,7 +1,7 @@ /** - * \file src/plugins/output/forwarder/src/ConnectionManager.h + * \file src/plugins/output/forwarder/src/connector/Future.h * \author Michal Sedlak - * \brief Connection manager + * \brief Future socket class * \date 2021 */ @@ -41,58 +41,37 @@ #pragma once -#include "ConnectionParams.h" -#include "Connection.h" -#include "SyncPipe.h" - -#include #include -#include -#include +#include #include -#include -#include - -class Connection; - -static constexpr long DEFAULT_BUFFER_SIZE = 4 * 1024 * 1024; -static constexpr int DEFAULT_RECONNECT_INTERVAL_SECS = 5; -class ConnectionManager -{ -friend class Connection; +#include "common.h" +/** + * \brief Class representing a value that will be set in the future + */ +class FutureSocket { public: - Connection & - add_client(ConnectionParams params); + /** + * \brief Check if the result is ready to be retrieved + */ + bool + ready(); - void - start(); - - void - stop(); + /** + * \brief Retrieve the result + */ + UniqueFd + retrieve(); + /** + * \brief Set the result and make it ready for retrieval + */ void - set_reconnect_interval(int number_of_seconds); + set(UniqueFd result); - void - set_connection_buffer_size(long number_of_bytes); - private: - long connection_buffer_size = DEFAULT_BUFFER_SIZE; - int reconnect_interval_secs = DEFAULT_RECONNECT_INTERVAL_SECS; - std::mutex mutex; - std::vector> active_connections; - std::vector> reconnect_connections; - std::thread send_thread; - std::thread reconnect_thread; - std::condition_variable reconnect_cv; - std::atomic exit_flag { false }; - SyncPipe pipe; - - void - send_loop(); - - void - reconnect_loop(); + UniqueFd m_result; + bool m_ready = false; + std::mutex m_mutex; }; diff --git a/src/plugins/output/forwarder/src/IPFIXMessage.h b/src/plugins/output/forwarder/src/connector/Pipe.cpp similarity index 64% rename from src/plugins/output/forwarder/src/IPFIXMessage.h rename to src/plugins/output/forwarder/src/connector/Pipe.cpp index 3d2fce63..7bf330c9 100644 --- a/src/plugins/output/forwarder/src/IPFIXMessage.h +++ b/src/plugins/output/forwarder/src/connector/Pipe.cpp @@ -1,7 +1,7 @@ /** - * \file src/plugins/output/forwarder/src/IPFIXMessage.h + * \file src/plugins/output/forwarder/src/connector/Pipe.cpp * \author Michal Sedlak - * \brief Simple IPFIX message wrapper + * \brief Pipe class * \date 2021 */ @@ -38,70 +38,50 @@ * if advised of the possibility of such damage. * */ +#include "connector/Pipe.h" -#pragma once +#include +#include -#include - -class IPFIXMessage +Pipe::Pipe() { -public: - IPFIXMessage(ipx_msg_ipfix_t *msg) - : msg(msg) - {} - - const ipx_session * - session() - { - ipx_msg_ctx *msg_ctx = ipx_msg_ipfix_get_ctx(msg); - return msg_ctx->session; + int fd[2]; + if (pipe(fd) != 0) { + throw errno_runtime_error(errno, "pipe"); } - uint8_t * - data() - { - return ipx_msg_ipfix_get_packet(msg); - } + m_readfd.reset(fd[0]); + m_writefd.reset(fd[1]); - fds_ipfix_msg_hdr * - header() - { - return (fds_ipfix_msg_hdr *)data(); - } + int flags; - uint16_t - length() - { - return ntohs(header()->length); + if ((flags = fcntl(m_readfd.get(), F_GETFL)) == -1) { + throw errno_runtime_error(errno, "fcntl"); } - - uint32_t - seq_num() - { - return ntohl(header()->seq_num); + if (fcntl(m_readfd.get(), F_SETFL, flags | O_NONBLOCK) == -1) { + throw errno_runtime_error(errno, "fcntl"); } - int - drec_count() - { - return ipx_msg_ipfix_get_drec_cnt(msg); + if ((flags = fcntl(m_writefd.get(), F_GETFL)) == -1) { + throw errno_runtime_error(errno, "fcntl"); } - - uint32_t - odid() - { - return ntohl(header()->odid); + if (fcntl(m_writefd.get(), F_SETFL, flags | O_NONBLOCK) == -1) { + throw errno_runtime_error(errno, "fcntl"); } +} - const fds_tsnapshot_t * - get_templates_snapshot() - { - if (drec_count() == 0) { - return NULL; - } - return ipx_msg_ipfix_get_drec(msg, 0)->rec.snap; +void +Pipe::poke(bool ignore_error) +{ + ssize_t ret = write(m_writefd.get(), "\x00", 1); + if (ret < 0 && !ignore_error) { + throw errno_runtime_error(errno, "write"); } +} -private: - ipx_msg_ipfix_t *msg; -}; \ No newline at end of file +void +Pipe::clear() +{ + char throwaway[16]; + while (read(m_readfd.get(), throwaway, 16) > 0) {} +} \ No newline at end of file diff --git a/src/plugins/output/forwarder/src/SyncPipe.h b/src/plugins/output/forwarder/src/connector/Pipe.h similarity index 68% rename from src/plugins/output/forwarder/src/SyncPipe.h rename to src/plugins/output/forwarder/src/connector/Pipe.h index aed128f8..cec0793b 100644 --- a/src/plugins/output/forwarder/src/SyncPipe.h +++ b/src/plugins/output/forwarder/src/connector/Pipe.h @@ -1,7 +1,7 @@ /** - * \file src/plugins/output/forwarder/src/SyncPipe.h + * \file src/plugins/output/forwarder/src/connector/Pipe.h * \author Michal Sedlak - * \brief Pipe used for synchronization of threads (when waiting on select) + * \brief Pipe class * \date 2021 */ @@ -41,52 +41,41 @@ #pragma once -#include -#include +#include "common.h" -#include - -class SyncPipe -{ +/** + * \brief Simple utility class around pipe used for interrupting poll + */ +class Pipe { public: - SyncPipe() - { - int pipefd[2]; - if (pipe(pipefd) != 0) { - throw std::string("Cannot create pipe"); - } - readfd = pipefd[0]; - writefd = pipefd[1]; + /** + * \brief The constructor + */ + Pipe(); - int flags = fcntl(readfd, F_GETFL, 0); - fcntl(readfd, F_SETFL, flags | O_NONBLOCK); - } + Pipe(const Pipe &) = delete; + Pipe(Pipe &&) = delete; + /** + * \brief Write to the pipe to trigger its write event + * \param ignore_error Do not throw on write error + * \throw std::runtime_erorr on write error if ignore_error is false + */ void - notify() - { - write(writefd, "A", 1); - } + poke(bool ignore_error = false); + /** + * \brief Read everything from the pipe and throw it away + */ void - clear() - { - char discard_buffer[128]; - while (1) { - int n = read(readfd, discard_buffer, 128); - if (n < 128) { - break; - } - } - } + clear(); - int - get_readfd() - { - return readfd; - } + /** + * \brief Get the read file descriptor + */ + int readfd() const { return m_readfd.get(); } private: - int readfd; - int writefd; + UniqueFd m_readfd; + UniqueFd m_writefd; }; diff --git a/src/plugins/output/forwarder/src/main.cpp b/src/plugins/output/forwarder/src/main.cpp index 9d08162c..1aa4a74c 100644 --- a/src/plugins/output/forwarder/src/main.cpp +++ b/src/plugins/output/forwarder/src/main.cpp @@ -40,7 +40,7 @@ */ #include "Forwarder.h" -#include "config.h" +#include "Config.h" #include @@ -60,14 +60,32 @@ IPX_API struct ipx_plugin_info ipx_plugin_info = { int ipx_plugin_init(ipx_ctx_t *ctx, const char *xml_config) { - auto forwarder = std::unique_ptr(new Forwarder(ctx)); + std::unique_ptr forwarder; + try { - parse_and_configure(ctx, xml_config, *forwarder); - } catch (std::string &error_message) { - IPX_CTX_ERROR(ctx, "%s", error_message.c_str()); - return IPX_ERR_FORMAT; + Config config(xml_config); + forwarder.reset(new Forwarder(config, ctx)); + + } catch (const std::invalid_argument &ex) { + IPX_CTX_ERROR(ctx, "%s", ex.what()); + return IPX_ERR_DENIED; + + } catch (const std::bad_alloc &ex) { + IPX_CTX_ERROR(ctx, "Memory error", 0); + return IPX_ERR_DENIED; + + } catch (const std::runtime_error &ex) { + IPX_CTX_ERROR(ctx, "%s", ex.what()); + return IPX_ERR_DENIED; + + } catch (const std::exception &ex) { + IPX_CTX_ERROR(ctx, "Caught exception %s", ex.what()); + return IPX_ERR_DENIED; + + } catch (...) { + IPX_CTX_ERROR(ctx, "Caught unknown exception", 0); + return IPX_ERR_DENIED; } - forwarder->start(); ipx_msg_mask_t mask = IPX_MSG_IPFIX | IPX_MSG_SESSION; ipx_ctx_subscribe(ctx, &mask, NULL); @@ -79,32 +97,48 @@ ipx_plugin_init(ipx_ctx_t *ctx, const char *xml_config) void ipx_plugin_destroy(ipx_ctx_t *ctx, void *priv) { - (void)ctx; - Forwarder *forwarder = (Forwarder *)(priv); - forwarder->stop(); + (void) ctx; + + Forwarder *forwarder = (Forwarder *) priv; delete forwarder; } int ipx_plugin_process(ipx_ctx_t *ctx, void *priv, ipx_msg_t *msg) { - Forwarder *forwarder = (Forwarder *)(priv); + Forwarder &forwarder = *(Forwarder *) priv; + try { + switch (ipx_msg_get_type(msg)) { + case IPX_MSG_IPFIX: - forwarder->on_ipfix_message(ipx_msg_base2ipfix(msg)); + forwarder.handle_ipfix_message(ipx_msg_base2ipfix(msg)); break; + case IPX_MSG_SESSION: - forwarder->on_session_message(ipx_msg_base2session(msg)); + forwarder.handle_session_message(ipx_msg_base2session(msg)); break; + default: assert(0); } - } catch (std::string &error_message) { - IPX_CTX_ERROR(ctx, "%s", error_message.c_str()); + + } catch (const std::bad_alloc &ex) { + IPX_CTX_ERROR(ctx, "Memory error", 0); + return IPX_ERR_DENIED; + + } catch (const std::runtime_error &ex) { + IPX_CTX_ERROR(ctx, "%s", ex.what()); + return IPX_ERR_DENIED; + + } catch (const std::exception &ex) { + IPX_CTX_ERROR(ctx, "Caught exception %s", ex.what()); + return IPX_ERR_DENIED; + + } catch (...) { + IPX_CTX_ERROR(ctx, "Caught unknown exception", 0); return IPX_ERR_DENIED; - } catch (std::bad_alloc &ex) { - IPX_CTX_ERROR(ctx, "Memory error"); - return IPX_ERR_NOMEM; } + return IPX_OK; } diff --git a/src/plugins/output/json-kafka/src/Config.cpp b/src/plugins/output/json-kafka/src/Config.cpp index 50f51342..42a021b8 100644 --- a/src/plugins/output/json-kafka/src/Config.cpp +++ b/src/plugins/output/json-kafka/src/Config.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include diff --git a/src/plugins/output/json/src/Config.cpp b/src/plugins/output/json/src/Config.cpp index 673a3093..0414fb40 100644 --- a/src/plugins/output/json/src/Config.cpp +++ b/src/plugins/output/json/src/Config.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include diff --git a/src/plugins/output/json/src/Storage.cpp b/src/plugins/output/json/src/Storage.cpp index 498d37f5..47ebad5c 100644 --- a/src/plugins/output/json/src/Storage.cpp +++ b/src/plugins/output/json/src/Storage.cpp @@ -276,6 +276,9 @@ Storage::convert_tset(struct ipx_ipfix_set *set, const struct fds_ipfix_msg_hdr // Iteration through all (Options) Templates in the Set while (fds_tset_iter_next(&tset_iter) == FDS_OK) { + // Buffer is empty + m_record.size_used = 0; + // Read and print single template convert_tmplt_rec(&tset_iter, set_id, hdr); @@ -285,9 +288,6 @@ Storage::convert_tset(struct ipx_ipfix_set *set, const struct fds_ipfix_msg_hdr return IPX_ERR_DENIED; } } - - // Buffer is empty - m_record.size_used = 0; } return IPX_OK; @@ -453,4 +453,4 @@ Storage::convert(struct fds_drec &rec, const fds_iemgr_t *iemgr, fds_ipfix_msg_h // Append the record with end of line character buffer_append("\n"); -} \ No newline at end of file +}