Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
196d625
update strom dependency which fixes disable remote fingerprint verifi…
timwu20 Oct 7, 2025
b875bbd
change str0m to 0.11.1
timwu20 Oct 20, 2025
925caa9
fix lint
timwu20 Oct 21, 2025
4200246
set and listen on ChannelBufferedAmountLow after stream closure
timwu20 Oct 20, 2025
85fae21
fix: multistream-select negotiation for outbound webrtc substreams
haikoschol Oct 27, 2025
2ec539b
close data channel immediately when substream is closed
haikoschol Nov 12, 2025
4e4a2d3
remove setting of buffered amount low threshold in str0m
haikoschol Nov 12, 2025
7947bf6
remove special case of receiving None instead of a SubstreamEvent
haikoschol Nov 12, 2025
3765d52
move handling of trailing newline to webrtc_listener_negotiate()
haikoschol Nov 12, 2025
fdedc4f
cover all negotiation responses included in tests
haikoschol Nov 13, 2025
1cd945b
Merge branch 'master' into haiko-webrtc-outbound-multistream-nego-fix
haikoschol Nov 13, 2025
6dcb15d
address review feedback
haikoschol Nov 13, 2025
9621e3c
extract loop to drain_trailing_protocols()
haikoschol Nov 13, 2025
daa8f71
fmt
haikoschol Nov 19, 2025
afd44c8
Merge branch 'master' into haiko-webrtc-outbound-multistream-nego-fix
haikoschol Nov 19, 2025
99f6547
consider truncated multistream messages invalid data
haikoschol Nov 19, 2025
c9a0140
Merge branch 'master' into haiko-webrtc-outbound-multistream-nego-fix
haikoschol Nov 25, 2025
90f0c86
Apply suggestions from code review
haikoschol Nov 26, 2025
c0a44c0
avoid Message::Protocols in webrtc_listener_negotiate()
haikoschol Nov 26, 2025
c27930a
remove handling of Event::ChannelBufferedAmountLow
haikoschol Dec 2, 2025
4ed539e
Merge branch 'master' into haiko-webrtc-outbound-multistream-nego-fix
haikoschol Dec 2, 2025
12801fd
Merge branch 'master' into haiko-webrtc-outbound-multistream-nego-fix
haikoschol Dec 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/target
.idea

34 changes: 15 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ rcgen = { version = "0.14.5", optional = true }
# End of Quic related dependencies.

# WebRTC related dependencies. WebRTC is an experimental feature flag. The dependencies must be updated.
str0m = { version = "0.9.0", optional = true }
str0m = { version = "0.11.1", optional = true }
# End of WebRTC related dependencies.

# Fuzzing related dependencies.
Expand Down
198 changes: 141 additions & 57 deletions src/multistream_select/dialer_select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@

use crate::{
codec::unsigned_varint::UnsignedVarint,
error::{self, Error, ParseError},
error::{self, Error, ParseError, SubstreamError},
multistream_select::{
protocol::{
webrtc_encode_multistream_message, HeaderLine, Message, MessageIO, Protocol,
ProtocolError,
ProtocolError, PROTO_MULTISTREAM_1_0,
},
Negotiated, NegotiationError, Version,
},
Expand Down Expand Up @@ -357,24 +357,57 @@ impl WebRtcDialerState {
&mut self,
payload: Vec<u8>,
) -> Result<HandshakeResult, crate::error::NegotiationError> {
let Message::Protocols(protocols) =
Message::decode(payload.into()).map_err(|_| ParseError::InvalidData)?
else {
return Err(crate::error::NegotiationError::MultistreamSelectError(
NegotiationError::Failed,
));
// All multistream-select messages are length-prefixed. Since this code path is not using
// multistream_select::protocol::MessageIO, we need to decode and remove the length here.
let remaining: &[u8] = &payload;
let (len, tail) = unsigned_varint::decode::usize(remaining).map_err(|error| {
tracing::debug!(
target: LOG_TARGET,
?error,
message = ?payload,
"Failed to decode length-prefix in multistream message");
error::NegotiationError::ParseError(ParseError::InvalidData)
})?;

let payload = tail[..len].to_vec();

let message = Message::decode(payload.into());

tracing::trace!(
target: LOG_TARGET,
?message,
"Decoded message while registering response",
);

let mut protocols = match message {
Ok(Message::Header(HeaderLine::V1)) => {
vec![PROTO_MULTISTREAM_1_0]
}
Ok(Message::Protocol(protocol)) => vec![protocol],
Ok(Message::Protocols(protocols)) => protocols,
Ok(Message::NotAvailable) =>
return match &self.state {
HandshakeState::WaitingProtocol => Err(
error::NegotiationError::MultistreamSelectError(NegotiationError::Failed),
),
_ => Err(error::NegotiationError::StateMismatch),
},
Ok(Message::ListProtocols) => return Err(error::NegotiationError::StateMismatch),
Err(_) => return Err(error::NegotiationError::ParseError(ParseError::InvalidData)),
};

match drain_trailing_protocols(tail, len) {
Ok(protos) => protocols.extend(protos),
Err(error) => return Err(error),
}

let mut protocol_iter = protocols.into_iter();
loop {
match (&self.state, protocol_iter.next()) {
(HandshakeState::WaitingResponse, None) =>
return Err(crate::error::NegotiationError::StateMismatch),
(HandshakeState::WaitingResponse, Some(protocol)) => {
let header = Protocol::try_from(&b"/multistream/1.0.0"[..])
.expect("valid multitstream-select header");

if protocol == header {
if protocol == PROTO_MULTISTREAM_1_0 {
self.state = HandshakeState::WaitingProtocol;
} else {
return Err(crate::error::NegotiationError::MultistreamSelectError(
Expand Down Expand Up @@ -405,10 +438,66 @@ impl WebRtcDialerState {
}
}

fn drain_trailing_protocols(
tail: &[u8],
len: usize,
) -> Result<Vec<Protocol>, error::NegotiationError> {
let mut protocols = vec![];
let mut remaining = &tail[len..];

loop {
if remaining.is_empty() {
break;
}

let (len, tail) = unsigned_varint::decode::usize(remaining).map_err(|error| {
tracing::debug!(
target: LOG_TARGET,
?error,
message = ?remaining,
"Failed to decode length-prefix in multistream message");
error::NegotiationError::ParseError(ParseError::InvalidData)
})?;

if len > tail.len() {
tracing::debug!(
target: LOG_TARGET,
message = ?tail,
length_prefix = len,
actual_length = tail.len(),
"Truncated multistream message",
);

return Err(error::NegotiationError::ParseError(ParseError::InvalidData));
}

let payload = tail[..len].to_vec();

match Message::decode(payload.into()) {
Ok(Message::Protocol(protocol)) => protocols.push(protocol),
Err(error) => {
tracing::debug!(
target: LOG_TARGET,
?error,
message = ?tail[..len],
"Failed to decode multistream message",
);
return Err(error::NegotiationError::ParseError(ParseError::InvalidData));
}
_ => return Err(error::NegotiationError::StateMismatch),
}

remaining = &tail[len..];
}

Ok(protocols)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::multistream_select::listener_select_proto;
use crate::multistream_select::{listener_select_proto, protocol::MSG_MULTISTREAM_1_0};
use bytes::BufMut;
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};

Expand Down Expand Up @@ -755,23 +844,18 @@ mod tests {
fn propose() {
let (mut dialer_state, message) =
WebRtcDialerState::propose(ProtocolName::from("/13371338/proto/1"), vec![]).unwrap();
let message = bytes::BytesMut::from(&message[..]).freeze();

let Message::Protocols(protocols) = Message::decode(message).unwrap() else {
panic!("invalid message type");
};
let mut bytes = BytesMut::with_capacity(32);
bytes.put_u8(MSG_MULTISTREAM_1_0.len() as u8);
let _ = Message::Header(HeaderLine::V1).encode(&mut bytes).unwrap();

assert_eq!(protocols.len(), 2);
assert_eq!(
protocols[0],
Protocol::try_from(&b"/multistream/1.0.0"[..])
.expect("valid multitstream-select header")
);
assert_eq!(
protocols[1],
Protocol::try_from(&b"/13371338/proto/1"[..])
.expect("valid multitstream-select header")
);
let proto = Protocol::try_from(&b"/13371338/proto/1"[..]).expect("valid protocol name");
bytes.put_u8((proto.as_ref().len() + 1) as u8); // + 1 for \n
let _ = Message::Protocol(proto).encode(&mut bytes).unwrap();

let expected_message = bytes.freeze().to_vec();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dq: This slightly changes the meaning of the messages.

Before we were expecting:

[Protocols(/multistream/1.0.0), Protocol(/13371338/proto/1)]

Now we are expecting:

// \n added
[Header(/multistream/1.0.0 /\n),

// \n counted but not checked
Protocol(/13371338/proto/1 [])

Hmm, one of those representation can't be spec compliant right?

Also are we ignoring malformated messages:

  • we decode the frame size correctly
  • but we don't check then if the last character is \n or not sufficeint bytes provided?

Copy link
Contributor Author

@haikoschol haikoschol Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the test ensures that the wire format that WebRtcDialerState::propose() produces is what we expect. Before it actually ensured that whatever propose() produces can be decoded with Message::decode(). So I did change the semantics of the test after all. But I would argue that the new meaning is more useful. We could also add decoding and assert we get the expected strings back, but due to the changes made, this can no longer be done by just calling Message::decode().


assert_eq!(message, expected_message);
}

#[test]
Expand All @@ -781,59 +865,59 @@ mod tests {
vec![ProtocolName::from("/sup/proto/1")],
)
.unwrap();
let message = bytes::BytesMut::from(&message[..]).freeze();

let Message::Protocols(protocols) = Message::decode(message).unwrap() else {
panic!("invalid message type");
};
let mut bytes = BytesMut::with_capacity(32);
bytes.put_u8(MSG_MULTISTREAM_1_0.len() as u8);
let _ = Message::Header(HeaderLine::V1).encode(&mut bytes).unwrap();

assert_eq!(protocols.len(), 3);
assert_eq!(
protocols[0],
Protocol::try_from(&b"/multistream/1.0.0"[..])
.expect("valid multitstream-select header")
);
assert_eq!(
protocols[1],
Protocol::try_from(&b"/13371338/proto/1"[..])
.expect("valid multitstream-select header")
);
assert_eq!(
protocols[2],
Protocol::try_from(&b"/sup/proto/1"[..]).expect("valid multitstream-select header")
);
let proto1 = Protocol::try_from(&b"/13371338/proto/1"[..]).expect("valid protocol name");
bytes.put_u8((proto1.as_ref().len() + 1) as u8); // + 1 for \n
let _ = Message::Protocol(proto1).encode(&mut bytes).unwrap();

let proto2 = Protocol::try_from(&b"/sup/proto/1"[..]).expect("valid protocol name");
bytes.put_u8((proto2.as_ref().len() + 1) as u8); // + 1 for \n
let _ = Message::Protocol(proto2).encode(&mut bytes).unwrap();

let expected_message = bytes.freeze().to_vec();

assert_eq!(message, expected_message);
}

#[test]
fn register_response_invalid_message() {
// send only header line
fn register_response_header_only() {
let mut bytes = BytesMut::with_capacity(32);
bytes.put_u8(MSG_MULTISTREAM_1_0.len() as u8);

let message = Message::Header(HeaderLine::V1);
message.encode(&mut bytes).map_err(|_| Error::InvalidData).unwrap();

let (mut dialer_state, _message) =
WebRtcDialerState::propose(ProtocolName::from("/13371338/proto/1"), vec![]).unwrap();

match dialer_state.register_response(bytes.freeze().to_vec()) {
Err(error::NegotiationError::MultistreamSelectError(NegotiationError::Failed)) => {}
Ok(HandshakeResult::NotReady) => {}
Err(err) => panic!("unexpected error: {:?}", err),
event => panic!("invalid event: {event:?}"),
}
}

#[test]
fn header_line_missing() {
// header line missing
let mut bytes = BytesMut::with_capacity(256);
let message = Message::Protocols(vec![
Protocol::try_from(&b"/13371338/proto/1"[..]).unwrap(),
Protocol::try_from(&b"/sup/proto/1"[..]).unwrap(),
]);
message.encode(&mut bytes).map_err(|_| Error::InvalidData).unwrap();
let proto = b"/13371338/proto/1";
let mut bytes = BytesMut::with_capacity(proto.len() + 2);
bytes.put_u8((proto.len() + 1) as u8);

let response = Message::Protocol(Protocol::try_from(&proto[..]).unwrap())
.encode(&mut bytes)
.expect("valid message encodes");

let response = bytes.freeze().to_vec();

let (mut dialer_state, _message) =
WebRtcDialerState::propose(ProtocolName::from("/13371338/proto/1"), vec![]).unwrap();

match dialer_state.register_response(bytes.freeze().to_vec()) {
match dialer_state.register_response(response) {
Err(error::NegotiationError::MultistreamSelectError(NegotiationError::Failed)) => {}
event => panic!("invalid event: {event:?}"),
}
Expand Down
Loading
Loading