Merge branch 'release/2026.11-xynomizithra' into develop

This commit is contained in:
benedettadavico
2026-06-09 16:24:28 +02:00
42 changed files with 2526 additions and 1894 deletions
+35 -5
View File
@@ -218,6 +218,11 @@ impl ManagedConnection {
"Managed to establish connection to {}", self.address
);
// disable Nagle: mix packets are latency-sensitive and flushed one at a time.
if let Err(err) = stream.set_nodelay(true) {
warn!(peer = %address, error = %err, "failed to set TCP_NODELAY on outbound mixnet connection");
}
// 3. perform noise handshake (if applicable)
let noise_start = tokio::time::Instant::now();
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
@@ -246,25 +251,42 @@ impl ManagedConnection {
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
let conn = Framed::new(noise_stream, NymCodec);
let mut conn = Framed::new(noise_stream, NymCodec);
// let the write buffer accumulate several packets before flushing (see run_io_loop)
conn.set_backpressure_boundary(OUTBOUND_WRITE_BUFFER);
// 4. start handling the framed stream
run_io_loop(conn, self.message_receiver, address).await;
}
}
/// Upper bound on how many already-queued packets we drain into a single flush.
/// Bounds the per-batch allocation and how often we re-check the read side; the actual
/// write coalescing is governed by the Framed backpressure boundary below.
const OUTBOUND_FLUSH_BATCH: usize = 1024;
/// Write-buffer high-water mark for the egress `Framed`: packets are coalesced up to
/// roughly this many bytes before a flush, trading a larger write burst for far fewer
/// syscalls (and noise frames) under load. Kept under the ~64KiB noise frame ceiling so
/// a flush is usually a single frame.
const OUTBOUND_WRITE_BUFFER: usize = 32 * 1024;
// The connection is unidirectional (send-only); we read from it solely to
// notice peer FIN/RST while idle so we can evict the cache entry before the
// next outbound send finds it stale.
async fn run_io_loop<T>(
conn: Framed<T, NymCodec>,
mut receiver: ReceiverStream<FramedNymPacket>,
receiver: ReceiverStream<FramedNymPacket>,
address: SocketAddr,
) where
T: AsyncRead + AsyncWrite + Unpin,
{
let (mut sink, mut stream) = conn.split();
// drain all currently-queued packets into one flush rather than flushing per packet,
// which otherwise caps egress throughput and backs up the per-connection queue under load
let mut receiver = receiver.ready_chunks(OUTBOUND_FLUSH_BATCH);
loop {
tokio::select! {
msg = stream.next() => {
@@ -305,14 +327,22 @@ async fn run_io_loop<T>(
);
break;
}
Some(packet) => {
if let Err(err) = sink.send(packet).await {
Some(batch) => {
// feed the whole ready batch, then flush once
let res = async {
for packet in batch {
sink.feed(packet).await?;
}
sink.flush().await
}
.await;
if let Err(err) = res {
debug!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packet to {address}: {err}"
"failed to forward packet batch to {address}: {err}"
);
break;
}
@@ -61,9 +61,12 @@ pub struct NodeFamily {
/// A pending invitation for a node to join a particular family.
///
/// Invitations are stored until they are accepted, rejected, revoked, or until the
/// chain advances past `expires_at` (in which case they remain in storage but are
/// treated as inert — there is no background process clearing expired invitations).
/// Invitations are stored until they are accepted, rejected, or revoked. Once the
/// chain advances past `expires_at` an invitation becomes inert but stays in storage
/// — there is no background process clearing expired invitations. A timed-out
/// invitation is cleared either when explicitly revoked/rejected, or when the family
/// issues a fresh invitation for the same node, which archives the stale one as
/// `Expired` and supersedes it.
#[cw_serde]
pub struct FamilyInvitation {
/// The family that issued the invitation.
@@ -107,8 +110,10 @@ pub struct PastFamilyMember {
/// Terminal status for an invitation that has been moved out of the pending set.
///
/// Note: timed-out invitations are not represented here — they are simply left in
/// the pending set (see `FamilyInvitation::expires_at`).
/// Note: an invitation that merely times out is **not** archived here on its own —
/// it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only
/// reaches `Expired` if the family issues a fresh invitation for the same node, which
/// supersedes and archives the stale one.
#[cw_serde]
pub enum FamilyInvitationStatus {
/// Still awaiting a response. Recorded with a timestamp for completeness even
@@ -121,11 +126,16 @@ pub enum FamilyInvitationStatus {
/// The family revoked the invitation at the given timestamp before it could
/// be accepted or rejected.
Revoked { at: u64 },
/// The invitation had already expired and was superseded by a fresh invitation
/// for the same node from the same family, issued at the given timestamp. This is
/// the only path that archives a timed-out invitation.
Expired { at: u64 },
}
/// Historical record of an invitation that has reached a terminal state
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
/// archived here — they remain in the pending map until explicitly cleared.
/// (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is
/// archived here only when a fresh invitation for the same node supersedes it
/// (status `Expired`); otherwise it stays in the pending map until explicitly cleared.
#[cw_serde]
pub struct PastFamilyInvitation {
/// The original invitation as it was issued.
+2 -4
View File
@@ -24,10 +24,8 @@ pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
// \/ TODO: this has to be updated once the contract is deployed
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str =
"n1na0vys0z077hq3zrz6pfea85zgv8ks3t5zysdt6y38c87q045hnsyf2g5x";
pub const ECASH_CONTRACT_ADDRESS: &str =
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
pub const GROUP_CONTRACT_ADDRESS: &str =
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-kkt"
description = "Key transport protocol for the Nym network"
version = "1.21.0"
version = "1.21.1"
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
edition = { workspace = true }
license.workspace = true
@@ -14,6 +14,7 @@ use tokio::sync::mpsc::Receiver;
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)]
pub enum PeerControlRequestTypeV2 {
AddPeer,
UpdatePeerPsk,
RemovePeer,
QueryPeer,
GetClientBandwidthByKey,
@@ -26,6 +27,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
fn from(req: &PeerControlRequest) -> Self {
match req {
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::UpdatePeerPsk { .. } => PeerControlRequestTypeV2::UpdatePeerPsk,
PeerControlRequest::PreAllocateIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
@@ -115,6 +117,15 @@ impl MockPeerControllerV2 {
)
.unwrap();
}
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
response_tx
.send(
*response
.downcast()
.expect("registered response has mismatched type"),
)
.unwrap();
}
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
response_tx
.send(
@@ -71,6 +71,7 @@ impl From<&Key> for KeyWrapper {
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq)]
pub enum PeerControlRequestType {
AddPeer { public_key: KeyWrapper },
UpdatePeerPsk { peer_key: KeyWrapper },
AllocatePeerIpPair {},
ReleaseIpPair { ip_pair: IpPair },
RemovePeer { key: KeyWrapper },
@@ -86,6 +87,7 @@ impl PeerControlRequestType {
pub fn peer_key(&self) -> Option<KeyWrapper> {
match self {
PeerControlRequestType::AddPeer { public_key } => Some(public_key.clone()),
PeerControlRequestType::UpdatePeerPsk { peer_key } => Some(peer_key.clone()),
PeerControlRequestType::AllocatePeerIpPair {} => None,
PeerControlRequestType::ReleaseIpPair { .. } => None,
PeerControlRequestType::RemovePeer { key } => Some(key.clone()),
@@ -109,6 +111,11 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
PeerControlRequest::AddPeer { peer, .. } => PeerControlRequestType::AddPeer {
public_key: (&peer.public_key).into(),
},
PeerControlRequest::UpdatePeerPsk { peer_key, .. } => {
PeerControlRequestType::UpdatePeerPsk {
peer_key: peer_key.into(),
}
}
PeerControlRequest::PreAllocateIpPair { .. } => {
PeerControlRequestType::AllocatePeerIpPair {}
}
@@ -271,6 +278,9 @@ impl MockPeerController {
}
response_tx.send_downcasted(response.content)
}
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
response_tx.send_downcasted(response.content)
}
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
response_tx.send_downcasted(response.content)
}
@@ -76,6 +76,12 @@ pub enum PeerControlRequest {
peer: Peer,
response_tx: oneshot::Sender<AddPeerControlResponse>,
},
/// Update PSK for an existing peer, without changing its IP allocation
UpdatePeerPsk {
peer_key: Key,
psk: Key,
response_tx: oneshot::Sender<UpdatePeerPskControlResponse>,
},
/// Attempt to allocate an IP pair from the pool
PreAllocateIpPair {
response_tx: oneshot::Sender<AllocatePeerControlResponse>,
@@ -118,6 +124,7 @@ pub enum PeerControlRequest {
}
pub type AddPeerControlResponse = Result<()>;
pub type UpdatePeerPskControlResponse = Result<()>;
pub type AllocatePeerControlResponse = Result<IpPair>;
pub type ReleaseIpPairControlResponse = Result<()>;
pub type RemovePeerControlResponse = Result<()>;
@@ -317,6 +324,50 @@ impl PeerController {
Ok(())
}
async fn handle_update_peer_psk_request(&mut self, peer_key: &Key, psk: Key) -> Result<()> {
// observation will get automatically added once dropped
let _metric_timer =
PROMETHEUS_METRICS.start_timer(PrometheusMetric::WireguardDefguardPeerPskUpdate);
nym_metrics::inc!("wg_peer_update_psk_attempts");
let Ok(Some(mut peer)) = self.handle_query_peer_by_key(peer_key).await else {
return Ok(());
};
let encoded_psk = psk.to_lower_hex();
peer.preshared_key = Some(psk);
// Account for bandwidth used so far *before* reconfiguring: `configure_peer`
// isn't guaranteed to preserve the kernel rx/tx counters, so fold the
// accrued bytes into the metrics first to avoid losing them on a reset.
if let Ok(host) = self.wg_api.read_interface_data() {
self.update_metrics(&host).await;
*self.host_information.write().await = host;
}
// Try to update WireGuard peer
if let Err(e) = self.wg_api.configure_peer(&peer) {
nym_metrics::inc!("wg_peer_update_psk_failed");
nym_metrics::inc!("wg_config_errors_total");
return Err(e.into());
};
// Persist the new PSK to disk so it survives a restart. Kernel-first: a
// failure here leaves the live session working, only risking drift on restart.
self.ecash_verifier
.storage()
.update_peer_psk(&peer_key.to_string(), Some(&encoded_psk))
.await?;
// Refresh again so the cached host information reflects the post-update state
if let Ok(host) = self.wg_api.read_interface_data() {
*self.host_information.write().await = host;
}
nym_metrics::inc!("wg_peer_update_psk_success");
Ok(())
}
/// Allocate IP pair from pool for a new peer registration
///
/// This only allocates IPs - the caller must handle database storage and
@@ -513,6 +564,15 @@ impl PeerController {
PeerControlRequest::AddPeer { peer, response_tx } => {
response_tx.send(self.handle_add_request(&peer).await).ok();
}
PeerControlRequest::UpdatePeerPsk {
peer_key,
psk,
response_tx,
} => {
response_tx
.send(self.handle_update_peer_psk_request(&peer_key, psk).await)
.ok();
}
PeerControlRequest::PreAllocateIpPair { response_tx } => {
response_tx.send(self.handle_ip_allocation_request()).ok();
}