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

Add TLS support #81

Merged
merged 30 commits into from
Jan 31, 2017
Merged

Add TLS support #81

merged 30 commits into from
Jan 31, 2017

Conversation

compressed
Copy link
Contributor

So here we go... TLS support take two!

The Options change and recent refactoring made this change much easier to make. I didn't need to modify the macro at all. I was able to re-use all the tests as well with some refactoring. I added the t! macro there in testing (it's common in a few other libraries), mainly because it provides better errors (line numbers) on mac. I can change those to unwraps though or change everything to t! if you want.

I think most of the changes are straight-forward. The only tricky part I ran into was that Options is not Send, so I needed to pull out the related TLS part of Options on both the client and server in order to make the compiler happy.

Note: I did a merge because the changes in both this codebase and my branch were so much that the rebase was turning out to be quite ugly. I believe you can do a merge and squash and GitHub though.

Fix #77

compressed and others added 15 commits January 12, 2017 14:32
When `-- features tls` is specified for tarpc, RPC communication can
also occur over a `TlsStream<TcpStream>` instead of a `TcpStream`.

The functional tests have been refactored to use a common set of
functions for constructing the client and server structs so that all
the tests are shared across non-tls and tls test runs.
A new version of `native-tls` has been released so we don’t need to
depend on the git version anymore.
@tikue
Copy link
Collaborator

tikue commented Jan 20, 2017

Thanks again for doing this! Hopefully I'll get through it all by the end of the weekend.

Copy link
Collaborator

@tikue tikue left a comment

Choose a reason for hiding this comment

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

I haven't looked at the tests yet, but everything else looks great. Thank you so much.

I'd like @shaladdle to do a once-over, as well.

@@ -21,7 +21,8 @@ before_script:

script:
- |
travis-cargo build && travis-cargo test
travis-cargo build && travis-cargo test &&
travis-cargo build -- --features tls && travis-cargo test -- --features tls

after_success:
- travis-cargo coveralls --no-sudo
Copy link
Collaborator

Choose a reason for hiding this comment

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

How does this work with features?

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 run just like cargo build --features tls. If you pass -- to travis-cargo then whatever follows is passed to cargo.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What about coveralls? Looks like it needs to have the tls feature enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally yes... However, coveralls is broken already in travis-cargo. I think there are a few PRs to fix it in their repo. Maybe we should have a separate PR just to remove travis-cargo. It may be easier than using that tool.

README.md Outdated
let (acceptor, client_cx) = tls_context();
HelloServer.listen(addr, server::Options::default()
.handle(core.handle())
.tls(acceptor)).wait().unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Building the client_cx seems pretty straightforward; maybe we could just inline it as .tls(TlsClientContext::new("foobar.com"))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea!

src/client.rs Outdated

/// Provides the connection Fn impl for Tls
#[doc(hidden)]
pub struct ConnectFn {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I could be mistaken, but I have a feeling this can be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yes, awesome!

Rust was originally complaining about private type in a public interface, but that was with an older version of the Connect trait. Making this private seems okay now. I like that it can be private again as well.

src/server.rs Outdated
inner: future::Either::B(future::ok(listen_with(new_service,
addr,
handle,
acceptor))),
}
}
}
}
/// Spawns a service that binds to the given address using the given handle.
#[doc(hidden)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

No longer needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/client.rs Outdated
use native_tls::TlsConnector;

/// TLS context
pub struct TlsClientContext {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't feel super strongly about this, but since it's already in the client::tls module, we might consider just calling it Context and people can alias it if they so choose. Thoughts? Maybe the tls module should be top-level and it should be tls::client::Context?

cc @shaladdle

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I prefer client::tls::Context. I prefer client/server to be top level, since those seem like core concepts that you'll always use with tarpc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I actually was considering that as well. I didn't particularly like putting Client in the name, but thought it would be useful if it was available at the root namespace; ::TlsContext wouldn't be very descriptive.

I like the idea of a root tls module.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Spoke a bit with Tim. I think he and I are on the same page now. Can we make the crate structure like this:

root

  • tls
  • client
  • server

The client and server mods can conditionally generate tls-specific methods or options, and the tls module can have types that are used in those methods/options. Does that seem reasonable?

src/client.rs Outdated
{
#[cfg(not(feature = "tls"))]
inner:
future::Either<
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add a type alias for part of this so we don't have to rewrite the entire thing in both branches of the cfg?

src/lib.rs Outdated
extern crate tokio_tls;
pub extern crate native_tls;

pub use client::tls::TlsClientContext;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Related to my other comment, I'm starting to feel more strongly that there should be a mod tls at the crate root, within which is a client module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I like this approach too. I will make those changes.

src/macros.rs Outdated
use util::FirstSocketAddr;
extern crate env_logger;

macro_rules! t {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is basically unwrap on steroids, right? Is there a mnemonic to remember 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.

Ha yes. I'm not sure on the history behind the naming though, other than it is short to type...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Short names are nice, but can we name it something that indicates what it does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could just call it unwrap! I guess.

src/lib.rs Outdated
cfg_if! {
if #[cfg(feature = "tls")] {
extern crate tokio_tls;
pub extern crate native_tls;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this have to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well we need to expose the TlsAcceptor type in the builder for the server options. Another option here though instead of just re-exporting the whole crate would be to just re-export the TlsAcceptor type itself.

I was also thinking on the client side it could take an option TlsConnector if people want to override the default. So we could get away with just re-exporting TlsConnector and TlsAcceptor from native_tls instead of the whole crate; that's probably a better idea.

src/server.rs Outdated
/// Additional options to configure how the server operates.
#[derive(Clone, Default)]
#[cfg_attr(feature = "tls", derive(Default))]
#[cfg_attr(not(feature = "tls"), derive(Clone, Default))]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment as above: I'd rather impl the intersection of possible traits for each feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Collaborator

@tikue tikue left a comment

Choose a reason for hiding this comment

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

One general comment: there are a lot of cfgs now. It's a little scary from a maintainability perspective, but I'm not sure if there's really a good way around it.

src/client.rs Outdated
use tokio_proto::BindClient;
#[cfg(feature = "tls")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use a cfg_if! to group all the tls imports together?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1, reducing the number of pieces sprayed around the code with cfg(feature..) is very desirable. If they could somehow just be confined to a single file/module, that would be even better, though I know that might be hard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, will do.

Copy link
Collaborator

@shaladdle shaladdle left a comment

Choose a reason for hiding this comment

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

Sending these now so they're not stale.

@@ -33,7 +36,12 @@ env_logger = "0.3"
futures-cpupool = "0.1"
clap = "2.0"

[target.'cfg(target_os = "macos")'.dev-dependencies]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this stuff work on osx/linux/windows? Is OSX the only one that needs an extra dependency like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This line will make this dependency apply only for osx. For linux, it internally uses the openssl crate, but we can make required data for testing purposes without needing to bring in the openssl crate.

For windows, I didn't write the test case since I don't actually have access to a windows machine and it seemed a little complicated based on the tests I read in the native-tls and tokio-tls crates.

README.md Outdated
## Example: Futures + TLS

By default, tarpc uses a `TcpStream` for communication, however you can also opt to use a
`TlsStream<TcpStream>` when the `tls` feature of `tarpc` is enabled.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think a sentence or two about what TLS is would be useful here (similar to the beginning of the readme that says what an RPC is).

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 added some additional context now. Let me know how this looks.

README.md Outdated

When using TLS, some additional information is required. You will need to make [`TlsAcceptor`] and
`TlsClientContext` structs; `TlsClientContext` requires a [`TlsConnector`]. The [`TlsAcceptor`] and
[`TlsConnector`] structs are defined in the [native-tls] crate which is exposed by `tarpc`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems like implicit advice to use these structs as exported by tarpc rather than having the user add the native-tls crate as a dependency. Is that the intention? If so is that approach used by any other libraries? I know we re-export some crates already, but my understanding is that those are there to make the macro standalone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the direction I was going. I've seen libraries that have public interfaces which require structs from other crates generally do this. You can see in reqwest and hyper. I think the best approach would be to just re-export only the external types we need, not the whole crate. I'll make that change when I do my updates.

README.md Outdated
[native-tls]: https:/sfackler/rust-native-tls

Enabling the `tls` feature does not require you to use TLS streams. You can still
use TCP streams as well or a combination of both stream types.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please clarify that you mean it's ok/possible to use both types of stream in the same binary, rather than being able to use either TLS or non-TLS stream types for the same server.

src/client.rs Outdated
use native_tls::TlsConnector;

/// TLS context
pub struct TlsClientContext {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I prefer client::tls::Context. I prefer client/server to be top level, since those seem like core concepts that you'll always use with tarpc.

src/client.rs Outdated
impl TlsClientContext {
/// Try to make a new `TlsClientContext`, providing the domain the client will
/// connect to.
pub fn try_new<S: Into<String>>(domain: S)
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

src/client.rs Outdated
@@ -91,9 +119,12 @@ impl<Req, Resp, E> fmt::Debug for Client<Req, Resp, E>
}

/// Additional options to configure how the client connects and operates.
#[derive(Clone, Default)]
#[cfg_attr(feature = "tls", derive(Default))]
#[cfg_attr(not(feature = "tls"), derive(Clone, Default))]
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

src/client.rs Outdated
use tokio_proto::BindClient;
#[cfg(feature = "tls")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1, reducing the number of pieces sprayed around the code with cfg(feature..) is very desirable. If they could somehow just be confined to a single file/module, that would be even better, though I know that might be hard.

@compressed
Copy link
Contributor Author

Thanks for all the comments. I'll work on these tomorrow!

Copy link
Collaborator

@shaladdle shaladdle left a comment

Choose a reason for hiding this comment

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

Ok, got through everything :). Thanks so much for the PR. Overall it looks good. Two requests:

Can you add one test that instantiates both a tls and non-tls server and uses both independently? This is mostly meant as a sanity check that something weird doesn't happen when using both.

I'm also curious about error cases related specifically to TLS. What is supposed to happen when a non-TLS client tries to connect to a TLS server? What about the opposite? Can we have tests for these cases?

src/macros.rs Outdated
use util::FirstSocketAddr;
extern crate env_logger;

macro_rules! t {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Short names are nice, but can we name it something that indicates what it does?

src/client.rs Outdated
use native_tls::TlsConnector;

/// TLS context
pub struct TlsClientContext {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Spoke a bit with Tim. I think he and I are on the same page now. Can we make the crate structure like this:

root

  • tls
  • client
  • server

The client and server mods can conditionally generate tls-specific methods or options, and the tls module can have types that are used in those methods/options. Does that seem reasonable?

let rx = match options.reactor {
Some(Reactor::Handle(handle)) => {
let tcp = TcpStream::connect(&addr, &handle).map(MultiplexConnect::new(handle));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of reproducing these three lines twice, can we only build the ConnectFn using cfg, and do the rest unconditionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I've implemented that.

src/client.rs Outdated
@@ -227,6 +352,7 @@ pub mod future {
ConnectFuture { inner: future::Either::B(rx.map_err(panic as fn(_) -> _).flatten()) }
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this blank line intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I have removed it.

src/lib.rs Outdated
@@ -59,6 +60,53 @@
//! }
//! ```
//!
//! Example usage with TLS:
//!
#![cfg_attr(feature = "tls", doc = " ```rust,no_run")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do these cfgs do?

Copy link
Contributor Author

@compressed compressed Jan 23, 2017

Choose a reason for hiding this comment

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

These provide some configurability for what runs during rustdoc's test.

So for these two:

#![cfg_attr(feature = "tls", doc = " ```ignore")]
#![cfg_attr(not(feature = "tls"), doc = " ```")]

It's interpreted something like this:

if feature != tls { return //! ``` };
if feature == tls { return //! ```ignore };

ignore just means don't run this test case at all. no_run is going to compile the test, but not actually run it. We want this for the tls use case because we need to add extra stuff like in the test cases to add the certs to the cert chain, which we don't need to here in the rustdoc.

For the tcp side actually we can run that now even with tls enabled, so the first set of conditions can be removed.

src/server.rs Outdated
@@ -50,34 +75,48 @@ pub fn listen<S, Req, Resp, E>(new_service: S, addr: SocketAddr, options: Option
Resp: Serialize + 'static,
E: Serialize + 'static
{
// similar to the client, since `Options` is not `Send`, we take the `TlsAcceptor` when it is
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Capitalize

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/server.rs Outdated
fn listen_with<S, Req, Resp, E>(new_service: S,
addr: SocketAddr,
handle: Handle,
_acceptor: Acceptor)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why _? Is this unused with tls off or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's correct. We don't use acceptor in the tcp workflow.

@tikue
Copy link
Collaborator

tikue commented Jan 23, 2017

I can't figure out how to reply to one of @shaladdle's comments, so I'm just writing it here. It would be great if a non-tls client trying to connect to a tls server receives a specific tls-related error, like Unauthenticated. Is that possible?

@compressed
Copy link
Contributor Author

I've made updates to incorporate most of the comments (and also updated to latest master).

Here are the outstanding items/discussion points (I think!):

  1. Test cases / error conditions: I added one test case before that tests both tcp and tls where they are configured properly, so that functionality is validated. However, I didn't add a test case for misconfigured clients. If you use a TLS client on a TCP server, the client connection just waits indefinitely, probably because the client is waiting for the tls handshake to begin. If you use a TCP client on a TLS server, the server logs an error when the client makes an RPC call and the client waits indefinitely (ERROR:tarpc::server: While processing incoming connections: connection closed via error, from here: https:/google/tarpc/blob/master/src/server.rs#L98). Ideally, it would be best to catch these misconfigurations at compile time, but I'm not sure the best way to do that. I was thinking we could require some reference from the server to make the client, but even that won't work because the server struct itself could be used to make both a tcp and a tls server... Not sure here.
  2. TlsClientContext name? I kept it as-is for now. Since we don't have tls::client etc., I thought we should keep client in the name. It also goes along with the other tls types: TlsAcceptor, TlsConnector, but open to change.
  3. Lots of cfgs. I'm not sure what to do about this either. The only way to eliminate most of them would be to make the interfaces generic over all stream types but we tried that before in the earlier versions of this PR, but then macro itself becomes quite unwieldly which I think is even worse.

@tikue
Copy link
Collaborator

tikue commented Jan 24, 2017

TlsClientContext name? I kept it as-is for now. Since we don't have tls::client etc., I thought we should keep client in the name. It also goes along with the other tls types: TlsAcceptor, TlsConnector, but open to change.

I still think it should be named Context. It can be used as client::tls::Context or aliased as ClientTlsContext or TlsClientContext. Also, I don't think there are good reasons to reexport it from the crate root.

@compressed
Copy link
Contributor Author

Okay thanks! Works for me. I've made that change now too.

Copy link
Collaborator

@shaladdle shaladdle left a comment

Choose a reason for hiding this comment

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

I think we're nearing the end here. Just (hopefully) some small things.

src/lib.rs Outdated
extern crate tokio_tls;
extern crate native_tls;

/// Re-exported TLS-related types
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do these need to be re-exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we have public interfaces now that use all these types, except Pkcs12, but you need that type to make the TlsAcceptor itself.

src/client.rs Outdated
}

impl Context {
/// Try to construct a new `Context`, providing the domain the client will
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/providing/provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, actually this comment is wrong. It's the domain used for validation, not connection. I will update that.

@compressed
Copy link
Contributor Author

Great! I've updated the comments for domain in the most recent commit to be more accurate.

@tikue
Copy link
Collaborator

tikue commented Jan 30, 2017

Relevant issue

src/lib.rs Outdated

/// Re-exported TLS-related types
pub mod tls {
pub use native_tls::Error as NativeTlsError;
Copy link
Collaborator

Choose a reason for hiding this comment

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

what about renaming the module native_tls and then pub use native_tls::Error? (and then just add it to the below pub use statement)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Used extern crate native_tls as native_tls_inner to avoid the name spacing problem.

src/macros.rs Outdated
const DOMAIN: &'static str = "foobar.com";

use client::tls::Context;
use ::native_tls::{Pkcs12, TlsAcceptor, TlsConnector};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the :: prefix is redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Copy link
Collaborator

@tikue tikue left a comment

Choose a reason for hiding this comment

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

Thanks again, @compressed, this is an incredibly important addition!

@compressed
Copy link
Contributor Author

You're welcome! I'm excited for it to land. Thanks for all the comments and working with me on it.

I just made one more minor change to add Error to the use above this line: https:/compressed/tarpc/blob/c6bc6a871be294303942f7d70980f49bbdc811ab/src/client.rs#L46 so then the type can just be Error not ::native_tls::Error in the new fn so that's why it's building again.

@shaladdle
Copy link
Collaborator

+1, this is great, @compressed. Thanks so much for contributing :)

@tikue tikue merged commit fafe569 into google:master Jan 31, 2017
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