Add DTLS transport for newer Wi-Fi/LAN Kit-20 firmware#136
Open
botts7 wants to merge 1 commit into
Open
Conversation
Newer Kit-20 dongle firmware (released ~2024 onward) encrypts the local
Modbus protocol with DTLS over UDP/8899. The standard `WIFIKIT-214028-READ`
discovery probe to UDP/48899 returns `dtls_port:8899` for affected hardware,
while plaintext requests on UDP/8899 are silently dropped — the existing
UdpInverterProtocol times out with no useful diagnostic.
This change adds an opt-in DTLS transport that runs the unchanged Modbus RTU
framing over DTLS. The application protocol (frames, register layout, slave
addresses, AA55 response prefix) is identical to the plaintext path; only
the transport differs.
Empirically observed handshake parameters:
- DTLS 1.2 negotiated by default with OpenSSL 3.0+ defaults
(cipher ECDHE-RSA-AES256-GCM-SHA384). DTLS 1.0 also accepted.
- Self-signed placeholder certificate, so peer verification is disabled.
- Appears to accept only one concurrent DTLS session.
Sessions are reused via keep_alive=True so the ~1 s handshake is paid once
per Inverter instance. On a stale-response retry the session is preserved
and the drain at the start of the next send absorbs any leftover data; only
real I/O / SSL exceptions tear the session down.
Usage:
inverter = await goodwe.connect(ip, family="DT", dtls=True)
Requires pyOpenSSL — installed via the new `[dtls]` extra. Auto-discovery
remains plaintext-only, so the family must be specified explicitly when
dtls=True.
Tested on a single-phase MS-family inverter (GW10K-MS-30) running a Kit-20
dongle whose discovery response includes `dtls_port:8899`. Existing 122
tests still pass; 10 new tests cover the DTLS transport's command
construction, retry/lock semantics, stale-response handling, and lifecycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Newer Wi-Fi/LAN Kit-20 dongles (firmware released ~2024 onward — referred to in #117 and #121 as the "Cyber Security dongle") encrypt their local Modbus protocol with DTLS over UDP/8899. The standard
WIFIKIT-214028-READdiscovery probe to UDP/48899 returnsdtls_port:8899for affected hardware; plaintext requests on UDP/8899 are silently dropped, which is whyUdpInverterProtocoltimes out against them without a useful diagnostic.Empirically observed handshake parameters
ECDHE-RSA-AES256-GCM-SHA384). DTLS 1.0 also accepted.CN=broker.goodwe-power.com), so peer verification must be disabled.The application protocol is unchanged — Modbus RTU framing, slave
0xF7, identical register layout to existing inverter families. Responses carry the sameAA55prefix thatvalidate_modbus_rtu_responsealready handles. Only the transport differs.Implementation
goodwe/dtls.pywithDTLSInverterProtocol, an async wrapper around pyOpenSSL'sDTLS_METHODdriven by memory BIOs over a connected UDP socket. Reuses the existing command/decode infrastructure unchanged.dtls=Trueargument togoodwe.connect()(and the family classes). Thegoodwe.dtlsmodule imports pyOpenSSL only on use, so the library still loads when the optional dependency isn't installed.[dtls]extra insetup.cfg(pip install goodwe[dtls]).keep_alive=True) so the ~1 s handshake is paid once perInverter. Stale-response retries preserve the session; only genuine I/O errors tear it down.Auto-discovery remains plaintext-only, so the family must be specified explicitly when
dtls=True.Tested — three inverters across two families
Both ET-family users independently confirmed: discovery returns
dtls_port:8899, and connecting withfamily="ET", dtls=Trueworks where plaintext timed out.Compatibility
tests/test_dtls.pycover the async wrapper, retry/lock semantics, stale-response handling, and command-construction parity with the UDP path. They mock the blocking DTLS I/O; the real handshake is exercised bytests/manual_dtls_check.py(excluded from the standard run, needs hardware).Open question
Happy to switch the
dtls=Trueboolean to atransport=enum if you'd prefer that for forward-compatibility.Closes #121
Possibly addresses #117, #95, #91