Skip to content

Build the DTLS connection at one site per caller - #120

Merged
QuiteYellow merged 2 commits into
mainfrom
feat/dtls-connection-seam
Oct 6, 2026
Merged

QuiteYellow merged 2 commits into
mainfrom
feat/dtls-connection-seam

Conversation

@QuiteYellow

@QuiteYellow QuiteYellow commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

A session built SSL.Context, called configure_context and built SSL.Connection inline in connect(), and the diagnostic did the same again in its own function. Reaching any engine other than pyOpenSSL from there costs an isinstance fork at both sites and a third in the reader loop, which is what the closed #115 had to do.

Both sites now go through one function that asks the provider for a connection and falls back to the context path. No provider carries a factory yet, so this changes no behaviour: the existing suite passes untouched, plus 9 tests of which 7 fail without the change.

The factory is private, and deliberately not a verb on AuthenticationProvider. That Protocol is runtime_checkable and __init__ gates on isinstance (dtls_session.py:549), so a required method would reject every third-party provider that has not grown one. @Jason-Morcos raised this on #117 and it is his call that shaped it; a public connection-provider interface stays available later as a separate opt-in thing. getattr dispatch on a private name is the existing pattern here rather than a new one, since connect() already looks up _authenticated_server_identity that way.

Two orderings matter and both have a test:

  • The session checks cancellation before and after the provider's work, never inside it. A configure_context already blocked on a PEM read runs to completion; what the checks guarantee is that no socket is created once cancellation is set. Two tests through the public connect() pin that. The certificate path is otherwise a verbatim move — traced against main with a real CertificateAuth, the call sequence is 16 identical steps.
  • The diagnostic asks the factory before the context path, because _validate_diagnostic_auth only duck-checks configure_context and a provider owning its own engine keeps that method. The order is the only thing deciding which engine sees the credential, and a session-working credential failing in the diagnostic would break what 47e09b3 added auth= for.

_client_hello_flight stays on OpenSSL: it takes no provider, and a credential's engine would turn a stateless liveness probe into a handshake attempt against the appliance.

Worth having whichever engine ships, which is why it is separate. feat/psk-engine-wiring stacks the PSK engine on top of this.

A session built SSL.Context, called configure_context and built
SSL.Connection inline in connect(), and the diagnostic did the same thing
again in its own function. Reaching any engine other than pyOpenSSL from
there means an isinstance fork at both sites and a third in the reader
loop, which is what the closed #115 had to do.

So both sites now go through one function that asks the provider for a
connection first and falls back to the context path. No provider carries a
factory yet, so nothing changes behaviour: the existing 947 tests pass
untouched.

The factory is private and deliberately not a verb on
AuthenticationProvider. That Protocol is runtime_checkable and __init__
gates on isinstance, so a required method would reject every third-party
provider that has not grown one -- Jason-Morcos's objection on #117, and
the reason Phase 0 of the plan is not being followed as written. A public
connection-provider interface stays available as a separate opt-in thing.

Two orderings matter and both have a test. The session checks cancellation
around the provider's work rather than inside it, so a configure_context
that loads a PEM chain off disk stays interruptible. The diagnostic asks
the factory before the context path, because _validate_diagnostic_auth
only duck-checks configure_context and a provider owning its own engine
keeps that method -- so the order is the only thing deciding which engine
sees the credential, and a session-working credential failing in the
diagnostic would break what 47e09b3 added auth= for.

_client_hello_flight stays on OpenSSL: it takes no provider, and a
credential's engine would turn a stateless liveness probe into a handshake
attempt.
@QuiteYellow

Copy link
Copy Markdown
Owner Author

@Jason-Morcos This is the seam from your note on #117, separated so it lands before anything is wired to it. Details are in issuecomment-6001440451; nothing here needs re-reading from that.

One judgement call in it is mine, and it is cheap to change now and awkward once the wiring stacks on top. You said a small internal factory. I read that as dispatch: getattr(self.auth, "_create_dtls_connection", None), which routes any provider that grows the attribute, built-in or not. The other reading is an explicit isinstance against the providers this package ships, keeping the path closed until a public interface opens it deliberately. I chose getattr because connect() already looks up _authenticated_server_identity that way, so it matches the file — but matching an existing pattern is not the same as deciding who should be able to reach the seam, and that decision is yours to make.

Which did you mean?

@Jason-Morcos

Copy link
Copy Markdown
Contributor

@QuiteYellow I meant the getattr dispatch you implemented. I'd keep it, rather than add a built-in-class allowlist. A third-party provider that deliberately supplies this private hook can use it; a provider implementing only configure_context() keeps working. That doesn't make the hook a supported public extension API, and we can still design one separately later.

The contract I'd spell out is: return a fresh, client-ready, socket-free connection for this attempt, with the requested MTU applied, and leave sockets, cancellation/deadline handling and session publication to the caller. The hook shouldn't start network I/O. That also makes the existing exception/callback expectations clear for anyone experimenting with another engine.

I reviewed f5cbde2 and ran 207 focused tests across the seam, diagnostic, auth/public API, certificate, cancellation, deadline and shutdown paths. All passed. I also exercised two extra cases through public connect() with a factory provider: cancellation during factory setup, and setup consuming the entire deadline. Both stopped before socket creation. The unchanged certificate path and diagnostic factory-first ordering look right; I don't see a blocker in this PR.

One small wording fix: the docstring says a slow configure_context() “stays interruptible.” We check cancellation after it returns, before networking; we don't interrupt a blocked PEM read/provider call. I'd describe it that way. That's a documentation correction, not a behavior regression introduced here.

Two corrections from @Jason-Morcos's review of f5cbde2 on #120.

The docstring said a slow configure_context "stays interruptible". It does
not: the checks run before and after the provider's work, so a call already
blocked on a PEM read is never interrupted. What they do guarantee is that
cancellation set during that work stops the attempt before any socket
exists -- narrower, and true. Two tests through the public connect() pin it,
cancellation inside the factory and a factory that eats the whole deadline,
each failing if a socket is opened. He had verified both by hand; this is
them written down.

The hook also had no written contract, leaving anyone trying another engine
to infer it from the pyOpenSSL path. Now stated: return a fresh connection
already in client state with mtu applied, own no socket, start no network
I/O, and raise like the memory-BIO subset of SSL.Connection that
_drive_dtls_handshake drives. The caller keeps the socket, the deadline and
cancellation handling, and session publication.

Also drops "built-in" from the description of which providers the getattr
dispatch reaches. He confirmed getattr is what he intended and that a
third-party provider deliberately supplying the private hook may use it, so
describing it as built-in-only was wrong. It stays unsupported as an
extension API; a real one gets designed separately.
@QuiteYellow

Copy link
Copy Markdown
Owner Author

Addressed in b3e2d81. You were right that "stays interruptible" overstated it: the wording now says what the checks promise, which is that cancellation set during a provider's work stops the attempt before a socket exists, while a call already blocked on a PEM read runs to completion. Your two cases are tests rather than a manual check, and the hook contract is in the docstring in the terms you gave it. The PR body carried the same wrong claim and is corrected too.

Full detail on #117, since the cookie correction belonged there anyway.

@QuiteYellow
QuiteYellow merged commit 22cbe18 into main Oct 6, 2026
8 checks passed
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