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

feat: QUIC Address discovery extension #2043

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

divagant-martian
Copy link

@divagant-martian divagant-martian commented Nov 14, 2024

Implements the Quic Address discovery extension IETF draft

  • Transport parameters are implemented with tests and 0rtt verification as spec'd
  • The spec declares two frames, one for informing of IpV4 addresses and one for informing IpV6 frames. This is implemented as a single structure with tests.
  • Transmission is done after the initial path has been established, when path validation is required, and sent with path responses. The last part is not required but provides a small reconfirmation of the observed external address to the peer, and it's allowed by the spec being observed address frames defined as probing frames. This is implemented with tests.
  • Retransmission is done by sending a fresh frame if it's identified as lost. This ensures that the path over which the frame is sent is always the correct one, i.e. That the reported observed address is always sent using the path for which the address was observed. This is implemented with tests.
  • Surfacing is done from the protocol implementation via Connection events.
  • Configuration is done with two methods to independently turn on or off the receiving and sending of reports.

@divagant-martian divagant-martian marked this pull request as ready for review November 21, 2024 17:26
Copy link
Collaborator

@Ralith Ralith left a comment

Choose a reason for hiding this comment

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

Thanks for working on this!

@@ -974,6 +981,8 @@ pub(crate) struct State {
send_buffer: Vec<u8>,
/// We buffer a transmit when the underlying I/O would block
buffered_transmit: Option<proto::Transmit>,
/// Our last external address reported by the peer.
pub(crate) observed_external_addr: watch::Sender<Option<SocketAddr>>,
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 pub(crate) necessary here?

@@ -636,6 +636,12 @@ impl Connection {
// May need to send MAX_STREAMS to make progress
conn.wake();
}

/// Track changed on our external address as reported by the peer.
pub fn observed_external_addr(&self) -> watch::Receiver<Option<SocketAddr>> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Explicitly document when this is None, whether it might contain spurious updates, and the influence of configuration and peer support.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure about exposing the watch::Receiver as part of the public API, that seems like it's leaking a bunch of implementation details?

@@ -68,6 +68,7 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread", "time", "macros
tracing-subscriber = { workspace = true }
tracing-futures = { workspace = true }
url = { workspace = true }
tokio-stream = "0.1.15"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is this used?

assert_matches!(conn.poll(), Some(Event::HandshakeDataReady));
assert_matches!(
conn.poll(),
Some(Event::ConnectionLost { reason }) if matches!(reason, ConnectionError::TransportError(_) )
Copy link
Collaborator

Choose a reason for hiding this comment

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

style nit: Why not Some(Event::ConnectionLost { reason: ConnectionError::TransportError(_) })?

/// NOTE: this test is the same as zero_rtt_happypath, changing client transport parameters on
/// resumption.
#[test]
fn address_discovery_zero_rtt_accepted() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What additional coverage does this case supply? I don't think changing client transport parameters are ever a concern for 0-RTT.


/// Conjuction of the information contained in the address discovery frames
/// ([`FrameType::OBSERVED_IPV4_ADDR`], [`FrameType::OBSERVED_IPV6_ADDR`]).
#[derive(Debug, PartialEq, Eq, Clone)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]

@@ -3760,6 +3874,8 @@ pub enum Event {
DatagramReceived,
/// One or more application datagrams have been sent after blocking
DatagramsUnblocked,
/// Received an observation of our external address from the peer.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Explicitly document whether this is necessarily different from the previous one, and the required configuration and peer support.

Comment on lines +2947 to +2948
// include in migration
migration_observed_addr = Some(observed)
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 a special case?

}

impl ObservedAddr {
pub(crate) fn new<N: Into<VarInt>>(remote: std::net::SocketAddr, seq_no: N) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we ever want to pass something that isn't already a VarInt in here? If not, skip the generic for simplicity.

Copy link
Member

Choose a reason for hiding this comment

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

Also, import SocketAddr instead of qualifying it.

///
/// When enabled, this is reported as a transport parameter.
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub(crate) enum Role {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like we go to a lot of trouble to convert this to and from a pair of bools. Would it it be simpler to represent it that way in the first place?

Copy link
Member

Choose a reason for hiding this comment

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

I also don't love the Role name for something arguably pretty niche, should we make the type name AddressDiscoveryRole instead? Or maybe with the bool fields it's no longer really a Role, either?


use crate::VarInt;

pub(crate) const TRANSPORT_PARAMETER_CODE: u64 = 0x9f81a176;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: constants last in the file, please.

///
/// When enabled, this is reported as a transport parameter.
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub(crate) enum Role {
Copy link
Member

Choose a reason for hiding this comment

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

I also don't love the Role name for something arguably pretty niche, should we make the type name AddressDiscoveryRole instead? Or maybe with the bool fields it's no longer really a Role, either?

Comment on lines +2930 to +2933
{
return Err(TransportError::PROTOCOL_VIOLATION(
"received OBSERVED_ADDRESS frame when not negotiated",
));
Copy link
Member

Choose a reason for hiding this comment

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

Could we yield this error from within should_report() instead? (Maybe also pass in packet.header.space()?)

Comment on lines +3016 to +3021
new_path.last_observed_addr_report = self.path.last_observed_addr_report.clone();
if let Some(report) = observed_addr {
if let Some(updated) = new_path.update_observed_addr_report(report) {
self.events.push_back(Event::ObservedAddr(updated));
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we push all/most of this logic into a method on PathData?

}

impl ObservedAddr {
pub(crate) fn new<N: Into<VarInt>>(remote: std::net::SocketAddr, seq_no: N) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

Also, import SocketAddr instead of qualifying it.

Comment on lines +1004 to +1011
let seq_no = bytes.get()?;
let ip = if is_ipv6 {
IpAddr::V6(bytes.get()?)
} else {
IpAddr::V4(bytes.get()?)
};
let port = bytes.get()?;
Ok(Self { seq_no, ip, port })
Copy link
Member

Choose a reason for hiding this comment

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

Ok(Self {
    seq_no: bytes.get()?,
    ip: match is_ipv6 {
        true => IpAddr::V6(bytes.get()?),
        false => IpAddr::V4(bytes.get()?),
    },
    port: bytes.get()?,
})

Comment on lines +438 to +450
if !params.address_discovery_role.is_disabled() {
// duplicate parameter
return Err(Error::Malformed);
}
let value: VarInt = r.get()?;
if len != value.size() {
return Err(Error::Malformed);
}
params.address_discovery_role = value.try_into()?;
tracing::debug!(
role = ?params.address_discovery_role,
"address discovery enabled for peer"
);
Copy link
Member

Choose a reason for hiding this comment

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

Suggest rolling this up into a Role::from_parameter() (name TBD) constructor.

Comment on lines +125 to +135
let mut external_addresses = conn.observed_external_addr();
tokio::spawn(async move {
loop {
if let Some(new_addr) = *external_addresses.borrow_and_update() {
info!(%new_addr, "new external address report");
}
if external_addresses.changed().await.is_err() {
break;
}
}
});
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is widely used enough that it merits real estate in these examples.

@@ -636,6 +636,12 @@ impl Connection {
// May need to send MAX_STREAMS to make progress
conn.wake();
}

/// Track changed on our external address as reported by the peer.
pub fn observed_external_addr(&self) -> watch::Receiver<Option<SocketAddr>> {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about exposing the watch::Receiver as part of the public API, that seems like it's leaking a bunch of implementation details?

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