Skip to content
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

combine_dtls_fragments helper #48

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

algesten
Copy link
Contributor

@algesten algesten commented Feb 3, 2024

# REQUIRES #47 TO LAND FIRST #

This PR builds upon #47. The aim is to provide a correct helper that combines DTLS fragments into a full message. The test case illustrates how the parser + combine helper are expected to be used together.

@algesten algesten marked this pull request as draft February 3, 2024 14:31
@algesten
Copy link
Contributor Author

algesten commented Feb 3, 2024

Will rebase when/if #47 lands.

@algesten algesten force-pushed the feature/combine-dtls-fragments branch 5 times, most recently from 8970f45 to 6a5f3d2 Compare February 3, 2024 15:25
@chifflier chifflier self-assigned this Mar 1, 2024
src/dtls_combine.rs Outdated Show resolved Hide resolved
@algesten algesten force-pushed the feature/combine-dtls-fragments branch from 6a5f3d2 to fdf3420 Compare April 23, 2024 10:06
@algesten algesten marked this pull request as ready for review April 23, 2024 10:12
@algesten
Copy link
Contributor Author

I believe this is ready to merge now.

/// 5. The DTLSMessageHandshakeBody in the message is not a Fragment
/// 6. The message_seq differs between the fragments.
///
/// Panics if there are more than 50 fragments.
Copy link
Member

Choose a reason for hiding this comment

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

(minor) This comment is not correct now, it will return an error (and not panic)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep will fix. Following the logic (if we keep that), this should be error case 7 or something.

Comment on lines +55 to +56
// Unwrap is OK, because we have at least one item (checked above).
let first_handshake = ordered.iter().next().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

(minor) The test is not obvious, since it relies on Ordered keeping the same number of items as fragments. A test here (even if redundant) would not be bad to avoid unwrap, and would not cause performance problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright. It's an invariant, in the sense that if I got this assumption wrong, there's something more serious wrong (bug). I generally don't like masking those as errors, as it would appear the user provided incorrect data when it's actually a bug in this code.

But happy to change to error.

.get(*idx)
.map(|h| h.fragment_offset)
// Somewhere outside the fragments length.
.unwrap_or(*idx as u32 + 1000)
Copy link
Member

Choose a reason for hiding this comment

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

What is this value? It seems dangerous to return a random offset if the fragment with this ID does not exist.
Should this return an error? Or is this a magic value?

Also, note that Option has a method map_or_else that could be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. Poor logic here. It's another invariant. idx comes from order which means it's impossible for .get(*idx) to return None. Probably better to change this to a direct array reference fragments[idx].fragment_offset

I fix.

Copy link
Member

Choose a reason for hiding this comment

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

This is a consequence of not using a sorted list.
Well, even if there is an invariant, it is not described (and a minimum would be to use assert/debug_assert to state it).

Ok((rest, Some(message)))
}

struct Ordered<'a, 'b>([usize; MAX_FRAGMENTS], &'a [DTLSMessageHandshake<'b>]);
Copy link
Member

Choose a reason for hiding this comment

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

I am missing the entire logic of this structure, this may require some more explanations.
As far as I understand, the field 0 is the sorted list of indices, the fragments being in the field 1.

Why not create and use directly a list of sorted fragments ? Or, to better deal with lifetimes, references to fragments ?

You would just have to iterate the list of fragments, and insert them in a sorted structure (list, tree, etc).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal here is to avoid an extra allocation. The user provides &[DTLSMessageHandshake], which could have been received in any order (given this is UDP). The order we want is in the fragment_offset field of each message. Thus, the Ordered iterator iterates the messages without any extra heap allocation.

Personally I think this is better than allocating, but I should provide this motivation as code doc. However if you prefer new Vec, I can make that too.

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for clarifying. The methods seems globally fine, and there is no need to switch to an allocation.

That said, I think the code needs some comments to be more readable later. This is my main concern.
Also, the fields could be named (for ex indices/fragments) for clarity, and avoid .0 and .1.

Comment on lines +95 to +108
if msg_type != handshake.msg_type {
// Error case 2
return Err(Err::Error(make_error(&*out, ErrorKind::Fail)));
}

if handshake.length != length {
// Error case 3
return Err(Err::Error(make_error(&*out, ErrorKind::Fail)));
}

if handshake.message_seq != message_seq {
// Error case 6
return Err(Err::Error(make_error(&*out, ErrorKind::Fail)));
}
Copy link
Member

Choose a reason for hiding this comment

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

This method will return Fail for every possible error, which will make debugging complicated. Instead, this could justify returning a different error kind. Maybe the context feature could help?

Copy link
Member

Choose a reason for hiding this comment

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

replying to the context suggestion: this may not easy to use here. Maybe a custom error type would be better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What if a made a custom error type with a subenum for the errors? Something like:

ErrorKind::DtlsCombine(DtlsCombineError)

Copy link
Member

Choose a reason for hiding this comment

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

The usual method in nom is to create a custom error type, an implement From<ErrorKind> and ParseError. See x509-parser errors for an example.

But, please wait before changing errors, because this will add code, and this can be done in a second step

@chifflier
Copy link
Member

I believe this is ready to merge now.

Hi,
After review I need more input on the choices you made (see above comments). The most important questions are on the Ordered type, others can be solved separately.
Thanks!

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