Skip to content

feat(grpc) Add channel credentials API + Insecure credentials#2495

Merged
arjan-bal merged 20 commits intohyperium:masterfrom
arjan-bal:channel-creds-api
Feb 16, 2026
Merged

feat(grpc) Add channel credentials API + Insecure credentials#2495
arjan-bal merged 20 commits intohyperium:masterfrom
arjan-bal:channel-creds-api

Conversation

@arjan-bal
Copy link
Collaborator

@arjan-bal arjan-bal commented Feb 4, 2026

This change introduces a channel credentials API to abstract over TLS/Insecure/Local credentials. Implementations for TLS and local credentials will follow in future PRs.

Key Changes

  • Renamed rt::TcpStream to GrpcEndpoint to reflect a more generic connection interface.
  • GrpcEndpoint is now sealed. This allows us to change the API in the future (e.g., to align with gRPC C++) without breaking changes. This restricts implementations of rt::Runtime and channel credentials to the grpc crate.

Other changes

  • Most credential components are pub(crate) as users can't to implement them due to GrpcEndpoint trait being sealed.
  • Restricted the rt module to pub(crate) and exposed Runtime as pub to resolve private_bounds lints when using runtime types in pub APIs.

Copy link
Collaborator

@sauravzg sauravzg left a comment

Choose a reason for hiding this comment

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

PR looks good overall. Please address the comments about unit test.

@arjan-bal arjan-bal assigned ankurmittal and LucioFranco and unassigned sauravzg Feb 10, 2026
pub trait Sealed {
type ContextType: ClientConnectionSecurityContext;
type Output<I>;
/// Performs the client-side authentication handshake on a raw endpoint.
Copy link
Member

Choose a reason for hiding this comment

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

dont think you need the duplicate docs on the sealed trait?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The trait-level documentation for the externally reachable sub-trait is quite abstract and doesn't cover the specific methods. Even though this method isn't exposed externally, I think keeping the docs is valuable for internal maintainability and for developers using IDE features like hover-to-preview. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

iirc the trait you actually interact with is the root one not the sealed one so I think just having docs on the main trait is enough and keeping the sealed one a mirror of the type interface makes more sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not quite following this. Could you provide a rough sketch of how the supertrait and subtrait would look? Also, please take a look at my comment below describing the existing approach.

Comment on lines 84 to 89
pub(crate) security_protocol: &'static str,
pub(crate) security_level: SecurityLevel,
pub(crate) security_context: C,
/// Stores extra data derived from the underlying protocol.
pub(crate) attributes: Attributes,
}
Copy link
Member

Choose a reason for hiding this comment

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

not a big fan of tons of pub(crates) makes refactoring things later much harder. Something we can maybe discuss as a group but I tend to want to keep a lot of things function based rather than field based.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed the use of exported fields throughout.

Comment on lines 55 to 58
(
Box<dyn GrpcEndpoint>,
ClientConnectionSecurityInfo<Box<dyn ClientConnectionSecurityContext>>,
),
Copy link
Member

Choose a reason for hiding this comment

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

Think this should just return a struct this is really hard to read due to being a tuple

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Introduced a HandshakeOutput struct to replace the tupple.

impl<T> DynClientChannelCredential for T
where
T: ClientChannelCredential,
T::Output<BoxEndpoint>: GrpcEndpoint + 'static,
Copy link
Member

Choose a reason for hiding this comment

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

if grpcEndpoint is almost always static just promote the 'static to the trait bounds trait Foo: 'static

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Moved the 'static bound to GrpcEndpoint trait.

.make_send()
.await?;

let boxed_stream: BoxEndpoint = Box::new(stream);
Copy link
Member

Choose a reason for hiding this comment

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

should be able to drop this type hint here, its funneled right into a fixed return type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're correct, removed.

async fn accept(
&self,
source: BoxEndpoint,
runtime: Arc<dyn Runtime>,
Copy link
Member

Choose a reason for hiding this comment

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

nit: probably should just make this GrpcRuntime or just grpc::Runtime that wraps the arc internally

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a GrpcRuntime type as suggested and removed Arc from all method signatures.

}
}

impl server::Sealed for InsecureServerChannelCredentials {
Copy link
Member

Choose a reason for hiding this comment

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

seems like the patterns of sealed traits have changed bit since I last looked at them, I would just say double check the patterns here https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ I trust predrag and we use semver checks. If you think this is following that I am good on that I trust you to make the right choice here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

After reading a few different sources, I realized the pattern I'm using isn't exactly the standard "sealed trait" pattern.

In my case, I want to ensure all the methods on the trait are only callable from within the crate. Most guides on the sealed pattern don't actually add methods to the sealed trait itself.

  1. The official Rust guidelines suggest adding a #[doc(hidden)] macro to private methods. However, this is just a gentleman's agreement. External crates can still call the method, though we reserve the right to break their code if they do.
  2. Predrag's blog suggests adding an unnameable type parameter to each private method. However, this pollutes the call sites.
  3. Jack Wrenn's blog mentions the technique I'm currently following: adding the private methods to a "pub-in-priv" trait. The only downside mentioned is that this pattern can't be applied to an existing public trait without causing breaking changes for downstream users.

In my opinion, Option 3 is the best solution for us. It guarantees that private methods are not callable externally, and the call sites remain clean. I've renamed these private traits to XXXInternal to avoid confusion with the regular Sealed pattern.

I would love to hear your thoughts on this approach.


/// Defines the common interface for all live gRPC wire protocols and supported
/// transport security protocols (e.g., TLS, ALTS).
pub trait ClientChannelCredential: client::Sealed + Sync {
Copy link
Member

Choose a reason for hiding this comment

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

Why do these need Sync? I assume they would also get send and 'static?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When using a load balancing policy that manages multiple TCP connections (e.g., round_robin, ringhash, or least_request), multiple concurrent connections may be established. Consequently, the same credentials object must be wrapped in an Arc to be shared during the handshaking process.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it should be 'static. The static bound was being applied in the channel constructor. I've moved it to the trait now.

/// [#96865]: https://github.com/rust-lang/rust/issues/96865
/// [`Future`]: core::future::Future
/// [`Send`]: core::marker::Send
pub trait SendFuture: core::future::Future {
Copy link
Member

Choose a reason for hiding this comment

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

This is interesting, and makes a ton of sense. Assuming this is totally fine since the compiler will noop this out.

Copy link
Member

@LucioFranco LucioFranco left a comment

Choose a reason for hiding this comment

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

Really nice work! This seems like a fantastic foundation going forward. Left some comments none are blocking.

Comment on lines 65 to 66
pub struct Authority<'a> {
pub(crate) host: &'a str,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we attaching a lifetime here? Should this be an owned String instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Accepting a reference allows implementations to avoid unnecessary copies in most cases. The channel likely maintains a url::Url type and uses the host_str method to retrieve the authority for the credentials. Requiring an owned string would force the channel to perform a copy, which would then be consumed by the credentials during the handshake.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's once per connection? It seems like avoiding having lifetimes on structs would be worth the cost of a clone per connection. And even if it was performance-critical, could the [sub?]channel save away an Authority instance that it passes (by reference already) to all the handshakers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The connect and accept methods are already accepting &Authority, so it should be possible for the channel to re-use the same Authority object. I've replaced the &str with a String in the host field.

Copy link
Collaborator

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

(I suppose I should specifically request changes so the PR doesn't appear green.)

Copy link
Collaborator

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Everything else LGTM, but I really, really want to work hard to avoid having lifetimes on any structs (especially public-visible ones) unless absolutely necessary, as it can quickly become a huge cognitive burden. E.g. as soon as you want to put it into another struct, that one needs a lifetime, too.

@arjan-bal arjan-bal merged commit 149f366 into hyperium:master Feb 16, 2026
21 checks passed
@arjan-bal arjan-bal deleted the channel-creds-api branch February 16, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants