Skip to content

Commit e8a3a1d

Browse files
Zainullin DamirZainullin Damir
authored andcommitted
Process plugins - Introduce OpenVPN process plugin
1 parent 982967b commit e8a3a1d

File tree

13 files changed

+742
-459
lines changed

13 files changed

+742
-459
lines changed

src/plugins/process/ovpn/CMakeLists.txt

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

33
add_library(ipfixprobe-process-ovpn MODULE
4-
src/ovpn.cpp
5-
src/ovpn.hpp
4+
src/openvpn.cpp
5+
src/openvpn.hpp
6+
src/openvpnContext.hpp
7+
src/openvpnFields.hpp
8+
src/openvpnOpcode.hpp
9+
src/openvpnProcessingState.hpp
10+
src/openvpnProcessingState.cpp
11+
src/rtpHeader.hpp
612
)
713

814
set_target_properties(ipfixprobe-process-ovpn PROPERTIES
915
CXX_VISIBILITY_PRESET hidden
1016
VISIBILITY_INLINES_HIDDEN YES
1117
)
1218

13-
target_include_directories(ipfixprobe-process-ovpn PRIVATE
19+
target_include_directories(ipfixprobe-process-ovpn PRIVATE
1420
${CMAKE_SOURCE_DIR}/include/
21+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/processPlugin
22+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/pluginFactory
23+
${CMAKE_SOURCE_DIR}/src/plugins/process/common
24+
${adaptmon_SOURCE_DIR}/lib/include/public/
1525
)
1626

1727
if(ENABLE_NEMEA)

src/plugins/process/ovpn/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# OpenVPN Plugin
2+
3+
Analyzes connections to identify OpenVPN traffic.
4+
5+
## Features
6+
7+
- Calculates and exports confidence that given flow is OpenVPN.
8+
9+
## Output Fields
10+
11+
| Field Name | Data Type | Description |
12+
| ----------------- | --------- | -------------------------------------------------------------- |
13+
| `OVPN_CONF_LEVEL` | `uint8_t` | Confidence that given flow is OpenVPN as a percentage (0-100). |
14+
15+
## Usage
16+
17+
### YAML Configuration
18+
19+
Add the plugin to your ipfixprobe YAML configuration:
20+
21+
```yaml
22+
process_plugins:
23+
- ovpn
24+
```
25+
26+
### CLI Usage
27+
28+
You can also enable the plugin directly from the command line:
29+
30+
`ipfixprobe -p ovpn ...`
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* @file
3+
* @brief Plugin for parsing ovpn traffic.
4+
* @author Karel Hynek <[email protected]>
5+
* @author Martin Ctrnacty <[email protected]>
6+
* @author Pavel Siska <[email protected]>
7+
* @author Damir Zainullin <[email protected]>
8+
* @date 2025
9+
*
10+
* Provides a plugin that calculates confidence level that given flow is OpenVPN,
11+
* stores it in per-flow plugin data, and exposes that field via FieldManager.
12+
*
13+
* @copyright Copyright (c) 2025 CESNET, z.s.p.o.
14+
*/
15+
16+
#include "openvpn.hpp"
17+
18+
#include "openvpnGetters.hpp"
19+
#include "openvpnOpcode.hpp"
20+
#include "rtpHeader.hpp"
21+
22+
#include <iostream>
23+
24+
#include <fieldGroup.hpp>
25+
#include <fieldManager.hpp>
26+
#include <flowRecord.hpp>
27+
#include <ipfixprobe/options.hpp>
28+
#include <pluginFactory.hpp>
29+
#include <pluginManifest.hpp>
30+
#include <pluginRegistrar.hpp>
31+
#include <utils/spanUtils.hpp>
32+
33+
namespace ipxp::process::ovpn {
34+
35+
static const PluginManifest ovpnPluginManifest = {
36+
.name = "ovpn",
37+
.description = "Ovpn process plugin for parsing ovpn traffic.",
38+
.pluginVersion = "1.0.0",
39+
.apiVersion = "1.0.0",
40+
.usage =
41+
[]() {
42+
OptionsParser parser("ovpn", "OpenVPN detector plugin");
43+
parser.usage(std::cout);
44+
},
45+
};
46+
47+
static FieldGroup
48+
createOpenVPNSchema(FieldManager& manager, FieldHandlers<OpenVPNFields>& handlers) noexcept
49+
{
50+
FieldGroup schema = manager.createFieldGroup("ovpn");
51+
52+
handlers.insert(
53+
OpenVPNFields::OVPN_CONF_LEVEL,
54+
schema.addScalarField("OVPN_CONF_LEVEL", getOVPNConfidenceLevelField));
55+
56+
return schema;
57+
}
58+
59+
OpenVPNPlugin::OpenVPNPlugin([[maybe_unused]] const std::string& params, FieldManager& manager)
60+
{
61+
createOpenVPNSchema(manager, m_fieldHandlers);
62+
}
63+
64+
static bool hasTLSClientHello(std::span<const std::byte> vpnPayload) noexcept
65+
{
66+
constexpr std::size_t contentTypeOffset = 0;
67+
constexpr std::byte handshakeContentType = std::byte {0x16};
68+
69+
constexpr std::size_t handshakeTypeOffset = 5;
70+
constexpr std::byte clientHelloHandshakeType = std::byte {0x1};
71+
72+
constexpr std::size_t encryptedHeaderSize = 28;
73+
74+
return (vpnPayload.size() > handshakeTypeOffset
75+
&& vpnPayload[contentTypeOffset] == handshakeContentType
76+
&& vpnPayload[handshakeTypeOffset] == clientHelloHandshakeType)
77+
|| (vpnPayload.size() > encryptedHeaderSize + handshakeTypeOffset
78+
&& vpnPayload[encryptedHeaderSize + contentTypeOffset] == handshakeContentType
79+
&& vpnPayload[encryptedHeaderSize + handshakeTypeOffset] == clientHelloHandshakeType);
80+
}
81+
82+
constexpr static bool
83+
isValidRTPHeader(const amon::Packet& packet, const PacketFeatures& features) noexcept
84+
{
85+
if (!features.tcp.has_value())
86+
return false;
87+
88+
if (features.ipPayloadLength < sizeof(RTPHeader))
89+
return false;
90+
91+
const RTPHeader* rtpHeader = reinterpret_cast<const RTPHeader*>(getPayload(packet).data());
92+
93+
if (rtpHeader->version != 2)
94+
return false;
95+
96+
if (rtpHeader->payloadType >= 72 && rtpHeader->payloadType <= 95)
97+
return false;
98+
99+
return true;
100+
}
101+
102+
constexpr static std::optional<std::size_t> getOpcodeOffset(const uint8_t l4Protocol)
103+
{
104+
constexpr std::size_t UDP = 17;
105+
if (l4Protocol == UDP) {
106+
return 0;
107+
}
108+
109+
constexpr std::size_t TCP = 6;
110+
if (l4Protocol == TCP) {
111+
return 1;
112+
}
113+
114+
return std::nullopt;
115+
}
116+
117+
bool OpenVPNPlugin::updateConfidenceLevel(
118+
const amon::Packet& packet,
119+
const FlowRecord& flowRecord,
120+
const PacketFeatures& features,
121+
OpenVPNContext& openVPNContext) noexcept
122+
{
123+
std::span<const std::byte> payload = getPayload(packet);
124+
if (payload.size() < 2) {
125+
return false;
126+
}
127+
128+
// TODO USE VALUES FROM DISSECTOR
129+
const std::optional<std::size_t> opcodeOffset = getOpcodeOffset(flowRecord.flowKey.l4Protocol);
130+
if (!opcodeOffset.has_value()) {
131+
return false;
132+
}
133+
134+
const OpenVPNOpcode opcode = static_cast<OpenVPNOpcode>(payload[*opcodeOffset]);
135+
136+
constexpr std::size_t openvpnHeaderSize = 14;
137+
const bool hasClientHello = payload.size() > openvpnHeaderSize
138+
&& hasTLSClientHello(payload.subspan(openvpnHeaderSize));
139+
140+
openVPNContext.processingState.processOpcode(
141+
opcode,
142+
features.direction ? flowRecord.flowKey.srcIp : flowRecord.flowKey.dstIp,
143+
features.direction ? flowRecord.flowKey.dstIp : flowRecord.flowKey.srcIp,
144+
hasClientHello,
145+
isValidRTPHeader(packet, features),
146+
features.ipPayloadLength);
147+
148+
return true;
149+
}
150+
151+
OnInitResult OpenVPNPlugin::onInit(const FlowContext& flowContext, void* pluginContext)
152+
{
153+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
154+
if (!updateConfidenceLevel(
155+
*flowContext.packetContext.packet,
156+
flowContext.flowRecord,
157+
*flowContext.packetContext.features,
158+
openVPNContext)) {
159+
return OnInitResult::ConstructedFinal;
160+
}
161+
162+
return OnInitResult::ConstructedNeedsUpdate;
163+
}
164+
165+
OnUpdateResult OpenVPNPlugin::onUpdate(const FlowContext& flowContext, void* pluginContext)
166+
{
167+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
168+
if (!updateConfidenceLevel(
169+
*flowContext.packetContext.packet,
170+
flowContext.flowRecord,
171+
*flowContext.packetContext.features,
172+
openVPNContext)) {
173+
return OnUpdateResult::Final;
174+
}
175+
176+
return OnUpdateResult::NeedsUpdate;
177+
}
178+
179+
OnExportResult OpenVPNPlugin::onExport(const FlowRecord& flowRecord, void* pluginContext)
180+
{
181+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
182+
// do not export ovpn for short flows, usually port scans
183+
const std::size_t packetsTotal = flowRecord.directionalData[Direction::Forward].packets
184+
+ flowRecord.directionalData[Direction::Reverse].packets;
185+
const std::optional<uint8_t> confidenceLevel
186+
= openVPNContext.processingState.getCurrentConfidenceLevel(packetsTotal);
187+
if (!confidenceLevel.has_value()) {
188+
return OnExportResult::Remove;
189+
}
190+
191+
openVPNContext.vpnConfidence = *confidenceLevel;
192+
m_fieldHandlers[OpenVPNFields::OVPN_CONF_LEVEL].setAsAvailable(flowRecord);
193+
194+
return OnExportResult::NoAction;
195+
}
196+
197+
void OpenVPNPlugin::onDestroy(void* pluginContext) noexcept
198+
{
199+
std::destroy_at(reinterpret_cast<OpenVPNContext*>(pluginContext));
200+
}
201+
202+
PluginDataMemoryLayout OpenVPNPlugin::getDataMemoryLayout() const noexcept
203+
{
204+
return {
205+
.size = sizeof(OpenVPNContext),
206+
.alignment = alignof(OpenVPNContext),
207+
};
208+
}
209+
210+
static const PluginRegistrar<
211+
OpenVPNPlugin,
212+
PluginFactory<ProcessPlugin, const std::string&, FieldManager&>>
213+
ovpnRegistrar(ovpnPluginManifest);
214+
215+
} // namespace ipxp::process::ovpn
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @file
3+
* @brief Plugin for parsing ovpn traffic.
4+
* @author Karel Hynek <[email protected]>
5+
* @author Martin Ctrnacty <[email protected]>
6+
* @author Pavel Siska <[email protected]>
7+
* @author Damir Zainullin <[email protected]>
8+
* @date 2025
9+
*
10+
* Provides a plugin that calculates confidence level that given flow is OpenVPN,
11+
* stores it in per-flow plugin data, and exposes that field via FieldManager.
12+
*
13+
* @copyright Copyright (c) 2025 CESNET, z.s.p.o.
14+
*/
15+
16+
#pragma once
17+
18+
#include "openvpnContext.hpp"
19+
#include "openvpnFields.hpp"
20+
#include "openvpnProcessingState.hpp"
21+
22+
#include <sstream>
23+
#include <string>
24+
25+
#include <fieldHandlersEnum.hpp>
26+
#include <fieldManager.hpp>
27+
#include <processPlugin.hpp>
28+
29+
namespace ipxp::process::ovpn {
30+
31+
/**
32+
* @class OpenVPNPlugin
33+
* @brief A plugin for detecting OpenVPN traffic.
34+
*/
35+
class OpenVPNPlugin : public ProcessPlugin {
36+
public:
37+
/**
38+
* @class OpenVPNPlugin
39+
* @brief A plugin for parsing OpenVPN traffic.
40+
*/
41+
OpenVPNPlugin(const std::string& params, FieldManager& manager);
42+
43+
/**
44+
* @brief Initializes plugin data for a new flow.
45+
*
46+
* Constructs `OpenVPNContext` in `pluginContext` and initializes state machine
47+
* to initial state.
48+
*
49+
* @param flowContext Contextual information about the flow to fill new record.
50+
* @param pluginContext Pointer to pre-allocated memory to create record.
51+
* @return Result of the initialization process.
52+
*/
53+
OnInitResult onInit(const FlowContext& flowContext, void* pluginContext) override;
54+
55+
/**
56+
* @brief Updates plugin data with values from new packet.
57+
*
58+
* Handles transitions in `OpenVPNContext`.
59+
*
60+
* @param flowContext Contextual information about the flow to be updated.
61+
* @param pluginContext Pointer to `OpenVPNContext`.
62+
* @return Result of the update, removes plugin if parsing fails.
63+
*/
64+
OnUpdateResult onUpdate(const FlowContext& flowContext, void* pluginContext) override;
65+
66+
/**
67+
* @brief Prepare the export data.
68+
*
69+
* Removes record if confidence level is too low.
70+
*
71+
* @param flowRecord The flow record containing aggregated flow data.
72+
* @param pluginContext Pointer to `OpenVPNContext`.
73+
* @return Result of the export process.
74+
*/
75+
OnExportResult onExport(const FlowRecord& flowRecord, void* pluginContext) override;
76+
77+
/**
78+
* @brief Cleans up and destroys `OpenVPNContext`.
79+
* @param pluginContext Pointer to `OpenVPNContext`.
80+
*/
81+
void onDestroy(void* pluginContext) noexcept override;
82+
83+
/**
84+
* @brief Provides the memory layout of `OpenVPNContext`.
85+
* @return Memory layout description for the plugin data.
86+
*/
87+
PluginDataMemoryLayout getDataMemoryLayout() const noexcept override;
88+
89+
private:
90+
bool updateConfidenceLevel(
91+
const amon::Packet& packet,
92+
const FlowRecord& flowRecord,
93+
const PacketFeatures& features,
94+
OpenVPNContext& openVPNContext) noexcept;
95+
96+
FieldHandlers<OpenVPNFields> m_fieldHandlers;
97+
};
98+
99+
} // namespace ipxp::process::ovpn

0 commit comments

Comments
 (0)