Files
buzz/crates/buzz-relay-mesh/src/endpoint.rs
Tyler ccb021d713 Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) (#1670)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
2026-07-14 16:56:48 -04:00

294 lines
9.4 KiB
Rust

use std::net::SocketAddr;
use iroh::{Endpoint, EndpointAddr, PublicKey, RelayMode, SecretKey, TransportAddr};
use crate::{MeshError, RuntimeId, ALPN};
/// Local iroh endpoint for the relay mesh.
///
/// Identity is the iroh/ed25519 public key of a boot-unique keypair generated
/// at process start.
#[derive(Debug, Clone)]
pub struct MeshEndpoint {
endpoint: Endpoint,
runtime_id: RuntimeId,
}
impl MeshEndpoint {
/// Generate a boot-unique mesh keypair and bind a mesh endpoint on `bind_addr`.
pub async fn bind(bind_addr: SocketAddr) -> Result<Self, MeshError> {
Self::bind_with_secret_key(SecretKey::generate(), bind_addr).await
}
/// Bind with an explicit keypair. Production should use [`Self::bind`] so
/// every process boot gets a fresh RuntimeId; tests use this for stable
/// identities.
pub async fn bind_with_secret_key(
secret_key: SecretKey,
bind_addr: SocketAddr,
) -> Result<Self, MeshError> {
let runtime_id = runtime_id_from_public_key(secret_key.public());
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.secret_key(secret_key)
.alpns(vec![ALPN.to_vec()])
.relay_mode(RelayMode::Disabled)
.bind_addr(bind_addr)
.map_err(|err| MeshError::Transport(err.to_string()))?
.bind()
.await
.map_err(|err| MeshError::Transport(err.to_string()))?;
Ok(Self {
endpoint,
runtime_id,
})
}
pub fn runtime_id(&self) -> RuntimeId {
self.runtime_id
}
pub fn endpoint(&self) -> Endpoint {
self.endpoint.clone()
}
pub fn addr(&self) -> EndpointAddr {
self.endpoint.addr()
}
/// The endpoint's directly-dialable IP socket addrs (no relay paths).
/// Lets consumers build advertise records without depending on iroh types.
pub fn ip_addrs(&self) -> Vec<SocketAddr> {
self.endpoint
.addr()
.addrs
.iter()
.filter_map(|ta| match ta {
TransportAddr::Ip(sock) => Some(*sock),
_ => None,
})
.collect()
}
pub async fn accept(&self) -> Result<Option<crate::peer::MeshPeer>, MeshError> {
let Some(incoming) = self.endpoint.accept().await else {
return Ok(None);
};
let conn = incoming
.await
.map_err(|err| MeshError::Transport(err.to_string()))?;
crate::peer::MeshPeer::from_connection(self.endpoint.clone(), conn).map(Some)
}
pub async fn connect(
&self,
peer_addr: EndpointAddr,
) -> Result<crate::peer::MeshPeer, MeshError> {
let conn = self
.endpoint
.connect(peer_addr, ALPN)
.await
.map_err(|err| MeshError::Transport(err.to_string()))?;
crate::peer::MeshPeer::from_connection(self.endpoint.clone(), conn)
}
}
pub fn runtime_id_from_public_key(public_key: PublicKey) -> RuntimeId {
RuntimeId(*public_key.as_bytes())
}
pub fn public_key_from_runtime_id(runtime_id: RuntimeId) -> Result<PublicKey, MeshError> {
PublicKey::from_bytes(&runtime_id.0).map_err(|err| MeshError::Transport(err.to_string()))
}
pub fn direct_addr(runtime_id: RuntimeId, addr: SocketAddr) -> Result<EndpointAddr, MeshError> {
Ok(EndpointAddr::from_parts(
public_key_from_runtime_id(runtime_id)?,
[TransportAddr::Ip(addr)],
))
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use iroh::SecretKey;
use tokio::time::timeout;
use uuid::Uuid;
use super::MeshEndpoint;
use crate::{
wire, FencedHeader, GoodbyeReason, MeshDatagram, MeshError, MeshStreamFrame, Profile,
RuntimeId, StreamHello, StreamRole,
};
fn loopback_any() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
}
fn fenced(owner_runtime_id: RuntimeId) -> FencedHeader {
FencedHeader {
session_id: Uuid::from_u128(0xABCD),
generation: 7,
owner_runtime_id,
}
}
async fn endpoint_pair() -> (MeshEndpoint, MeshEndpoint) {
let a =
MeshEndpoint::bind_with_secret_key(SecretKey::from_bytes(&[1u8; 32]), loopback_any())
.await
.unwrap();
let b =
MeshEndpoint::bind_with_secret_key(SecretKey::from_bytes(&[2u8; 32]), loopback_any())
.await
.unwrap();
(a, b)
}
async fn connected_pair() -> (
crate::peer::MeshPeer,
crate::peer::MeshPeer,
RuntimeId,
RuntimeId,
) {
let (a, b) = endpoint_pair().await;
let a_runtime_id = a.runtime_id();
let b_runtime_id = b.runtime_id();
let b_addr = b.addr();
let accept = tokio::spawn(async move { b.accept().await.unwrap().unwrap() });
let a_peer = a.connect(b_addr).await.unwrap();
let b_peer = accept.await.unwrap();
(a_peer, b_peer, a_runtime_id, b_runtime_id)
}
#[tokio::test]
async fn two_endpoints_connect_with_alpn_and_authenticated_identity() {
let (a_peer, b_peer, a_runtime_id, b_runtime_id) = connected_pair().await;
assert_eq!(a_peer.runtime_id(), b_runtime_id);
assert_eq!(b_peer.runtime_id(), a_runtime_id);
assert!(a_peer.max_datagram_size().expect("datagrams enabled") > 0);
}
#[tokio::test]
async fn reliable_stream_roundtrip_carries_mesh_stream_frame() {
let (a_peer, b_peer, _a_runtime_id, b_runtime_id) = connected_pair().await;
let fenced = fenced(b_runtime_id);
let hello = MeshStreamFrame::Hello(StreamHello {
sender: RuntimeId([9u8; 32]),
role: StreamRole::Session {
fenced,
profile: Profile::ReliableStream,
},
});
let data = MeshStreamFrame::Data {
fenced,
payload: b"goose bytes".to_vec(),
};
let goodbye = MeshStreamFrame::Goodbye {
fenced,
reason: GoodbyeReason::SessionEnded,
};
let recv = tokio::spawn(async move {
let mut stream = b_peer.accept_bi().await.unwrap();
let first = stream.recv_frame().await.unwrap().unwrap();
let second = stream.recv_frame().await.unwrap().unwrap();
let third = stream.recv_frame().await.unwrap().unwrap();
(first, second, third)
});
let mut stream = a_peer.open_bi().await.unwrap();
stream.send_frame(hello.clone()).await.unwrap();
stream.send_frame(data.clone()).await.unwrap();
stream.send_frame(goodbye.clone()).await.unwrap();
stream.finish().unwrap();
let (got_hello, got_data, got_goodbye) = timeout(Duration::from_secs(5), recv)
.await
.unwrap()
.unwrap();
assert_eq!(got_hello, hello);
assert_eq!(got_data, data);
assert_eq!(got_goodbye, goodbye);
}
#[tokio::test]
async fn datagram_roundtrip_carries_mesh_datagram() {
let (a_peer, b_peer, _a_runtime_id, b_runtime_id) = connected_pair().await;
let dgram = MeshDatagram {
fenced: fenced(b_runtime_id),
seq: 1,
payload: vec![13, 37, 42],
};
a_peer.send_datagram(&dgram).unwrap();
let got = timeout(Duration::from_secs(5), b_peer.recv_datagram())
.await
.unwrap()
.unwrap();
assert_eq!(got, dgram);
}
#[tokio::test]
async fn oversized_datagram_is_rejected_before_send() {
let (a_peer, _b_peer, _a_runtime_id, b_runtime_id) = connected_pair().await;
let max = a_peer.max_datagram_size().expect("datagrams enabled");
let dgram = MeshDatagram {
fenced: fenced(b_runtime_id),
seq: 1,
payload: vec![0u8; max + 1],
};
let err = a_peer.send_datagram(&dgram).unwrap_err();
assert!(matches!(
err,
MeshError::DatagramTooLarge { size, max: limit } if size > limit
));
}
#[tokio::test]
async fn opus_sized_datagrams_clear_empirical_local_loss_gate() {
let (a_peer, b_peer, _a_runtime_id, b_runtime_id) = connected_pair().await;
let payload_len = 1 /* Dawn huddle peer_index */ + 8 /* v2 audio header */ + 160;
let encoded_len = wire::encode(&MeshDatagram {
fenced: fenced(b_runtime_id),
seq: 0,
payload: vec![0u8; payload_len],
})
.unwrap()
.len();
assert!(encoded_len <= a_peer.max_datagram_size().expect("datagrams enabled"));
let count = 64u64;
for seq in 0..count {
a_peer
.send_datagram(&MeshDatagram {
fenced: fenced(b_runtime_id),
seq,
payload: vec![seq as u8; payload_len],
})
.unwrap();
tokio::task::yield_now().await;
}
let mut got = Vec::new();
for _ in 0..count {
got.push(
timeout(Duration::from_secs(5), b_peer.recv_datagram())
.await
.unwrap()
.unwrap()
.seq,
);
}
got.sort_unstable();
assert_eq!(got, (0..count).collect::<Vec<_>>());
}
}