Skip to content

automotive, j1939: scanning for CAs - #5164

Open
BenGardiner wants to merge 5 commits into
secdev:masterfrom
BenGardiner:j1939-soft-sockets-again
Open

BenGardiner wants to merge 5 commits into
secdev:masterfrom
BenGardiner:j1939-soft-sockets-again

Conversation

@BenGardiner

@BenGardiner BenGardiner commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

This adds j1939 scanning for Controller Applications on-top-of the soft socket support.

The changes aim to introduce only those J1939 value enumeration definitions which can be sourced from freely available locations on the internet. As such, there is not a complete list of the values.

I don't intend any impacts on other parts of the libraries.

fixes missing sr1() functionality in J1939 soft socket on master

LLM coding tools were used in the development of this PR: copilot and gemini, various models.

@BenGardiner

Copy link
Copy Markdown
Contributor Author

@polybassa

Comment thread scapy/contrib/automotive/j1939/__init__.py Outdated
Comment thread scapy/contrib/automotive/j1939/__init__.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_dm_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_name.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_name.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
@BenGardiner

Copy link
Copy Markdown
Contributor Author

thanks @polybassa for the review. I can do almost all of that right now. There's a couple things that are either answering your questions or require me to ask you questions first.

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch from fbcb5a9 to 4757249 Compare September 14, 2026 13:12
@BenGardiner

Copy link
Copy Markdown
Contributor Author

I noticed that in the rebase of the scanner code to your replacement soft socket the scanners were no longer cleanly relying on sr() / sr1() via answers() logic. I'll work on bringing that back, fixing the things above I didn't have questions about and then refactoring the scanners to use the answers() logic...

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch 3 times, most recently from 97acab8 to f664c8a Compare September 17, 2026 09:58
@BenGardiner

Copy link
Copy Markdown
Contributor Author

Hi @polybassa while I think this is ready for your next review, it might not be merged in this form. There are 'Feature' commits and then 'fixes' on them. e.g. FFfffFffFfffffFfff. To merge you would probably want the fixes squashed into the features. You may even want one squashed commit -- in which case you might consider merging the first commit separately since it is implementing missing sr1() functionality in the current J1939SoftSocket on master.

@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.76146% with 56 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.30%. Comparing base (c5e9a5b) to head (b99ba54).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
scapy/contrib/j1939.py 91.00% 17 Missing ⚠️
scapy/contrib/automotive/j1939/j1939_scanner.py 97.41% 14 Missing ⚠️
scapy/contrib/automotive/j1939/j1939_dm.py 89.10% 11 Missing ⚠️
scapy/contrib/automotive/j1939/__init__.py 79.41% 7 Missing ⚠️
scapy/contrib/automotive/j1939/j1939_dm_scanner.py 96.39% 4 Missing ⚠️
scapy/contrib/automotive/j1939/j1939_name.py 96.70% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5164      +/-   ##
==========================================
+ Coverage   81.16%   81.30%   +0.13%     
==========================================
  Files         393      398       +5     
  Lines       98107    99350    +1243     
==========================================
+ Hits        79629    80776    +1147     
- Misses      18478    18574      +96     
Files with missing lines Coverage Δ
scapy/layers/can.py 93.14% <100.00%> (+0.04%) ⬆️
scapy/contrib/automotive/j1939/j1939_name.py 96.70% <96.70%> (ø)
scapy/contrib/automotive/j1939/j1939_dm_scanner.py 96.39% <96.39%> (ø)
scapy/contrib/automotive/j1939/__init__.py 79.41% <79.41%> (ø)
scapy/contrib/automotive/j1939/j1939_dm.py 89.10% <89.10%> (ø)
scapy/contrib/automotive/j1939/j1939_scanner.py 97.41% <97.41%> (ø)
scapy/contrib/j1939.py 91.08% <91.00%> (+1.22%) ⬆️

... and 27 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch 3 times, most recently from 3b5967c to 4d7cb29 Compare September 17, 2026 12:41
@BenGardiner

Copy link
Copy Markdown
Contributor Author

I'm going to look closer at https://github.com/secdev/scapy/actions/runs/35222531093/job/105206012569?pr=5164 -- that seems like there could be something wrong with the soft socket...

@BenGardiner

Copy link
Copy Markdown
Contributor Author

I'm going to look closer at https://github.com/secdev/scapy/actions/runs/35222531093/job/105206012569?pr=5164 -- that seems like there could be something wrong with the soft socket...

yep that was a race in close() of the soft socket -- just like we had in isotp soft socket.

I have a fix

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch 5 times, most recently from 35d5188 to 625c334 Compare September 17, 2026 21:30
@BenGardiner

Copy link
Copy Markdown
Contributor Author

sorry the checks may be skipped now due to a rate limit...

I think I resolved them but pretty hard to tell locally without waiting for the github runners

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch from 625c334 to 58adda4 Compare September 17, 2026 21:42
@BenGardiner

Copy link
Copy Markdown
Contributor Author

no sorry. skipped due to a typo.

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch from 58adda4 to 811dfd9 Compare September 17, 2026 23:28
@BenGardiner

Copy link
Copy Markdown
Contributor Author

ok... ok well that was a journey... over to you then @polybassa

@polybassa

Copy link
Copy Markdown
Contributor

Please have a look on the failed ci tests

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch from 7109f14 to d4e4544 Compare September 27, 2026 23:37
@BenGardiner

Copy link
Copy Markdown
Contributor Author

Please have a look on the failed ci tests

yup sorry about that -- the import CANSocket pattern is pretty fragile for my agents it seems.

Copilot AI and others added 2 commits September 28, 2026 00:48
AI-Assisted: Yes Kimi 2.7 / GPT 5.4 codex / Copilot
…allback (heuristic)

Implement answers(), clone_with(), and copy() on J1939 to support sr1()
for directed and broadcast requests with session tracking and fallback
heuristics.

Add identifier property and setter to J1939_CAN to satisfy the CAN
interface for python-can backends, and add a defensive fallback in
_can_send() converting to CAN on AttributeError.

AI-Assisted: yes (Gemini 3.8 Flash)
@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch 2 times, most recently from 03cbd3e to ffb02cd Compare September 28, 2026 12:34
Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py
(preserving the underlying CAN socket).
"""
if reconnect_handler is not None:
probe = reconnect_handler()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code could be unified with the code above

# byte 3: total packets = 2
# byte 4: max packets per CTS = 0xFF (no limit)
# bytes 5-7: PGN being transferred (probe PGN = 0x0000FF)
rts_payload = struct.pack(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using a scapy packet here?

Comment thread scapy/contrib/j1939.py
Comment on lines +302 to +318
def clone_with(self, payload=None, **kargs):
# type: (Optional[Any], **Any) -> J1939
pkt = super(J1939, self).clone_with(payload=payload, **kargs)
pkt.priority = kargs.get('priority', self.priority)
pkt.pgn = kargs.get('pgn', self.pgn)
pkt.src = kargs.get('src', self.src)
pkt.dst = kargs.get('dst', self.dst)
return pkt

def copy(self):
# type: () -> J1939
clone = super(J1939, self).copy()
clone.priority = self.priority
clone.pgn = self.pgn
clone.src = self.src
clone.dst = self.dst
return clone

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit weird, what's it for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. yes it is weird . This exists because the transport protocol sessions are in information in the CAN ID; and the CAN ID isn't in the payload of the message.

In more detail (some of this is in the interim comments above, but this is hopefully much clearer)

We had a problem iterating over a packet ([x for x in pkt] or for p in to_send):

  • internally invokes Packet.iter(), which yields packets created via self.clone_with(payload=payl, **done2).

  • When using sr1(J1939(data, pgn=0xFECA, dst=0x10)), the packet pipeline iterates and copies the sent packet.

  • When an incoming frame arrived, J1939.answers(other) was evaluated against other. Without these overrides, other.pgn, other.dst, and other.src had reverted to 0 and 0xFF, causing answers() to fail to
    correlate the response with the request.

Overriding clone_with() and copy() on J1939 ensures that priority, pgn, src, and dst are preserved during copies, cloning, and sr1() iteration

@polybassa polybassa self-assigned this Sep 30, 2026


@contextlib.contextmanager
def j1939_get_sock(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j1939_scanner.py currently contains quite a lot of socket-framework code (_j1939_get_cansocket_cls, _open_sa_filtered_sock, j1939_resolve_probe_sock, j1939_resolve_broadcast_sock, j1939_get_sock). This makes the individual scan methods much harder to follow and couples J1939 scanning to concrete CAN socket implementations.

Could we reduce this to one socket boundary and let the scan implementations operate on a J1939Socket? For example:

@contextmanager
def _j1939_socket(sock, reconnect_handler=None, **kwargs):
    if reconnect_handler is None:
        with J1939Socket(sock, **kwargs) as s:
            yield s
        return
    raw = reconnect_handler()
    try:
        with J1939Socket(raw, **kwargs) as s:
            yield s
    finally:
        raw.close()

Then a scan should ideally be little more than construct J1939Request → sock.sr1(...).

If per-target CAN filtering is required for performance, I'd prefer exposing that via a socket factory / CANSocket configuration instead of having the J1939 scanner detect PythonCANSocket/NativeCANSocket, recover the channel, and reconstruct the backend itself.

PythonCANSocket = None # type: ignore[assignment, misc]


def _j1939_get_cansocket_cls():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing and branching on concrete CAN backends (PythonCANSocket / NativeCANSocket) inside the J1939 scanner feels like a layer violation. A J1939 scan shouldn't need to know which CANSocket alias loaded, how to recover .channel, or how to reconstruct an equivalent filtered socket.

Prefer either using the passed socket as-is, or an explicitly supplied reconnect_handler / socket factory—without reverse-engineering the backend. That also avoids the kind of import/CI fragility already noted in this PR.

# --- Technique: unicast DM PGN probe


def j1939_scan_dm_pgn(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remains one of the clearer non-Scapy-like parts: the docstring notes it could use .sr1(), but the implementation manually builds the request, flushes, registers a sniff callback, parses responses, early-stops, and paces.

I'd prefer the Scapy-like baseline first:

req = J1939Request(req_pgn=pgn, dst=target_da, src=src_addr)
resp = sock.sr1(req, timeout=sniff_time, verbose=False)

with answers() expressive enough for direct PGN response / NACK / TP announcement as needed. Optimize the hot path only after measuring that normal sr1() is inadequate—this would remove a surprising amount of custom scanner code.

Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_name.py Outdated
Comment thread scapy/contrib/automotive/j1939/j1939_name.py Outdated
Comment thread scapy/contrib/j1939.py
ps = self.pgn & 0xFF
return j1939_to_can_id(self.priority, 0, dp, pf, ps, self.src)

def answers(self, other):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fast scanners can benefit from correlation without a full transport socket, but I wouldn't put every scanner relationship into generic J1939.answers(). Prefer splitting by packet type, e.g. J1939_TP_CM_CTS.answers() matching RTS by PGN, and likewise for DM/application packets.

J1939.answers() should stay limited to envelope concerns (src/dst where applicable, request-PGN matching, maybe generic PGN equivalence)—not a database of scanner-specific conversations. That still gives sr1() cheap answers() dispatch without needing a complete TP session.

Comment thread scapy/contrib/automotive/j1939/j1939_scanner.py
@polybassa

Copy link
Copy Markdown
Contributor

Hi Ben, here is some final review. I think we are almost ready to merge. Thanks for your patience

@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch 4 times, most recently from e324023 to cea01b5 Compare October 2, 2026 17:40
Adds a scanner to identify Controller Applications in a J1939 network,
various scanning techniques are provided including both broadcast and
unicast.

AI-Assisted: yes (Gemini 3.8 Flash)
@BenGardiner
BenGardiner force-pushed the j1939-soft-sockets-again branch from cea01b5 to b99ba54 Compare October 4, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants