in neqo-transport/src/connection/mod.rs [2432:2616]
fn output_path(
&mut self,
path: &PathRef,
now: Instant,
closing_frame: Option<&ClosingFrame>,
) -> Res<SendOption> {
let mut initial_sent = None;
let mut packet_tos = None;
let mut needs_padding = false;
let grease_quic_bit = self.can_grease_quic_bit();
let version = self.version();
// Determine how we are sending packets (PTO, etc..).
let profile = self.loss_recovery.send_profile(&path.borrow(), now);
qdebug!("[{self}] output_path send_profile {profile:?}");
// Frames for different epochs must go in different packets, but then these
// packets can go in a single datagram
let mut encoder = Encoder::with_capacity(profile.limit());
for space in PacketNumberSpace::iter() {
// Ensure we have tx crypto state for this epoch, or skip it.
let Some((epoch, tx)) = self.crypto.states.select_tx_mut(self.version, space) else {
continue;
};
let header_start = encoder.len();
let (pt, mut builder) = Self::build_packet_header(
&path.borrow(),
epoch,
encoder,
tx,
&self.address_validation,
version,
grease_quic_bit,
);
let pn = Self::add_packet_number(
&mut builder,
tx,
self.loss_recovery.largest_acknowledged_pn(space),
);
// The builder will set the limit to 0 if there isn't enough space for the header.
if builder.is_full() {
encoder = builder.abort();
break;
}
// Configure the limits and padding for this packet.
let aead_expansion = tx.expansion();
needs_padding |= builder.set_initial_limit(
&profile,
aead_expansion,
self.paths
.primary()
.ok_or(Error::InternalError)?
.borrow()
.pmtud(),
);
builder.enable_padding(needs_padding);
if builder.is_full() {
encoder = builder.abort();
break;
}
// Add frames to the packet.
let payload_start = builder.len();
let (mut tokens, mut ack_eliciting, mut padded) = (Vec::new(), false, false);
if let Some(close) = closing_frame {
self.write_closing_frames(close, &mut builder, space, now, path, &mut tokens);
} else {
(tokens, ack_eliciting, padded) =
self.write_frames(path, space, &profile, &mut builder, header_start != 0, now);
}
if builder.packet_empty() {
// Nothing to include in this packet.
encoder = builder.abort();
continue;
}
// If we don't have a TOS for this UDP datagram yet (i.e. `tos` is `None`), get it,
// adding a `RecoveryToken::EcnEct0` to `tokens` in case of loss.
let tos = packet_tos.get_or_insert_with(|| path.borrow().tos(&mut tokens));
self.log_packet(
packet::MetaData::new_out(
path,
pt,
pn,
builder.len() + aead_expansion,
&builder.as_ref()[payload_start..],
*tos,
),
now,
);
self.stats.borrow_mut().packets_tx += 1;
let tx = self
.crypto
.states
.tx_mut(self.version, epoch)
.ok_or(Error::InternalError)?;
encoder = builder.build(tx)?;
self.crypto.states.auto_update()?;
if ack_eliciting {
self.idle_timeout.on_packet_sent(now);
}
let sent = SentPacket::new(
pt,
pn,
now,
ack_eliciting,
tokens,
encoder.len() - header_start,
);
if padded {
needs_padding = false;
self.loss_recovery.on_packet_sent(path, sent, now);
} else if pt == PacketType::Initial && (self.role == Role::Client || ack_eliciting) {
// Packets containing Initial packets might need padding, and we want to
// track that padding along with the Initial packet. So defer tracking.
initial_sent = Some(sent);
needs_padding = true;
} else {
if pt == PacketType::Handshake && self.role == Role::Client {
needs_padding = false;
}
self.loss_recovery.on_packet_sent(path, sent, now);
}
// Track which packet types are sent with which ECN codepoints. For
// coalesced packets, this increases the counts for each packet type
// contained in the coalesced packet. This is per Section 13.4.1 of
// RFC 9000.
self.stats.borrow_mut().ecn_tx[pt] += IpTosEcn::from(*tos);
if space == PacketNumberSpace::Handshake {
if self.role == Role::Client {
// We're sending a Handshake packet, so we can discard Initial keys.
self.discard_keys(PacketNumberSpace::Initial, now);
} else if self.role == Role::Server && self.state == State::Confirmed {
// We could discard handshake keys in set_state,
// but wait until after sending an ACK.
self.discard_keys(PacketNumberSpace::Handshake, now);
}
}
// If the client has more CRYPTO data queued up, do not coalesce if
// this packet is an Initial. Without this, 0-RTT packets could be
// coalesced with the first Initial, which some server (e.g., ours)
// do not support, because they may not save packets they can't
// decrypt yet.
if self.role == Role::Client
&& space == PacketNumberSpace::Initial
&& !self.crypto.streams.is_empty(space)
{
break;
}
}
if encoder.is_empty() {
qdebug!("TX blocked, profile={profile:?}");
Ok(SendOption::No(profile.paced()))
} else {
// Perform additional padding for Initial packets as necessary.
let mut packets: Vec<u8> = encoder.into();
if let Some(mut initial) = initial_sent.take() {
if needs_padding && packets.len() < profile.limit() {
qdebug!(
"[{self}] pad Initial from {} to PLPMTU {}",
packets.len(),
profile.limit()
);
initial.track_padding(profile.limit() - packets.len());
// These zeros aren't padding frames, they are an invalid all-zero coalesced
// packet, which is why we don't increase `frame_tx.padding` count here.
packets.resize(profile.limit(), 0);
}
self.loss_recovery.on_packet_sent(path, initial, now);
}
path.borrow_mut().add_sent(packets.len());
Ok(SendOption::Yes(path.borrow_mut().datagram(
packets,
packet_tos.unwrap_or_default(),
&mut self.stats.borrow_mut(),
)))
}
}