-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): derive explicit BiDi loopback connect targets #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
seonghobae
merged 18 commits into
feat/webdriver-bidi-websocket-endpoint
from
feat/webdriver-bidi-explicit-connect-target
Aug 26, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
48ad304
test(core): require explicit BiDi connect target
seonghobae 3cc6711
test(core): format explicit BiDi connect target regressions
seonghobae 2e9898d
feat(core): derive explicit BiDi loopback connect targets
seonghobae aace3a8
feat(core): export explicit BiDi connect target contract
seonghobae 2e43ad6
style(core): format explicit BiDi connect target
seonghobae 6a21bed
test(core): require BiDi connect target changelog evidence
seonghobae 7e30360
docs(core): record explicit BiDi connect target boundary
seonghobae dc5cfd8
test(core): preserve BiDi endpoint across resolver handoff
seonghobae 3a9506c
fix(core): retain correlated BiDi endpoint on resolver handoff
seonghobae d34c528
test(core): require exact BiDi socket peer verification
seonghobae d7cf226
feat(core): verify exact BiDi socket peer
seonghobae 89fc9c3
feat(core): export verified BiDi peer contract
seonghobae f62d204
test(core): require BiDi socket-peer release evidence
seonghobae d52cdfe
docs(changelog): record BiDi socket-peer verification
seonghobae fa510a0
fix(changelog): preserve direct TCP release wording
seonghobae d8f45f8
merge: carry ChromeDriver session compatibility into connect targets
seonghobae 036412a
merge: carry ChromeDriver session compatibility into socket peer veri…
seonghobae 1db9faa
Merge pull request #191 from ContextualWisdomLab/feat/webdriver-bidi-…
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
206 changes: 206 additions & 0 deletions
206
crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| //! Explicit no-DNS connection targets for correlated WebDriver BiDi endpoints. | ||
| //! | ||
| //! This boundary converts only literal loopback listener identities into exact socket metadata. | ||
| //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS | ||
| //! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, | ||
| //! the typed error preserves the correlated endpoint instead of discarding its session evidence. | ||
| //! A separately observed connected peer must also match the approved socket destination exactly | ||
| //! before it becomes verified transport metadata. These values do not open a socket, authenticate | ||
| //! a process, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. | ||
|
|
||
| use std::{ | ||
| fmt, | ||
| net::{Ipv4Addr, Ipv6Addr, SocketAddr}, | ||
| }; | ||
|
|
||
| use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; | ||
|
|
||
| /// An exact loopback socket destination derived from one correlated WebDriver BiDi endpoint. | ||
| /// | ||
| /// The destination is inert connection metadata. It proves only that the already-admitted endpoint | ||
| /// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the | ||
| /// expected WebDriver session id. A runtime connector must independently establish a connection and | ||
| /// verify its observed peer before treating that transport as the approved destination. TLS, | ||
| /// WebSocket, process, policy, and browser authority remain separate boundaries. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct WebDriverBiDiWebSocketConnectTarget { | ||
| socket_addr: SocketAddr, | ||
| requires_tls: bool, | ||
| session_id: String, | ||
| } | ||
|
|
||
| impl WebDriverBiDiWebSocketConnectTarget { | ||
| /// Return the exact loopback socket destination without performing name resolution. | ||
| #[must_use] | ||
| pub const fn socket_addr(&self) -> SocketAddr { | ||
| self.socket_addr | ||
| } | ||
|
|
||
| /// Return whether the admitted endpoint requires a TLS-protected WebSocket transport. | ||
| #[must_use] | ||
| pub const fn requires_tls(&self) -> bool { | ||
| self.requires_tls | ||
| } | ||
|
|
||
| /// Return the exact WebDriver session id established by the preceding correlation boundary. | ||
| #[must_use] | ||
| pub fn session_id(&self) -> &str { | ||
| &self.session_id | ||
| } | ||
|
|
||
| /// Consume this approved destination and verify one observed connected socket peer exactly. | ||
| /// | ||
| /// Matching requires the complete [`SocketAddr`]—IP address and port—to equal the approved | ||
| /// no-DNS destination. A mismatch consumes the target and fails closed, preventing a connector | ||
| /// from accidentally reusing the same authority after observing a different peer. Success | ||
| /// produces inert verified-peer metadata only; it does not authenticate an OS process, | ||
| /// negotiate TLS, perform a WebSocket handshake, or grant browser/Agent authority. | ||
| pub fn verify_connected_peer( | ||
| self, | ||
| observed_peer: SocketAddr, | ||
| ) -> Result<VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError> { | ||
| let expected = self.socket_addr; | ||
| if observed_peer != expected { | ||
| return Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { | ||
| expected, | ||
| actual: observed_peer, | ||
| }); | ||
| } | ||
|
|
||
| Ok(VerifiedWebDriverBiDiSocketPeer { | ||
| connect_target: self, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| /// Inert metadata proving that a connected peer exactly matched the approved BiDi destination. | ||
| /// | ||
| /// This value carries only the destination, TLS requirement, and correlated WebDriver session id | ||
| /// already established by preceding boundaries. It does not prove process identity, TLS peer | ||
| /// identity, WebSocket protocol state, browser authenticity, policy authorization, or Agent action | ||
| /// authority. | ||
| #[derive(Debug, PartialEq, Eq)] | ||
| pub struct VerifiedWebDriverBiDiSocketPeer { | ||
| connect_target: WebDriverBiDiWebSocketConnectTarget, | ||
| } | ||
|
|
||
| impl VerifiedWebDriverBiDiSocketPeer { | ||
| /// Return the exact approved and observed socket peer address. | ||
| #[must_use] | ||
| pub const fn socket_addr(&self) -> SocketAddr { | ||
| self.connect_target.socket_addr() | ||
| } | ||
|
|
||
| /// Return whether the correlated endpoint still requires TLS before WebSocket use. | ||
| #[must_use] | ||
| pub const fn requires_tls(&self) -> bool { | ||
| self.connect_target.requires_tls() | ||
| } | ||
|
|
||
| /// Return the exact correlated WebDriver session id. | ||
| #[must_use] | ||
| pub fn session_id(&self) -> &str { | ||
| self.connect_target.session_id() | ||
| } | ||
| } | ||
|
|
||
| /// Fail-closed errors while verifying an observed BiDi socket peer. | ||
| #[derive(Debug, PartialEq, Eq)] | ||
| pub enum WebDriverBiDiSocketPeerVerificationError { | ||
| /// The connected peer differed from the exact destination approved before connection. | ||
| PeerMismatch { | ||
| /// Exact socket address that the connector was authorized to reach. | ||
| expected: SocketAddr, | ||
| /// Socket peer address observed after connection. | ||
| actual: SocketAddr, | ||
| }, | ||
| } | ||
|
|
||
| impl fmt::Display for WebDriverBiDiSocketPeerVerificationError { | ||
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| match self { | ||
| Self::PeerMismatch { .. } => formatter.write_str( | ||
| "connected WebDriver BiDi socket peer does not match the approved destination", | ||
| ), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl std::error::Error for WebDriverBiDiSocketPeerVerificationError {} | ||
|
|
||
| impl CorrelatedWebDriverBiDiWebSocketEndpoint { | ||
| /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. | ||
| /// | ||
| /// Literal IPv4 and IPv6 loopback hosts become an exact [`SocketAddr`]. Any admitted host that | ||
| /// is not an IP literal—including `localhost`—fails closed so the caller must perform an | ||
| /// explicit, separately trusted name-resolution step rather than inheriting ambient resolver | ||
| /// authority. The name-resolution-required error retains this correlated endpoint so that | ||
| /// trusted resolver handoff does not require reconstructing or recorrelation of session evidence. | ||
| /// This method performs no DNS lookup, socket I/O, peer authentication, TLS, or WebSocket | ||
| /// handshake. | ||
| pub fn into_explicit_connect_target( | ||
| self, | ||
| ) -> Result<WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError> { | ||
| let socket_addr = if let Ok(ipv4) = self.host().parse::<Ipv4Addr>() { | ||
| SocketAddr::from((ipv4, self.port())) | ||
| } else if let Ok(ipv6) = self.host().parse::<Ipv6Addr>() { | ||
| SocketAddr::from((ipv6, self.port())) | ||
| } else { | ||
| return Err( | ||
| WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired { | ||
| correlated_endpoint: self, | ||
| }, | ||
| ); | ||
| }; | ||
|
seonghobae marked this conversation as resolved.
|
||
|
|
||
| Ok(WebDriverBiDiWebSocketConnectTarget { | ||
| socket_addr, | ||
| requires_tls: self.is_secure(), | ||
| session_id: self.session_id().to_owned(), | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| /// Fail-closed errors while deriving an explicit WebDriver BiDi socket destination. | ||
| #[derive(Debug, PartialEq, Eq)] | ||
| pub enum WebDriverBiDiWebSocketConnectTargetError { | ||
| /// The admitted endpoint used a host name and therefore requires explicit trusted resolution. | ||
| NameResolutionRequired { | ||
| /// The still-correlated endpoint that must be handed to a separately trusted resolver. | ||
| correlated_endpoint: CorrelatedWebDriverBiDiWebSocketEndpoint, | ||
| }, | ||
| } | ||
|
|
||
| impl WebDriverBiDiWebSocketConnectTargetError { | ||
| /// Borrow the correlated endpoint preserved for an explicit trusted resolver handoff. | ||
| #[must_use] | ||
| pub const fn correlated_endpoint(&self) -> &CorrelatedWebDriverBiDiWebSocketEndpoint { | ||
| match self { | ||
| Self::NameResolutionRequired { | ||
| correlated_endpoint, | ||
| } => correlated_endpoint, | ||
| } | ||
| } | ||
|
|
||
| /// Recover the correlated endpoint for an explicit trusted resolver handoff. | ||
| #[must_use] | ||
| pub fn into_correlated_endpoint(self) -> CorrelatedWebDriverBiDiWebSocketEndpoint { | ||
| match self { | ||
| Self::NameResolutionRequired { | ||
| correlated_endpoint, | ||
| } => correlated_endpoint, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Display for WebDriverBiDiWebSocketConnectTargetError { | ||
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| match self { | ||
| Self::NameResolutionRequired { .. } => formatter.write_str( | ||
| "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution", | ||
| ), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl std::error::Error for WebDriverBiDiWebSocketConnectTargetError {} | ||
107 changes: 107 additions & 0 deletions
107
crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| use std::{error::Error, net::SocketAddr}; | ||
|
|
||
| use originweave_core::{ | ||
| CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiSocketPeerVerificationError, | ||
| WebDriverBiDiWebSocketEndpoint, | ||
| }; | ||
|
|
||
| const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; | ||
|
|
||
| fn connect_target(endpoint: &str) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { | ||
| let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); | ||
| assert!(admitted.is_ok(), "{admitted:?}"); | ||
| let Ok(admitted) = admitted else { | ||
| unreachable!("asserted valid endpoint") | ||
| }; | ||
|
|
||
| let correlated: Result<CorrelatedWebDriverBiDiWebSocketEndpoint, _> = | ||
| admitted.correlate_session_id(SESSION_ID); | ||
| assert!(correlated.is_ok(), "{correlated:?}"); | ||
| let Ok(correlated) = correlated else { | ||
| unreachable!("asserted correlated endpoint") | ||
| }; | ||
|
|
||
| let target = correlated.into_explicit_connect_target(); | ||
| assert!(target.is_ok(), "{target:?}"); | ||
| let Ok(target) = target else { | ||
| unreachable!("asserted literal loopback target") | ||
| }; | ||
| target | ||
| } | ||
|
|
||
| #[test] | ||
| fn exact_connected_peer_becomes_verified_transport_metadata() { | ||
| let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); | ||
| let target = connect_target(&endpoint); | ||
| let peer = SocketAddr::from(([127, 0, 0, 1], 9443)); | ||
|
|
||
| let verified = target.verify_connected_peer(peer); | ||
| assert!(verified.is_ok(), "{verified:?}"); | ||
| let Ok(verified) = verified else { | ||
| return; | ||
| }; | ||
|
|
||
| assert_eq!(verified.socket_addr(), peer); | ||
| assert!(verified.requires_tls()); | ||
| assert_eq!(verified.session_id(), SESSION_ID); | ||
| } | ||
|
|
||
| #[test] | ||
| fn connected_peer_with_wrong_port_fails_closed() { | ||
| let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); | ||
| let target = connect_target(&endpoint); | ||
| let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); | ||
|
|
||
| let result = target.verify_connected_peer(actual); | ||
| assert_eq!( | ||
| result, | ||
| Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { | ||
| expected: SocketAddr::from(([127, 0, 0, 1], 9515)), | ||
| actual, | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn connected_peer_with_different_address_fails_closed() { | ||
| let endpoint = format!("ws://[::1]:9515/session/{SESSION_ID}"); | ||
| let target = connect_target(&endpoint); | ||
| let actual = SocketAddr::from(([127, 0, 0, 1], 9515)); | ||
|
|
||
| let result = target.verify_connected_peer(actual); | ||
| assert!(matches!( | ||
| result, | ||
| Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_loopback_observed_peer_cannot_inherit_approved_loopback_authority() { | ||
| let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); | ||
| let target = connect_target(&endpoint); | ||
| let actual = SocketAddr::from(([192, 0, 2, 10], 9515)); | ||
|
|
||
| let result = target.verify_connected_peer(actual); | ||
| assert!(matches!( | ||
| result, | ||
| Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn peer_mismatch_error_is_deterministic_and_source_free() { | ||
| let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); | ||
| let target = connect_target(&endpoint); | ||
| let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); | ||
|
|
||
| let result = target.verify_connected_peer(actual); | ||
| let Err(error) = result else { | ||
| return; | ||
| }; | ||
|
|
||
| assert_eq!( | ||
| error.to_string(), | ||
| "connected WebDriver BiDi socket peer does not match the approved destination" | ||
| ); | ||
| assert!(error.source().is_none()); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.