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

Fix panic on STUN binding failure while multiplexing #488

Merged
merged 2 commits into from
Mar 27, 2024

Conversation

OxleyS
Copy link
Contributor

@OxleyS OxleyS commented Mar 21, 2024

An unrelated bug I had in my application was causing STUN to fail, and a Class::Failure message to come back. Because like in the chat example, I'm still using a single multiplexed UDP socket, this message first went to the Rtc::accepts() of a separate client that had not negotiated, which caused it to panic with "Remote ICE Credentials".

This is the same underlying panic as in #461, but this time the logic was allowed to fall through to the stun_credentials() call because we were only doing the preceding candidate-pair test for Class::Success responses, not Class::Failure.

I wanted to add a test for this, but with the current field visibility on StunMessage, I couldn't create a failure message in a test. I wasn't sure if adding a helper to impl StunMessage was appropriate.

@algesten
Copy link
Owner

Which panic are you hitting?

@OxleyS
Copy link
Contributor Author

OxleyS commented Mar 21, 2024

This one, because it is a STUN response we hit the else branch here, which expects remote credentials.

@algesten
Copy link
Owner

I think the correct fix would be:

--- a/src/ice/agent.rs
+++ b/src/ice/agent.rs
@@ -799,6 +799,9 @@ impl IceAgent {
                 trace!("Message rejected, transaction ID does not belong to any of our candidate pairs");
                 return false;
             }
+        } else {
+            trace!("Message reject, it is not a succesful binding response");
+            return false;
         }
 
         let (_, password) = self.stun_credentials(!message.is_response());

@OxleyS
Copy link
Contributor Author

OxleyS commented Mar 21, 2024

That patch would end up rejecting everything that's not a successful binding response, since all other message types, including things like binding requests, would fall through to that else case as well.

I believe the effect you intended was to just reject all non-success binding responses? That would also fix the panic, although arguably IceAgent should be accepting all traffic intended for it, even if it doesn't react to failed responses in any special way.

(Aside: Class::Indication and Class::Unknown wouldn't trigger panics with either of our solutions).

@algesten
Copy link
Owner

Taking a step back.

accept should, as you say, accept all traffic targeted at this Rtc instance.

@OxleyS OxleyS force-pushed the fix-stun-failure-panic branch 2 times, most recently from 15efbc4 to cfd034b Compare March 25, 2024 12:31
@OxleyS
Copy link
Contributor Author

OxleyS commented Mar 25, 2024

I think we have our bases covered here then - STUN requests get handled as before, and now both STUN success and failure gets accepted if it corresponds to a STUN request from us (or rejected before the credentials check if not).

@algesten
Copy link
Owner

I still don't like this change.

What kind of responses are we talking about that we suddenly accept? What about middleboxes? We should be very specific with what we accept. Enumerate the cases.

@OxleyS
Copy link
Contributor Author

OxleyS commented Mar 26, 2024

I believe this enumeration is exhaustive:

  1. Method::Binding, Class::Request: Unchanged, rejects on a local/remote ufrag mismatch and then checks integrity. Cannot panic because it does not require remote credentials to integrity-check.
  2. Method::Binding, Class::Success: Unchanged, rejects if the transaction ID matches no binding request from us, then checks integrity. If the transaction ID matches, the integrity-check would panic without remote credentials, but we do not generate binding requests without remote credentials, so panic is effectively impossible.
  3. Any method, Class::Failure: Changed, previous behavior would only check integrity, panicking if remote credentials have not been set. New behavior matches the Class::Success case.
  4. Method::Unknown, Class::Success: Changed, both previous and new behavior match the Class::Failure case.
  5. Method::Unknown, Class::Request: Unchanged, only checks integrity. Cannot panic because it does not require remote credentials to integrity-check.
  6. Any method, Class::Indication or Class::Unknown: Unchanged, only checks integrity. Cannot panic because it does not require remote credentials to integrity-check.

This PR was aimed at fixing the (Method::Binding, Class::Failure) case specifically, but it seems both (Method::Unknown, Class::Success) and (Method::Unknown, Class::Failure) had the same panic bug. This PR unintentionally incidentally prevents these two from panicking as well, by subjecting them to the same transaction-ID-match test as a binding success would be. It's hard to have a good answer for what to do with Method::Unknown, but this seems like a reasonable-enough way to handle them.

Considering the confusion that seems to exist around the exact behavior of accepts() (and I made several mistakes in the process of putting the above enumeration together!), perhaps we should consider rewriting this method to use an exhaustive match, even if it means we can't use is_response() and similar utilities.

@algesten
Copy link
Owner

Considering the confusion that seems to exist around the exact behavior of accepts() (and I made several mistakes in the process of putting the above enumeration together!), perhaps we should consider rewriting this method to use an exhaustive match, even if it means we can't use is_response() and similar utilities.

Agree. I also think this function should default to false at the end and explicitly enumerate the cases where we do accept the message. So it's the opposite of today really.

@OxleyS
Copy link
Contributor Author

OxleyS commented Mar 26, 2024

Okay, I'll take a stab at doing that tomorrow.

src/ice/agent.rs Show resolved Hide resolved
let method = message.method();
let class = message.class();
match (method, class) {
(StunMethod::Binding, StunClass::Request | StunClass::Indication) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RFC 5389 indicates that requests and indications should be validated in a similar manner.

Comment on lines +829 to +838
(StunMethod::Binding, StunClass::Unknown) => {
// Without a known class, it's impossible to know how to validate the message
trace!("Message rejected, unknown STUN class");
false
}
(StunMethod::Unknown, _) => {
// Without a known method, it's impossible to know how to validate the message
trace!("Message rejected, unknown STUN method");
false
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are behavior changes from my previous enumeration. Since processing is so method and class-dependent, not knowing either means we can't really know how to even verify that this message is valid, or for this instance.

src/io/mod.rs Show resolved Hide resolved
src/io/stun.rs Show resolved Hide resolved
src/io/stun.rs Show resolved Hide resolved
Copy link
Owner

@algesten algesten left a comment

Choose a reason for hiding this comment

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

Looks great!

Thank you for doing this!

Small question

src/ice/agent.rs Show resolved Hide resolved
}
}
(StunMethod::Binding, StunClass::Success | StunClass::Failure) => {
let belongs_to_a_candidate_pair = self
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe we should check nominated pair here first, to make a less compute intensive happy path?

Copy link
Contributor Author

@OxleyS OxleyS Mar 27, 2024

Choose a reason for hiding this comment

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

Ah, yes, you're right! We even talked about that in the other PR, I guess it got lost amongst the other discussion happening there. Taking a second look, the discussion in the other PR was for Rtc::accepts(), not IceAgent::accepts_message(), and that fast path is indeed being used there.

I'll do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've looked deeper into the code, and while adding this fast path is possible, it's surprisingly risky and would be better left to a separate PR, if we deem it necessary at all.

The issue is that our nominated pair IceAgent.nominated_send is not an index into the pairs array, but rather a PairId. Resolving that to an index would require a linear scan of the pairs array anyway, negating any benefits of checking it first. Storing an index instead would be very difficult because we sort pairs by priority and prune failed pairs often, making the bookkeeping very error-prone.

Choose a reason for hiding this comment

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

Alright! Thanks for checking!

@algesten algesten merged commit ddbbe2f into algesten:main Mar 27, 2024
22 checks passed
@OxleyS OxleyS deleted the fix-stun-failure-panic branch March 27, 2024 08:42
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.

None yet

3 participants