-
Notifications
You must be signed in to change notification settings - Fork 124
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
refactor(client): replace mio with tokio #1612
Conversation
- Use `tokio` instead of `mio`. - Refactor `neqo-client` to be more consistent with `neqo-server`. - Introduce `read_datagram` and `ready`. - Introduce `ClientRunner` and `old::ClientRunner` (consistent with `ServersRunner`). - Fold `handle_test` into `client` (now `ClientRunner::new`).
neqo-http3/src/client_events.rs
Outdated
@@ -268,6 +268,7 @@ impl Http3ClientEvents { | |||
|
|||
/// Add a new `AuthenticationNeeded` event | |||
pub(crate) fn authentication_needed(&self) { | |||
self.remove(|evt| matches!(evt, Http3ClientEvent::AuthenticationNeeded)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running the http3
QUIC interop test, there is a race condition. Two AuthenticationNeeded
events are enqueued.
The neqo-client
handler handles both events.
Lines 727 to 729 in 9489511
Http3ClientEvent::AuthenticationNeeded => { | |
client.authenticated(AuthenticationStatus::Ok, Instant::now()); | |
} |
On the first call to client.authenticated
*self.auth_required = false;
is set.
Lines 634 to 643 in 9489511
/// Call this function to mark the peer as authenticated. | |
/// | |
/// # Panics | |
/// | |
/// If the handshake doesn't need to be authenticated. | |
pub fn authenticated(&mut self, status: AuthenticationStatus) { | |
assert!(self.state.authentication_needed()); | |
*self.auth_required = false; | |
self.state = HandshakeState::Authenticated(status.into()); | |
} |
On the second call to client.authenticated
the assert panics assert!(self.state.authentication_needed());
.
The above patch makes sure there is only ever a single AuthenticationNeeded
event in the queue by removing any existing ones. Is this a valid patch? Does this change in ordering violate any invariants, i.e. does anything depend on the original order?
As an alternative, one could only call client.authenticated
on not-yet-authenticated connections.
Alternative suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably need to avoid generating that event multiple times. I see that we generate that in response to being in the AuthenticationPending
state on the TLS connection. The idea must have been that the state would not be asserted multiple times (that is, we would block until someone called authenticated()
without calling handshake()
again). Either that or we would de-duplicate the event successfully.
That's clearly not the case, so we're generating multiple events. I don't see an obvious fix for this other than just tracking the state. Try this:
diff --git a/neqo-transport/src/connection/mod.rs b/neqo-transport/src/connection/mod.rs
index 2de38841..1141765f 100644
--- a/neqo-transport/src/connection/mod.rs
+++ b/neqo-transport/src/connection/mod.rs
@@ -2578,24 +2578,30 @@ impl Connection {
fn handshake(
&mut self,
now: Instant,
packet_version: Version,
space: PacketNumberSpace,
data: Option<&[u8]>,
) -> Res<()> {
qtrace!([self], "Handshake space={} data={:0x?}", space, data);
+ let was_authentication_pending =
+ self.crypto.tls.state() == HandshakeState::AuthenticationPending;
let try_update = data.is_some();
match self.crypto.handshake(now, space, data)? {
HandshakeState::Authenticated(_) | HandshakeState::InProgress => (),
- HandshakeState::AuthenticationPending => self.events.authentication_needed(),
+ HandshakeState::AuthenticationPending => {
+ if !was_authentication_pending {
+ self.events.authentication_needed()
+ }
+ }
HandshakeState::EchFallbackAuthenticationPending(public_name) => self
.events
.ech_fallback_authentication_needed(public_name.clone()),
HandshakeState::Complete(_) => {
if !self.state.connected() {
self.set_connected(now)?;
}
}
_ => {
unreachable!("Crypto state should not be new or failed after successful handshake")
Of course, that will need a test.
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #1612 +/- ##
==========================================
- Coverage 87.33% 87.33% -0.01%
==========================================
Files 124 124
Lines 39276 39288 +12
==========================================
+ Hits 34302 34312 +10
- Misses 4974 4976 +2 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @martinthomson. I reverted my hot-fix, I added your patch and a test. Would you mind taking another look?
All QUIC interop tests, apart from the always failing amplification
test, succeed now.
#[test] | ||
fn emit_authentication_needed_once() { | ||
let mut client = default_client(); | ||
|
||
let mut server = Connection::new_server( | ||
test_fixture::LONG_CERT_KEYS, | ||
test_fixture::DEFAULT_ALPN, | ||
Rc::new(RefCell::new(CountingConnectionIdGenerator::default())), | ||
ConnectionParameters::default(), | ||
) | ||
.expect("create a server"); | ||
|
||
let client1 = client.process(None, now()); | ||
assert!(client1.as_dgram_ref().is_some()); | ||
|
||
// The entire server flight doesn't fit in a single packet because the | ||
// certificate is large, therefore the server will produce 2 packets. | ||
let server1 = server.process(client1.as_dgram_ref(), now()); | ||
assert!(server1.as_dgram_ref().is_some()); | ||
let server2 = server.process(None, now()); | ||
assert!(server2.as_dgram_ref().is_some()); | ||
|
||
let authentication_needed_count = |client: &mut Connection| { | ||
client | ||
.events() | ||
.filter(|e| matches!(e, ConnectionEvent::AuthenticationNeeded)) | ||
.count() | ||
}; | ||
|
||
// Upon receiving the first packet, the client has the server certificate, | ||
// but not yet all required handshake data. It moves to | ||
// `HandshakeState::AuthenticationPending` and emits a | ||
// `ConnectionEvent::AuthenticationNeeded` event. | ||
let _ = client.process(server1.as_dgram_ref(), now()); | ||
assert_eq!(1, authentication_needed_count(&mut client)); | ||
|
||
// The `AuthenticationNeeded` event is still pending a call to | ||
// `Connection::authenticated`. On receiving the second packet from the | ||
// server, the client must not emit a another | ||
// `ConnectionEvent::AuthenticationNeeded`. | ||
let _ = client.process(server2.as_dgram_ref(), now()); | ||
assert_eq!(0, authentication_needed_count(&mut client)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on crypto_frame_split
. Let me know what you think.
let was_authentication_pending = | ||
*self.crypto.tls.state() == HandshakeState::AuthenticationPending; | ||
let try_update = data.is_some(); | ||
match self.crypto.handshake(now, space, data)? { | ||
HandshakeState::Authenticated(_) | HandshakeState::InProgress => (), | ||
HandshakeState::AuthenticationPending => self.events.authentication_needed(), | ||
HandshakeState::AuthenticationPending => { | ||
if !was_authentication_pending { | ||
self.events.authentication_needed() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you Martin. Your patch suggestion helped a lot.
// `HandshakeState::AuthenticationPending` and emits a | ||
// `ConnectionEvent::AuthenticationNeeded` event. | ||
let _ = client.process(server1.as_dgram_ref(), now()); | ||
assert_eq!(1, authentication_needed_count(&mut client)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, after checking here, I think we're OK, but this is a tiny bit brittle in the sense that it depends on having the server certificate small enough to fit in the first packet, but large enough to force the CertificateVerify into the next packet. I've added commentary to that effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed on this being brittle. Thanks for the additions.
There are two server implementations based on neqo: 1. https://github.com/mozilla/neqo/tree/main/neqo-bin/src/server - http3 and http09 implementation - used for manual testing and QUIC Interop 2. https://searchfox.org/mozilla-central/source/netwerk/test/http3server/src/main.rs - used to test Firefox I assume one was once an exact copy of the other. Both implement their own I/O, event loop, ... Since then, the two implementations diverged significantly. Especially (1) saw a lot of improvements in recent months: - mozilla#1564 - mozilla#1569 - mozilla#1578 - mozilla#1581 - mozilla#1604 - mozilla#1612 - mozilla#1676 - mozilla#1692 - mozilla#1707 - mozilla#1708 - mozilla#1727 - mozilla#1753 - mozilla#1756 - mozilla#1766 - mozilla#1772 - mozilla#1786 - mozilla#1787 - mozilla#1788 - mozilla#1794 - mozilla#1806 - mozilla#1808 - mozilla#1848 - mozilla#1866 At this point, bugs in (2) are hard to fix, see e.g. mozilla#1801. This commit merges (2) into (1), thus removing all duplicate logic and having (2) benefit from all the recent improvements to (1).
There are two server implementations based on neqo: 1. https://github.com/mozilla/neqo/tree/main/neqo-bin/src/server - http3 and http09 implementation - used for manual testing and QUIC Interop 2. https://searchfox.org/mozilla-central/source/netwerk/test/http3server/src/main.rs - used to test Firefox I assume one was once an exact copy of the other. Both implement their own I/O, event loop, ... Since then, the two implementations diverged significantly. Especially (1) saw a lot of improvements in recent months: - mozilla#1564 - mozilla#1569 - mozilla#1578 - mozilla#1581 - mozilla#1604 - mozilla#1612 - mozilla#1676 - mozilla#1692 - mozilla#1707 - mozilla#1708 - mozilla#1727 - mozilla#1753 - mozilla#1756 - mozilla#1766 - mozilla#1772 - mozilla#1786 - mozilla#1787 - mozilla#1788 - mozilla#1794 - mozilla#1806 - mozilla#1808 - mozilla#1848 - mozilla#1866 At this point, bugs in (2) are hard to fix, see e.g. mozilla#1801. This commit merges (2) into (1), thus removing all duplicate logic and having (2) benefit from all the recent improvements to (1).
* refactor(bin): introduce server/http3.rs and server/http09.rs The QUIC Interop Runner requires an http3 and http09 implementation for both client and server. The client code is already structured into an http3 and an http09 implementation since #1727. This commit does the same for the server side, i.e. splits the http3 and http09 implementation into separate Rust modules. * refactor: merge mozilla-central http3 server into neqo-bin There are two server implementations based on neqo: 1. https://github.com/mozilla/neqo/tree/main/neqo-bin/src/server - http3 and http09 implementation - used for manual testing and QUIC Interop 2. https://searchfox.org/mozilla-central/source/netwerk/test/http3server/src/main.rs - used to test Firefox I assume one was once an exact copy of the other. Both implement their own I/O, event loop, ... Since then, the two implementations diverged significantly. Especially (1) saw a lot of improvements in recent months: - #1564 - #1569 - #1578 - #1581 - #1604 - #1612 - #1676 - #1692 - #1707 - #1708 - #1727 - #1753 - #1756 - #1766 - #1772 - #1786 - #1787 - #1788 - #1794 - #1806 - #1808 - #1848 - #1866 At this point, bugs in (2) are hard to fix, see e.g. #1801. This commit merges (2) into (1), thus removing all duplicate logic and having (2) benefit from all the recent improvements to (1). * Move firefox.rs to mozilla-central * Reduce HttpServer trait functions * Extract constructor * Remove unused deps * Remove clap color feature Nice to have. Adds multiple dependencies. Hard to justify for mozilla-central.
tokio
instead ofmio
.neqo-client
to be more consistent withneqo-server
.read_datagram
andready
.ClientRunner
andold::ClientRunner
(consistent withServersRunner
).handle_test
intoclient
(nowClientRunner::new
).Companion to #1581.
See #1535 for details.
With the above additional refactorings the diff is rather difficult to read. Let me know if you would prefer separate pull requests instead.