Skip to content

Add Layer Build and Validation for DoIP (Diagnostic over IP) Support #1655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: dev
Choose a base branch
from

Conversation

raissi-oussema
Copy link

@raissi-oussema raissi-oussema commented Dec 7, 2024

DoIP Protocol Overview

The Diagnostic over IP (DoIP) protocol is used in automotive diagnostic systems to facilitate communication between diagnostic tools and ECUs (Electronic Control Units) over IP-based networks. It enables remote diagnostics, configuration, and software updates over Ethernet, offering an efficient and scalable solution for modern vehicles.

Header Structure (8 bytes)

protocol version (1 byte)
invert protocol version (1 byte)
payload type (2 bytes)
payload length (4 bytes)

Pyload types / code / structure

  • "Generic DOIP header Nack" (0x0000) (header + nackCode (1 byte : required))
  • "Vehicle identification request" (0x0001) (header)
  • "Vehicle identification request with EID" (0x0002) (header + EID (6 bytes : required))
  • "Vehicle identification request with VIN" (0x0003) (header + VIN (17 bytes : required))
  • "Vehicle announcement message" (0x0004) (header + VIN (17 bytes : required) + logical address (2bytes : required) + EID (6 bytes : required) + GID (6 bytes : required) + further action (1 bytes : required)+ sync status (1 bytes : optional)).
  • "Routing activation request" (0x0005) (header + source address (2 bytes : required) + activation type (1 byte:required) + reservedISO (4 bytes : required) + reservedOEM (4 bytes : optional)).
  • "Routing activation response" (0x0006) (header + tester adress(2 bytes : required) + response code (1 byte:required) + reservedISO (4 bytes : required) + reservedOEM (4 bytes : optional)).
  • "Alive check request" (0x0007) (header)
  • "Alive check response" (0x0008) (header + source address (2 byte : required))
  • "DOIP entity status request" (0x4001) (header)
  • "DOIP entity status response" (0x4002) (header + node type (1 byte : required) + max concurrent socket (1 byte : required) + currently opened sockets (1 byte : required) + max data size(4 bytes : optional))
  • "Diagnostic power mode request information" (0x4003) (header)
  • "Diagnostic power mode response information" (0x4004) (header + power mode (1 byte : required))
  • "Diagnostic message" (0x8001) (header + source address (2 bytes : required) + target address (2 bytes : required) + UDS message (n bytes : required ; n > 1))
  • "Diagnostic message Ack" (0x8002) (header + source address (2 bytes : required) + target address (2 bytes : required) + ack code (1 byte : required)) + previous message (n bytes : optional ; n>0))
  • "Diagnostic message Nack" (0x8003) (header + source address (2 bytes : required) + target address (2 bytes : required) + Nack code (1 byte : required)) + previous message (n bytes : optional ; n>0))

@Dimi1010
Copy link
Collaborator

Dimi1010 commented Dec 8, 2024

As per the contributing guidelines, please retarget the PR to the dev branch instead of the master.

@tigercosmos tigercosmos changed the base branch from master to dev December 8, 2024 09:37
@egecetin egecetin closed this Dec 8, 2024
@egecetin egecetin reopened this Dec 8, 2024
@egecetin egecetin linked an issue Dec 8, 2024 that may be closed by this pull request
@raissi-oussema
Copy link
Author

Observed several issues in the CI pipelines, likely due to missing definitions for htobe16 and other endianness conversion macros. All tests and pre-commit checks are passed in my linux machine.
Adding an import for "EndianPortable.h" in last PR is expected to resolve these problems.

Comment on lines 250 to 253
/**
* Diagnostic over IP protocol (DOIP)
*/
const ProtocolType DOIP = 38;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ID 38 is already taken by DHCPv6 protocol. The next available ID is 58, please add it after GTPv2

@@ -437,7 +437,6 @@ namespace pcpp

HttpResponseStatusCode() = default;

// cppcheck-suppress noExplicitConstructor
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removing this cppcheck-suppress comment?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cppcheck flags this as an incorrect suppression on my local machine. Let me know if you think I should revert it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interesting. Which version of CPPcheck are you using?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.7 as recommanded in CONTRIBUTING.md and still flags incorrect suppression, I'm keeping this changes so can I commit my recents updates,

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why update this file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing as my previous comment, I only removed some suppress-checks detected as incorrect

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add one pcap file with all of these packets?

Copy link
Author

@raissi-oussema raissi-oussema Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes sure, I'll revert suppress-checks and add pcap file containig all tested packets in next PR, also try to cover some missing checks based on codecov feedback

Copy link

codecov bot commented Dec 10, 2024

Codecov Report

Attention: Patch coverage is 96.72055% with 71 lines in your changes missing coverage. Please review.

Project coverage is 83.66%. Comparing base (74322eb) to head (a274d70).
Report is 6 commits behind head on dev.

Files with missing lines Patch % Lines
Packet++/src/DoIpLayer.cpp 94.42% 24 Missing and 32 partials ⚠️
Packet++/header/DoIpLayer.h 92.34% 9 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1655      +/-   ##
==========================================
+ Coverage   83.10%   83.66%   +0.55%     
==========================================
  Files         283      286       +3     
  Lines       48929    51108    +2179     
  Branches    10303    10988     +685     
==========================================
+ Hits        40664    42759    +2095     
- Misses       7113     7177      +64     
- Partials     1152     1172      +20     
Flag Coverage Δ
alpine320 75.67% <90.17%> (+0.60%) ⬆️
fedora40 ?
fedora42 75.78% <90.09%> (?)
macos-13 81.15% <93.73%> (+0.54%) ⬆️
macos-14 81.15% <93.73%> (+0.54%) ⬆️
macos-15 81.13% <93.73%> (+0.55%) ⬆️
mingw32 70.95% <76.07%> (+0.19%) ⬆️
mingw64 70.92% <76.17%> (+0.20%) ⬆️
npcap 85.39% <92.82%> (+0.32%) ⬆️
rhel94 75.54% <90.07%> (+0.56%) ⬆️
ubuntu2004 58.59% <59.38%> (+0.03%) ⬆️
ubuntu2004-zstd 58.70% <59.38%> (+0.03%) ⬆️
ubuntu2204 75.47% <90.12%> (+0.57%) ⬆️
ubuntu2204-icpx 61.45% <60.68%> (-0.05%) ⬇️
ubuntu2404 75.74% <90.18%> (+0.59%) ⬆️
ubuntu2404-arm64 75.71% <90.18%> (?)
unittest 83.66% <96.72%> (+0.55%) ⬆️
windows-2019 85.41% <92.82%> (+0.32%) ⬆️
windows-2022 85.44% <92.82%> (+0.32%) ⬆️
winpcap 85.56% <92.82%> (+0.30%) ⬆️
xdp 52.07% <90.12%> (+1.50%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@seladb
Copy link
Owner

seladb commented Jan 3, 2025

@raissi-oussema are you planning to continue working on this PR?

@raissi-oussema
Copy link
Author

Hi, I was engaged with other tasks, but I plan to get back to this PR soon. Thanks for your understanding!

raissi-oussema and others added 2 commits January 6, 2025 01:52
.improve  maps searchs for doipEnumsToStrings
.cover more uses cases based on codecov feedback
@raissi-oussema
Copy link
Author

Design suggestions or code improvements are always welcome and greatly appreciated.

@seladb
Copy link
Owner

seladb commented Jan 8, 2025

@raissi-oussema to make it easier to review, do you think you can add some documentation on the DoIP protocol to the PR body?

It'd mostly be helpful to get more details on the header structure and different possible message

@raissi-oussema
Copy link
Author

@seladb I need support for CI pipelines, I can't figure out why are they still failing. And a clear documentation was successfully added to PR body to make it easier for you to start the code review.

@Dimi1010
Copy link
Collaborator

Dimi1010 commented Jan 12, 2025

@seladb I need support for CI pipelines, I can't figure out why are they still failing. And a clear documentation was successfully added to PR body to make it easier for you to start the code review.

Doxigen pipeline:
/__w/PcapPlusPlus/PcapPlusPlus/Packet++/header/DoIpLayer.h:42: error: Compound pcpp::DoIpLayer is not documented. (warning treated as error, aborting now)

XDP pipeline:
That is an issue with the CI image. Merge latest changes from dev branch and it should be fine.

VS pipeline:
Tbh, no idea om that one. It seems the opencoverage download link stopped working for a bit or something.

@raissi-oussema
Copy link
Author

@seladb I need support for CI pipelines, I can't figure out why are they still failing. And a clear documentation was successfully added to PR body to make it easier for you to start the code review.

Doxigen pipeline: /__w/PcapPlusPlus/PcapPlusPlus/Packet++/header/DoIpLayer.h:42: error: Compound pcpp::DoIpLayer is not documented. (warning treated as error, aborting now)

XDP pipeline: That is an issue with the CI image. Merge latest changes from dev branch and it should be fine.

VS pipeline: Tbh, no idea om that one. It seems the opencoverage download link stopped working for a bit or something.

What could be the problem for dioxygen pipeline, doipLayer.h is well documented [line 42] ?

// implement abstract methods

/**
* TODO, parse UDS layer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for another PR? If so, should the remaining data be parsed as a generic payload layer for now?

Copy link
Author

@raissi-oussema raissi-oussema Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_NextLayer is intended to be the UDS layer, which has not been implemented yet. In the future, as more knowledge is gained, either I or another contributor may add this functionality. For now, I suggest parsing it as a generic layer, as you mentioned.
PS: the nextLayer will be parsed only when the payloadType is 0x8001.

Copy link
Author

@raissi-oussema raissi-oussema Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your feedback is highly appertiated, what do you think about adding this snippet of code:

void  parseNextLayer() override
		{
			if (getPayloadType() == DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE)
			{
				size_t headerLen = sizeof(doiphdr);

				if (m_DataLen <= headerLen + 2 /*source address size*/ + 2 /*target address size*/)
					return;

				uint8_t* payload = m_Data + (headerLen + 2 + 2);
				size_t payloadLen = m_DataLen - (headerLen + 2 + 2);
				m_NextLayer = new PayloadLayer(payload, payloadLen, this, m_Packet);
				return;
			}
		}`

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your feedback is highly appertiated, what do you think about adding this snippet of code:

void  parseNextLayer() override
		{
			if (getPayloadType() == DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE)
			{
				size_t headerLen = sizeof(doiphdr);

				if (m_DataLen <= headerLen + 2 /*source address size*/ + 2 /*target address size*/)
					return;

				uint8_t* payload = m_Data + (headerLen + 2 + 2);
				size_t payloadLen = m_DataLen - (headerLen + 2 + 2);
				m_NextLayer = new PayloadLayer(payload, payloadLen, this, m_Packet);
				return;
			}
		}`

Seems good. Can the source address and target address used in that type of message be accessed from this layer? Since we are excluding it from the generic payload.

A minor tip, having headerLen be marked as constexpr might allow some compiler optimizations (such as the arithmetic using it + a literal being computed during compilation and hardcoded if possible).

Also why the return statement that is at the end of the block anyway?

Copy link
Author

@raissi-oussema raissi-oussema Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dimi1010 , this approach appears both safer and cleaner. It allows us to construct a DiagnosticMessage directly from the current layer, providing direct access to the diagnostic data. Using this data, we can then build a generic PayloadLayer.

void parseNextLayer() override
{
	DiagnosticMessageData diagnosticMessage;

	if (diagnosticMessage.buildFromLayer(this))
	{
		m_NextLayer = new PayloadLayer(diagnosticMessage.diagnosticData.data(),
				                             diagnosticMessage.diagnosticData.size(), this, m_Packet);
	}
}

buildFromLayer safely parses the current layer and verifies whether it represents a valid diagnostic message.
what do you think ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but my question stands. DiagnosticMessageData has two other members (sourceAddress and targetAddress) which at the moment I don't see how the user can access them easily. They have neither accessors in the current DoIPLayer or are included as part of the UDSLayer (currently PayloadLayer).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceAddress and targetAddress are public members of DiagnosticMessageData class just like diagnosticData and they are not part of the UDS layer:

uint16_t sourceAddress;              /**< Source address of the message. */
uint16_t targetAddress;              /**< Target address for the diagnostic message. */
std::vector<uint8_t> diagnosticData; /**< Diagnostic message data with dynamic length. */

the user can access these fields by the method buildFromLayer .
I've made this dummy function just to show how to access to these fields :

void DoIpLayer::resolveDiagMessageFields()
	{
		DiagnosticMessageData diagnosticMessage;
		if (diagnosticMessage.buildFromLayer(this))
		{
			uint16_t srcAddr = diagnosticMessage.sourceAddress;
			uint16_t targetAddr = diagnosticMessage.targetAddress;
			std::vector<uint8_t> diagData = diagnosticMessage.diagnosticData;
		}
	}

Do you think this implementation need more improvement ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if i am understanding correctly the sequence is this?

  1. User somehow receives a DoIPLayer from a Packet
  2. User checks the payload type (via DoIPLayer::getPayloadType())
  3. Depending on the payload type user uses T::buildFromLayer(DoIPLayer) (T being the corresponding message struct) to populate the data from the layer into the struct.

Am I understanding the flow correctly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are absolutely correct.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that works fine. 👍

Copy link
Owner

@seladb seladb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a few more comments, please address them and I'll do another round of reviews

Comment on lines 224 to 229
DiagnosticMessageData diagnosticMessage;
if (diagnosticMessage.buildFromLayer(*this))
{
// handle UDS layer as generic PayloadLayer for now.
m_NextLayer = new PayloadLayer(diagnosticMessage.diagnosticData.data(),
diagnosticMessage.diagnosticData.size(), this, m_Packet);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseNextLayer shouldn't generate new layers, it should just parse them if they exist. In this case since the next layer is always a generic PayloadLayer, it should get the data beyond the header

Copy link
Author

@raissi-oussema raissi-oussema Mar 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what we're doing here: creating a diagnostic message is to validate if we are dealing with a valid diagnostic message using buildFromLayer, data then are passed generic PayloadLayer. The only case where we need this nextLayer is when the payload type is a diagnostic message
you can refer to this comment for more details.

Copy link
Owner

@seladb seladb Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if diagnosticMessage.buildFromLayer() returns false? We should still parse the rest of the data which is also a PayloadLayer. Since we don't parse a more specific layer, using DiagnosticMessageData is not needed here

Copy link
Author

@raissi-oussema raissi-oussema Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If diagnosticMessage.buildFromLayer() returns false, it means there is no UDS layer and no need to parse any additional data and m_NextLayer would remain unset.
So if I understand you correctly, you’re suggesting that we should always generate a PayloadLayer containing the data beyond the header, regardless of the payload type?
If that’s the case, the UDS layer should only be represented when the payload type is DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE.
I'm not entirely sure whether defining m_NextLayer for all payload types is necessary. Could this lead to any misinterpretation or incorrect handling of certain payload types?

For example, if we have a DoIP message with the type DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_NEG_ACK and m_NextLayer is not nullptr, it would be misleading since this type does not have any additional layers (DoIP is the final layer like all other type except DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE ).
If I’ve misunderstood how parseNextLayer works, I'm open to discuss it further.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way packet parsing works is that a layer should always parse the next layer if there's additional data left in the buffer. If the layer knows what the next layer should be - it should create that layer, otherwise it should create a PayloadLayer which is another way of saying "there is some data left in the buffer but I don't know what it is".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was not yet addressed - parseNextLayer should always create a layer (PayloadLayer or other) from the remaining data

Copy link
Author

@raissi-oussema raissi-oussema Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My last update was :

	void DoIpLayer::parseNextLayer()
	{
		if (getPayloadType() == DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE)
		{
			size_t headerLen = sizeof(doiphdr);

			if (m_DataLen <= headerLen + 2 /*source address size*/ + 2 /*target address size*/)
				return;

			uint8_t* payload = m_Data + (headerLen + 2 + 2);
			size_t payloadLen = m_DataLen - (headerLen + 2 + 2);
			m_NextLayer = new PayloadLayer(payload, payloadLen, this, m_Packet);
		}
	}

like first implementation with @Dimi1010 ,
the only case that we have remaining data to be parsed is when we have this specific type DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE, else we are dealing with an invalid doip packet,
@seladb , @Dimi1010 , what do you think ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to provide a corrected version of the code if you think I missed anything.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raissi-oussema just to make sure I understand: if the message isn't of type DoIpPayloadTypes::DIAGNOSTIC_MESSAGE_TYPE) then there is no layer after the DoIP message (meaning the DoIP message is the last message in the packet)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seladb, yes absolutely

@raissi-oussema raissi-oussema requested a review from seladb March 30, 2025 02:32
raissi-oussema and others added 4 commits March 30, 2025 04:59
. Move DoIpEnumToStringPayloadType, DoIpEnumToStringProtocolVersion maps to DoIpLayer.cpp
. Remove serializeData (...) funct and use memcpy
. Make buildLayer private
. Improve getPayloadType() func
. Add test for invalid paylaodType
. Reduce toString() presentation
Comment on lines 224 to 229
DiagnosticMessageData diagnosticMessage;
if (diagnosticMessage.buildFromLayer(*this))
{
// handle UDS layer as generic PayloadLayer for now.
m_NextLayer = new PayloadLayer(diagnosticMessage.diagnosticData.data(),
diagnosticMessage.diagnosticData.size(), this, m_Packet);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was not yet addressed - parseNextLayer should always create a layer (PayloadLayer or other) from the remaining data

@raissi-oussema
Copy link
Author

@Dimi1010 , could you please support, what could be the reason of failed pipeline here ?

@Dimi1010
Copy link
Collaborator

Dimi1010 commented Apr 9, 2025

@Dimi1010 , could you please support, what could be the reason of failed pipeline here ?

from the doxygen pipeline

/__w/PcapPlusPlus/PcapPlusPlus/Packet++/header/DoIpLayerData.h:1091: warning: Compound pcpp::DiagnosticPowerModeRequestData is not documented.
/__w/PcapPlusPlus/PcapPlusPlus/Packet++/header/DoIpLayerData.h:1066: warning: Compound pcpp::EntityStatusRequestData is not documented.
/__w/PcapPlusPlus/PcapPlusPlus/Packet++/header/DoIpLayerData.h:1116: warning: Compound pcpp::VehicleIdentificationRequestData is not documented.

@raissi-oussema raissi-oussema requested a review from seladb April 10, 2025 12:38
/// - `activationType`: The type of activation requested.
/// - `reservedIso`: Reserved bytes as defined by ISO specifications.
/// - `reservedOem`: Reserved bytes as defined by OEM specifications, only if present.
bool buildFromLayer(const DoIpLayer& doipLayer);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started reviewing DoIpLayerData.h, and one of the first things I looked for was how a user can get the DoIP data (meaning one of these classes that inherit from IDoIpMessageData). From what I understand the user needs to do something like this:

pcpp::DoIpLayer* doipLayer = myPacket.getLayerOfType<pcpp::DoIpLayer>();

if (doipLayer->getPayloadType() == pcpp::DoIpPayloadTypes::GENERIC_HEADER_NEG_ACK)
{
	pcpp::GenericHeaderNackData data;
	if (!data.buildFromLayer(*doipLayer))
	{
		throw std::invalid_argument("Invalid GENERIC_HEADER_NEG_ACK message");
	}

	// do something with `data`...
}
else
{
	// this is not a GENERIC_HEADER_NEG_ACK message
}

While this approach can work, I think it's not very clear for users. My understanding is that a DoIP message has the following structure:

  • 8 first bytes: fixed header of type doiphdr
  • Additional payload with size of 0 or more, according to the nessagetype
  • In some messages (probably only Diagnostic message (0x8001)?) - there might be additional bytes that we can consider another PayloadLayer

What I suggest is a different structure that is a bit similar to what we do in BgpLayer:

class DoIpLayer : Layer
{
	// should have a protected constructor - this class is abstract and cannot be instantiated

	// contains most of the getters/setters it currently has

	// Takes raw data and creates one of the child classes of DoIpLayer, according to the payload type
	static DoIpLayer * parseDoIpLayer(uint8_t* data, size_t dataLen, Layer* prevLayer, Packet* packet);
}

class RoutingActivationRequest : public DoIpLayer
{
	// Should have a public constructor that takes the relevant params and creates the layer fields (sourceAddress, activationType, etc...)

	// getters and setters
	uint16_t getSourceAddress();
	void setSourceAddress(uint16_t value);

	DoIpActivationTypes getActivationType();
	void setActivationType(DoIpActivationTypes activationType);

	...
}

class RoutingActivationResponse: public DoIpLayer
{
	// Should have a public constructor that takes the relevant params and creates the layer fields (sourceAddress, respondCode, etc...)

	// getters and setters
	uint16_t getSourceAddress();
	void setSourceAddress(uint16_t value);

	DoIpRoutingResponseCodes getResponseCode();
	void setResponseCode(DoIpRoutingResponseCodes responseCode);

	...
}

class GenericHeaderNack: public DoIpLayer
{
	// Should have a public constructor that takes the relevant params and creates the layer fields (genericNackCode)

	// getters and setters
	DoIpGenericHeaderNackCodes getGenericNackCode();
	void setGenericNackCode(DoIpGenericHeaderNackCodes code);
}

// do the same for the other classes that currently inherit from `IDoIpMessageData`
...
...

// in UdpLayer.cpp

void UdpLayer::parseNextLayer()
{
	...
	else if ((DoIpLayer::isDoIpPort(portSrc) || DoIpLayer::isDoIpPort(portDst)) &&
	         (DoIpLayer::isDataValid(udpData, udpDataLen)))
		m_NextLayer = DoIpLayer::parseDoIpLayer(udpData, udpDataLen, this, m_Packet);
	...
}

If you do that the user can use the DoIP layer as follows:

auto genericHeaderNack = myPacket.getLayerOfType<pcpp::GenericHeaderNack>();
if (genericHeaderNack != nullptr)
{
	// do something with `genericHeaderNack`...
}

I think this is a simpler implementation of the layer which is simpler for users and is more aligned with other layers we already have (like BgpLayer, but there are others too).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sure, I'll proceed for the new solution if you feel it is more aligned with other layers implementation.
So all classes in doipLayerData.cpp will be modified as suggested and moved to doipLayer.cpp file.
Is that correct ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think we can move all classes and enums to DoIpLayer.h and DoIpLayer.cpp and remove DoIpLayerData.h and DoIpLayerData.cpp

@raissi-oussema
Copy link
Author

@seladb , @Dimi1010 , refactoring is done, with more improvements, could you please check and provide feedbacks.

raissi-oussema and others added 3 commits April 19, 2025 14:23
.Remove doipLayerData.cpp and doipLayerData.h
.Move all enum classes to doipLayer.h
.Move string maps to doipLayer.cpp
.Change desidgn structure to :
.All derived classes for all doipPayloadTypes are inherited from doipLayer itself witch is an abstract interface
with protected constructor
.Change isLayerDataValid, isProtocolVersionValid, isPayloadTypeValid, to a static func to validate layer from
.Make getters and setters for all derived classes and improve dissecting and crafting logic.
.Enhance logging errors
@seladb
Copy link
Owner

seladb commented Apr 19, 2025

@raissi-oussema I triggered CI and some tests fail, can you please check?

Comment on lines 16 to 29
/// @brief Length of the External Identifier (EID) field.
#define DOIP_EID_LEN 6

/// @brief Length of the Group Identifier (GID) field.
#define DOIP_GID_LEN 6

/// @brief Length of the Vehicle Identification Number (VIN) field.
#define DOIP_VIN_LEN 17

/// @brief Length of the Reserved ISO field.
#define DOIP_RESERVED_ISO_LEN 4

/// @brief Length of the Reserved OEM field.
#define DOIP_RESERVED_OEM_LEN 4
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we use constexpr here instead of macros?

NB: Might need to be inline constexpr since they are in header.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dimi1010 ,I skimmed through this topic and noticed that inline constexpr is only available since C++17.

Comment on lines 675 to 690
DoIpProtocolVersion parsedVersion = static_cast<DoIpProtocolVersion>(version);

switch (parsedVersion)
{
case DoIpProtocolVersion::DefaultVersion:
case DoIpProtocolVersion::ReservedVersion:
case DoIpProtocolVersion::Version01Iso2010:
case DoIpProtocolVersion::Version02Iso2012:
case DoIpProtocolVersion::Version03Iso2019:
case DoIpProtocolVersion::Version04Iso2019_AMD1:
if (parsedVersion == DoIpProtocolVersion::UnknownVersion ||
parsedVersion == DoIpProtocolVersion::ReservedVersion ||
(parsedVersion == DoIpProtocolVersion::DefaultVersion &&
type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_VIN &&
type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_EID &&
type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: parsedVersion can't be UnknownVersion in the if statement. There is no case for it.

Nit: I feel like it would be cleaner to have the reserved version fail in the reserved case, instead of combining all the tests in one big if statement.

Also, having the default version case after the reserved case in the switch order can simplify the if by checking only for the type inside the DefaultVersion case and letting the control flow fall through to the following cases if the type is valid.

(Unsure if we have warnings for fall though cases tho, tho)

Example:

case ReservedVersion:
{
  // Error message here.
  return false;
}
case DefaultVersion:
{
  if(type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_VIN &&
			     type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_EID &&
			     type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST)
  {
     // Error message here.
     return false;
  }

  // Notice no break statement. Control flow falls through.
}
case ...:
case ...:
case ...:
  /* Rest of the parsing code */ 

Copy link
Author

@raissi-oussema raissi-oussema Apr 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree with you, I have enhanced the check to :

inline bool DoIpLayer::isProtocolVersionValid(uint8_t version, uint8_t inVersion, DoIpPayloadTypes type)
	{
		DoIpProtocolVersion parsedVersion = static_cast<DoIpProtocolVersion>(version);

		switch (parsedVersion)
		{
			case DoIpProtocolVersion::ReservedVersion:
			{
				PCPP_LOG_ERROR("[Malformed doip packet]: Reserved ISO DoIP protocol version detected: 0x"
								<< std::hex << static_cast<int>(version));
				return false;
			}
			case DoIpProtocolVersion::DefaultVersion:
				if (type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_VIN &&
						type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST_WITH_EID &&
						type != DoIpPayloadTypes::VEHICLE_IDENTIFICATION_REQUEST)
						{
							PCPP_LOG_ERROR("[Malformed doip packet]: Invalid/unsupported DoIP version!");
							return false;
						}
			case DoIpProtocolVersion::Version01Iso2010:
			case DoIpProtocolVersion::Version02Iso2012:
			case DoIpProtocolVersion::Version03Iso2019:
			case DoIpProtocolVersion::Version04Iso2019_AMD1:
			{
				if (version != static_cast<uint8_t>(~inVersion))
				{
					PCPP_LOG_ERROR("[Malformed doip packet]: Protocol version and inverse version mismatch! Version: 0x"
								<< std::hex << static_cast<int>(version) << ", Inverted: 0x"
								<< static_cast<int>(inVersion));
					return false;
				}
				return true;
			}
			default:
				PCPP_LOG_ERROR("[Malformed doip packet]: Unknown DoIP protocol version: 0x" << std::hex
																							<< static_cast<int>(version));
				return false;
		}
	}

looks much cleaner.

@raissi-oussema
Copy link
Author

I'm about trying to modify some other parsing logic using some structs to store fields like in announcement, routing request... and remove private vars.
I'll try to share solution in 2 days.
@seladb , @Dimi1010

@raissi-oussema
Copy link
Author

@seladb, could you please have a look, I've made serval changes in this current version and layer now is more aligned with other layer implementation, you can start reviewing this PR,
Waiting for your feedbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

support of doip (diagnostic over IP protocol)
5 participants