GVCP support - #1357
GVCP support#1357tigercosmos wants to merge 4 commits into
Conversation
|
@seladb The PR is ready. Please take a look. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #1357 +/- ##
==========================================
+ Coverage 82.61% 82.82% +0.21%
==========================================
Files 339 342 +3
Lines 61875 62972 +1097
Branches 13036 12947 -89
==========================================
+ Hits 51115 52159 +1044
- Misses 9877 9930 +53
Partials 883 883
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Don't know why the fuzz tests failed? |
seladb
left a comment
There was a problem hiding this comment.
I just started reviewing this PR because I still need to learn this protocol. But here are a few general comments
|
|
||
| GvcpCommand getCommand() const | ||
| { | ||
| return static_cast<GvcpCommand>(netToHost16(command)); |
There was a problem hiding this comment.
I think we can move all implementations that use netToHost16 to GvcpLayer.cpp so we don't expose SystemUtils.h in the header file
There was a problem hiding this comment.
Since it's one line function, I think it's clearer to be in the header. I forward declare the function, so we don't need to include the whole header file.
There was a problem hiding this comment.
I don't think it's a good practice to put this in the header file, I vote we don't forward declare and move the implementation to the cpp file
seladb
left a comment
There was a problem hiding this comment.
@tigercosmos here is my suggestion of the structure we can use, which follows ideas implemented in other PcapPlusPlus layers. This code snippet is just the high level interface and doesn't include all the methods, implementation and lower level details, but it's mostly to demonstrate my suggestion.
Please let me know what you think and we can discuss it further.
struct gvcp_command_header
{
uint8_t gcvpCommand = internal::kGvcpMagicNumber; // always fixed
uint8_t flag = 0; // 0-3 bits are specified by each command, 4-6 bits are reserved, 7 bit is acknowledge
uint16_t command = 0;
uint16_t dataSize = 0;
uint16_t requestId = 0;
};
struct gvcp_ack_header
{
uint16_t status = 0;
uint16_t command = 0;
uint16_t dataSize = 0;
uint16_t ackId = 0;
};
struct gvcp_discovery_message : public gvcp_command_header
{
};
struct gvcp_discovery_ack_message : public gvcp_ack_header
{
uint16_t versionMajor = 0;
uint16_t versionMinor = 0;
uint32_t deviceMode = 0;
uint16_t reserved = 0;
uint8_t macAddress[6] = { 0 };
uint32_t supportedIpConfigOptions = 0;
uint32_t ipConfigCurrent = 0;
uint8_t reserved2[12] = { 0 };
uint32_t ipAddress = 0;
uint8_t reserved3[12];
uint32_t subnetMask = 0;
uint8_t reserved4[12] = { 0 };
uint32_t defaultGateway = 0;
char manufacturerName[32] = { 0 };
char modelName[32] = { 0 };
char deviceVersion[32] = { 0 };
char manufacturerSpecificInformation[48] = { 0 };
char serialNumber[16] = { 0 };
char userDefinedName[16] = { 0 };
};
class GvcpLayer : public Layer
{
public:
GvcpLayer() = delete;
static bool isGvcpPort(uint16_t port);
static GvcpLayer* parseGvcpLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet);
// implement Layer's abstract methods
void parseNextLayer() override;
void computeCalculateFields() override;
OsiModelLayer getOsiModelLayer() const override;
};
class GvcpRequestLayer : public GvcpLayer
{
public:
GvcpRequestLayer() = delete;
static bool isDataValid(const uint8_t* data, size_t dataLen);
GvcpFlag getFlag() const;
void setFlag(const GvcpFlag flag);
GvcpCommand getCommand() const;
uint16_t getLength() const;
uint16_t getRequestId() const;
void setRequestId(uint16_t requestId);
};
class GvcpAckLayer : public GvcpLayer
{
public:
GvcpAckLayer() = delete;
static bool isDataValid(const uint8_t* data, size_t dataLen);
GvcpResponseStatus getStatus() const;
void setStatus(GvcpResponseStatus status);
GvcpCommand getCommand() const;
uint16_t getLength() const;
uint16_t getAckId() const;
void setAckId(uint16_t ackId);
};
class GvcpDiscoveryRequestLayer : public GvcpRequestLayer
{
public:
// constructors
GvcpDiscoveryRequestLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet);
GvcpDiscoveryRequestLayer();
// implement Layer's abstract methods
std::string toString() const override;
size_t getHeaderLen() const override;
};
class GvcpDiscoveryAckLayer : public GvcpAckLayer
{
// constructors
GvcpDiscoveryAckLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet);
GvcpDiscoveryAckLayer(/** a list of all params: uint16_t majorVersion, uint16_t minorVersion, uint32_t deviceMode, ...*/);
// getters and setters
std::pair<uint16_t, uint16_t> getVersion() const;
pcpp::MacAddress getMacAddress() const;
void setMacAddress(const pcpp::MacAddress macAddress);
// ...
// implement Layer's abstract methods
std::string toString() const override;
size_t getHeaderLen() const override;
};
class GvcpForceIpRequestLayer : public GvcpRequestLayer
{
// ...
};
class GvcpForceIpAcktLayer : public GvcpAckLayer
{
// ...
};|
@seladb Reading your sample code, basically I got your intention. So "Discovery" and "ForceIP" have their own layer, and for other commands, they will fall back to general |
Yes, other messages should fall under |
|
@seladb The code has been updated, could you take a look? |
|
@seladb ping. |
Sorry for the delay, I'll review it soon |
|
@tigercosmos I didn't review the entire PR because this comment wasn't addressed yet. Can you please address this and I'll review the rest of the PR? |
@seladb What do you mean not changed? I have changed all layers. Or do you mean putting the header definition inside the layer? I don't do that because the layer will be too big. |
|
|
||
| GvcpCommand getCommand() const | ||
| { | ||
| return static_cast<GvcpCommand>(netToHost16(command)); |
There was a problem hiding this comment.
I don't think it's a good practice to put this in the header file, I vote we don't forward declare and move the implementation to the cpp file
| GvcpRequestHeader() = default; | ||
|
|
||
| GvcpRequestHeader(GvcpFlag flag, GvcpCommand command, uint16_t dataSize, uint16_t requestId) | ||
| : flag(flag), command(hostToNet16(static_cast<uint16_t>(command))), dataSize(hostToNet16(dataSize)), | ||
| requestId(hostToNet16(requestId)) | ||
| {} |
There was a problem hiding this comment.
Why do we need c'tors for a struct? 🤔
What we usually do is declare all properties as public and then we don't need c'tors.
These structs are internal to the GVCP layers classes and shouldn't be exposed outside, so it doesn't matter if their properties are public.
If you want you can move these structs as protected inside of the GVCP layer classes
There was a problem hiding this comment.
I added a constructor for it because the byte order will change, and it's better to handle it by the constructor. In addition, GvcpRequestHeader is public in purpose here. Users are free to get the raw header if they want, so there is also a public function getGvcpHeader() in the layers. I don't think we should stop users from getting the header.
There was a problem hiding this comment.
I think I mentioned it in some comments, but I'm not sure.... In my opinion there is no need to expose both getGvcpHeader() and getters/setters for each property, that will create a somewhat confusing API.
In older protocols we used to only expose the "raw header" (like getGvcpHeader()), but in newer protocols we usually expose getters and setters which I think provides a cleaner API. We also got feedback from users that they prefer the getters/setters over the "raw header"
There was a problem hiding this comment.
Sure, let me not expose it.
There was a problem hiding this comment.
they are under internal now
| */ | ||
| GvcpCommand getCommand() const | ||
| { | ||
| return static_cast<GvcpCommand>(netToHost16(getGvcpHeader()->command)); |
There was a problem hiding this comment.
We can do:
return getGvcpHeader()->getCommand();| * @param[in] data A pointer to the data including the header and the payload | ||
| * @param[in] dataSize The size of the data in bytes | ||
| */ | ||
| GvcpAcknowledgeLayer(const uint8_t* data, size_t dataSize); |
There was a problem hiding this comment.
ditto: why do we need this c'tor if we have the c'tor below?
| GvcpAckHeader* getGvcpHeader() const | ||
| { | ||
| return reinterpret_cast<GvcpAckHeader*>(m_Data); // the header is at the beginning of the data | ||
| } |
There was a problem hiding this comment.
ditto: this method can be private/protected
| */ | ||
| GvcpResponseStatus getStatus() const | ||
| { | ||
| return static_cast<GvcpResponseStatus>((netToHost16(getGvcpHeader()->status))); |
There was a problem hiding this comment.
This method should handle cases where getGvcpHeader()->status is an unexpected value and return GvcpResponseStatus::Unknown
| */ | ||
| GvcpCommand getCommand() const | ||
| { | ||
| return static_cast<GvcpCommand>(netToHost16(getGvcpHeader()->command)); |
There was a problem hiding this comment.
ditto: we can do:
return getGvcpHeader()->getCommand();
|
@tigercosmos a gentle reminder to address the PR comments ☝️ |
Thanks. I will continue on this after October. Quite busy these days. |
|
Ops, I didn't notice this. Changed. |
|
@tigercosmos this PR is open for a long time, do you plan to continue working on it or should we close it? |
|
Let's close it. |
Rework of seladb#1357 on top of the current dev branch, addressing the open review comments: - Keep the wire structs plain (no constructors/methods) under pcpp::internal and move all byte-order handling to GvcpLayer.cpp. - Drop the raw-buffer copy constructors; layers are built either from a parsed packet or from typed arguments. - getHeaderLen() covers the whole GVCP message since GVCP is always the last layer; message data is exposed via getPayloadData(). - getCommand()/getStatus() map unexpected values to Unknown using a switch instead of a global unordered_set. - parseGvcpLayer() validates the data, passes prevLayer/packet to every layer it creates and UdpLayer falls back to PayloadLayer. - Use protocol type 65 (57 is taken by GTPv2). - Fix getVersion() byte order and the unbounded reads of the fixed-size string fields of the discovery acknowledge. - Add setters and typed constructors for Discovery and Force IP messages, and creation / edit / malformed-data tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
97a74a1 to
e52bfdc
Compare
Rework of seladb#1357 on top of the current dev branch, addressing the open review comments: - Keep the wire structs plain (no constructors/methods) under pcpp::internal and move all byte-order handling to GvcpLayer.cpp. - Drop the raw-buffer copy constructors; layers are built either from a parsed packet or from typed arguments. - getHeaderLen() covers the whole GVCP message since GVCP is always the last layer; message data is exposed via getPayloadData(). - getCommand()/getStatus() map unexpected values to Unknown using a switch instead of a global unordered_set. - parseGvcpLayer() validates the data, passes prevLayer/packet to every layer it creates and UdpLayer falls back to PayloadLayer. - Use protocol type 65 (57 is taken by GTPv2). - Fix getVersion() byte order and the unbounded reads of the fixed-size string fields of the discovery acknowledge. - Add setters and typed constructors for Discovery and Force IP messages, and creation / edit / malformed-data tests.
e52bfdc to
0ee3f5d
Compare
Dimi1010
left a comment
There was a problem hiding this comment.
LGTM code wise. Tho, I haven't gone in depth into the actual spec.
3c87bd8 to
5027426
Compare
|
@seladb All issues fixed. Please take a look, thanks |
Add parsing, creation and editing of GVCP messages on UDP port 3956. - GvcpRequestLayer and GvcpAcknowledgeLayer represent all messages. Discovery and Force IP messages have their own layer classes. - A layer holds the GVCP header and the message fields that its class knows. parseNextLayer() puts the rest of the message in a PayloadLayer, and computeCalculateFields() updates the data size. - The header and body structs are protected or private members of the layer classes. - The request and acknowledge constructors reject a value of the wrong kind. Command values are even, and acknowledge values are odd. - GvcpDiscoveryRequestLayer exposes the allow-broadcast-acknowledge flag. - The command and status enums are nested in GvcpLayer. They print by name, and values that the spec does not define map to Unknown. - UdpLayer falls back to PayloadLayer if the data is not valid GVCP. - The protocol type is 65, because GTPv2 uses 57.
5027426 to
010ba10
Compare
seladb
left a comment
There was a problem hiding this comment.
A few small comments, otherwise LGTM
| struct GvcpVersion | ||
| { | ||
| /// The version major number | ||
| uint16_t major; | ||
| /// The version minor number | ||
| uint16_t minor; | ||
| }; |
There was a problem hiding this comment.
Why did we keep it outside of the layer class? It can be a public struct inside GvcpDiscoveryAcknowledgeLayer
There was a problem hiding this comment.
Done, it's GvcpDiscoveryAcknowledgeLayer::GvcpVersion now.
|
|
||
| /// GVCP response status can be returned in an acknowledge message or a GVSP header. | ||
| /// See more in the spec "Table 19-1: List of Standard Status Codes" | ||
| enum class GvcpResponseStatus : uint16_t |
There was a problem hiding this comment.
This enum can move to GvcpAcknowledgeLayer
There was a problem hiding this comment.
Done, it's GvcpAcknowledgeLayer::GvcpResponseStatus now.
Move GvcpResponseStatus into GvcpAcknowledgeLayer and GvcpVersion into GvcpDiscoveryAcknowledgeLayer, the layers that own these fields.
There was a problem hiding this comment.
Before you merge, can you also implement serializeLayer()?
You can see several examples of how to implement it in this PR: #2277
If you don't have time feel free to merge this PR as is and I'll add it later.
There was a problem hiding this comment.
Done, I added serializeLayer() to all the GVCP layers.
Implement serializeLayer() for the GVCP layers. Each layer writes its fields after the fields of its parent class: the command and data size, then the request or acknowledge header fields, then the Discovery or Force IP fields. Command and status values are written as a number and as a name.
Related to #1321
This PR adds a layer for the GigE Vision Control Protocol (GVCP). The layer supports parsing, creation and editing of GVCP messages on UDP port 3956.
Design
GvcpRequestLayerandGvcpAcknowledgeLayerrepresent all messages. Discovery and Force IP messages have their own layer classes.parseNextLayer()puts the rest of the message in aPayloadLayer.computeCalculateFields()sets the data size field to the size of the data after the header.std::invalid_argumentfor a value of the wrong kind. Command values are even, and acknowledge values are odd.GvcpDiscoveryRequestLayerexposes the allow-broadcast-acknowledge flag.operator<<prints commands and statuses by name. Values that the spec does not define map toUnknown.UdpLayerfalls back toPayloadLayerif the data is not valid GVCP.65, because GTPv2 uses57.Tests
The tests cover parsing, creation, editing and malformed data.