Skip to content

Commit 8cea357

Browse files
authored
Add SSL support for DoIP sockets (#4327)
* Add SSL functionality to DoIP sockets * Cleanup DoIP Sockets to not have tons of different objects
1 parent 19eeee5 commit 8cea357

File tree

2 files changed

+491
-65
lines changed

2 files changed

+491
-65
lines changed

scapy/contrib/automotive/doip.py

+178-65
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import struct
1212
import socket
1313
import time
14+
import ssl
1415

1516
from scapy.contrib.automotive import log_automotive
1617
from scapy.fields import (
@@ -27,7 +28,7 @@
2728
XStrField,
2829
)
2930
from scapy.packet import Packet, bind_layers, bind_bottom_up
30-
from scapy.supersocket import StreamSocket
31+
from scapy.supersocket import StreamSocket, SSLStreamSocket
3132
from scapy.layers.inet import TCP, UDP
3233
from scapy.contrib.automotive.uds import UDS
3334
from scapy.data import MTU
@@ -39,6 +40,7 @@
3940
Optional,
4041
)
4142

43+
4244
# ISO 13400-2 sect 9.2
4345

4446

@@ -247,8 +249,8 @@ def post_build(self, pkt, pay):
247249
This will set the Field 'payload_length' to the correct value.
248250
"""
249251
if self.payload_length is None:
250-
pkt = pkt[:4] + struct.pack("!I", len(pay) + len(pkt) - 8) + \
251-
pkt[8:]
252+
pkt = pkt[:4] + struct.pack(
253+
"!I", len(pay) + len(pkt) - 8) + pkt[8:]
252254
return pkt + pay
253255

254256
def extract_padding(self, s):
@@ -259,13 +261,24 @@ def extract_padding(self, s):
259261
return b"", None
260262

261263

262-
class DoIPSocket(StreamSocket):
263-
""" Custom StreamSocket for DoIP communication. This sockets automatically
264-
sends a routing activation request as soon as a TCP connection is
264+
bind_bottom_up(UDP, DoIP, sport=13400)
265+
bind_bottom_up(UDP, DoIP, dport=13400)
266+
bind_layers(UDP, DoIP, sport=13400, dport=13400)
267+
268+
bind_layers(TCP, DoIP, sport=13400)
269+
bind_layers(TCP, DoIP, dport=13400)
270+
271+
bind_layers(DoIP, UDS, payload_type=0x8001)
272+
273+
274+
class DoIPSocket(SSLStreamSocket):
275+
"""Socket for DoIP communication. This sockets automatically
276+
sends a routing activation request as soon as a TCP or TLS connection is
265277
established.
266278
267279
:param ip: IP address of destination
268280
:param port: destination port, usually 13400
281+
:param tls_port: destination port for TLS connection, usually 3496
269282
:param activate_routing: If true, routing activation request is
270283
automatically sent
271284
:param source_address: DoIP source address
@@ -275,84 +288,158 @@ class DoIPSocket(StreamSocket):
275288
the routing activation request
276289
:param reserved_oem: Optional parameter to set value for reserved_oem field
277290
of routing activation request
291+
:param force_tls: Skip establishing of a TCP connection and directly try to
292+
connect via SSL/TLS
293+
:param context: Optional ssl.SSLContext object for initialization of ssl socket
294+
connections.
278295
279296
Example:
280297
>>> socket = DoIPSocket("169.254.0.131")
281298
>>> pkt = DoIP(payload_type=0x8001, source_address=0xe80, target_address=0x1000) / UDS() / UDS_RDBI(identifiers=[0x1000])
282299
>>> resp = socket.sr1(pkt, timeout=1)
283300
""" # noqa: E501
284-
def __init__(self, ip='127.0.0.1', port=13400, activate_routing=True,
285-
source_address=0xe80, target_address=0,
286-
activation_type=0, reserved_oem=b""):
287-
# type: (str, int, bool, int, int, int, bytes) -> None
301+
302+
def __init__(self,
303+
ip='127.0.0.1', # type: str
304+
port=13400, # type: int
305+
tls_port=3496, # type: int
306+
activate_routing=True, # type: bool
307+
source_address=0xe80, # type: int
308+
target_address=0, # type: int
309+
activation_type=0, # type: int
310+
reserved_oem=b"", # type: bytes
311+
force_tls=False, # type: bool
312+
context=None # type: Optional[ssl.SSLContext]
313+
): # type: (...) -> None
288314
self.ip = ip
289315
self.port = port
316+
self.tls_port = tls_port
317+
self.activate_routing = activate_routing
290318
self.source_address = source_address
319+
self.target_address = target_address
320+
self.activation_type = activation_type
321+
self.reserved_oem = reserved_oem
291322
self.buffer = b""
292-
self._init_socket()
293-
294-
if activate_routing:
295-
self._activate_routing(
296-
source_address, target_address, activation_type, reserved_oem)
323+
self.force_tls = force_tls
324+
self.context = context
325+
try:
326+
self._init_socket(socket.AF_INET)
327+
except Exception:
328+
self.close()
329+
raise
297330

298331
def recv(self, x=MTU, **kwargs):
299332
# type: (Optional[int], **Any) -> Optional[Packet]
300-
if self.buffer:
301-
len_data = self.buffer[:8]
302-
else:
303-
len_data = self.ins.recv(8, socket.MSG_PEEK)
304-
if len(len_data) != 8:
305-
return None
333+
if len(self.buffer) < 8:
334+
self.buffer += self.ins.recv(8)
335+
if len(self.buffer) < 8:
336+
return None
337+
len_data = self.buffer[:8]
306338

307339
len_int = struct.unpack(">I", len_data[4:8])[0]
308340
len_int += 8
309-
self.buffer += self.ins.recv(len_int - len(self.buffer))
310341

311-
if len(self.buffer) != len_int:
342+
self.buffer += self.ins.recv(len_int - len(self.buffer))
343+
if len(self.buffer) < len_int:
312344
return None
345+
pktbuf = self.buffer[:len_int]
346+
self.buffer = self.buffer[len_int:]
313347

314-
pkt = self.basecls(self.buffer, **kwargs) # type: Packet
315-
self.buffer = b""
348+
pkt = self.basecls(pktbuf, **kwargs) # type: Packet
316349
return pkt
317350

318351
def _init_socket(self, sock_family=socket.AF_INET):
319352
# type: (int) -> None
353+
connected = False
320354
s = socket.socket(sock_family, socket.SOCK_STREAM)
355+
s.settimeout(5)
321356
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
322357
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
323-
addrinfo = socket.getaddrinfo(self.ip, self.port, proto=socket.IPPROTO_TCP)
324-
s.connect(addrinfo[0][-1])
325-
StreamSocket.__init__(self, s, DoIP)
326-
327-
def _activate_routing(self,
328-
source_address, # type: int
329-
target_address, # type: int
330-
activation_type, # type: int
331-
reserved_oem=b"" # type: bytes
332-
): # type: (...) -> None
358+
359+
if not self.force_tls:
360+
addrinfo = socket.getaddrinfo(self.ip, self.port, proto=socket.IPPROTO_TCP)
361+
s.connect(addrinfo[0][-1])
362+
connected = True
363+
SSLStreamSocket.__init__(self, s, DoIP)
364+
365+
if not self.activate_routing:
366+
return
367+
368+
activation_return = self._activate_routing()
369+
else:
370+
# Let's overwrite activation_return to force TLS Connection
371+
activation_return = 0x07
372+
373+
if activation_return == 0x10:
374+
# Routing successfully activated.
375+
return
376+
elif activation_return == 0x07:
377+
# Routing activation denied because the specified activation
378+
# type requires a secure TLS TCP_DATA socket.
379+
if self.context is None:
380+
raise ValueError("SSLContext 'context' can not be None")
381+
if connected:
382+
s.close()
383+
s = socket.socket(sock_family, socket.SOCK_STREAM)
384+
s.settimeout(5)
385+
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
386+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
387+
388+
ss = self.context.wrap_socket(s)
389+
addrinfo = socket.getaddrinfo(
390+
self.ip, self.tls_port, proto=socket.IPPROTO_TCP)
391+
ss.connect(addrinfo[0][-1])
392+
SSLStreamSocket.__init__(self, ss, DoIP)
393+
394+
if not self.activate_routing:
395+
return
396+
397+
activation_return = self._activate_routing()
398+
if activation_return == 0x10:
399+
# Routing successfully activated.
400+
return
401+
else:
402+
raise Exception(
403+
"DoIPSocket activate_routing failed with "
404+
"routing_activation_response 0x%x" % activation_return)
405+
406+
elif activation_return == -1:
407+
raise Exception("DoIPSocket._activate_routing failed")
408+
else:
409+
raise Exception(
410+
"DoIPSocket activate_routing failed with "
411+
"routing_activation_response 0x%x!" % activation_return)
412+
413+
def _activate_routing(self): # type: (...) -> int
333414
resp = self.sr1(
334-
DoIP(payload_type=0x5, activation_type=activation_type,
335-
source_address=source_address, reserved_oem=reserved_oem),
415+
DoIP(payload_type=0x5, activation_type=self.activation_type,
416+
source_address=self.source_address, reserved_oem=self.reserved_oem),
336417
verbose=False, timeout=1)
337418
if resp and resp.payload_type == 0x6 and \
338419
resp.routing_activation_response == 0x10:
339-
self.target_address = target_address or \
340-
resp.logical_address_doip_entity
420+
self.target_address = (
421+
self.target_address or resp.logical_address_doip_entity)
341422
log_automotive.info(
342423
"Routing activation successful! Target address set to: 0x%x",
343424
self.target_address)
344425
else:
345426
log_automotive.error(
346427
"Routing activation failed! Response: %s", repr(resp))
347428

429+
if resp and resp.payload_type == 0x6:
430+
return resp.routing_activation_response
431+
else:
432+
return -1
433+
348434

349435
class DoIPSocket6(DoIPSocket):
350-
""" Custom StreamSocket for DoIP communication over IPv6.
351-
This sockets automatically sends a routing activation request as soon as
352-
a TCP connection is established.
436+
"""Socket for DoIP communication. This sockets automatically
437+
sends a routing activation request as soon as a TCP or TLS connection is
438+
established.
353439
354440
:param ip: IPv6 address of destination
355441
:param port: destination port, usually 13400
442+
:param tls_port: destination port for TLS connection, usually 3496
356443
:param activate_routing: If true, routing activation request is
357444
automatically sent
358445
:param source_address: DoIP source address
@@ -362,29 +449,49 @@ class DoIPSocket6(DoIPSocket):
362449
the routing activation request
363450
:param reserved_oem: Optional parameter to set value for reserved_oem field
364451
of routing activation request
452+
:param force_tls: Skip establishing of a TCP connection and directly try to
453+
connect via SSL/TLS
454+
:param context: Optional ssl.SSLContext object for initialization of ssl socket
455+
connections.
365456
366457
Example:
367458
>>> socket = DoIPSocket6("2001:16b8:3f0e:2f00:21a:37ff:febf:edb9")
368459
>>> socket_link_local = DoIPSocket6("fe80::30e8:80ff:fe07:6d43%eth1")
369460
>>> pkt = DoIP(payload_type=0x8001, source_address=0xe80, target_address=0x1000) / UDS() / UDS_RDBI(identifiers=[0x1000])
370461
>>> resp = socket.sr1(pkt, timeout=1)
371462
""" # noqa: E501
372-
def __init__(self, ip='::1', port=13400, activate_routing=True,
373-
source_address=0xe80, target_address=0,
374-
activation_type=0, reserved_oem=b""):
375-
# type: (str, int, bool, int, int, int, bytes) -> None
463+
464+
def __init__(self,
465+
ip='::1', # type: str
466+
port=13400, # type: int
467+
tls_port=3496, # type: int
468+
activate_routing=True, # type: bool
469+
source_address=0xe80, # type: int
470+
target_address=0, # type: int
471+
activation_type=0, # type: int
472+
reserved_oem=b"", # type: bytes
473+
force_tls=False, # type: bool
474+
context=None # type: Optional[ssl.SSLContext]
475+
): # type: (...) -> None
376476
self.ip = ip
377477
self.port = port
478+
self.tls_port = tls_port
479+
self.activate_routing = activate_routing
378480
self.source_address = source_address
481+
self.target_address = target_address
482+
self.activation_type = activation_type
483+
self.reserved_oem = reserved_oem
379484
self.buffer = b""
380-
super(DoIPSocket6, self)._init_socket(socket.AF_INET6)
381-
382-
if activate_routing:
383-
super(DoIPSocket6, self)._activate_routing(
384-
source_address, target_address, activation_type, reserved_oem)
485+
self.force_tls = force_tls
486+
self.context = context
487+
try:
488+
self._init_socket(socket.AF_INET6)
489+
except Exception:
490+
self.close()
491+
raise
385492

386493

387-
class UDS_DoIPSocket(DoIPSocket):
494+
class _UDS_DoIPSocketBase(StreamSocket):
388495
"""
389496
Application-Layer socket for DoIP endpoints. This socket takes care about
390497
the encapsulation of UDS packets into DoIP packets.
@@ -394,11 +501,14 @@ class UDS_DoIPSocket(DoIPSocket):
394501
>>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000])
395502
>>> resp = socket.sr1(pkt, timeout=1)
396503
"""
504+
397505
def send(self, x):
398506
# type: (Union[Packet, bytes]) -> int
399507
if isinstance(x, UDS):
400-
pkt = DoIP(payload_type=0x8001, source_address=self.source_address,
401-
target_address=self.target_address) / x
508+
pkt = DoIP(payload_type=0x8001,
509+
source_address=self.source_address, # type: ignore
510+
target_address=self.target_address # type: ignore
511+
) / x
402512
else:
403513
pkt = x
404514

@@ -407,35 +517,38 @@ def send(self, x):
407517
except AttributeError:
408518
pass
409519

410-
return super(UDS_DoIPSocket, self).send(pkt)
520+
return super().send(pkt)
411521

412522
def recv(self, x=MTU, **kwargs):
413523
# type: (Optional[int], **Any) -> Optional[Packet]
414-
pkt = super(UDS_DoIPSocket, self).recv(x, **kwargs)
524+
pkt = super().recv(x, **kwargs)
415525
if pkt and pkt.payload_type == 0x8001:
416526
return pkt.payload
417527
else:
418528
return pkt
419529

420530

421-
class UDS_DoIPSocket6(DoIPSocket6, UDS_DoIPSocket):
531+
class UDS_DoIPSocket(_UDS_DoIPSocketBase, DoIPSocket):
422532
"""
423533
Application-Layer socket for DoIP endpoints. This socket takes care about
424534
the encapsulation of UDS packets into DoIP packets.
425535
426536
Example:
427-
>>> socket = UDS_DoIPSocket6("2001:16b8:3f0e:2f00:21a:37ff:febf:edb9")
537+
>>> socket = UDS_DoIPSocket("169.254.117.238")
428538
>>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000])
429539
>>> resp = socket.sr1(pkt, timeout=1)
430540
"""
431541
pass
432542

433543

434-
bind_bottom_up(UDP, DoIP, sport=13400)
435-
bind_bottom_up(UDP, DoIP, dport=13400)
436-
bind_layers(UDP, DoIP, sport=13400, dport=13400)
437-
438-
bind_layers(TCP, DoIP, sport=13400)
439-
bind_layers(TCP, DoIP, dport=13400)
544+
class UDS_DoIPSocket6(_UDS_DoIPSocketBase, DoIPSocket6):
545+
"""
546+
Application-Layer socket for DoIP endpoints. This socket takes care about
547+
the encapsulation of UDS packets into DoIP packets.
440548
441-
bind_layers(DoIP, UDS, payload_type=0x8001)
549+
Example:
550+
>>> socket = UDS_DoIPSocket6("2001:16b8:3f0e:2f00:21a:37ff:febf:edb9")
551+
>>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000])
552+
>>> resp = socket.sr1(pkt, timeout=1)
553+
"""
554+
pass

0 commit comments

Comments
 (0)