Skip to content

Commit d7bf6be

Browse files
Zainullin DamirZainullin Damir
authored andcommitted
Process plugins - Introduce SSADetector process plugin
1 parent 6fd16e0 commit d7bf6be

File tree

10 files changed

+513
-459
lines changed

10 files changed

+513
-459
lines changed

src/plugins/process/ssaDetector/CMakeLists.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
project(ipfixprobe-process-ssadetector VERSION 1.0.0 DESCRIPTION "ipfixprobe-process-ssadetector plugin")
22

33
add_library(ipfixprobe-process-ssadetector MODULE
4-
src/ssadetector.cpp
5-
src/ssadetector.hpp
4+
src/ssaDetector.cpp
5+
src/ssaDetector.hpp
6+
src/ssaDetectorContext.hpp
7+
src/ssaDetectorFields.hpp
8+
src/packetStorage.hpp
69
)
710

811
set_target_properties(ipfixprobe-process-ssadetector PROPERTIES
912
CXX_VISIBILITY_PRESET hidden
1013
VISIBILITY_INLINES_HIDDEN YES
1114
)
1215

13-
target_include_directories(ipfixprobe-process-ssadetector PRIVATE
16+
target_include_directories(ipfixprobe-process-ssadetector PRIVATE
1417
${CMAKE_SOURCE_DIR}/include/
18+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/processPlugin
19+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/pluginFactory
20+
${CMAKE_SOURCE_DIR}/src/plugins/process/common
21+
${adaptmon_SOURCE_DIR}/lib/include/public/
1522
)
1623

1724
if(ENABLE_NEMEA)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SSADetector Plugin
2+
3+
Analyzes connections to identify encrypted tunnels.
4+
5+
## Features
6+
7+
- Calculates and exports confidence that given flow is a tunnel.
8+
- Detection is base on identification of encrypted TCP syn-synack-ack tuples.
9+
10+
## Output Fields
11+
12+
| Field Name | Data Type | Description |
13+
| ---------------- | --------- | --------------------------------------------------------------- |
14+
| `SSA_CONF_LEVEL` | `uint8_t` | Confidence that given flow is a tunnel as a percentage (0-100). |
15+
16+
## Usage
17+
18+
### YAML Configuration
19+
20+
Add the plugin to your ipfixprobe YAML configuration:
21+
22+
```yaml
23+
process_plugins:
24+
- ssadetector
25+
```
26+
27+
### CLI Usage
28+
29+
You can also enable the plugin directly from the command line:
30+
31+
`ipfixprobe -p ssadetector ...`
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @file packetStorage.hpp
3+
* @brief Declaration of PacketStorage class for SSA Detector plugin.
4+
* @author Damir Zainullin <[email protected]>
5+
* @date 2025
6+
*
7+
* @copyright Copyright (c) 2025 CESNET, z.s.p.o.
8+
*/
9+
10+
#pragma once
11+
12+
#include <array>
13+
#include <chrono>
14+
#include <cstddef>
15+
#include <vector>
16+
17+
#include <amon/types/Timestamp.hpp>
18+
#include <directionalField.hpp>
19+
#include <timestamp.hpp>
20+
21+
namespace ipxp::process::ssaDetector {
22+
23+
/**
24+
* @class PacketStorage
25+
* @brief Stores timestamps of packets categorized by their lengths and directions.
26+
* This class is used in the SSA Detector plugin to track packet timings
27+
* for different packet sizes and directions.
28+
*/
29+
class PacketStorage {
30+
public:
31+
constexpr static std::size_t MIN_PACKET_SIZE = 60;
32+
constexpr static std::size_t MAX_PACKET_SIZE = 150;
33+
constexpr static std::size_t MAX_PACKET_TIMEDIFF_NS
34+
= std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(3)).count();
35+
36+
constexpr static bool isValid(const std::size_t length) noexcept
37+
{
38+
return length >= MIN_PACKET_SIZE && length <= MAX_PACKET_SIZE;
39+
}
40+
41+
constexpr void insert(
42+
const std::size_t length,
43+
const amon::types::Timestamp timestamp,
44+
const Direction direction) noexcept
45+
{
46+
timestamps.resize(length - MIN_PACKET_SIZE);
47+
timestamps[length - MIN_PACKET_SIZE][direction] = timestamp;
48+
}
49+
50+
constexpr bool hasSimilarPacketsRecently(
51+
const std::size_t length,
52+
const std::size_t maxSizeDiff,
53+
const amon::types::Timestamp now,
54+
const Direction direction) noexcept
55+
{
56+
const std::size_t endIndex = length - MIN_PACKET_SIZE;
57+
const std::size_t startIndex = endIndex > maxSizeDiff ? endIndex - maxSizeDiff : 0;
58+
59+
for (std::size_t i = startIndex; i <= endIndex; ++i) {
60+
if (now.nanoseconds() > timestamps[i][direction].nanoseconds()
61+
&& (now.nanoseconds() - timestamps[i][direction].nanoseconds())
62+
< MAX_PACKET_TIMEDIFF_NS) {
63+
return true;
64+
}
65+
}
66+
67+
return false;
68+
}
69+
70+
void clear() noexcept { timestamps.clear(); }
71+
72+
private:
73+
std::vector<DirectionalField<amon::types::Timestamp>> timestamps;
74+
};
75+
76+
} // namespace ipxp::process::ssaDetector
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* @file
3+
* @brief Plugin for parsing basicplus traffic.
4+
* @author Jiri Havranek <[email protected]>
5+
* @author Pavel Siska <[email protected]>
6+
* @date 2025
7+
*
8+
* Copyright (c) 2025 CESNET
9+
*
10+
* SPDX-License-Identifier: BSD-3-Clause
11+
*/
12+
13+
#include "ssaDetector.hpp"
14+
15+
#include "ssaDetectorGetters.hpp"
16+
17+
#include <iostream>
18+
19+
#include <fieldGroup.hpp>
20+
#include <fieldManager.hpp>
21+
#include <flowRecord.hpp>
22+
#include <ipfixprobe/options.hpp>
23+
#include <pluginFactory.hpp>
24+
#include <pluginManifest.hpp>
25+
#include <pluginRegistrar.hpp>
26+
#include <utils.hpp>
27+
28+
namespace ipxp::process::ssaDetector {
29+
30+
static const PluginManifest ssaDetectorPluginManifest = {
31+
.name = "ssadetector",
32+
.description = "Ssadetector process plugin for parsing vpn_automaton traffic.",
33+
.pluginVersion = "1.0.0",
34+
.apiVersion = "1.0.0",
35+
.usage =
36+
[]() {
37+
OptionsParser parser(
38+
"ssadetector",
39+
"Check traffic for SYN-SYNACK-ACK sequence to find possible network tunnels.");
40+
parser.usage(std::cout);
41+
},
42+
};
43+
44+
static FieldGroup createSSADetectorSchema(
45+
FieldManager& fieldManager,
46+
FieldHandlers<SSADetectorFields> handlers) noexcept
47+
{
48+
FieldGroup schema = fieldManager.createFieldGroup("ssadetector");
49+
50+
handlers.insert(
51+
SSADetectorFields::SSA_CONF_LEVEL,
52+
schema.addScalarField("SSA_CONF_LEVEL", getSSAConfLevelField));
53+
return schema;
54+
}
55+
56+
SSADetectorPlugin::SSADetectorPlugin(
57+
[[maybe_unused]] const std::string& params,
58+
FieldManager& manager)
59+
{
60+
createSSADetectorSchema(manager, m_fieldHandlers);
61+
}
62+
63+
void SSADetectorPlugin::updatePacketsData(
64+
const amon::Packet& packet,
65+
const Direction direction,
66+
SSADetectorContext& ssaContext) noexcept
67+
{
68+
const std::optional<std::size_t> ipPayloadLength = getIPPayloadLength(packet);
69+
70+
if (!ipPayloadLength.has_value() || !PacketStorage::isValid(*ipPayloadLength)) {
71+
return;
72+
}
73+
74+
constexpr std::size_t MaxSynToSynAckSizeDiff = 12;
75+
const bool foundTCPHandshake
76+
= ssaContext.processingState.synAckPackets.hasSimilarPacketsRecently(
77+
*ipPayloadLength,
78+
MaxSynToSynAckSizeDiff,
79+
packet.timestamp,
80+
static_cast<Direction>(!direction));
81+
82+
if (foundTCPHandshake) {
83+
ssaContext.processingState.synPackets.clear();
84+
ssaContext.processingState.synAckPackets.clear();
85+
ssaContext.processingState.suspects++;
86+
if (ssaContext.processingState.suspectLengths.size()
87+
!= ssaContext.processingState.suspectLengths.capacity()) {
88+
ssaContext.processingState.suspectLengths.push_back(*ipPayloadLength);
89+
}
90+
return;
91+
}
92+
93+
constexpr std::size_t MaxSynAckToSynSizeDiff = 10;
94+
const bool correspondingSynFound
95+
= ssaContext.processingState.synPackets.hasSimilarPacketsRecently(
96+
*ipPayloadLength,
97+
MaxSynAckToSynSizeDiff,
98+
packet.timestamp,
99+
static_cast<Direction>(!direction));
100+
if (correspondingSynFound) {
101+
ssaContext.processingState.synAckPackets.insert(
102+
*ipPayloadLength,
103+
packet.timestamp,
104+
direction);
105+
}
106+
107+
ssaContext.processingState.synPackets.insert(*ipPayloadLength, packet.timestamp, direction);
108+
}
109+
110+
OnInitResult SSADetectorPlugin::onInit(const FlowContext& flowContext, void* pluginContext)
111+
{
112+
constexpr std::size_t MIN_FLOW_LENGTH = 30;
113+
if (flowContext.flowRecord.directionalData[Direction::Forward].packets
114+
+ flowContext.flowRecord.directionalData[Direction::Reverse].packets
115+
< MIN_FLOW_LENGTH) {
116+
return OnInitResult::PendingConstruction;
117+
}
118+
119+
auto& ssaContext = *std::construct_at(reinterpret_cast<SSADetectorContext*>(pluginContext));
120+
updatePacketsData(*flowContext.packetContext.packet, flowContext.packetDirection, ssaContext);
121+
122+
return OnInitResult::ConstructedNeedsUpdate;
123+
}
124+
125+
OnUpdateResult SSADetectorPlugin::onUpdate(const FlowContext& flowContext, void* pluginContext)
126+
{
127+
auto& ssaContext = *reinterpret_cast<SSADetectorContext*>(pluginContext);
128+
updatePacketsData(*flowContext.packetContext.packet, flowContext.packetDirection, ssaContext);
129+
130+
return OnUpdateResult::NeedsUpdate;
131+
}
132+
133+
constexpr static double calculateUniqueRatio(auto&& container) noexcept
134+
{
135+
std::sort(container.begin(), container.end());
136+
auto last = std::unique(container.begin(), container.end());
137+
return static_cast<double>(std::distance(container.begin(), last)) / container.size();
138+
}
139+
140+
OnExportResult SSADetectorPlugin::onExport(const FlowRecord& flowRecord, void* pluginContext)
141+
{
142+
auto& ssaContext = *reinterpret_cast<SSADetectorContext*>(pluginContext);
143+
// do not export for small packets flows
144+
constexpr double HIGH_NUM_SUSPECTS_MAX_RATIO = 0.2;
145+
146+
const std::size_t packetsTotal = flowRecord.directionalData[Direction::Forward].packets
147+
+ flowRecord.directionalData[Direction::Reverse].packets;
148+
constexpr std::size_t MIN_PACKETS = 30;
149+
if (packetsTotal <= MIN_PACKETS) {
150+
return OnExportResult::Remove;
151+
}
152+
153+
constexpr std::size_t MIN_SUSPECTS_COUNT = 3;
154+
if (ssaContext.processingState.suspects < MIN_SUSPECTS_COUNT) {
155+
return OnExportResult::Remove;
156+
}
157+
158+
constexpr std::size_t MIN_SUSPECTS_RATIO = 2500;
159+
if (double(packetsTotal) / double(ssaContext.processingState.suspects) > MIN_SUSPECTS_RATIO) {
160+
return OnExportResult::Remove;
161+
}
162+
163+
const double uniqueRatio = calculateUniqueRatio(ssaContext.processingState.suspectLengths);
164+
constexpr std::size_t LOW_NUM_SUSPECTS_THRESHOLD = 15;
165+
constexpr double LOW_NUM_SUSPECTS_MAX_RATIO = 0.6;
166+
if (ssaContext.processingState.suspects < LOW_NUM_SUSPECTS_THRESHOLD
167+
&& uniqueRatio > LOW_NUM_SUSPECTS_MAX_RATIO) {
168+
return OnExportResult::Remove;
169+
}
170+
171+
constexpr std::size_t MID_NUM_SUSPECTS_THRESHOLD = 40;
172+
constexpr double MID_NUM_SUSPECTS_MAX_RATIO = 0.4;
173+
if (ssaContext.processingState.suspects < MID_NUM_SUSPECTS_THRESHOLD
174+
&& uniqueRatio > MID_NUM_SUSPECTS_MAX_RATIO) {
175+
return OnExportResult::Remove;
176+
}
177+
178+
if (uniqueRatio > HIGH_NUM_SUSPECTS_MAX_RATIO) {
179+
return OnExportResult::Remove;
180+
}
181+
182+
ssaContext.confidence = 1;
183+
m_fieldHandlers[SSADetectorFields::SSA_CONF_LEVEL].setAsAvailable(flowRecord);
184+
return OnExportResult::NoAction;
185+
}
186+
187+
void SSADetectorPlugin::onDestroy(void* pluginContext) noexcept
188+
{
189+
std::destroy_at(reinterpret_cast<SSADetectorContext*>(pluginContext));
190+
}
191+
192+
PluginDataMemoryLayout SSADetectorPlugin::getDataMemoryLayout() const noexcept
193+
{
194+
return {
195+
.size = sizeof(SSADetectorContext),
196+
.alignment = alignof(SSADetectorContext),
197+
};
198+
}
199+
200+
static const PluginRegistrar<
201+
SSADetectorPlugin,
202+
PluginFactory<ProcessPlugin, const std::string&, FieldManager&>>
203+
ssaDetectorRegistrar(ssaDetectorPluginManifest);
204+
205+
} // namespace ipxp::process::ssaDetector

0 commit comments

Comments
 (0)