Skip to content

Commit f2ca4e2

Browse files
Zainullin DamirZainullin Damir
authored andcommitted
Process plugins - Introduce OpenVPN process plugin
1 parent 7cc669e commit f2ca4e2

File tree

13 files changed

+748
-459
lines changed

13 files changed

+748
-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: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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 <amon/layers/TCP.hpp>
25+
#include <fieldGroup.hpp>
26+
#include <fieldManager.hpp>
27+
#include <flowRecord.hpp>
28+
#include <ipfixprobe/options.hpp>
29+
#include <pluginFactory.hpp>
30+
#include <pluginManifest.hpp>
31+
#include <pluginRegistrar.hpp>
32+
#include <utils/spanUtils.hpp>
33+
34+
namespace ipxp::process::ovpn {
35+
36+
static const PluginManifest ovpnPluginManifest = {
37+
.name = "ovpn",
38+
.description = "Ovpn process plugin for parsing ovpn traffic.",
39+
.pluginVersion = "1.0.0",
40+
.apiVersion = "1.0.0",
41+
.usage =
42+
[]() {
43+
OptionsParser parser("ovpn", "OpenVPN detector plugin");
44+
parser.usage(std::cout);
45+
},
46+
};
47+
48+
static FieldGroup
49+
createOpenVPNSchema(FieldManager& manager, FieldHandlers<OpenVPNFields>& handlers) noexcept
50+
{
51+
FieldGroup schema = manager.createFieldGroup("ovpn");
52+
53+
handlers.insert(
54+
OpenVPNFields::OVPN_CONF_LEVEL,
55+
schema.addScalarField("OVPN_CONF_LEVEL", getOVPNConfidenceLevelField));
56+
57+
return schema;
58+
}
59+
60+
OpenVPNPlugin::OpenVPNPlugin([[maybe_unused]] const std::string& params, FieldManager& manager)
61+
{
62+
createOpenVPNSchema(manager, m_fieldHandlers);
63+
}
64+
65+
static bool hasTLSClientHello(std::span<const std::byte> vpnPayload) noexcept
66+
{
67+
constexpr std::size_t contentTypeOffset = 0;
68+
constexpr std::byte handshakeContentType = std::byte {0x16};
69+
70+
constexpr std::size_t handshakeTypeOffset = 5;
71+
constexpr std::byte clientHelloHandshakeType = std::byte {0x1};
72+
73+
constexpr std::size_t encryptedHeaderSize = 28;
74+
75+
return (vpnPayload.size() > handshakeTypeOffset
76+
&& vpnPayload[contentTypeOffset] == handshakeContentType
77+
&& vpnPayload[handshakeTypeOffset] == clientHelloHandshakeType)
78+
|| (vpnPayload.size() > encryptedHeaderSize + handshakeTypeOffset
79+
&& vpnPayload[encryptedHeaderSize + contentTypeOffset] == handshakeContentType
80+
&& vpnPayload[encryptedHeaderSize + handshakeTypeOffset] == clientHelloHandshakeType);
81+
}
82+
83+
constexpr static bool isValidRTPHeader(const amon::Packet& packet) noexcept
84+
{
85+
auto tcp = getLayerView<amon::layers::TCPView>(packet, packet.layout.l4);
86+
if (!tcp.has_value())
87+
return false;
88+
89+
const std::optional<std::size_t> ipPayloadLength = getIPPayloadLength(packet);
90+
if (!ipPayloadLength.has_value() || *ipPayloadLength < sizeof(RTPHeader))
91+
return false;
92+
93+
const RTPHeader* rtpHeader = reinterpret_cast<const RTPHeader*>(getPayload(packet).data());
94+
95+
if (rtpHeader->version != 2)
96+
return false;
97+
98+
if (rtpHeader->payloadType >= 72 && rtpHeader->payloadType <= 95)
99+
return false;
100+
101+
return true;
102+
}
103+
104+
constexpr static std::optional<std::size_t> getOpcodeOffset(const uint8_t l4Protocol)
105+
{
106+
constexpr std::size_t UDP = 17;
107+
if (l4Protocol == UDP) {
108+
return 0;
109+
}
110+
111+
constexpr std::size_t TCP = 6;
112+
if (l4Protocol == TCP) {
113+
return 1;
114+
}
115+
116+
return std::nullopt;
117+
}
118+
119+
bool OpenVPNPlugin::updateConfidenceLevel(
120+
const amon::Packet& packet,
121+
const FlowRecord& flowRecord,
122+
const Direction direction,
123+
OpenVPNContext& openVPNContext) noexcept
124+
{
125+
std::span<const std::byte> payload = getPayload(packet);
126+
if (payload.size() < 2) {
127+
return false;
128+
}
129+
130+
const std::optional<std::size_t> ipPayloadLength = getIPPayloadLength(packet);
131+
if (!ipPayloadLength.has_value()) {
132+
return false;
133+
}
134+
135+
const std::optional<std::size_t> opcodeOffset = getOpcodeOffset(flowRecord.flowKey.l4Protocol);
136+
if (!opcodeOffset.has_value()) {
137+
return false;
138+
}
139+
140+
const OpenVPNOpcode opcode = static_cast<OpenVPNOpcode>(payload[*opcodeOffset]);
141+
142+
constexpr std::size_t openvpnHeaderSize = 14;
143+
const bool hasClientHello = payload.size() > openvpnHeaderSize
144+
&& hasTLSClientHello(payload.subspan(openvpnHeaderSize));
145+
146+
openVPNContext.processingState.processOpcode(
147+
opcode,
148+
direction ? flowRecord.flowKey.srcIp : flowRecord.flowKey.dstIp,
149+
direction ? flowRecord.flowKey.dstIp : flowRecord.flowKey.srcIp,
150+
hasClientHello,
151+
isValidRTPHeader(packet),
152+
*ipPayloadLength);
153+
154+
return true;
155+
}
156+
157+
OnInitResult OpenVPNPlugin::onInit(const FlowContext& flowContext, void* pluginContext)
158+
{
159+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
160+
if (!updateConfidenceLevel(
161+
*flowContext.packetContext.packet,
162+
flowContext.flowRecord,
163+
flowContext.packetDirection,
164+
openVPNContext)) {
165+
return OnInitResult::ConstructedFinal;
166+
}
167+
168+
return OnInitResult::ConstructedNeedsUpdate;
169+
}
170+
171+
OnUpdateResult OpenVPNPlugin::onUpdate(const FlowContext& flowContext, void* pluginContext)
172+
{
173+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
174+
if (!updateConfidenceLevel(
175+
*flowContext.packetContext.packet,
176+
flowContext.flowRecord,
177+
flowContext.packetDirection,
178+
openVPNContext)) {
179+
return OnUpdateResult::Final;
180+
}
181+
182+
return OnUpdateResult::NeedsUpdate;
183+
}
184+
185+
OnExportResult OpenVPNPlugin::onExport(const FlowRecord& flowRecord, void* pluginContext)
186+
{
187+
auto& openVPNContext = *reinterpret_cast<OpenVPNContext*>(pluginContext);
188+
// do not export ovpn for short flows, usually port scans
189+
const std::size_t packetsTotal = flowRecord.directionalData[Direction::Forward].packets
190+
+ flowRecord.directionalData[Direction::Reverse].packets;
191+
const std::optional<uint8_t> confidenceLevel
192+
= openVPNContext.processingState.getCurrentConfidenceLevel(packetsTotal);
193+
if (!confidenceLevel.has_value()) {
194+
return OnExportResult::Remove;
195+
}
196+
197+
openVPNContext.vpnConfidence = *confidenceLevel;
198+
m_fieldHandlers[OpenVPNFields::OVPN_CONF_LEVEL].setAsAvailable(flowRecord);
199+
200+
return OnExportResult::NoAction;
201+
}
202+
203+
void OpenVPNPlugin::onDestroy(void* pluginContext) noexcept
204+
{
205+
std::destroy_at(reinterpret_cast<OpenVPNContext*>(pluginContext));
206+
}
207+
208+
PluginDataMemoryLayout OpenVPNPlugin::getDataMemoryLayout() const noexcept
209+
{
210+
return {
211+
.size = sizeof(OpenVPNContext),
212+
.alignment = alignof(OpenVPNContext),
213+
};
214+
}
215+
216+
static const PluginRegistrar<
217+
OpenVPNPlugin,
218+
PluginFactory<ProcessPlugin, const std::string&, FieldManager&>>
219+
ovpnRegistrar(ovpnPluginManifest);
220+
221+
} // 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 ProcessPluginCRTP<OpenVPNPlugin> {
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 Direction direction,
94+
OpenVPNContext& openVPNContext) noexcept;
95+
96+
FieldHandlers<OpenVPNFields> m_fieldHandlers;
97+
};
98+
99+
} // namespace ipxp::process::ovpn

0 commit comments

Comments
 (0)