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

refactor(client): replace mio with tokio #1612

Merged
merged 7 commits into from
Feb 6, 2024

Conversation

mxinden
Copy link
Collaborator

@mxinden mxinden commented Feb 1, 2024

  • 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).

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.

neqo-client/src/main.rs Outdated Show resolved Hide resolved
- 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`).
@@ -268,6 +268,7 @@ impl Http3ClientEvents {

/// Add a new `AuthenticationNeeded` event
pub(crate) fn authentication_needed(&self) {
self.remove(|evt| matches!(evt, Http3ClientEvent::AuthenticationNeeded));
Copy link
Collaborator Author

@mxinden mxinden Feb 3, 2024

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.

Http3ClientEvent::AuthenticationNeeded => {
client.authenticated(AuthenticationStatus::Ok, Instant::now());
}

On the first call to client.authenticated *self.auth_required = false; is set.

/// 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?

Copy link
Member

@martinthomson martinthomson Feb 5, 2024

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.

@mxinden mxinden marked this pull request as ready for review February 3, 2024 17:35
@codecov-commenter
Copy link

codecov-commenter commented Feb 4, 2024

Codecov Report

Attention: 219 lines in your changes are missing coverage. Please review.

Comparison is base (9a394e9) 87.33% compared to head (9434e17) 87.33%.

Files Patch % Lines
neqo-client/src/main.rs 0.00% 219 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator Author

@mxinden mxinden left a 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?

image

All QUIC interop tests, apart from the always failing amplification test, succeed now.

Comment on lines 1139 to 1181
#[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));
}
Copy link
Collaborator Author

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.

Comment on lines +2588 to +2597
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()
}
}
Copy link
Collaborator Author

@mxinden mxinden Feb 5, 2024

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));
Copy link
Member

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.

Copy link
Collaborator Author

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.

@martinthomson martinthomson merged commit 3587c23 into mozilla:main Feb 6, 2024
9 checks passed
mxinden added a commit to mxinden/neqo that referenced this pull request May 4, 2024
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).
KershawChang pushed a commit to KershawChang/neqo that referenced this pull request May 7, 2024
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).
github-merge-queue bot pushed a commit that referenced this pull request May 8, 2024
* 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.
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.

3 participants