-
Notifications
You must be signed in to change notification settings - Fork 957
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
feat: add WebTransport protocol #4874
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great start! I've left some comments :)
transports/tls/src/lib.rs
Outdated
pub fn make_server_config_with_cert( | ||
certificate: Certificate, | ||
private_key: PrivateKey, | ||
alpn: Vec<Vec<u8>> | ||
) -> Result<rustls::ServerConfig, certificate::GenError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to find a way to make generation of these certificates deterministic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the corresponding go-libp2p pull request libp2p/go-libp2p#1833.
transports/tls/src/lib.rs
Outdated
@@ -79,3 +80,23 @@ pub fn make_server_config( | |||
|
|||
Ok(crypto) | |||
} | |||
|
|||
/// Create a TLS server configuration for libp2p. | |||
pub fn make_server_config_with_cert( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think special-casing this for webtransport would be better.
transports/quic/src/transport.rs
Outdated
.map_err(ConnectError)?; | ||
Connecting::new(connecting, handshake_timeout).await | ||
if wt { | ||
todo!("Here should be part that creates connection to a WT server") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like I mentioned in the issue, I don't think we need to support dialing of WT addresses.
let this = self.get_mut(); | ||
|
||
let incoming = this.incoming.get_or_insert_with(|| { | ||
let accept = this.session.accept_bi(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you Arc
the WebTransportSession
then you can clone it here and fix the lifetime issues.
|
||
/// A certificate fingerprint that is assumed to be created using the SHA256 hash algorithm. | ||
#[derive(Eq, PartialEq, Copy, Clone)] | ||
pub struct Fingerprint([u8; 32]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if we should eventually upstream (parts) of this to rust-multiaddr
and have Protocol::Certhash
take a Fingerprint
.
/// Formats this fingerprint as uppercase hex, separated by colons (`:`). | ||
/// | ||
/// This is the format described in <https://www.rfc-editor.org/rfc/rfc4572#section-5>. | ||
pub fn to_sdp_format(self) -> String { | ||
self.0.map(|byte| format!("{byte:02X}")).join(":") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for anything SDP related in WebTransport :)
if request.method() == &Method::CONNECT { | ||
let session = WebTransportSession::accept(request, stream, h3_conn) | ||
.await.map_err(|e| WebTransportError::Http3Error(e))?; | ||
Ok((peer_id, session)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to run a noise
security handshake here, assuming the type
query parameter was set to noise
, see https://github.com/libp2p/specs/tree/master/webtransport#security-handshake.
let ext = request.extensions(); | ||
let proto = ext.get::<Protocol>(); | ||
if Some(&Protocol::WEB_TRANSPORT) == proto { | ||
if request.method() == &Method::CONNECT { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to validate the path as well, see https://github.com/libp2p/specs/tree/master/webtransport#webtransport-http-endpoint.
transports/quic/src/transport.rs
Outdated
); | ||
|
||
let timeout = self.handshake_timeout.clone(); | ||
let fut = if self.web_transport_addr { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of deciding this based on a boolean, it would be great if we could first accept the QUIC connection and dynamically decide based on what the client actually does.
In the current state, this means we cannot share a listener on the same port for WT and regular QUIC connections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand the quinn
docs correctly, we can do connecting.await
which gives us a quinn::Connection
. We can then call Connection::handshake_data
and downcast that to https://docs.rs/quinn/latest/quinn/crypto/rustls/struct.HandshakeData.html.
From there, we can inspect the ALPN protocol and decide whether this is a regular libp2p QUIC connection or in fact a WT transport connection.
Using that, we can then branch correctly on what we want to do, assuming the user is actually listening on both transports! If the user only said listen_on
without /webtransport
, then we should reject the connection. Similarly, if we are only listening on /webtransport
, we should (probably?) reject regular QUIC connections.
I think to achieve this, we will need to slightly modify our listen_on
behaviour to remember, which port we are listening on and try to find an existing listener on the same port to add the specific functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current state, this means we cannot share a listener on the same port for WT and regular QUIC connections.
It would be really nice if we could achieve this but I'd also be okay if we land this in a 2nd iteration.
# Conflicts: # Cargo.lock
@dgarus Please ping me once you'd like me to take another look here! |
Hi! Ok. |
I don't think there is one at the moment :( In |
See also: #3049 |
Really, I don't like the idea of generating a cert deterministically because I think it's bad for security. What is the problem with fresh certs? |
Can you elaborate the security problem that you are seeing? The essence of the certificate is the private key that it is signed with. The node's private key should be sufficient to produce such a key via a HKDF. The problem with "fresh" certificates is that they are useless unless you communicate the certificate hash to the remote who wants to connect. If we generate a new certificate every time on startup, all addresses that we have previously given out via identify or what other nodes have stored in their kademlia routing table are now useless. |
I thought about it one more time, and it looks ok. |
If a peer's private key is exposed then we have bigger problems I think :) |
@thomaseizinger
What are your thoughts on sharing an endpoint rather than a listener? Please take a look how I did a |
@thomaseizinger Hi! |
Sorry for the delay, I was on vacation! Will take a look now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay. I've left some comments, let me know what you think!
h3 = { git = "https://github.com/hyperium/h3" } | ||
h3-quinn = { git = "https://github.com/hyperium/h3" } | ||
h3-webtransport = { git = "https://github.com/hyperium/h3" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there no released versions of these libraries yet? We won't be able to merge this without released versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is because there hasn't yet been a release of h3-webtransport
with h3 0.0.4
. See https://crates.io/crates/h3-webtransport/0.1.0/dependencies. I'd suggest nudging the maintainers for a new release!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OMG
Thanks, I'll do it a bit later
fn same_cert_hashes(&self, cert_hashes: &Vec<Multihash<64>>) -> bool { | ||
if self.webtransport.cert_hashes.len() != cert_hashes.len() { | ||
return false | ||
} | ||
let mut set: HashSet<Multihash<64>> = self.webtransport.cert_hashes | ||
.clone().into_iter().collect(); | ||
for hash in cert_hashes { | ||
set.remove(hash); | ||
} | ||
|
||
set.is_empty() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we solve this more elegantly using HashSet::symmetric_difference
?
transports/quic/src/config.rs
Outdated
} | ||
|
||
pub fn server_quinn_config(&mut self | ||
) -> Result<(quinn::ServerConfig, libp2p_noise::Config, Vec<Multihash<64>>), certificate::GenError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quinn
is currently a private dependency and it would be great if we can keep it that way!
#[error(transparent)] | ||
CertificateGenerationError(#[from] certificate::GenError), | ||
|
||
#[error("WebTransport error.")] | ||
WebTransportError, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, the enum is not non_exhaustive
so this would be a breaking change. Could we (in a first version at least) not add these variants? Worst case we could "hide" them in a custom IO error using io::Error::new()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I've not checked where these errors are used and whether we could avoid them otherwise).
let mut h3_conn = h3::server::builder() | ||
.enable_webtransport(true) | ||
.enable_connect(true) | ||
.enable_datagram(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can set this to false
, we don't support datagrams.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably de-duplicate this one with a macro_rules!
:)
@@ -37,7 +37,7 @@ pub use futures_rustls::TlsStream; | |||
pub use upgrade::Config; | |||
pub use upgrade::UpgradeError; | |||
|
|||
const P2P_ALPN: [u8; 6] = *b"libp2p"; | |||
pub const P2P_ALPN: [u8; 6] = *b"libp2p"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would pub(crate)
be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not reviewed this in detail because I am not sure this is useful to us as long as we don't have deterministic certificates.
Instead of rolling certificates, I'd suggest (something like) the following:
- Give our users a simple API to generate and parse certificates
- Allow the user to pass in a list of certificates in the QUIC config, explicitly for WebTransport
- For the QUIC functionality, we can generate one similarly to how we do it currently
- When the user calls
listen_on
for webtransport, we validate the certificates have a valid date range (the webtransport spec requires +/- 14 days I think)
Whilst not super convenient, this allows users to externally manage certificates until we can generate them deterministically. What is important is that we give users some way of restarting a libp2p-node and retain the certificate. WebTransport is of no use if the certificate hashes change constantly.
Does this make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, @thomaseizinger ! I agree with you.
Give our users a simple API to generate and parse certificates
What should it look like?
I could suggest, for example, defining in the settings a folder where WT will save autogenerated certs. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could suggest, for example, defining in the settings a folder where WT will save autogenerated certs. WDYT?
I would like to avoid interacting with the file system as much as possible. My suggestion would be:
libp2p::webtransport::Transport::new
takes a list of certificates (of typelibp2p::webtransport::Certificate
)libp2p::webtransport::Certificate::generate
allows users generate a new certificate with certain parameters (validity date etc)libp2p::webtransport::Certificate::{parse,to_bytes}
allow users to serialize and deserialize certificates
I would completely leave it up to the user to roll those certificates. For an MVP, that is good enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly! I look at it from the side of an application developer, but it's a library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine, a user started node with WT and a few certs. But when certs become outdated, the user has to restart node with a new list of certs. And this restart is a wrong thing, IMHO.
WDYT about adding a new cert to WT cert's list?
Also, I think, to manage certs, users should have the ability to get this list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remember correctly, then the max age of the cert is two weeks in order to use the certHashes
feature. I think for an MVP, it is acceptable having to restart a node every two weeks to rotate the certs.
Let's start with that. We can always add more APIs later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly! I look at it from the side of an application developer, but it's a library.
It is very important to take the look of the app developer, otherwise we won't be building useful APIs :)
But we also need to make sure we offer flexible APIs and don't provide some that are too opinionated.
@@ -306,7 +331,7 @@ impl<P: Provider> Transport for GenTransport<P> { | |||
&mut self, | |||
addr: Multiaddr, | |||
) -> Result<Self::Dial, TransportError<Self::Error>> { | |||
let (socket_addr, _version, peer_id) = | |||
let (socket_addr, _version, peer_id, _is_webtransport) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we also need to validate that we aren't dialing a webtransport address.
let endpoint = match self.get_existed_endpoint(&socket_addr, &cert_hashes) { | ||
Some(res) => res, | ||
None => Self::new_endpoint(self.config.endpoint_config(), Some(server_config), socket)?, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure this will work as intended. Using the same endpoint
in two Listener
s will make them compete, no?
Let's consider the following scenario:
- User calls
listen_on
for/quic-v1
- User calls
listen_on
for/webtransport
using the same port - Another peer attempts to connect using the
/webtransport
address
The incoming connection will be picked up by one of the two listeners. Until we inspect the Connecting
struct, we don't actually know whether it is a QUIC or a WebTransport connection.
So I think what we need to do is:
- At most have 1
Listener
for a given socket address - The listener needs to be one of 3 modes:
- Only QUIC
- Only WT
- Both
- When we receive a
listen_on
for a socket with an existing listener, we need to update its mode - For incoming connections, we need to inspect it whether it is a QUIC or WT connection and check whether the listener supports whatever is incoming. If yes, upgrade it accordingly.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I'll try it
@thomaseizinger Hi, Thomas! |
Btw @dgarus, if you want to push the deterministic certificate story forward: The next step would be to open an issue with |
# Conflicts: # transports/quic/Cargo.toml # transports/quic/src/config.rs # transports/quic/src/transport.rs # transports/tls/src/verifier.rs
Actual PR |
Description
related to #2993
Notes & open questions
Change checklist