Skip to content

Add S7CommPlus V2 protocol support (TLS + IntegrityId)#646

Merged
gijzelaerr merged 29 commits into
masterfrom
feature/s7commplus-v2
Mar 24, 2026
Merged

Add S7CommPlus V2 protocol support (TLS + IntegrityId)#646
gijzelaerr merged 29 commits into
masterfrom
feature/s7commplus-v2

Conversation

@gijzelaerr
Copy link
Copy Markdown
Owner

@gijzelaerr gijzelaerr commented Mar 20, 2026

Summary

  • Adds V2 protocol support for S7-1200/1500 PLCs with modern firmware
  • TLS 1.3 wrapping of COTP socket with OMS exporter secret extraction
  • Dual IntegrityId counters (read vs write) inserted into V2 PDU headers
  • Password legitimation module: legacy SHA-1 XOR (older firmware) and AES-256-CBC (newer firmware)
  • V2 server emulator with TLS and IntegrityId tracking for testing
  • 39 new tests covering protocol constants, key derivation, legitimation, IntegrityId tracking

Architecture

V2 changes are minimal on top of the V1 scaffolding:

  • connection.py: TLS activation, IntegrityId in send_request()
  • protocol.py: READ_FUNCTION_CODES set, LegitimationId enum
  • legitimation.py: New module for password authentication
  • server.py: V2 emulation for unit tests
  • client.py/async_client.py: password parameter (legitimation deferred)

Test plan

  • 39 V2-specific tests pass
  • All 578 existing tests pass
  • mypy strict mode: 0 errors
  • ruff: all checks pass

🤖 Generated with Claude Code

Closes #611

@gijzelaerr gijzelaerr force-pushed the s7commplus-scaffolding branch from 16fe10e to 7f2dd2d Compare March 20, 2026 10:26
gijzelaerr and others added 12 commits March 20, 2026 12:37
Adds the snap7.s7commplus package as a foundation for future S7CommPlus
protocol support, targeting all S7-1200/1500 PLCs (V1/V2/V3/TLS).

Includes:
- Protocol constants (opcodes, function codes, data types, element IDs)
- VLQ encoding/decoding (Variable-Length Quantity, the S7CommPlus wire format)
- Codec for frame headers, request/response headers, and typed values
- Connection skeleton with multi-version support (V1/V2/V3/TLS)
- Client stub with symbolic variable access API
- 86 passing tests for VLQ and codec modules

The wire protocol (VLQ, data types, object model) is the same across all
protocol versions -- only the session authentication layer differs. The
protocol version is auto-detected from the PLC's CreateObject response.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server emulator (snap7/s7commplus/server.py):
- Full PLC memory model with thread-safe data blocks
- Named variable registration with type metadata
- Handles CreateObject/DeleteObject session management
- Handles Explore (browse registered DBs and variables)
- Handles GetMultiVariables/SetMultiVariables (read/write)
- Multi-client support (threaded)
- CPU state management

Async client (snap7/s7commplus/async_client.py):
- asyncio-based S7CommPlus client with Lock for concurrent safety
- Same API as sync client: db_read, db_write, db_read_multi, explore
- Native COTP/TPKT transport using asyncio streams

Updated sync client and connection to be functional for V1:
- CreateObject/DeleteObject session lifecycle
- Send/receive with S7CommPlus framing over COTP/TPKT
- db_read, db_write, db_read_multi operations

Integration tests (25 new tests):
- Server unit tests (data blocks, variables, CPU state)
- Sync client <-> server: connect, read, write, multi-read, explore
- Async client <-> server: connect, read, write, concurrent reads
- Data persistence across client sessions
- Multiple concurrent clients with unique sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reframe protocol version descriptions around interoperability rather
than security vulnerabilities. Remove CVE references and replace
implementation-specific language with neutral terminology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite async tests to use asyncio.run() instead of @pytest.mark.asyncio
since pytest-asyncio is not a project dependency. Also apply ruff
formatting fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add pytest-asyncio to test dependencies and set asyncio_mode=auto.
Restore async test methods with @pytest.mark.asyncio instead of
asyncio.run() wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix ruff formatting violations and mypy type errors in S7CommPlus code
that caused pre-commit CI to fail. Add end-to-end test suite for
validating S7CommPlus against a real S7-1200/1500 PLC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sync client improvements

Support bytes-type remote TSAP (e.g. "SIMATIC-ROOT-HMI") in ISOTCPConnection,
extend S7CommPlus protocol handling, and improve async client and server emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… diagnostics

Adds hex dumps and detailed parsing at every protocol layer (ISO-TCP,
S7CommPlus connection, client) plus 6 new diagnostic e2e tests that
probe different payload formats and function codes against real hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite client payload encoding/decoding to use the correct S7CommPlus
protocol format with ItemAddress structures (SymbolCrc, AccessArea,
AccessSubArea, LIDs), ObjectQualifier, and proper PValue response
parsing. Previously the client used a simplified custom format that
only worked with the emulated server, causing ERROR2 responses from
real S7-1200/1500 PLCs.

- client.py: Correct GetMultiVariables/SetMultiVariables request format
- async_client.py: Reuse corrected payload builders from client.py
- codec.py: Add ItemAddress, ObjectQualifier, PValue encode/decode
- protocol.py: Add Ids constants (DB_ACCESS_AREA_BASE, etc.)
- server.py: Update to parse/generate the corrected wire format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
S7CommPlus protocol uses 1-based LID byte offsets, but the client was
sending 0-based offsets. This caused real PLCs to reject all db_read
and db_write requests. Also fixes lint issues in e2e test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the missing SetMultiVariables session handshake step that echoes
ServerSessionVersion (attr 306) back to the PLC after CreateObject. Without
this, PLCs reject data operations with ERROR2 (0x05A9).

For PLCs that don't provide ServerSessionVersion or don't support S7CommPlus
data operations, the client transparently falls back to the legacy S7 protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…network interfaces

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@gijzelaerr gijzelaerr force-pushed the s7commplus-scaffolding branch from 7f2dd2d to 61ba9f0 Compare March 20, 2026 10:37
gijzelaerr and others added 13 commits March 20, 2026 12:46
Adds the snap7.s7commplus package as a foundation for future S7CommPlus
protocol support, targeting all S7-1200/1500 PLCs (V1/V2/V3/TLS).

Includes:
- Protocol constants (opcodes, function codes, data types, element IDs)
- VLQ encoding/decoding (Variable-Length Quantity, the S7CommPlus wire format)
- Codec for frame headers, request/response headers, and typed values
- Connection skeleton with multi-version support (V1/V2/V3/TLS)
- Client stub with symbolic variable access API
- 86 passing tests for VLQ and codec modules

The wire protocol (VLQ, data types, object model) is the same across all
protocol versions -- only the session authentication layer differs. The
protocol version is auto-detected from the PLC's CreateObject response.

Reference: thomas-v2/S7CommPlusDriver (C#, LGPL-3.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server emulator (snap7/s7commplus/server.py):
- Full PLC memory model with thread-safe data blocks
- Named variable registration with type metadata
- Handles CreateObject/DeleteObject session management
- Handles Explore (browse registered DBs and variables)
- Handles GetMultiVariables/SetMultiVariables (read/write)
- Multi-client support (threaded)
- CPU state management

Async client (snap7/s7commplus/async_client.py):
- asyncio-based S7CommPlus client with Lock for concurrent safety
- Same API as sync client: db_read, db_write, db_read_multi, explore
- Native COTP/TPKT transport using asyncio streams

Updated sync client and connection to be functional for V1:
- CreateObject/DeleteObject session lifecycle
- Send/receive with S7CommPlus framing over COTP/TPKT
- db_read, db_write, db_read_multi operations

Integration tests (25 new tests):
- Server unit tests (data blocks, variables, CPU state)
- Sync client <-> server: connect, read, write, multi-read, explore
- Async client <-> server: connect, read, write, concurrent reads
- Data persistence across client sessions
- Multiple concurrent clients with unique sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reframe protocol version descriptions around interoperability rather
than security vulnerabilities. Remove CVE references and replace
implementation-specific language with neutral terminology.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite async tests to use asyncio.run() instead of @pytest.mark.asyncio
since pytest-asyncio is not a project dependency. Also apply ruff
formatting fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add pytest-asyncio to test dependencies and set asyncio_mode=auto.
Restore async test methods with @pytest.mark.asyncio instead of
asyncio.run() wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix ruff formatting violations and mypy type errors in S7CommPlus code
that caused pre-commit CI to fail. Add end-to-end test suite for
validating S7CommPlus against a real S7-1200/1500 PLC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sync client improvements

Support bytes-type remote TSAP (e.g. "SIMATIC-ROOT-HMI") in ISOTCPConnection,
extend S7CommPlus protocol handling, and improve async client and server emulator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… diagnostics

Adds hex dumps and detailed parsing at every protocol layer (ISO-TCP,
S7CommPlus connection, client) plus 6 new diagnostic e2e tests that
probe different payload formats and function codes against real hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite client payload encoding/decoding to use the correct S7CommPlus
protocol format with ItemAddress structures (SymbolCrc, AccessArea,
AccessSubArea, LIDs), ObjectQualifier, and proper PValue response
parsing. Previously the client used a simplified custom format that
only worked with the emulated server, causing ERROR2 responses from
real S7-1200/1500 PLCs.

- client.py: Correct GetMultiVariables/SetMultiVariables request format
- async_client.py: Reuse corrected payload builders from client.py
- codec.py: Add ItemAddress, ObjectQualifier, PValue encode/decode
- protocol.py: Add Ids constants (DB_ACCESS_AREA_BASE, etc.)
- server.py: Update to parse/generate the corrected wire format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
S7CommPlus protocol uses 1-based LID byte offsets, but the client was
sending 0-based offsets. This caused real PLCs to reject all db_read
and db_write requests. Also fixes lint issues in e2e test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the missing SetMultiVariables session handshake step that echoes
ServerSessionVersion (attr 306) back to the PLC after CreateObject. Without
this, PLCs reject data operations with ERROR2 (0x05A9).

For PLCs that don't provide ServerSessionVersion or don't support S7CommPlus
data operations, the client transparently falls back to the legacy S7 protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements V2 protocol support for S7-1200/1500 PLCs with modern firmware:
- TLS 1.3 activation between InitSSL and CreateObject
- OMS exporter secret extraction for legitimation key derivation
- Dual IntegrityId counters (read vs write) in V2 PDU headers
- Password legitimation module (legacy SHA-1 XOR + new AES-256-CBC)
- V2 server emulator with TLS and IntegrityId tracking
- 39 new tests covering all V2 components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add cryptography as optional dep: pip install python-snap7[s7commplus]
- Implement connection.authenticate() with challenge/response flow
- Wire up legitimation in client.connect(password=...)
- Auto-detect legacy (SHA-1 XOR) vs new (AES-256-CBC) auth mode
- Add legitimation protocol methods: get challenge, send response
- 46 tests now (was 39), including AES roundtrip verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gijzelaerr gijzelaerr force-pushed the feature/s7commplus-v2 branch from 89275fb to 4fd702a Compare March 20, 2026 10:46
Resolve merge conflicts keeping all V2 additions (TLS, IntegrityId,
legitimation) while incorporating scaffolding branch updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gijzelaerr gijzelaerr changed the base branch from s7commplus-scaffolding to master March 24, 2026 13:20
Resolve conflicts: keep both hypothesis test dep (from master) and
s7commplus optional dep (from V2 branch). Regenerate lockfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread snap7/s7commplus/legitimation.py Dismissed
Comment thread snap7/s7commplus/legitimation.py Dismissed
Comment thread tests/test_s7commplus_v2.py Dismissed
Comment thread tests/test_s7commplus_v2.py Dismissed
gijzelaerr and others added 2 commits March 24, 2026 15:34
- Skip TestBuildNewResponse when cryptography package is unavailable
  (CI only installs [test] extras, not [s7commplus])
- Fix extra blank line in protocol.py that failed ruff format check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gijzelaerr gijzelaerr merged commit 40d21ed into master Mar 24, 2026
37 checks passed
@gijzelaerr gijzelaerr added this to the 3.1 milestone Mar 24, 2026
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.

S7CommPlus V2 protocol support

2 participants