FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

IPv6 Support by jacobschaer · Pull Request #9 · jacobschaer/python-doipclient · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (3) All 1 file type selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
43 changes: 33 additions & 10 deletions doipclient/client.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import ipaddress
import socket
import struct
import time
Expand Down Expand Up @@ -106,8 +107,8 @@ class DoIPClient:
particularly for use with UDS - especially with scripts that tend to go through instructions as fast as possible.

:param ecu_ip_address: This is the IP address of the target ECU. This should be a string representing an IPv4
address like "192.168.1.1". Like the logical_address, if you don't know the value for your ECU, utilize the
await_vehicle_announcement() method.
address like "192.168.1.1" or an IPv6 address like "2001:db8::". Like the logical_address, if you don't know the
value for your ECU, utilize the await_vehicle_announcement() method.
:type ecu_ip_address: str
:param ecu_logical_address: The logical address of the target ECU. This should be an integer. According to the
specification, the correct range is 0x0001 to 0x0DFF ("VM specific"). If you don't know the logical address,
Expand All @@ -128,7 +129,8 @@ class DoIPClient:
this should be 0x0E00 to 0x0FFF. Can typically be left as default.
:type client_logical_address: int
:param client_ip_address: If specified, attempts to bind to this IP as the source for both UDP and TCP communication.
Useful if you have multiple network adapters.
Useful if you have multiple network adapters. Can be an IPv4 or IPv6 address just like `ecu_ip_address`, though
the type should match.
:type client_ip_address: str, optional
:param use_secure: Enables TLS if True. Untested. Should be combined with changing tcp_port to 3496.
:type use_secure: bool
Expand All @@ -138,6 +140,7 @@ class DoIPClient:
:type auto_reconnect_tcp: bool

:raises ConnectionRefusedError: If the activation request fails
:raises ValueError: If the IPAddress is neither an IPv4 nor an IPv6 address
"""

def __init__(
Expand All @@ -164,9 +167,18 @@ def __init__(
self._udp_parser = Parser()
self._tcp_parser = Parser()
self._protocol_version = protocol_version
self._connect()
self._auto_reconnect_tcp = auto_reconnect_tcp
self._tcp_close_detected = False

# Check the ECU IP type to determine socket family
# Will raise ValueError if neither a valid IPv4, nor IPv6 address
if type(ipaddress.ip_address(self._ecu_ip_address)) == ipaddress.IPv6Address:
self._address_family = socket.AF_INET6
else:
self._address_family = socket.AF_INET

self._connect()

if self._activation_type is not None:
result = self.request_activation(self._activation_type, disable_retry=True)
if result.response_code != RoutingActivationResponse.ResponseCode.Success:
Expand All @@ -185,23 +197,32 @@ def __exit__(self, type, value, traceback):
self.close()

@classmethod
def await_vehicle_announcement(cls, udp_port=UDP_DISCOVERY, timeout=None):
def await_vehicle_announcement(
cls, udp_port=UDP_DISCOVERY, timeout=None, ipv6=False
):
"""Receive Vehicle Announcement Message

When an ECU first turns on, it's supposed to broadcast a Vehicle Announcement Message over UDP 3 times
to assist DoIP clients in determining ECU IP's and Logical Addresses.
to assist DoIP clients in determining ECU IP's and Logical Addresses. Will use an IPv4 socket by default,
though this can be overridden with the `ipv6` parameter.

:param udp_port: The UDP port to listen on. Per the spec this should be 13400, but some VM's use a custom
one.
:type udp_port: int, optional
:param timeout: Maximum amount of time to wait for message
:type timeout: float, optional
:param ipv6: Bool forcing IPV6 socket instead of IPV4 socket
:type ipv6: bool, optional
:return: IP Address of ECU and VehicleAnnouncementMessage object
:rtype: tuple
:raises TimeoutError: If vehicle announcement not received in time
"""
start_time = time.time()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
if not ipv6:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
if timeout is not None:
Expand Down Expand Up @@ -272,7 +293,9 @@ def read_doip(
# There were no responses in the parser, so we need to read off the network
# and feed that to the parser until we find another DoIP message

if (transport == DoIPClient.TransportType.TRANSPORT_TCP) and self._tcp_close_detected:
if (
transport == DoIPClient.TransportType.TRANSPORT_TCP
) and self._tcp_close_detected:
# The caller is looking for TCP responses, but there were no messages
# returned from the parser and the socket has been closed (so no further
# responses are expected). It's safe to stop looking early and raise
Expand Down Expand Up @@ -618,7 +641,7 @@ def receive_diagnostic(self, timeout=None):

def _connect(self):
"""Helper to establish socket communication"""
self._tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._tcp_sock = socket.socket(self._address_family, socket.SOCK_STREAM)
self._tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self._tcp_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
if self._client_ip_address is not None:
Expand All @@ -627,7 +650,7 @@ def _connect(self):
self._tcp_sock.settimeout(A_PROCESSING_TIME)
self._tcp_close_detected = False

self._udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._udp_sock = socket.socket(self._address_family, socket.SOCK_DGRAM)
self._udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._udp_sock.settimeout(A_PROCESSING_TIME)
if self._client_ip_address is not None:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setuptools.setup(
name="doipclient",
version="1.0.4",
version="1.0.5",
description="A Diagnostic over IP (DoIP) client implementing ISO-13400-2.",
long_description=long_description,
author="Jacob Schaer",
Expand Down
29 changes: 28 additions & 1 deletion tests/test_client.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def __init__(self):
self._bound_ip = None
self._bound_port = None
self.timeout = None
self.opts = {}

def construct(self, network, type):
self._network = network
Expand All @@ -125,7 +126,8 @@ def connect(self, address):
self._ip, self._port = address

def setsockopt(self, socket_type, opt_type, opt_value):
pass
self.opts[socket_type] = self.opts.get(socket_type, {})
self.opts[socket_type][opt_type] = opt_value

def settimeout(self, timeout):
self.timeout = timeout
Expand Down Expand Up @@ -554,3 +556,28 @@ def test_send_generic(mock_socket):
def test_message_ids():
for payload_type, message in payload_type_to_message.items():
assert payload_type == message.payload_type


def test_invalid_ip():
with pytest.raises(
ValueError, match=r"does not appear to be an IPv4 or IPv6 address"
):
sut = DoIPClient(test_ip + "a", test_logical_address)


def test_ipv4(mock_socket):
sut = DoIPClient(test_ip, test_logical_address)
assert mock_socket._network == socket.AF_INET
assert mock_socket.opts == {
socket.SOL_SOCKET: {socket.SO_REUSEADDR: True},
socket.IPPROTO_TCP: {socket.TCP_NODELAY: True},
}


def test_ipv6(mock_socket):
sut = DoIPClient("2001:db8::", test_logical_address)
assert mock_socket._network == socket.AF_INET6
assert mock_socket.opts == {
socket.SOL_SOCKET: {socket.SO_REUSEADDR: True},
socket.IPPROTO_TCP: {socket.TCP_NODELAY: True},
}

Back | FazBrowse Home | New Git URL