Repository: bearcove/loona (language: rust)

Issue:
h2: If we send a GOAWAY, we shouldn't be surprised at a connection reset
**Describe the bug**

Not really a bug but see this log, that's the end of `http2/5.1.2`:

```
 WARN fluke::h2::server: connection error: max concurrent streams exceeded (more than 32) (MaxConcurrentStreamsExceeded { max_concurrent_streams: 32 }) (code ProtocolError)
DEBUG fluke::h2::server: Sending GoAway last_stream_id=65 error_code=ProtocolError
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 29, payload: BodyChunk }
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 63, payload: Headers }
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 31, payload: BodyChunk }
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
        ✔ 1: Sends HEADERS frames that causes their advertised concurrent stream limit to be exceeded
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 33, payload: BodyChunk }

Finished in 0.0596 seconds
1 tests, 1 passed, 0 skipped, 0 failed
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 35, payload: BodyChunk }
DEBUG fluke::h2::server: Handler completed successfully, gave us a responder
DEBUG fluke::h2::server: Writing ev=H2Event { stream_id: 37, payload: BodyChunk }
DEBUG fluke::h2::server: caught error from one of the tasks: read error: read_into for read_and_parse::<fluke::h2::parse::Frame> / Read(
    Error {
        msg: "read_into for read_and_parse::<fluke::h2::parse::Frame>",
        source: Os {
            code: 104,
            kind: ConnectionReset,
            message: "Connection reset by peer",
        },
    },
)
ERROR fluke_h2spec: error serving client 127.0.0.1:60488: read error: read_into for read_and_parse::<fluke::h2::parse::Frame>
DEBUG fluke::h2::server: Handler returned an error: could not send event to h2 connection handler
DEBUG fluke::h2::server: Handler returned an error: could not send event to h2 connection handler
 WARN fluke::h2::encode: could not send event to h2 connection handler
 WARN fluke::h2::encode: could not send event to h2 connection handler
```

**Expected behavior**

If we've sent a GOAWAY, we should fully expect to 1) not be able to write further messages, 2) get a connection reset when we try to read more messages.

This can probably be done by just having a "has_sent_goaway" flag somewhere.


Discussion / hints:

Fix patch:
diff --git a/crates/fluke/src/h2/read.rs b/crates/fluke/src/h2/read.rs
index 1b09bdf1..277bb4cd 100644
--- a/crates/fluke/src/h2/read.rs
+++ b/crates/fluke/src/h2/read.rs
@@ -41,6 +41,12 @@ pub(crate) struct H2ReadContext<D: ServerDriver + 'static> {
     hpack_dec: fluke_hpack::Decoder<'static>,
     // TODO: kill, cf. https://github.com/hapsoc/fluke/issues/121
     continuation_state: ContinuationState,
+
+    /// Whether we've sent a GOAWAY frame.
+    pub goaway_sent: bool,
+
+    /// Whether we've received a GOAWAY frame.
+    pub goaway_recv: bool,
 }
 
 impl<D: ServerDriver + 'static> H2ReadContext<D> {
@@ -55,6 +61,8 @@ impl<D: ServerDriver + 'static> H2ReadContext<D> {
             state,
             hpack_dec,
             continuation_state: ContinuationState::Idle,
+            goaway_sent: false,
+            goaway_recv: false,
         }
     }
 
@@ -65,15 +73,38 @@ impl<D: ServerDriver + 'static> H2ReadContext<D> {
     ) -> eyre::Result<()> {
         loop {
             let frame;
-            (client_buf, frame) =
-                match read_and_parse(Frame::parse, &mut transport_r, client_buf, 128).await? {
-                    Some((client_buf, frame)) => (client_buf, frame),
-                    None => {
-                        // TODO: this can be fine, if we've sent a GO_AWAY
-                        debug!("h2 client closed connection");
-                        return Err(eyre::Report::from(ConnectionClosed));
+            let frame_res = read_and_parse(Frame::parse, &mut transport_r, client_buf, 128).await;
+            let maybe_frame = match frame_res {
+                Ok(inner) => inner,
+                Err(e) => {
+                    // if this is a connection reset and we've sent a goaway, ignore it
+                    if self.goaway_sent {
+                        if let Some(io_error) = e.root_cause().downcast_ref::<std::io::Error>() {
+                            if io_error.kind() == std::io::ErrorKind::ConnectionReset {
+                                debug!("ignoring connection reset after goaway");
+                                return Ok(());
+                            }
+                        }
                     }
-                };
+                    return Err(e.wrap_err("h2 read"));
+                }
+            };
+            (client_buf, frame) = match maybe_frame {
+                Some((client_buf, frame)) => (client_buf, frame),
+                None => {
+                    if self.goaway_sent {
+                        debug!("peer connection reset after we sent a GOAWAY");
+                        return Ok(());
+                    }
+                    if self.goaway_recv {
+                        debug!("peer connection reset after we received a GOAWAY");
+                        return Ok(());
+                    }
+
+                    debug!("peer connection reset, without a GOAWAY");
+                    return Err(eyre::Report::from(ConnectionClosed));
+                }
+            };
 
             debug!(?frame, "Received");
 
@@ -399,7 +430,12 @@ impl<D: ServerDriver + 'static> H2ReadContext<D> {
                     return Err(eyre::eyre!("could not send H2 ping event").into());
                 }
             }
-            FrameType::GoAway => todo!(),
+            FrameType::GoAway => {
+                self.goaway_recv = true;
+
+                // TODO: this should probably have other effects than setting
+                // this flag.
+            }
             FrameType::WindowUpdate => match frame.stream_id.0 {
                 0 => {
                     debug!("TODO: ignoring connection-wide window update");
@@ -485,11 +521,17 @@ impl<D: ServerDriver + 'static> H2ReadContext<D> {
         }
     }
 
-    async fn send_goaway(&self, err: H2ConnectionError) {
+    async fn send_goaway(&mut self, err: H2ConnectionError) {
         // TODO: this should change the global server state: we should ignore
         // any streams higher than the last stream id we've seen after
         // we've done that.
 
+        if self.goaway_sent {
+            debug!("goaway already sent, ignoring");
+            return;
+        }
+
+        self.goaway_sent = true;
         if self
             .ev_tx
             .send(H2ConnEvent::GoAway {
diff --git a/crates/fluke/src/h2/server.rs b/crates/fluke/src/h2/server.rs
index 24963672..9dff2572 100644
--- a/crates/fluke/src/h2/server.rs
+++ b/crates/fluke/src/h2/server.rs
@@ -1,13 +1,12 @@
 use std::rc::Rc;
 
-use futures_util::TryFutureExt;
 use tracing::debug;
 
 use crate::{
     h2::{
         parse::{self, Frame, FrameType, StreamId},
         read::H2ReadContext,
-        types::{ConnState, ConnectionClosed, H2ConnEvent},
+        types::{ConnState, H2ConnEvent},
     },
     util::read_and_parse,
     ServerDriver,
@@ -72,32 +71,85 @@ pub async fn serve(
     let (ev_tx, ev_rx) = tokio::sync::mpsc::channel::<H2ConnEvent>(32);
 
     let mut h2_read_cx = H2ReadContext::new(driver.clone(), ev_tx.clone(), state);
-    let read_task = h2_read_cx.read_loop(client_buf, transport_r);
-
-    let write_task = super::write::h2_write_loop(ev_rx, transport_w, out_scratch);
-
-    let res = tokio::try_join!(
-        read_task.map_err(LoopError::Read),
-        write_task.map_err(LoopError::Write),
-    );
-    if let Err(e) = &res {
-        if let LoopError::Read(r) = e {
-            if r.downcast_ref::<ConnectionClosed>().is_some() {
-                return Ok(());
+
+    enum Outcome {
+        PeerGone,
+        Ok,
+    }
+
+    let mut outcome = Outcome::Ok;
+
+    {
+        let mut read_task = std::pin::pin!(h2_read_cx.read_loop(client_buf, transport_r));
+        let mut write_task =
+            std::pin::pin!(super::write::h2_write_loop(ev_rx, transport_w, out_scratch));
+
+        tokio::select! {
+            res = &mut read_task => {
+                match res {
+                    Err(e) => {
+                        return Err(e.wrap_err("h2 read (finished first)"));
+                    }
+                    Ok(()) => {
+                        debug!("read task finished, waiting on write task now");
+                        let res = write_task.await;
+                        match res {
+                            Err(e) => {
+                                if is_peer_gone(&e) {
+                                    outcome = Outcome::PeerGone;
+                                } else {
+                                    return Err(e.wrap_err("h2 write (finished second)"));
+                                }
+                            }
+                            Ok(()) => {
+                                debug!("write task finished okay");
+                            }
+                        }
+                    }
+                }
+            },
+            res = write_task.as_mut() => {
+                match res {
+                    Err(e) => {
+                        if is_peer_gone(&e) {
+                            outcome = Outcome::PeerGone;
+                        } else {
+                            return Err(e.wrap_err("h2 write (finished first)"));
+                        }
+                    }
+                    Ok(()) => {
+                        debug!("write task finished, giving up read task");
+                    }
+                }
+            },
+        };
+    };
+
+    match outcome {
+        Outcome::PeerGone => {
+            if h2_read_cx.goaway_sent {
+                debug!("write task failed with broken pipe, but we already sent a goaway, so we're good");
+            } else {
+                return Err(eyre::eyre!(
+                    "peer closed connection without sending a goaway"
+                ));
             }
         }
-        debug!("caught error from one of the tasks: {e} / {e:#?}");
+        Outcome::Ok => {
+            // all goodt hen!
+        }
     }
-    res?;
 
     Ok(())
 }
 
-#[derive(thiserror::Error, Debug)]
-enum LoopError {
-    #[error("read error: {0}")]
-    Read(eyre::Report),
-
-    #[error("write error: {0}")]
-    Write(eyre::Report),
+fn is_peer_gone(e: &eyre::Report) -> bool {
+    if let Some(io_error) = e.root_cause().downcast_ref::<std::io::Error>() {
+        matches!(
+            io_error.kind(),
+            std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset
+        )
+    } else {
+        false
+    }
 }
diff --git a/crates/fluke/src/h2/types.rs b/crates/fluke/src/h2/types.rs
index 25988af9..a7ce2dc0 100644
--- a/crates/fluke/src/h2/types.rs
+++ b/crates/fluke/src/h2/types.rs
@@ -272,5 +272,5 @@ impl fmt::Debug for H2EventPayload {
 }
 
 #[derive(thiserror::Error, Debug)]
-#[error("connection closed")]
+#[error("the peer closed the connection unexpectedly")]
 pub(crate) struct ConnectionClosed;


Test patch:
diff --git a/test-crates/fluke-h2spec/src/main.rs b/test-crates/fluke-h2spec/src/main.rs
index d2294b88..4d98579b 100644
--- a/test-crates/fluke-h2spec/src/main.rs
+++ b/test-crates/fluke-h2spec/src/main.rs
@@ -133,7 +133,7 @@ pub(crate) async fn run_server(ln: TcpListener) -> color_eyre::Result<()> {
 
         tokio::task::spawn_local(async move {
             if let Err(e) = fluke::h2::serve(stream.into_halves(), conf, client_buf, driver).await {
-                tracing::error!("error serving client {}: {}", addr, e);
+                tracing::error!("error serving client {addr}: {e}, {e:?}");
             }
         });
     }