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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions core/generic_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
protocol_probes,
request_payload,
response_rule,
validate_modbus_device_id_response,
validate_modbus_tcp_response,
)

Expand Down Expand Up @@ -2305,15 +2306,16 @@ def _modbus_tcp_identify(self, ip: str, port: int) -> Optional[dict]:
try:
sock.connect((ip, port))
sock.sendall(MODBUS_TCP_DEVICE_ID)
response = sock.recv(260)
response = receive_response(sock, 0.45, max_bytes=260, complete=modbus_tcp_complete)
duration_ms = round((time.time() - start) * 1000, 1)
if response:
response_ok, response_note, valid_frame = validate_modbus_device_id_response(response, 1, 1)
if response_ok:
return {
"type": "TCP连通 + Modbus TCP设备识别",
"type": "TCP连通 + Modbus TCP异常响应" if valid_frame[7] == 0xAB else "TCP连通 + Modbus TCP设备识别",
"request": f"TCP connect; {self._bytes_to_hex(MODBUS_TCP_DEVICE_ID)}",
"response": self._bytes_to_hex(response),
"response": self._bytes_to_hex(valid_frame),
"status": "有响应",
"summary": f"{ip}:{port} 对Modbus TCP设备识别请求有响应",
"summary": f"{ip}:{port} {response_note}",
"duration_ms": duration_ms,
}
except OSError:
Expand Down Expand Up @@ -2349,16 +2351,22 @@ def _modbus_tcp_standard_probe(self, ip: str, port: int) -> Optional[dict]:
try:
sock.connect((ip, port))
sock.sendall(frame)
response = sock.recv(260)
response = receive_response(sock, 0.45, max_bytes=260, complete=modbus_tcp_complete)
duration_ms = round((time.time() - start) * 1000, 1)
if response:
status_note = self._modbus_tcp_response_note(response)
response_ok, response_note, valid_frame = validate_modbus_tcp_response(
response, int.from_bytes(frame[:2], "big"), unit_id, 3,
accept_exception=True, expected_count=count,
)
if response_ok:
is_exception = valid_frame[7] == 0x83
if is_exception:
response_note = f"设备返回Modbus异常码0x{valid_frame[8]:02X},仅确认协议可达"
return {
"type": "TCP连通 + Modbus TCP标准参数只读验证",
"type": "TCP连通 + Modbus TCP异常响应" if is_exception else "TCP连通 + Modbus TCP标准参数只读验证",
"request": f"{label}; {self._bytes_to_hex(frame)}",
"response": self._bytes_to_hex(response),
"response": self._bytes_to_hex(valid_frame),
"status": "有响应",
"summary": f"{ip}:{port} 对标准只读寄存器请求有响应;{status_note}",
"summary": f"{ip}:{port} 对标准只读寄存器请求返回有效协议帧;{response_note}",
"duration_ms": duration_ms,
}
except OSError:
Expand Down
52 changes: 52 additions & 0 deletions core/protocol_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


READ_ONLY_MODBUS_FUNCTIONS = (1, 2, 3, 4)
MODBUS_EXCEPTION_CODES = (1, 2, 3, 4, 5, 6, 8, 10, 11)


class ProtocolConfigError(ValueError):
Expand Down Expand Up @@ -293,6 +294,8 @@ def validate_modbus_tcp_response(data: bytes, transaction_id: int, unit_id: int,
if declared_length != 3:
return False, "Modbus TCP异常响应长度不正确", frame
code = frame[8]
if code not in MODBUS_EXCEPTION_CODES:
return False, "Modbus TCP异常码未定义", frame
if accept_exception:
return True, f"设备返回Modbus异常码0x{code:02X},按配置允许异常响应", frame
return False, f"设备返回Modbus异常码0x{code:02X}", frame
Expand All @@ -308,3 +311,52 @@ def validate_modbus_tcp_response(data: bytes, transaction_id: int, unit_id: int,
if byte_count != expected_bytes:
return False, "Modbus TCP响应数量与请求不匹配", frame
return True, "返回有效Modbus TCP响应", frame


def validate_modbus_device_id_response(data: bytes, transaction_id: int,
unit_id: int) -> Tuple[bool, str, bytes]:
"""Validate the first page of a 43/14 basic-ID reply; never infer a model."""
validate_modbus_target(unit_id, tcp=True)
if len(data) < 9:
return False, "设备识别响应长度不足", b""
if int.from_bytes(data[:2], "big") != transaction_id or data[2:4] != b"\x00\x00":
return False, "设备识别事务号或协议标识不匹配", b""
length = int.from_bytes(data[4:6], "big")
if not 3 <= length <= 254 or len(data) < 6 + length:
return False, "设备识别长度字段不正确", b""
frame = data[:6 + length]
if frame[6] != unit_id:
return False, "设备识别单元标识不匹配", frame
if frame[7] == 0xAB:
if length != 3:
return False, "设备识别异常响应长度不正确", frame
if frame[8] not in MODBUS_EXCEPTION_CODES:
return False, "设备识别异常码未定义", frame
return True, f"设备返回Modbus异常码0x{frame[8]:02X},仅确认协议可达", frame
if length < 8 or frame[7:10] != b"\x2B\x0E\x01":
return False, "设备识别功能、MEI或访问码不匹配", frame
conformity, more, next_object, count = frame[10:14]
if conformity not in (1, 2, 3, 0x81, 0x82, 0x83) or more not in (0, 0xFF):
return False, "设备识别能力或分页标识无效", frame
if more == 0 and next_object != 0:
return False, "设备识别末页的后续对象标识无效", frame
if not 1 <= count <= 3 or (more == 0 and count != 3):
return False, "基本设备识别对象为空或缺失", frame
offset, last_object = 14, -1
for expected_object in range(count):
if offset + 2 > len(frame):
return False, "设备识别对象头不完整", frame
object_id, size = frame[offset:offset + 2]
offset += 2
if object_id != expected_object or offset + size > len(frame):
return False, "设备识别对象次序或长度无效", frame
last_object = object_id
offset += size
if offset != len(frame):
return False, "设备识别对象数量与长度不匹配", frame
if more == 0xFF and (count == 3 or next_object != last_object + 1):
return False, "设备识别分页未前进", frame
note = f"返回有效基本设备识别响应,含{count}个对象"
if more == 0xFF:
note += ";存在后续分页,本次不继续查询"
return True, note, frame
17 changes: 17 additions & 0 deletions docs/modbus-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Modbus TCP discovery response integrity / 发现响应校验

TCP connection and Modbus response are separate evidence. An open port remains a TCP connection result when protocol discovery receives unrelated, truncated or mismatched bytes; it must not be relabelled Modbus merely because data arrived.

端口连通与协议有效性分别记录。标准寄存器发现使用与配置读检查相同的MBAP关联、功能和数量验证;基本设备识别验证固定43/14、MEI14、访问码1、结构字段与有界对象列表。两条路径均使用既有有界收包逻辑,支持TCP任意分片;超时预算仍为0.45秒、上限260字节,socket始终在finally关闭。

The read probe reuses the existing response validator and preserves configured unit IDs, including TCP0. The fallback device-identification probe retains its existing transaction1/unit1/basic-object0 request. It validates the first returned page only; pagination metadata is reported without issuing more requests. Valid Modbus exceptions indicate protocol reachability, not successful register reads or identified model names. Returned object bytes are not authenticated device identity.

The fixed basic-object0 response must start at object0 and contain a nonempty contiguous prefix of basic objects0/1/2. A final first page contains all three mandatory objects; a continuation points to the next remaining basic object. Empty/non-progressing pages and regular/extended objects are refused. TCP response validators reject exception bytes outside the defined code set1/2/3/4/5/6/8/0A/0B, even when exceptions are allowed. This shared TCP check also covers configured reads; RTU exception handling is unchanged by this correction.

兼容范围:既有只读请求字节、配置轮询上限和超时不变。合法异常帧保留为协议可达,摘要明确异常;缺失或错位事务/单元、非法长度、错功能/MEI/访问码和不完整对象不会产生Modbus分类。基础识别不提供厂商认证、完整设备型谱或自动分页;其他访问码及厂商扩展不由本固定查询处理。

Verification uses deterministic byte sockets, all single-split positions for representative responses and real loopback TCP. It does not connect to field hardware or establish electrical performance. The broader application still needs independent security and hardware validation; this bounded correction does not certify the service.

Protocol references: [Modbus Application Protocol V1.1b3, §6.21 and §7](https://www.modbus.org/file/secure/modbusprotocolspecification.pdf), [Modbus TCP/IP Implementation Guide, MBAP header](https://www.modbus.org/file/secure/messagingimplementationguide.pdf).

Implementation, tests and documentation are AI-assisted. / 本次实现、测试与说明为AI辅助。
Loading
Loading