Repository: secdev/scapy
Issue category: Bug Report

DoIP functional addressing responses not associated with preceding request
### Brief description

When sending a request over DoIP using functional addressing, the DoIP source addesses of the ECU responses will be different from the target address of the preceding request, as the ECUs respond from their actual address instead of the functional "broadcast address" used in the request. `DoIP.hashret` effectively filters out these "mismatched" packets, so when using `DoIPSocket.sr(...)` with a functional target address, no responses will ever be returned.

### Scapy version

2.6.0

### Python version

3.11

### Operating system

Linux 6.1.21-v8+

### Additional environment information

_No response_

### How to reproduce

```python3
from scapy.contrib.automotive.doip import *
from scapy.contrib.automotive.uds import *

ip="169.254.207.236"
sock = DoIPSocket(ip)

ans, unans = sock.sr(DoIP(payload_type=0x8001, source_address=0xe80, target_address=0xe400) / UDS() / UDS_TP(), multi=True, timeout=2)
```

### Actual result

```console
INFO: Routing activation successful! Target address set to: 0x3107
Begin emission:
Finished sending 1 packets.
.........
Received 9 packets, got 0 answers, remaining 1 packets
```

### Expected result

```console
INFO: Routing activation successful! Target address set to: 0x3107
Begin emission:
Finished sending 1 packets.
.********
Received 9 packets, got 8 answers, remaining 0 packets
```

This result was obtained by monkey-patching DoIP.hashret to ignore the addresses, based on a config setting, analogous to how scapy handles IP broadcast traffic with the `conf.checkIPaddr` setting (see below)

### Related resources

Just for reference, this is the monkey-patch I'm using at the moment to enable me to run functional addressing requests (by disabling the setting whenever I need it):

```python3
from scapy.config import conf
from scapy.contrib.automotive.doip import DoIP

def hashret_patched(self):
    if self.payload_type in [0x8001, 0x8002, 0x8003]:
        if conf.checkDoIPaddr:
            return bytes(self)[:2] + struct.pack(
                "H", self.target_address ^ self.source_address)
        return bytes(self)[:2]
    return bytes(self)[:2]

conf.checkDoIPaddr = True
DoIP.hashret = hashret_patched
```

But there is maybe/probably a better way.


Functions to edit:
scapy/contrib/automotive/doip.py:DoIP.hashret

Patch:
diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py
index b9d279bcf6b..3fcbdc5dad4 100644
--- a/scapy/contrib/automotive/doip.py
+++ b/scapy/contrib/automotive/doip.py
@@ -239,10 +239,7 @@ def answers(self, other):
 
     def hashret(self):
         # type: () -> bytes
-        if self.payload_type in [0x8001, 0x8002, 0x8003]:
-            return bytes(self)[:2] + struct.pack(
-                "H", self.target_address ^ self.source_address)
-        return bytes(self)[:2]
+        return bytes(self)[:3]
 
     def post_build(self, pkt, pay):
         # type: (bytes, bytes) -> bytes

Test patch: diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts
index 9a7d61e3b7d..b5963a0d817 100644
--- a/test/contrib/automotive/doip.uts
+++ b/test/contrib/automotive/doip.uts
@@ -10,6 +10,8 @@
 
 = Load Contrib Layer
 
+from test.testsocket import TestSocket, cleanup_testsockets, UnstableSocket
+
 load_contrib("automotive.doip", globals_dict=globals())
 load_contrib("automotive.uds", globals_dict=globals())
 
@@ -406,6 +408,30 @@ assert pkts[0][DoIP].payload_length == 2
 assert pkts[0][DoIP:2].payload_length == 7
 assert pkts[1][DoIP].payload_length == 103
 
+= Doip logical addressing
+
+filename = scapy_path("/test/pcaps/doip_functional_request.pcap.gz")
+tx_sock = TestSocket(DoIP)
+rx_sock = TestSocket(DoIP)
+tx_sock.pair(rx_sock)
+
+for pkt in PcapReader(filename):
+    if pkt.haslayer(DoIP):
+        tx_sock.send(pkt[DoIP])
+
+ans, unans = rx_sock.sr(DoIP(bytes(DoIP(payload_type=0x8001, source_address=0xe80, target_address=0xe400) / UDS() / UDS_TP())), multi=True, timeout=0.1, verbose=False)
+
+cleanup_testsockets()
+
+ans.summary()
+if unans:
+    unans.summary()
+
+assert len(ans) == 8
+ans.summary()
+assert len(unans) == 0
+
+
 + DoIP Communication tests
 
 = Load libraries
diff --git a/test/pcaps/doip_functional_request.pcap.gz b/test/pcaps/doip_functional_request.pcap.gz
new file mode 100644
index 00000000000..c2b9e9cf35f
Binary files /dev/null and b/test/pcaps/doip_functional_request.pcap.gz differ