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

bindings: match tcp EOF behavior #4323

Merged
merged 4 commits into from
Dec 20, 2023
Merged

bindings: match tcp EOF behavior #4323

merged 4 commits into from
Dec 20, 2023

Conversation

lrstewart
Copy link
Contributor

@lrstewart lrstewart commented Dec 12, 2023

Description of changes:

A customer pointed out that s2n-tls's stream returns a different error kind than a TCP stream does when encountering an unexpected EOF. It seems desirable to match the TCP behavior, and in this case our ErrorType::ConnectionClosed certainly seems equivalent to std::io::ErrorKind::UnexpectedEof. I updated the error translation.

Call-outs:

This is a minor breaking change, since we're changing the io error kind we return. But the previous kind was just std::io::ErrorKind::Other, and I do think this improvement is worth it. The wrapped s2n-tls error remains unchanged.

Testing:

I added some tests that can cleanly check our behavior vs the raw TCP stream's behavior. It seems like a good test suite to expand on later.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@github-actions github-actions bot added the s2n-core team label Dec 12, 2023
@lrstewart lrstewart force-pushed the rust_errors branch 2 times, most recently from cf30596 to 8ade6eb Compare December 12, 2023 19:16
@lrstewart lrstewart marked this pull request as ready for review December 12, 2023 22:54
server_builder: A,
client_builder: B,
) -> Result<TestPairList, Box<dyn std::error::Error>>
where
Copy link
Contributor

Choose a reason for hiding this comment

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

The types on this feel pretty complicated. I wonder if we could slightly shift things around to avoid some of the complexity?

Maybe instead of trying to pass the tcp stream back from this fixture constructor, you could just add another test case explicitly for the TCP streams? And then directly construct the streams in each test case?

Also having a generic method to assert on the read_from_closed or write_to_closed behaviors might be a nice extra check that both tcp, tls13, and tls12 all share the same behaviors.

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'm pretty attached to the idea of an array of things that should behave the same way though. If we have different test cases for TCP streams vs TLS streams, then they could end up testing for different behaviors / allowing differences. There isn't an enforcement mechanism that the behavior is the same, which is the point of this test suite.

Like, I'd love us to expand these tests to cover checking that behaviors other than handling EOF are the same between the streams.

I wonder if I could add TLS1.2 as a third element in the array tho 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And while the implementation is pretty complex, calling it is pretty simple, so I'm not super worried about the complexity.

Copy link
Contributor

@jmayclin jmayclin Dec 13, 2023

Choose a reason for hiding this comment

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

If we have different test cases for TCP streams vs TLS streams, then they could end up testing for different behaviors / allowing differences.

My thinking was that you could enforce that through an assert method.

fn assert_write_to_closed_behavior<S>(reader: S, writer: S) 
where
    S: AsyncRead + AsyncWrite // we can use a generic here to avoid relying on TLS specifics
{
    drop(writer);
    let result = reader.read_u8().await;
    assert!(result.is_err());
    let error = result.unwrap_err();
    assert!(error.kind() == std::io::ErrorKind::UnexpectedEof);
}

And then your actual unit test could be something like

fn read_from_closed_behavior() {
    let (client_tcp, server_tcp) = get_streams();
    assert_write_to_closed_behavior(client_tcp, server_tcp)
    let (client_tls13, server_tls13) = ...
    assert_write_to_closed_behavior(client_tls13, server_tls13)
}

Copy link
Contributor Author

@lrstewart lrstewart Dec 19, 2023

Choose a reason for hiding this comment

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

I made this change, but idk if I like it. It moves the unique logic (like assert_write_to_closed_behavior) into a separate method while every test has to repeat the common setup logic (creating streams). It was only really reasonable when I created another helper method to abstract away tls stream creation.

It feels very inverted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And if I want to add another kind of stream, I'm going to have to modify every test rather than just the stream setup method.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like that the checks are now abstracted out into their own little unit of logic, but I see your concern about the stream setup. I think we can hurdle that when we come to it. There is a round-about way of defining a stream harness that takes in the "behavior assertion" closure that would get rid of the duplication, but if the streams keep increasing I agree that the dyn list approach might make more sense.

server_builder: A,
client_builder: B,
) -> Result<TestPairList, Box<dyn std::error::Error>>
where
Copy link
Contributor

Choose a reason for hiding this comment

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

I like that the checks are now abstracted out into their own little unit of logic, but I see your concern about the stream setup. I think we can hurdle that when we come to it. There is a round-about way of defining a stream harness that takes in the "behavior assertion" closure that would get rid of the duplication, but if the streams keep increasing I agree that the dyn list approach might make more sense.

bindings/rust/s2n-tls-tokio/tests/tcp.rs Outdated Show resolved Hide resolved
@lrstewart lrstewart enabled auto-merge (squash) December 20, 2023 19:57
@lrstewart lrstewart merged commit a9a07a2 into aws:main Dec 20, 2023
28 checks passed
@lrstewart lrstewart deleted the rust_errors branch December 20, 2023 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants