Skip to content

feat(protocol): discover advertised OCF secure ports - #36

Open
atc722 wants to merge 2 commits into
QuiteYellow:mainfrom
atc722:agent/ocf-secure-endpoint-discovery
Open

feat(protocol): discover advertised OCF secure ports#36
atc722 wants to merge 2 commits into
QuiteYellow:mainfrom
atc722:agent/ocf-secure-endpoint-discovery

Conversation

@atc722

@atc722 atc722 commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • add a bounded known-host discover_ocf_secure_ports() API for device-advertised OCF secure ports
  • build discovery on the shared CoAP GET, response-classification, option, and Block2 machinery in protocol/coap.py
  • query unfiltered /oic/res first for source-bound coaps endpoints, then fall back to /oic/res?rt=oic.r.doxm within the same monotonic deadline
  • keep advertisements as candidates only; DTLS liveness and authenticated device identity remain separate checks

What directory discovery adds

probe_dtls_ports() verifies only the candidate ports supplied by its caller. Directory discovery obtains the ports advertised by the device itself, including valid dynamic ports outside a caller's fixed candidate band, and then lets the caller pass those candidates to the existing stateless probe.

This distinction is also visible in the current issue #16 evidence: the advertised secure endpoint can fall outside both the standard OCF port and the bridge's fixed dynamic candidates. Discovery therefore generates candidates; probing verifies listeners. Neither step authenticates a device by itself.

Scope and WD86 context

Following the maintainer feedback, this revision contains only the known-host unicast API. The identity-aware multicast API has been removed from this PR and preserved on atc722:agent/ocf-identity-multicast-held for a later proposal with a concrete caller.

In this discussion, WD86 is shorthand for the observed internal SmartThings/firmware family AWM-KR-M64-24-WD86 of a Samsung washer-dryer combo (oic.d.washer); it is not presented as a retail model number. Redacted testing showed that its OCF host can expose multiple logical device containers, so exact di-to-source-to-endpoint binding is still required by that downstream target-selection path. The package does not yet have a production caller for that multicast API, which is why it is held rather than included here. Device-specific addresses, UUIDs, and advertised port values are intentionally omitted.

Bounds and safety

  • one 3-second default monotonic deadline shared by the primary lookup, fallback, and retries
  • fresh token, Block2 accumulator, and peer pin for the fallback transfer
  • response-source-bound IP-literal coaps endpoints; no DNS lookup of an advertised hostname
  • at most 32 Block2 blocks and 64 KiB of accumulated payload
  • accepted separate CON responses are acknowledged; malformed, partial, stale-token, and cross-source responses fail closed
  • addresses, tokens, UUIDs, ports, and payload bytes stay out of result and internal-state representations
  • no multicast discovery, DTLS probe, authentication attempt, ownership transfer, credential acquisition, control command, or OCF security-resource write

Validation

  • python -m pytest -q: 239 passed locally on Python 3.13
  • Ruff check on all changed Python files
  • git diff --check origin/main...HEAD
  • python tools/check_share_safety.py --changed-since origin/main
  • wheel and sdist build, distribution-content check, and isolated imports from both artifacts

The repository CI matrix remains responsible for Python 3.11, 3.12, and 3.14 coverage on the updated head.

@QuiteYellow

Copy link
Copy Markdown
Owner

Thanks for taking this on.

Two things I'd like to work through before merging, both about scope rather than correctness.

The CoAP layer

ocf_discovery.py carries its own CoAP client: _ResponseBlock, _MAX_BLOCKS = 32, _build_request, _decode_response_block, _option_values, _decode_uint_option. Most of that already exists in protocol/coap.py (build_coap, parse_coap, encode_options, block_value), and the Block2 reassembly loop is a second copy of what DtlsCoapSession.get() does.

Two CoAP implementations in one package means every wire-level fix has to be made twice, and the Samsung-specific ones are the ones that hurt. Token-stable Block2 and the ACK-plus-separate-CON pattern took a long time to get right in the session; a second parser that hasn't met those devices will rediscover them the hard way. Can the discovery path build on coap.py for encode and decode, and keep only the parts that are actually specific to discovery (multicast socket setup, response collection, di matching)?

Two entry points, and what they add over probing

The PR ships discover_ocf_secure_ports() and discover_ocf_secure_ports_multicast() as independent APIs. The reference implementations expose one discovery call with a filter: iotivity-lite does multicast or unicast GET /oic/res, reads the eps array off the returned links, and picks the coaps:// endpoint. Selecting on ?rt=oic.r.doxm is a reasonable fallback for a device that only advertises eps on its security resources, but I'd expect eps to be the primary path and doxm the fallback, not the other way round.

probe_dtls_ports() (added in 1a35cd5) already races the OCF port band and resolves a live secure port in under a second, and the bridge uses it for this today. What does directory discovery do that probing doesn't? Whatever the answer is, put it in the module docstring: it's the reason we'd carry two mechanisms instead of one.

What I'd like to do

Land the known-host unicast path on its own, built on coap.py, reading eps first with the doxm query as fallback. Hold the multicast identity path until something calls it. That gets a smaller change reviewed properly and gives issue #16 something to test against, and the multicast half stays on the branch rather than being thrown away.

Your bounds work is the strongest part of this and I'd keep all of it: the shared deadline across retries, the per-round datagram cap, the exact normalized di match with no pi fallback, and treating advertisements as candidates that still require a stateless DTLS check. Keeping addresses and UUIDs out of the result reprs matches what endpoint.py does and I'm glad you followed it without being asked.

Happy to look at a reduced version whenever you have time. If you'd rather I did the coap.py rewiring myself and pushed it to your branch, say so and I will.

@atc722
atc722 force-pushed the agent/ocf-secure-endpoint-discovery branch from 1ca0875 to a9b7af6 Compare August 15, 2026 02:25
@atc722 atc722 changed the title feat(protocol): discover advertised OCF secure endpoints feat(protocol): discover advertised OCF secure ports Aug 15, 2026
@atc722

atc722 commented Aug 15, 2026

Copy link
Copy Markdown
Author

Thanks — I followed the reduced-scope direction and updated the branch myself.

  • The PR now exposes only the known-host unicast API. The identity-aware multicast work is preserved on atc722:agent/ocf-identity-multicast-held and will wait for a concrete consumer.
  • The branch is rebuilt on the current main / v0.1.6.
  • CoAP GET construction, response classification, ACK-plus-separate-CON handling, and bounded token-stable Block2 assembly now live in protocol/coap.py and are shared with DtlsCoapSession.
  • Discovery now reads secure, source-bound eps entries from unfiltered GET /oic/res first, and uses GET /oic/res?rt=oic.r.doxm only as a legacy fallback. Both stages share one monotonic deadline.
  • The module docstring now explains the distinction from probe_dtls_ports(): directory discovery learns advertised candidates that the caller may not know to probe, while probing only validates the explicit candidates it receives. Advertisements remain candidates and still require stateless DTLS and authenticated device-identity checks before use.

One clarification on the held multicast path: WD86 is shorthand here for the observed internal SmartThings/firmware family AWM-KR-M64-24-WD86 of a Samsung washer-dryer combo (oic.d.washer), not a retail model number. Redacted testing showed multiple logical OCF device containers on its host, so exact di-to-source-to-endpoint binding remains necessary for that downstream target-selection path. I agree that the public API should return with that concrete caller rather than remain in this PR. Device-specific addresses, UUIDs, and port values are omitted.

Validation on the updated head:

  • 239 tests passed locally on Python 3.13
  • Ruff, git diff --check, and share-safety passed
  • exact-head wheel/sdist build, distribution check, and isolated imports passed

The new GitHub Actions run is currently awaiting maintainer approval, so I am not carrying forward the previous head's 8/8 result.

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.

2 participants