Skip to content

Commit 8e44878

Browse files
authored
#1754 Add Modbus Support (#1823)
1 parent e06a49d commit 8e44878

File tree

12 files changed

+353
-10
lines changed

12 files changed

+353
-10
lines changed

Packet++/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_library(
3030
src/Layer.cpp
3131
src/LdapLayer.cpp
3232
src/LLCLayer.cpp
33+
src/ModbusLayer.cpp
3334
src/MplsLayer.cpp
3435
src/NdpLayer.cpp
3536
src/NflogLayer.cpp

Packet++/header/ModbusLayer.h

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#pragma once
2+
3+
#include "Layer.h"
4+
5+
/// @file
6+
/// This file contains classes for parsing, creating and editing Modbus packets.
7+
8+
/// @namespace pcpp
9+
/// @brief The main namespace for the PcapPlusPlus lib
10+
namespace pcpp
11+
{
12+
13+
#pragma pack(push, 1)
14+
/// @struct modbus_header
15+
/// MODBUS Application Protocol header
16+
struct modbus_header
17+
{
18+
/// For synchronization between messages of server and client
19+
uint16_t transactionId;
20+
/// 0 for Modbus/TCP
21+
uint16_t protocolId;
22+
/// Number of remaining bytes in this frame starting from the unit id
23+
uint16_t length;
24+
/// Unit identifier
25+
uint8_t unitId;
26+
/// Function code
27+
uint8_t functionCode;
28+
};
29+
#pragma pack(pop)
30+
static_assert(sizeof(modbus_header) == 8, "modbus_header size is not 8 bytes");
31+
32+
/// @class ModbusLayer
33+
/// Represents the MODBUS Application Protocol layer
34+
class ModbusLayer : public Layer
35+
{
36+
public:
37+
/// @brief Enum class representing Modbus function codes.
38+
/// This enumeration defines the standard Modbus function codes used in request and response PDUs.
39+
/// Each value corresponds to a specific operation defined by the Modbus protocol.
40+
enum class ModbusFunctionCode : uint8_t
41+
{
42+
/// Read coil status (0x01)
43+
ReadCoils = 1,
44+
45+
/// Read discrete input status (0x02)
46+
ReadDiscreteInputs = 2,
47+
48+
/// Read holding registers (0x03)
49+
ReadHoldingRegisters = 3,
50+
51+
/// Read input registers (0x04)
52+
ReadInputRegisters = 4,
53+
54+
/// Write a single coil (0x05)
55+
WriteSingleCoil = 5,
56+
57+
/// Write a single holding register (0x06)
58+
WriteSingleHoldingRegister = 6,
59+
60+
/// Write multiple coils (0x0F)
61+
WriteMultipleCoils = 15,
62+
63+
/// Write multiple holding registers (0x10)
64+
WriteMultipleHoldingRegisters = 16,
65+
66+
/// Report slave ID (0x11)
67+
ReadSlaveId = 17,
68+
69+
/// Unknown or unsupported function code (0xFF)
70+
UnknownFunction = 0xFF
71+
};
72+
73+
/// @struct ModbusReadInputRegisters
74+
/// Represents a Modbus request to read input registers.
75+
struct ModbusReadInputRegisters
76+
{
77+
uint16_t startingAddress; ///< Starting address of the input registers to read
78+
uint16_t quantity; ///< Number of input registers to read
79+
};
80+
81+
/// A constructor that creates the layer from an existing packet raw data
82+
/// @param[in] data A pointer to the raw data
83+
/// @param[in] dataLen Size of the data in bytes
84+
/// @param[in] prevLayer A pointer to the previous layer
85+
/// @param[in] packet A pointer to the Packet instance where layer will be stored in
86+
ModbusLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet)
87+
: Layer(data, dataLen, prevLayer, packet, Modbus)
88+
{}
89+
90+
/// A constructor that creates the layer from user inputs
91+
/// @param[in] transactionId Transaction ID
92+
/// @param[in] unitId Unit ID
93+
ModbusLayer(uint16_t transactionId, uint8_t unitId);
94+
95+
/// @brief Check if a port is a valid MODBUS port
96+
/// @param port Port number to check
97+
/// @note MODBUS uses port 502, so this function checks if the port is equal to 502
98+
/// @return true if the port is valid, false otherwise
99+
static bool isModbusPort(uint16_t port)
100+
{
101+
return port == 502;
102+
}
103+
104+
/// @return MODBUS message type
105+
uint16_t getTransactionId() const;
106+
107+
/// @return MODBUS protocol id
108+
uint16_t getProtocolId() const;
109+
110+
/// @return MODBUS remaining bytes in frame starting from the unit id
111+
/// @note This is the length of the MODBUS payload + unit_id, not the entire packet
112+
uint16_t getLength() const;
113+
114+
/// @return MODBUS unit id
115+
uint8_t getUnitId() const;
116+
117+
/// @return MODBUS function code
118+
ModbusFunctionCode getFunctionCode() const;
119+
120+
/// @brief set the MODBUS transaction id
121+
/// @param transactionId transaction id
122+
void setTransactionId(uint16_t transactionId);
123+
124+
/// @brief set the MODBUS header unit id
125+
/// @param unitId unit id
126+
void setUnitId(uint8_t unitId);
127+
128+
/// @brief set the MODBUS header function code
129+
/// @param functionCode function code
130+
void setFunctionCode(ModbusFunctionCode functionCode);
131+
132+
// Overridden methods
133+
134+
/// Does nothing for this layer (ModbusLayer is always last)
135+
void parseNextLayer() override
136+
{}
137+
138+
/// @brief Get the length of the MODBUS header
139+
/// @return Length of the MODBUS header in bytes
140+
size_t getHeaderLen() const override
141+
{
142+
return sizeof(modbus_header);
143+
}
144+
145+
/// Does nothing for this layer
146+
void computeCalculateFields() override
147+
{}
148+
149+
/// @return A string representation of the layer most important data (should look like the layer description in
150+
/// Wireshark)
151+
std::string toString() const override;
152+
153+
/// @return The OSI Model layer this protocol belongs to
154+
OsiModelLayer getOsiModelLayer() const override
155+
{
156+
return OsiModelApplicationLayer;
157+
}
158+
159+
private:
160+
/// @return A pointer to the MODBUS header
161+
modbus_header* getModbusHeader() const;
162+
};
163+
164+
} // namespace pcpp

Packet++/header/ProtocolType.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ namespace pcpp
248248
/// File Transfer Protocol (FTP) Data channel
249249
const ProtocolType FTPData = 60;
250250

251+
/// Modbus protocol
252+
const ProtocolType Modbus = 61;
253+
251254
/// FTP protocol family (FTPControl and FtpData protocols)
252255
const ProtocolTypeFamily FTP = 0x3c29;
253256

Packet++/src/ModbusLayer.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#include "ModbusLayer.h"
2+
#include "EndianPortable.h"
3+
#include <iostream>
4+
#include <iomanip>
5+
#include <cstring>
6+
#include "Logger.h"
7+
8+
namespace pcpp
9+
{
10+
ModbusLayer::ModbusLayer(uint16_t transactionId, uint8_t unitId)
11+
{
12+
const int16_t pduSize = sizeof(ModbusReadInputRegisters); // Currently only supporting Read Input Registers
13+
const size_t headerLen = sizeof(modbus_header);
14+
15+
m_DataLen = headerLen + pduSize;
16+
m_Data = new uint8_t[m_DataLen]{};
17+
memset(m_Data, 0, m_DataLen);
18+
19+
// Initialize the header fields to default values
20+
modbus_header* header = getModbusHeader();
21+
header->transactionId = htobe16(transactionId);
22+
header->protocolId = 0; // 0 for Modbus/TCP
23+
header->length = htobe16(pduSize + 2); // Length includes unitId and functionCode
24+
header->unitId = unitId;
25+
header->functionCode = static_cast<uint8_t>(ModbusFunctionCode::ReadInputRegisters);
26+
}
27+
28+
modbus_header* ModbusLayer::getModbusHeader() const
29+
{
30+
return (modbus_header*)m_Data;
31+
}
32+
33+
uint16_t ModbusLayer::getTransactionId() const
34+
{
35+
return be16toh(getModbusHeader()->transactionId);
36+
}
37+
38+
uint16_t ModbusLayer::getProtocolId() const
39+
{
40+
return be16toh(getModbusHeader()->protocolId);
41+
}
42+
43+
uint16_t ModbusLayer::getLength() const
44+
{
45+
return be16toh(getModbusHeader()->length);
46+
}
47+
48+
uint8_t ModbusLayer::getUnitId() const
49+
{
50+
return getModbusHeader()->unitId;
51+
}
52+
53+
ModbusLayer::ModbusFunctionCode ModbusLayer::getFunctionCode() const
54+
{
55+
switch (getModbusHeader()->functionCode)
56+
{
57+
case 1:
58+
return ModbusFunctionCode::ReadCoils;
59+
case 2:
60+
return ModbusFunctionCode::ReadDiscreteInputs;
61+
case 3:
62+
return ModbusFunctionCode::ReadHoldingRegisters;
63+
case 4:
64+
return ModbusFunctionCode::ReadInputRegisters;
65+
case 5:
66+
return ModbusFunctionCode::WriteSingleCoil;
67+
case 6:
68+
return ModbusFunctionCode::WriteSingleHoldingRegister;
69+
case 15:
70+
return ModbusFunctionCode::WriteMultipleCoils;
71+
case 16:
72+
return ModbusFunctionCode::WriteMultipleHoldingRegisters;
73+
case 17:
74+
return ModbusFunctionCode::ReadSlaveId;
75+
default:
76+
return ModbusFunctionCode::UnknownFunction;
77+
}
78+
}
79+
80+
void ModbusLayer::setTransactionId(uint16_t transactionId)
81+
{
82+
getModbusHeader()->transactionId = htobe16(transactionId);
83+
}
84+
85+
void ModbusLayer::setUnitId(uint8_t unitId)
86+
{
87+
getModbusHeader()->unitId = unitId;
88+
}
89+
90+
void ModbusLayer::setFunctionCode(ModbusLayer::ModbusFunctionCode functionCode)
91+
{
92+
getModbusHeader()->functionCode = static_cast<uint8_t>(functionCode);
93+
}
94+
95+
std::string ModbusLayer::toString() const
96+
{
97+
return "Modbus Layer, Transaction ID: " + std::to_string(getTransactionId()) +
98+
", Protocol ID: " + std::to_string(getProtocolId()) + ", Length: " + std::to_string(getLength()) +
99+
", Unit ID: " + std::to_string(getUnitId()) +
100+
", Function Code: " + std::to_string(static_cast<uint8_t>(getFunctionCode()));
101+
}
102+
} // namespace pcpp

Packet++/src/TcpLayer.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "SmtpLayer.h"
2020
#include "LdapLayer.h"
2121
#include "GtpLayer.h"
22+
#include "ModbusLayer.h"
2223
#include "PacketUtils.h"
2324
#include "Logger.h"
2425
#include "DeprecationUtils.h"
@@ -464,6 +465,10 @@ namespace pcpp
464465
{
465466
constructNextLayer<GtpV2Layer>(payload, payloadLen, m_Packet);
466467
}
468+
else if (ModbusLayer::isModbusPort(portDst))
469+
{
470+
constructNextLayer<ModbusLayer>(payload, payloadLen, m_Packet);
471+
}
467472
else
468473
{
469474
constructNextLayer<PayloadLayer>(payload, payloadLen, m_Packet);

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,17 @@ PcapPlusPlus currently supports parsing, editing and creation of packets of the
277277
42. FTP
278278
43. HTTP headers (request & response)
279279
44. LDAP
280-
45. NTP (v3, v4)
281-
46. PEM decoder and encoder
282-
47. Radius
283-
48. S7 Communication (S7comm)
284-
49. SMTP
285-
50. SOME/IP
286-
51. SSH - parsing only (no editing capabilities)
287-
52. Telnet - parsing only (no editing capabilities)
288-
53. X509 certificates - parsing only (no editing capabilities)
289-
54. Generic payload
280+
45. Modbus
281+
46. NTP (v3, v4)
282+
47. PEM decoder and encoder
283+
48. Radius
284+
49. S7 Communication (S7comm)
285+
50. SMTP
286+
51. SOME/IP
287+
52. SSH - parsing only (no editing capabilities)
288+
53. Telnet - parsing only (no editing capabilities)
289+
54. X509 certificates - parsing only (no editing capabilities)
290+
55. Generic payload
290291
291292
## DPDK And PF_RING Support
292293

Tests/Packet++Test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ add_executable(
2323
Tests/IPv6Tests.cpp
2424
Tests/LdapTests.cpp
2525
Tests/LLCTests.cpp
26+
Tests/ModbusTests.cpp
2627
Tests/NflogTests.cpp
2728
Tests/NtpTests.cpp
2829
Tests/PacketTests.cpp
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
000c29af7ffe109add4e060d080045000040c6e64000400658880a0101ea0a0a0555c8d301f6e072efbac405c33a8018ffffd5d900000101080a37c3fee900ba2a0f001100000006ff0402580064
118 Bytes
Binary file not shown.

Tests/Packet++Test/TestDefinition.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ PTF_TEST_CASE(CiscoHdlcParsingTest);
318318
PTF_TEST_CASE(CiscoHdlcLayerCreationTest);
319319
PTF_TEST_CASE(CiscoHdlcLayerEditTest);
320320

321+
// Implemented in ModbusTests.cpp
322+
PTF_TEST_CASE(ModbusLayerCreationTest);
323+
PTF_TEST_CASE(ModbusLayerParsingTest);
324+
321325
// Implemented in X509Tests.cpp
322326
PTF_TEST_CASE(X509ParsingTest);
323327
PTF_TEST_CASE(X509VariantsParsingTest);

0 commit comments

Comments
 (0)