Skip to content

Commit eaa5a3b

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

File tree

10 files changed

+512
-459
lines changed

10 files changed

+512
-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: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
constexpr void SSADetectorPlugin::updatePacketsData(
64+
const std::size_t length,
65+
const amon::types::Timestamp timestamp,
66+
const Direction direction,
67+
SSADetectorContext& ssaContext) noexcept
68+
{
69+
if (!PacketStorage::isValid(length)) {
70+
return;
71+
}
72+
73+
constexpr std::size_t MaxSynToSynAckSizeDiff = 12;
74+
const bool foundTCPHandshake
75+
= ssaContext.processingState.synAckPackets.hasSimilarPacketsRecently(
76+
length,
77+
MaxSynToSynAckSizeDiff,
78+
timestamp,
79+
static_cast<Direction>(!direction));
80+
81+
if (foundTCPHandshake) {
82+
ssaContext.processingState.synPackets.clear();
83+
ssaContext.processingState.synAckPackets.clear();
84+
ssaContext.processingState.suspects++;
85+
if (ssaContext.processingState.suspectLengths.size()
86+
!= ssaContext.processingState.suspectLengths.capacity()) {
87+
ssaContext.processingState.suspectLengths.push_back(length);
88+
}
89+
return;
90+
}
91+
92+
constexpr std::size_t MaxSynAckToSynSizeDiff = 10;
93+
const bool correspondingSynFound
94+
= ssaContext.processingState.synPackets.hasSimilarPacketsRecently(
95+
length,
96+
MaxSynAckToSynSizeDiff,
97+
timestamp,
98+
static_cast<Direction>(!direction));
99+
if (correspondingSynFound) {
100+
ssaContext.processingState.synAckPackets.insert(length, timestamp, direction);
101+
}
102+
103+
ssaContext.processingState.synPackets.insert(length, timestamp, direction);
104+
}
105+
106+
OnInitResult SSADetectorPlugin::onInit(const FlowContext& flowContext, void* pluginContext)
107+
{
108+
constexpr std::size_t MIN_FLOW_LENGTH = 30;
109+
if (flowContext.flowRecord.directionalData[Direction::Forward].packets
110+
+ flowContext.flowRecord.directionalData[Direction::Reverse].packets
111+
< MIN_FLOW_LENGTH) {
112+
return OnInitResult::PendingConstruction;
113+
}
114+
115+
auto& ssaContext = *std::construct_at(reinterpret_cast<SSADetectorContext*>(pluginContext));
116+
updatePacketsData(
117+
flowContext.packetContext.features->ipPayloadLength,
118+
flowContext.packetContext.packet->timestamp,
119+
flowContext.packetContext.features->direction,
120+
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(
129+
flowContext.packetContext.features->ipPayloadLength,
130+
flowContext.packetContext.packet->timestamp,
131+
flowContext.packetContext.features->direction,
132+
ssaContext);
133+
134+
return OnUpdateResult::NeedsUpdate;
135+
}
136+
137+
constexpr static double calculateUniqueRatio(auto&& container) noexcept
138+
{
139+
std::sort(container.begin(), container.end());
140+
auto last = std::unique(container.begin(), container.end());
141+
return static_cast<double>(std::distance(container.begin(), last)) / container.size();
142+
}
143+
144+
OnExportResult SSADetectorPlugin::onExport(const FlowRecord& flowRecord, void* pluginContext)
145+
{
146+
auto& ssaContext = *reinterpret_cast<SSADetectorContext*>(pluginContext);
147+
// do not export for small packets flows
148+
constexpr double HIGH_NUM_SUSPECTS_MAX_RATIO = 0.2;
149+
150+
const std::size_t packetsTotal = flowRecord.directionalData[Direction::Forward].packets
151+
+ flowRecord.directionalData[Direction::Reverse].packets;
152+
constexpr std::size_t MIN_PACKETS = 30;
153+
if (packetsTotal <= MIN_PACKETS) {
154+
return OnExportResult::Remove;
155+
}
156+
157+
constexpr std::size_t MIN_SUSPECTS_COUNT = 3;
158+
if (ssaContext.processingState.suspects < MIN_SUSPECTS_COUNT) {
159+
return OnExportResult::Remove;
160+
}
161+
162+
constexpr std::size_t MIN_SUSPECTS_RATIO = 2500;
163+
if (double(packetsTotal) / double(ssaContext.processingState.suspects) > MIN_SUSPECTS_RATIO) {
164+
return OnExportResult::Remove;
165+
}
166+
167+
const double uniqueRatio = calculateUniqueRatio(ssaContext.processingState.suspectLengths);
168+
constexpr std::size_t LOW_NUM_SUSPECTS_THRESHOLD = 15;
169+
constexpr double LOW_NUM_SUSPECTS_MAX_RATIO = 0.6;
170+
if (ssaContext.processingState.suspects < LOW_NUM_SUSPECTS_THRESHOLD
171+
&& uniqueRatio > LOW_NUM_SUSPECTS_MAX_RATIO) {
172+
return OnExportResult::Remove;
173+
}
174+
175+
constexpr std::size_t MID_NUM_SUSPECTS_THRESHOLD = 40;
176+
constexpr double MID_NUM_SUSPECTS_MAX_RATIO = 0.4;
177+
if (ssaContext.processingState.suspects < MID_NUM_SUSPECTS_THRESHOLD
178+
&& uniqueRatio > MID_NUM_SUSPECTS_MAX_RATIO) {
179+
return OnExportResult::Remove;
180+
}
181+
182+
if (uniqueRatio > HIGH_NUM_SUSPECTS_MAX_RATIO) {
183+
return OnExportResult::Remove;
184+
}
185+
186+
ssaContext.confidence = 1;
187+
m_fieldHandlers[SSADetectorFields::SSA_CONF_LEVEL].setAsAvailable(flowRecord);
188+
return OnExportResult::NoAction;
189+
}
190+
191+
void SSADetectorPlugin::onDestroy(void* pluginContext) noexcept
192+
{
193+
std::destroy_at(reinterpret_cast<SSADetectorContext*>(pluginContext));
194+
}
195+
196+
PluginDataMemoryLayout SSADetectorPlugin::getDataMemoryLayout() const noexcept
197+
{
198+
return {
199+
.size = sizeof(SSADetectorContext),
200+
.alignment = alignof(SSADetectorContext),
201+
};
202+
}
203+
204+
static const PluginRegistrar<
205+
SSADetectorPlugin,
206+
PluginFactory<ProcessPlugin, const std::string&, FieldManager&>>
207+
ssaDetectorRegistrar(ssaDetectorPluginManifest);
208+
209+
} // namespace ipxp::process::ssaDetector

0 commit comments

Comments
 (0)