Merge branch 'release/2026.11-xynomizithra' into develop
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user