can-utils/tests/test_isotpsniffer.py

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"