353 lines
12 KiB
Python
353 lines
12 KiB
Python
# SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
import subprocess
|
|
import time
|
|
import os
|
|
import pytest
|
|
import tempfile
|
|
import signal
|
|
import pty
|
|
import sys
|
|
import select
|
|
|
|
# Note: bin_path and interface fixtures are provided implicitly by conftest.py
|
|
|
|
def check_isotp_support(bin_path, interface):
|
|
"""
|
|
Helper to check if ISO-TP kernel module is loaded/available.
|
|
Reused logic to prevent failing tests on systems without can-isotp.
|
|
"""
|
|
isotpsend = os.path.join(bin_path, "isotpsend")
|
|
if not os.path.exists(isotpsend):
|
|
return False
|
|
|
|
try:
|
|
proc = subprocess.Popen(
|
|
[isotpsend, "-s", "123", "-d", "321", interface],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
try:
|
|
outs, errs = proc.communicate(input="00", timeout=0.5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
outs, errs = proc.communicate()
|
|
|
|
if "socket: Protocol not supported" in errs or "socket: Protocol not supported" in outs:
|
|
return False
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def test_isotpsniffer_usage(bin_path):
|
|
"""
|
|
Test usage/help output for isotpsniffer.
|
|
|
|
Manual Reproduction:
|
|
Run: ./isotpsniffer -h
|
|
Expect: Output containing "Usage: isotpsniffer".
|
|
"""
|
|
isotpsniffer = os.path.join(bin_path, "isotpsniffer")
|
|
|
|
if not os.path.exists(isotpsniffer):
|
|
pytest.skip("isotpsniffer binary not found")
|
|
|
|
# Running with -h usually triggers error output about missing argument for option 'h'
|
|
# or just prints usage.
|
|
result = subprocess.run(
|
|
[isotpsniffer, "-h"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
assert "Usage: isotpsniffer" in result.stderr or "Usage: isotpsniffer" in result.stdout
|
|
|
|
@pytest.mark.parametrize("payload_hex, desc", [
|
|
("11 22 33", "Single Frame"),
|
|
("00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE", "Multi Frame (Segmentation)")
|
|
])
|
|
def test_isotpsniffer_traffic(bin_path, can_interface, payload_hex, desc):
|
|
"""
|
|
Test that isotpsniffer correctly captures ISO-TP traffic.
|
|
|
|
Description:
|
|
1. Start isotpsniffer in background using a PTY (pseudo-terminal).
|
|
It sniffs raw CAN traffic and decodes ISO-TP.
|
|
2. Start isotprecv (Background).
|
|
It acts as the ISO-TP destination node, handling Flow Control (FC).
|
|
This matches the manual reproduction where isotprecv is running.
|
|
3. Run isotpsend to generate traffic.
|
|
4. Read sniffer output continuously.
|
|
5. Stop sniffer with SIGINT.
|
|
6. Verify sniffer output contains the payload bytes.
|
|
|
|
Manual Reproduction (Single Frame):
|
|
1. Terminal 1: ./isotpsniffer -s 123 -d 321 vcan0
|
|
2. Terminal 2: ./isotprecv -s 321 -d 123 vcan0
|
|
3. Terminal 3: echo "11 22 33" | ./isotpsend -s 123 -d 321 vcan0
|
|
4. Observe Terminal 1 output.
|
|
|
|
Manual Reproduction (Multi Frame):
|
|
1. Terminal 1: ./isotpsniffer -s 123 -d 321 vcan0
|
|
2. Terminal 2: ./isotprecv -s 321 -d 123 vcan0
|
|
3. Terminal 3: echo "00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE" | ./isotpsend -s 123 -d 321 vcan0
|
|
4. Observe Terminal 1 output.
|
|
"""
|
|
isotpsniffer = os.path.join(bin_path, "isotpsniffer")
|
|
isotpsend = os.path.join(bin_path, "isotpsend")
|
|
isotprecv = os.path.join(bin_path, "isotprecv")
|
|
|
|
for tool in [isotpsniffer, isotpsend, isotprecv]:
|
|
if not os.path.exists(tool):
|
|
pytest.skip(f"{tool} not found")
|
|
|
|
if not check_isotp_support(bin_path, can_interface):
|
|
pytest.skip("ISO-TP kernel support missing")
|
|
|
|
SRC_ID = "123"
|
|
DST_ID = "321"
|
|
|
|
print(f"\nDEBUG: Starting test '{desc}'")
|
|
|
|
# 1. Start Sniffer with PTY
|
|
# We use PTY to force line buffering and ensure captured output
|
|
master_fd, slave_fd = pty.openpty()
|
|
|
|
sniff_cmd = [isotpsniffer, "-s", SRC_ID, "-d", DST_ID, can_interface]
|
|
print(f"DEBUG: Starting sniffer: {' '.join(sniff_cmd)}")
|
|
sniff_proc = subprocess.Popen(
|
|
sniff_cmd,
|
|
stdin=slave_fd,
|
|
stdout=slave_fd,
|
|
stderr=slave_fd,
|
|
close_fds=True
|
|
)
|
|
os.close(slave_fd)
|
|
|
|
output_bytes = b""
|
|
|
|
try:
|
|
time.sleep(1.0)
|
|
|
|
if sniff_proc.poll() is not None:
|
|
pytest.fail(f"isotpsniffer process died during startup. RC={sniff_proc.returncode}")
|
|
|
|
# 2. Start Receiver (isotprecv)
|
|
recv_cmd = [isotprecv, "-s", DST_ID, "-d", SRC_ID, can_interface]
|
|
print(f"DEBUG: Starting receiver: {' '.join(recv_cmd)}")
|
|
recv_proc = subprocess.Popen(
|
|
recv_cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
try:
|
|
time.sleep(1.0)
|
|
|
|
if recv_proc.poll() is not None:
|
|
_, err = recv_proc.communicate()
|
|
pytest.fail(f"isotprecv failed to start. RC={recv_proc.returncode}. Stderr: {err}")
|
|
|
|
# 3. Send Data (isotpsend)
|
|
send_cmd = [isotpsend, "-s", SRC_ID, "-d", DST_ID, can_interface]
|
|
print(f"DEBUG: Sending data: {' '.join(send_cmd)} with payload '{payload_hex}'")
|
|
|
|
subprocess.run(
|
|
send_cmd,
|
|
input=payload_hex,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
timeout=2
|
|
)
|
|
|
|
# 4. Capture sniffer output
|
|
start_wait = time.time()
|
|
expected_parts = payload_hex.split()
|
|
print("DEBUG: Waiting for data in sniffer output...")
|
|
|
|
while time.time() - start_wait < 4:
|
|
r, _, _ = select.select([master_fd], [], [], 0.1)
|
|
if master_fd in r:
|
|
try:
|
|
chunk = os.read(master_fd, 4096)
|
|
if chunk:
|
|
output_bytes += chunk
|
|
except OSError:
|
|
pass
|
|
|
|
current_out = output_bytes.decode('utf-8', errors='replace')
|
|
if all(part in current_out for part in expected_parts):
|
|
print("DEBUG: Found all expected parts in output.")
|
|
break
|
|
|
|
if sniff_proc.poll() is not None:
|
|
break
|
|
|
|
finally:
|
|
if recv_proc.poll() is None:
|
|
recv_proc.terminate()
|
|
recv_proc.wait()
|
|
|
|
finally:
|
|
if sniff_proc.poll() is None:
|
|
print("DEBUG: Sending SIGINT to sniffer.")
|
|
sniff_proc.send_signal(signal.SIGINT)
|
|
try:
|
|
end_time = time.time() + 1
|
|
while time.time() < end_time:
|
|
r, _, _ = select.select([master_fd], [], [], 0.1)
|
|
if master_fd in r:
|
|
try:
|
|
chunk = os.read(master_fd, 4096)
|
|
except OSError:
|
|
break
|
|
if sniff_proc.poll() is not None:
|
|
break
|
|
finally:
|
|
if sniff_proc.poll() is None:
|
|
sniff_proc.kill()
|
|
sniff_proc.wait()
|
|
|
|
os.close(master_fd)
|
|
|
|
output = output_bytes.decode('utf-8', errors='replace')
|
|
print(f"--- Sniffer Output ({desc}) ---\n{output}")
|
|
|
|
expected_parts = payload_hex.split()
|
|
missing_bytes = [b for b in expected_parts if b not in output]
|
|
|
|
assert not missing_bytes, f"Missing bytes in sniffer output: {missing_bytes}. Output:\n{output}"
|
|
|
|
def test_isotp_protocol_compliance(bin_path, can_interface):
|
|
"""
|
|
Verify raw ISO-TP protocol behavior on the bus using candump.
|
|
This ensures that segmentation and flow control are actually happening.
|
|
|
|
Scenario: Multi-Frame Transfer (>7 bytes)
|
|
|
|
Manual Reproduction:
|
|
1. Terminal 1: candump vcan0
|
|
2. Terminal 2: ./isotprecv -s 321 -d 123 vcan0
|
|
3. Terminal 3: echo "00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE" | ./isotpsend -s 123 -d 321 vcan0
|
|
|
|
Expect in Terminal 1 (Raw Frames):
|
|
- First Frame (FF) from Sender: ID=123, PCI=0x1...
|
|
- Flow Control (FC) from Receiver: ID=321, PCI=0x3...
|
|
- Consecutive Frames (CF) from Sender: ID=123, PCI=0x2...
|
|
"""
|
|
isotpsend = os.path.join(bin_path, "isotpsend")
|
|
isotprecv = os.path.join(bin_path, "isotprecv")
|
|
candump = os.path.join(bin_path, "candump")
|
|
|
|
for tool in [isotpsend, isotprecv, candump]:
|
|
if not os.path.exists(tool):
|
|
pytest.skip(f"{tool} not found")
|
|
|
|
if not check_isotp_support(bin_path, can_interface):
|
|
pytest.skip("ISO-TP kernel support missing")
|
|
|
|
SRC_ID = "123" # 0x07B
|
|
DST_ID = "321" # 0x141
|
|
# 14 bytes payload -> requires segmentation (FF + CF)
|
|
payload_hex = "00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE"
|
|
|
|
# 1. Start candump (Raw Monitor)
|
|
# We use a temp file to avoid pipe buffering issues
|
|
with tempfile.TemporaryFile(mode='w+') as dump_out:
|
|
dump_proc = subprocess.Popen(
|
|
[candump, can_interface],
|
|
stdout=dump_out,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
|
|
# 2. Start Receiver
|
|
recv_proc = subprocess.Popen(
|
|
[isotprecv, "-s", DST_ID, "-d", SRC_ID, can_interface],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.PIPE
|
|
)
|
|
|
|
try:
|
|
time.sleep(1.0) # Wait for startup
|
|
|
|
# 3. Start Sender
|
|
subprocess.run(
|
|
[isotpsend, "-s", SRC_ID, "-d", DST_ID, can_interface],
|
|
input=payload_hex,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
timeout=2
|
|
)
|
|
|
|
time.sleep(1.0) # Allow transmission to complete
|
|
|
|
finally:
|
|
recv_proc.terminate()
|
|
recv_proc.wait()
|
|
dump_proc.terminate()
|
|
dump_proc.wait()
|
|
|
|
# 4. Analyze Raw Dump
|
|
dump_out.seek(0)
|
|
raw_traffic = dump_out.read()
|
|
print(f"--- Raw CAN Traffic ---\n{raw_traffic}")
|
|
|
|
# Check for ISO-TP Protocol Elements
|
|
# Note: candump format is usually "interface ID [len] data..."
|
|
|
|
# 1. Check for First Frame (FF) sent by SRC (123)
|
|
# FF PCI starts with nibble 1 (e.g., 10 0E ...)
|
|
# We look for ID 123 and data starting with 1
|
|
ff_found = False
|
|
|
|
# 2. Check for Flow Control (FC) sent by DST (321)
|
|
# FC PCI starts with nibble 3 (e.g., 30 00 00)
|
|
fc_found = False
|
|
|
|
# 3. Check for Consecutive Frame (CF) sent by SRC (123)
|
|
# CF PCI starts with nibble 2 (e.g., 21 ...)
|
|
cf_found = False
|
|
|
|
for line in raw_traffic.splitlines():
|
|
if SRC_ID in line:
|
|
# Check data bytes for Sender
|
|
# Typical line: "vcan0 123 [8] 10 0E 00 11 22 33 44 55"
|
|
parts = line.split()
|
|
if "[" in parts[2]: # Format with [len]
|
|
data_idx = 3
|
|
else: # Format without [len]
|
|
data_idx = 2
|
|
|
|
if len(parts) > data_idx:
|
|
first_byte = int(parts[data_idx], 16)
|
|
if (first_byte & 0xF0) == 0x10:
|
|
ff_found = True
|
|
elif (first_byte & 0xF0) == 0x20:
|
|
cf_found = True
|
|
|
|
elif DST_ID in line:
|
|
# Check data bytes for Receiver (FC)
|
|
parts = line.split()
|
|
if "[" in parts[2]:
|
|
data_idx = 3
|
|
else:
|
|
data_idx = 2
|
|
|
|
if len(parts) > data_idx:
|
|
first_byte = int(parts[data_idx], 16)
|
|
if (first_byte & 0xF0) == 0x30:
|
|
fc_found = True
|
|
|
|
assert ff_found, "Missing ISO-TP First Frame (FF - 0x1...) in raw traffic"
|
|
assert fc_found, "Missing ISO-TP Flow Control (FC - 0x3...) in raw traffic. Kernel stack might not be responding."
|
|
assert cf_found, "Missing ISO-TP Consecutive Frame (CF - 0x2...) in raw traffic"
|