Repository navigation
Ask the doxm lookup first, and say which silent-directory case the bridge saw - #118
Conversation
_advertised_ports logged one sentence for every result carrying no secure port: "directory on 5683 advertised no secure port". Two different outcomes reach that branch. One is a directory that answered and held nothing usable. The other is response_received=False, where nothing came back at all: asleep, silent on the plaintext port, or unreachable from the container. On issue #110 a reporter quoted that line for a dryer that had not answered on 5683, and read it as an appliance with no secure port. The comment above the call said the repr separated the two cases, which put the distinction in the operand list of a sentence that had already made the call. Branch on response_received and name which happened. The repr stays on both, since the attempt count and error code still carry the detail. The test fake tied response_received to the port list, so the two branches could not be told apart in tests; it now takes the flag. Run against the bridge host, two addresses that answer nothing both return response_received=False with error_code='no_ocf_response' -- the result that used to print the wrong sentence.
discover_ocf_secure_ports read the whole /oic/res directory first and fell back to ?rt=oic.r.doxm only when that advertised no secure port. Every request this module sends asks with Accept 60, the OIC 1.1 dialect, which has no eps key -- so the eps entry the unfiltered pass leads with cannot appear in any answer these appliances give. Accept 10000 is the dialect that would carry one, and both appliances here refuse it with 4.06, the oven on plaintext and DTLS alike (2026-09-14). The directory pass was paying for a representation it could not use. Both advertised forms are read from whichever answer arrives, so the order changes no coverage. Measured on the two appliances here on 2026-10-04: the filtered lookup answers in 149 and 147 bytes in a single datagram, where the unfiltered directory takes 1727 and 1629 bytes over two Block2 blocks. Discovery now resolves the same secure port in one request instead of two, on both. Both lookups stay, because a device advertising eps on a link other than doxm is reachable only through the unfiltered directory. The choice is exposed as order, defaulting to doxm-first. The time budget follows the lookup rather than its position: the filtered request is capped at a second, and at half of a shorter timeout, so a directory read behind it keeps the rest of the deadline. Reversing the old rule instead would have left the Block2 transfer the scrap. The docstrings name the dialect each request asks in and record the 4.06, so the next reader does not switch dialect to draw out an eps and remove the directory instead. DtlsCoapSession manages ACCEPT for that same reason, which is now written where it is enforced.
|
Reviewed this against the branch before merging it. The reordering is sound for the two appliances here, and the measurements in the description hold. What it does not account for is the fallthrough rule, which the swap leaves pointing the wrong way. The second lookup is now unreachable for the device it was kept for
That rule arrived with #36, which added this function, and it arrived with a reason: the description lists "malformed, partial, stale-token, and cross-source responses fail closed" under its bounds and safety heading. So the question is what failing closed was taken to mean there, and two other items on that same list answer it. A stale token and a rejected piggyback ACK each discard the response and go on to the second lookup, and the tests added in that commit pin the behaviour by asserting a port comes back over two attempts: So That device is the one three places in this branch say the directory is kept for: the description above, the module docstring at line 24, and This shape is absent from both appliances here. Each answers the filtered lookup with a A slow filtered answer takes the directory down with itLine 913 returns early on The budget makes that reachable.
|
Making ?rt=oic.r.doxm the first lookup left the fallthrough rule pointing the wrong way. discover_ocf_secure_ports went on to the second lookup for an unanswered first request and for a valid representation carrying no usable port, and returned for everything else. That cost nothing while the directory held the veto, because it had already seen every link the device advertises. Behind the narrow lookup it makes a device advertising its only trustworthy eps outside doxm unreachable, which is the device the module docstring and docs/appliance-compatibility.md both say the directory is kept for. A filtered answer that merely ran out of its share of the budget is labelled malformed as well, so that ended the operation the same way, inside a fraction of the deadline. Only a usable port ends the operation now. A malformed transfer, undecodable CBOR and a cross-source eps from the first lookup each record a diagnosis in first_error and go on to the second, which reports it when no port comes back there either. endpoint_unavailable still returns, because sends themselves failed and a second lookup over those routes would ask nothing. #36 added this function and listed "malformed, partial, stale-token, and cross-source responses fail closed" among its bounds, so the early return had a stated intent. Two items on that list fix what it meant: a stale token and a rejected piggyback ACK each discard the response and go on to the second lookup, which the tests added in that commit pin by asserting a port comes back over two attempts. Failing closed there is refusing the content of an answer, and that is what this keeps. Nothing from a failed lookup is carried forward, and every port the second one yields faces the same source check. Two descriptions of the time budget said cap where the second order means floor: the filtered lookup is capped at its share running first, and guaranteed that share running second, because the directory ahead of it stops early to leave it. The order table is selected by value, where tuple identity happened to work only because the table stores one shared object, so a third order written with an inline query literal would have taken the directory's budget in silence. _advertised_ports gains a third branch. endpoint_unavailable with no attempts means the host did not resolve or no route opened, where "no answer from the directory" claims the appliance was asked.
Closes #94. Closes #111.
Two fixes from my own issues, plus a third commit for the fallthrough rule that the doxm-first change left pointing the wrong way. The discovery numbers below were measured on the dryer and the oven here.
Ask the doxm lookup first (#94)
discover_ocf_secure_portsread the whole/oic/resdirectory first and only fell back to?rt=oic.r.doxmwhen that advertised no secure port. Every request the module sends asks with Accept 60, the OIC 1.1 dialect, which has noepskey, so theepsentry the unfiltered pass leads with cannot appear in any answer these two appliances give. Accept 10000 is the dialect that would carry one, and both refuse it with 4.06, the oven on plaintext and DTLS alike (2026-09-14).Both advertised forms are read from whichever representation arrives, so the order changes no coverage. What it changes is the cost, measured 2026-10-04:
GET /oic/resGET /oic/res?rt=oic.r.doxmattempts=1→ 49155attempts=1→ 49154attempts=2→ 49155attempts=2→ 49154Same port either way, in half the requests. One correction to the issue as filed: it described the unfiltered pass as fetching and discarding a 9 KiB directory, a figure vmonkey measured on their board in #36. These two appliances serve under 2 KB, so the saving here is the round trip and not the bytes.
Both lookups stay, because a device advertising
epson a link other than doxm is reachable only through the unfiltered directory. The choice is a neworderparameter, defaulting to'doxm-first', with'directory-first'reversing the two.The filtered lookup's share of
timeoutis one second, or half of a shortertimeout. Running first it is capped there, so the directory behind it keeps the rest of the deadline; running second it is guaranteed that much instead, because the directory ahead of it stops early to leave it. A single rule would be wrong in one of the two orders, and keeping the old one unchanged would have handed the Block2 transfer the scrap.Docstrings name the dialect each request asks in and record the 4.06, so the next reader does not switch dialect to draw out an
epsand remove the directory instead.DtlsCoapSessionmanagesACCEPTfor that same reason, which is now written down where it is enforced.Read the directory when the first lookup answers unusably
Putting the narrow lookup first left it behind the fallthrough rule the old order had written.
discover_ocf_secure_portswent on to the second lookup for an unanswered first request and for a valid representation carrying no usable port, and returned for everything else. That cost nothing while the directory ran first, because the directory had already seen every link the device advertises.Behind the filtered lookup it costs two states. A transfer answered in part and never finished is labelled malformed, which is indistinguishable from bad content at the call site, and the filtered lookup's share is a hard cap when it runs first: a slow narrow answer therefore ended the whole operation inside a fraction of the deadline with the directory never asked. A filtered answer carrying only a cross-source
epsended it the same way. Neither appliance here produced either state in the runs above, where the filtered lookup answered with a usable port on the first attempt, so both are reasoned from the code rather than measured.Only a usable port ends the operation now, apart from
endpoint_unavailable, where the sends themselves failed and a second lookup over those routes would ask nothing. A malformed transfer, undecodable CBOR and a cross-sourceepseach record a diagnosis infirst_errorand go on to the second lookup, which reports it when no port comes back there either.#36 added this function and listed "malformed, partial, stale-token, and cross-source responses fail closed" among its bounds, so the early return had a stated intent. The stale-token item on that list says what it meant:
classify_coap_responsereturnsRESPONSE_IGNOREfor a token mismatch, so a stale answer to the first lookup leaves it recording no response at all, and the second lookup has always run.test_non_request_rejects_piggyback_ack_and_uses_fallbackpins that shape for a non-request response. Failing closed there is refusing the content of an answer rather than abandoning the operation, and that is what this keeps: nothing from a failed lookup is carried forward, and every port the second one yields faces the same source check.Two notes on the section above.
'directory-first'reverses the lookup order and inherits this rule, so under it a partial directory read also goes on to the filtered lookup, where before this PR it returned. Neither value reproduces the old behaviour exactly. And the order table is selected by value, where tuple identity happened to work only because the table stores one shared object, so a third order written with an inline query literal would have taken the directory's budget in silence.Name which silent-directory case the bridge saw (#111)
_advertised_portslogged one sentence for every result carrying no secure port:directory on 5683 advertised no secure port. On #110 that line was quoted for a dryer that had not answered on 5683, where the wording invites the reading that the appliance has no secure port.Three outcomes reach that branch, and it now says which.
attempts == 0separates a request that was never sent, since discovery reportsendpoint_unavailablewith no attempts when the host does not resolve or no route opens.response_receivedthen separates a device that was asked and stayed silent from one that answered carrying no usable secure port. The repr stays on all three.The answered case reads "a lookup on 5683 answered with no secure port". The issue proposed "directory on 5683 advertised no secure port"; with two lookups either one can be the one that answered.
Two addresses on the bridge host that answer nothing return
response_received=Falsewitherror_code='no_ocf_response', the result that used to print the wrong sentence. The test fake tiedresponse_receivedto the port list, so the branches could not be told apart in tests; it now takes the flag and the attempt count.Checks
947 tests pass on 3.12, and the tests covering the files this touches pass on 3.11.
docs/api.mdis regenerated and share safety is clean.The bridge ran against both appliances for the discovery numbers above, at the first two commits: same ports as before, both sessions connected and seeded, 0 errors and 0 timeouts in the first poll window. The third commit changes only the paths where the first lookup fails to yield a port, and on both appliances it yielded one, so that run did not reach it. Tests cover those paths instead.