pub mod hello;
pub mod reconnect;
#[cfg(test)]
mod tests;
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, mpsc, Mutex as AsyncMutex, RwLock, Semaphore};
pub use self::hello::{
HelloEnvelope, HelloKind, PeerIdentity, CLOSE_APP_MISMATCH, CLOSE_HELLO_PROTOCOL, HELLO_TIMEOUT,
};
use self::reconnect::ReconnectBackoff;
use crate::network::{NetworkPeer, NetworkPeerEvent, NetworkProvider, PeerAddr};
use crate::transport::websocket::{WebSocketTransport, WsFramedStream};
use crate::transport::{FramedStream, StreamTransport};
#[derive(Debug, Clone)]
pub struct PeerState {
pub id: String,
pub generation: u64,
pub name: String,
pub ip: IpAddr,
pub online: bool,
pub ws_connected: bool,
pub connection_type: String,
pub os: Option<String>,
pub last_seen: Option<String>,
pub identity: Option<PeerIdentity>,
pub identity_suppressed: bool,
}
impl PeerState {
pub fn peer_ref(&self) -> String {
format_peer_ref(&self.id, self.generation)
}
pub fn published_device_id(&self) -> Option<&str> {
if self.identity_suppressed {
return None;
}
self.identity.as_ref().map(|i| i.device_id.as_str())
}
}
pub fn format_peer_ref(tailscale_id: &str, generation: u64) -> String {
format!("{tailscale_id}:{generation}")
}
pub(crate) fn parse_peer_ref(s: &str) -> Option<(&str, u64)> {
let (ts, generation) = s.rsplit_once(':')?;
if ts.is_empty() || ts.contains(':') || generation.is_empty() {
return None;
}
if !generation.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some((ts, generation.parse().ok()?))
}
#[derive(Debug, Clone)]
pub enum PeerEvent {
Joined(PeerState),
Left(PeerState),
Updated(PeerState),
Identity(PeerState),
WsConnected(String),
WsDisconnected(String),
AuthRequired { url: String },
}
#[derive(Debug, Clone, Default)]
pub struct BroadcastReport {
pub attempted: usize,
pub queued: usize,
pub failed: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct IncomingMessage {
pub from: String,
pub data: Vec<u8>,
pub received_at: Instant,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("unknown peer: {0}")]
UnknownPeer(String),
#[error("peer gone: {0}")]
PeerGone(String),
#[error("peer offline: {0}")]
PeerOffline(String),
#[error("connect failed: {0}")]
ConnectFailed(String),
#[error("send failed: {0}")]
SendFailed(String),
#[error("reconnect backoff: retry after {retry_after:?}")]
ReconnectBackoff {
retry_after: Duration,
},
#[error("transport error: {0}")]
Transport(#[from] crate::transport::TransportError),
}
struct WsConnectionHandle {
send_tx: mpsc::Sender<Vec<u8>>,
close_tx: mpsc::Sender<()>,
#[allow(dead_code)]
peer_id: String,
#[allow(dead_code)]
connected_at: Instant,
}
pub struct PeerRegistry<N: NetworkProvider + 'static> {
network: Arc<N>,
ws_transport: Arc<WebSocketTransport<N>>,
peers: Arc<RwLock<HashMap<String, PeerState>>>,
by_device: Arc<RwLock<HashMap<String, String>>>,
next_generation: Arc<RwLock<HashMap<String, u64>>>,
ws_connections: Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peer_backoffs: Arc<RwLock<HashMap<String, ReconnectBackoff>>>,
connecting: Arc<RwLock<HashSet<String>>>,
event_tx: broadcast::Sender<PeerEvent>,
incoming_tx: broadcast::Sender<IncomingMessage>,
background_tasks: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
eager_identity: bool,
eager_identity_sem: Arc<Semaphore>,
eager_identity_jitter_ms: u64,
identity_inflight: Arc<AsyncMutex<HashSet<String>>>,
}
#[derive(Debug, Clone)]
pub struct PeerRegistryOptions {
pub eager_identity: bool,
pub eager_identity_concurrency: usize,
pub eager_identity_jitter_ms: u64,
}
impl Default for PeerRegistryOptions {
fn default() -> Self {
Self {
eager_identity: true,
eager_identity_concurrency: 4,
eager_identity_jitter_ms: 250,
}
}
}
impl<N: NetworkProvider + 'static> PeerRegistry<N> {
pub fn new(network: Arc<N>, ws_transport: Arc<WebSocketTransport<N>>) -> Self {
Self::with_options(network, ws_transport, PeerRegistryOptions::default())
}
pub fn with_options(
network: Arc<N>,
ws_transport: Arc<WebSocketTransport<N>>,
options: PeerRegistryOptions,
) -> Self {
let (event_tx, _) = broadcast::channel(256);
let (incoming_tx, _) = broadcast::channel(1024);
let concurrency = options.eager_identity_concurrency.max(1);
Self {
network,
ws_transport,
peers: Arc::new(RwLock::new(HashMap::new())),
by_device: Arc::new(RwLock::new(HashMap::new())),
next_generation: Arc::new(RwLock::new(HashMap::new())),
ws_connections: Arc::new(RwLock::new(HashMap::new())),
peer_backoffs: Arc::new(RwLock::new(HashMap::new())),
connecting: Arc::new(RwLock::new(HashSet::new())),
event_tx,
incoming_tx,
background_tasks: std::sync::Mutex::new(Vec::new()),
eager_identity: options.eager_identity,
eager_identity_sem: Arc::new(Semaphore::new(concurrency)),
eager_identity_jitter_ms: options.eager_identity_jitter_ms,
identity_inflight: Arc::new(AsyncMutex::new(HashSet::new())),
}
}
pub async fn start(&self) {
self.spawn_peer_event_loop();
self.spawn_accept_loop().await;
}
fn spawn_peer_event_loop(&self) {
let mut events = self.network.peer_events();
let peers = self.peers.clone();
let by_device = self.by_device.clone();
let next_generation = self.next_generation.clone();
let ws_connections = self.ws_connections.clone();
let event_tx = self.event_tx.clone();
let schedule_ctx = EagerScheduleCtx {
eager_identity: self.eager_identity,
peers: self.peers.clone(),
by_device: self.by_device.clone(),
ws_connections: self.ws_connections.clone(),
peer_backoffs: self.peer_backoffs.clone(),
connecting: self.connecting.clone(),
event_tx: self.event_tx.clone(),
incoming_tx: self.incoming_tx.clone(),
ws_transport: self.ws_transport.clone(),
network: self.network.clone(),
eager_identity_sem: self.eager_identity_sem.clone(),
eager_identity_jitter_ms: self.eager_identity_jitter_ms,
identity_inflight: self.identity_inflight.clone(),
};
let handle = tokio::spawn(async move {
loop {
match events.recv().await {
Ok(NetworkPeerEvent::Joined(network_peer)) => {
let generation = {
let mut gens = next_generation.write().await;
let e = gens.entry(network_peer.id.clone()).or_insert(0);
*e += 1;
*e
};
let state = network_peer_to_state(&network_peer, generation);
let online = state.online;
let peer_id = network_peer.id.clone();
let peer_event = PeerEvent::Joined(state.clone());
{
let mut map = peers.write().await;
map.insert(network_peer.id.clone(), state);
}
let _ = event_tx.send(peer_event);
tracing::info!(
peer_id = %network_peer.id,
generation,
peer_name = %network_peer.hostname,
"session: peer joined"
);
if online {
schedule_ctx.schedule(peer_id);
}
}
Ok(NetworkPeerEvent::Left(peer_id)) => {
let handle = {
let mut conns = ws_connections.write().await;
conns.remove(&peer_id)
};
if let Some(handle) = handle {
let _ = handle.close_tx.send(()).await;
let _ = event_tx.send(PeerEvent::WsDisconnected(peer_id.clone()));
tracing::info!(
peer_id = %peer_id,
"session: closed WS connection for departing peer"
);
}
let removed = {
let mut map = peers.write().await;
map.remove(&peer_id)
};
if let Some(mut removed) = removed {
let promote = {
let mut by_dev = by_device.write().await;
if let Some(uid) = removed.published_device_id() {
if by_dev.get(uid).map(|s| s.as_str()) == Some(peer_id.as_str())
{
by_dev.remove(uid);
Some(uid.to_string())
} else {
None
}
} else if let Some(ref ident) = removed.identity {
let _ = ident;
None
} else {
None
}
};
if let Some(uid) = promote {
let mut map = peers.write().await;
let mut by_dev = by_device.write().await;
if let Some(promoted) = map.values_mut().find(|p| {
p.id != peer_id
&& p.online
&& p.identity
.as_ref()
.map(|i| i.device_id == uid)
.unwrap_or(false)
&& p.identity_suppressed
}) {
promoted.identity_suppressed = false;
by_dev.insert(uid.clone(), promoted.id.clone());
let snap = promoted.clone();
drop(map);
drop(by_dev);
let _ = event_tx.send(PeerEvent::Identity(snap));
tracing::info!(
device_id = %uid,
"session: promoted suppressed ULID claimant after holder left"
);
}
}
removed.online = false;
removed.ws_connected = false;
let _ = event_tx.send(PeerEvent::Left(removed));
tracing::info!(peer_id = %peer_id, "session: peer left");
} else {
tracing::debug!(
peer_id = %peer_id,
"session: left for unknown peer; no event"
);
}
}
Ok(NetworkPeerEvent::Updated(network_peer)) => {
let mut state = network_peer_to_state(&network_peer, 0);
let became_online_without_identity;
{
let mut map = peers.write().await;
if let Some(existing) = map.get(&network_peer.id) {
state.generation = existing.generation;
state.ws_connected = existing.ws_connected;
state.identity = existing.identity.clone();
state.identity_suppressed = existing.identity_suppressed;
became_online_without_identity = !existing.online
&& state.online
&& existing.published_device_id().is_none();
} else {
let mut gens = next_generation.write().await;
let e = gens.entry(network_peer.id.clone()).or_insert(0);
*e += 1;
state.generation = *e;
became_online_without_identity =
state.online && state.published_device_id().is_none();
}
map.insert(network_peer.id.clone(), state.clone());
}
let peer_id = network_peer.id.clone();
let _ = event_tx.send(PeerEvent::Updated(state));
tracing::debug!(
peer_id = %network_peer.id,
"session: peer updated"
);
if became_online_without_identity {
schedule_ctx.schedule(peer_id);
}
}
Ok(NetworkPeerEvent::AuthRequired { url }) => {
let _ = event_tx.send(PeerEvent::AuthRequired { url });
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(
missed = n,
"session: peer event receiver lagged, missed {n} events"
);
}
Err(broadcast::error::RecvError::Closed) => {
tracing::debug!("session: peer event channel closed");
break;
}
}
}
});
self.background_tasks.lock().unwrap().push(handle);
}
async fn spawn_accept_loop(&self) {
let ws_transport = self.ws_transport.clone();
let ws_connections = self.ws_connections.clone();
let peers = self.peers.clone();
let by_device = self.by_device.clone();
let event_tx = self.event_tx.clone();
let incoming_tx = self.incoming_tx.clone();
let mut listener = match ws_transport.listen().await {
Ok(l) => l,
Err(e) => {
tracing::error!("session: failed to start WS listener: {e}");
return;
}
};
let handle = tokio::spawn(async move {
loop {
match listener.accept().await {
Some(stream) => {
let peer_id = stream.remote_peer_id().to_string();
let remote_identity = stream.remote_identity().cloned();
tracing::info!(
peer_id = %peer_id,
device_id = remote_identity.as_ref().map(|i| i.device_id.as_str()),
"session: accepted incoming WS connection"
);
let handle = spawn_connection_task(
stream,
peer_id.clone(),
ws_connections.clone(),
peers.clone(),
event_tx.clone(),
incoming_tx.clone(),
);
{
let mut conns = ws_connections.write().await;
conns.insert(peer_id.clone(), handle);
}
{
let mut map = peers.write().await;
let mut by_dev = by_device.write().await;
if let Some(state) = map.get_mut(&peer_id) {
state.ws_connected = true;
}
if let Some(identity) = remote_identity {
let outcomes =
apply_identity(&mut map, &mut by_dev, &peer_id, identity);
emit_identity_outcomes(&event_tx, outcomes);
}
}
let _ = event_tx.send(PeerEvent::WsConnected(peer_id));
}
None => {
tracing::debug!("session: WS listener closed");
break;
}
}
}
});
self.background_tasks.lock().unwrap().push(handle);
}
pub async fn peers(&self) -> Vec<PeerState> {
let map = self.peers.read().await;
map.values().cloned().collect()
}
pub fn on_peer_change(&self) -> broadcast::Receiver<PeerEvent> {
self.event_tx.subscribe()
}
pub async fn send(&self, peer_id: &str, data: &[u8]) -> Result<(), SessionError> {
let peer_id = self.resolve_routing_key(peer_id).await?;
self.ensure_ws_connected(&peer_id).await?;
let conns = self.ws_connections.read().await;
let handle = conns
.get(&peer_id)
.ok_or_else(|| SessionError::SendFailed("connection missing after connect".into()))?;
handle
.send_tx
.send(data.to_vec())
.await
.map_err(|_| SessionError::SendFailed("connection task closed".to_string()))
}
pub async fn ensure_ws_connected(&self, peer_id: &str) -> Result<(), SessionError> {
{
let conns = self.ws_connections.read().await;
if conns.contains_key(peer_id) {
return Ok(());
}
}
let peer_addr = {
let map = self.peers.read().await;
let state = map
.get(peer_id)
.ok_or_else(|| SessionError::UnknownPeer(peer_id.to_string()))?;
if !state.online {
return Err(SessionError::PeerOffline(peer_id.to_string()));
}
PeerAddr {
ip: Some(state.ip),
hostname: state.name.clone(),
dns_name: None,
}
};
{
let backoffs = self.peer_backoffs.read().await;
if let Some(backoff) = backoffs.get(peer_id) {
if backoff.should_retry().is_none() {
let retry_after = backoff.retry_after();
return Err(SessionError::ReconnectBackoff { retry_after });
}
}
}
{
let already = {
let connecting = self.connecting.read().await;
connecting.contains(peer_id)
};
if already {
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(20)).await;
let conns = self.ws_connections.read().await;
if conns.contains_key(peer_id) {
return Ok(());
}
let connecting = self.connecting.read().await;
if !connecting.contains(peer_id) {
break;
}
}
let conns = self.ws_connections.read().await;
if conns.contains_key(peer_id) {
return Ok(());
}
}
let mut connecting = self.connecting.write().await;
if connecting.contains(peer_id) {
return Err(SessionError::ConnectFailed(
"connection already in progress".to_string(),
));
}
connecting.insert(peer_id.to_string());
}
tracing::info!(peer_id = %peer_id, "session: connecting WS");
let connect_result = self.ws_transport.connect(&peer_addr).await;
{
let mut connecting = self.connecting.write().await;
connecting.remove(peer_id);
}
let ws_stream = match connect_result {
Ok(stream) => {
let mut backoffs = self.peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.success();
stream
}
Err(e) => {
let mut backoffs = self.peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.failure();
return Err(SessionError::ConnectFailed(e.to_string()));
}
};
if let Some(claimed) = ws_stream.remote_identity() {
if claimed.tailscale_id != peer_id {
tracing::warn!(
peer_id = %peer_id,
claimed = %claimed.tailscale_id,
"session: dialed peer answered as a different tailscale_id; dropping connection"
);
return Err(SessionError::ConnectFailed(format!(
"hello identity mismatch: dialed {peer_id}, answerer claims {}",
claimed.tailscale_id
)));
}
}
let remote_identity = ws_stream.remote_identity().cloned();
let handle = spawn_connection_task(
ws_stream,
peer_id.to_string(),
self.ws_connections.clone(),
self.peers.clone(),
self.event_tx.clone(),
self.incoming_tx.clone(),
);
{
let mut conns = self.ws_connections.write().await;
conns.insert(peer_id.to_string(), handle);
}
{
let mut map = self.peers.write().await;
let mut by_dev = self.by_device.write().await;
if let Some(state) = map.get_mut(peer_id) {
state.ws_connected = true;
}
if let Some(identity) = remote_identity {
let outcomes = apply_identity(&mut map, &mut by_dev, peer_id, identity);
emit_identity_outcomes(&self.event_tx, outcomes);
}
}
let _ = self
.event_tx
.send(PeerEvent::WsConnected(peer_id.to_string()));
Ok(())
}
pub async fn ensure_identity(&self, peer_id: &str) -> Result<(), SessionError> {
{
let map = self.peers.read().await;
match map.get(peer_id) {
Some(s) if s.published_device_id().is_some() => return Ok(()),
Some(s) if !s.online => {
return Err(SessionError::PeerOffline(peer_id.to_string()));
}
None => return Err(SessionError::UnknownPeer(peer_id.to_string())),
_ => {}
}
}
self.ensure_ws_connected(peer_id).await
}
async fn resolve_routing_key(&self, peer_id: &str) -> Result<String, SessionError> {
let map = self.peers.read().await;
if map.contains_key(peer_id) {
return Ok(peer_id.to_string());
}
{
let by_dev = self.by_device.read().await;
if let Some(ts) = by_dev.get(peer_id) {
return Ok(ts.clone());
}
}
if let Some(found) = map.values().find(|p| {
p.name == peer_id
|| (!p.identity_suppressed
&& p.identity
.as_ref()
.map(|i| i.device_name == peer_id)
.unwrap_or(false))
}) {
return Ok(found.id.clone());
}
if let Some((ts, generation)) = parse_peer_ref(peer_id) {
return match map.get(ts) {
Some(p) if p.generation == generation => Ok(ts.to_string()),
_ => Err(SessionError::PeerGone(peer_id.to_string())),
};
}
Err(SessionError::UnknownPeer(peer_id.to_string()))
}
pub async fn broadcast(&self, data: &[u8]) -> BroadcastReport {
let conns = self.ws_connections.read().await;
let mut report = BroadcastReport {
attempted: conns.len(),
..Default::default()
};
for (peer_id, handle) in conns.iter() {
if handle.send_tx.send(data.to_vec()).await.is_err() {
tracing::warn!(
peer_id = %peer_id,
"session: broadcast send failed (connection task closed)"
);
report.failed.push(peer_id.clone());
} else {
report.queued += 1;
}
}
report
}
pub fn subscribe(&self) -> broadcast::Receiver<IncomingMessage> {
self.incoming_tx.subscribe()
}
#[cfg(test)]
pub(crate) fn test_inject_incoming(&self, from: &str, data: Vec<u8>) {
let _ = self.incoming_tx.send(IncomingMessage {
from: from.to_string(),
data,
received_at: Instant::now(),
});
}
#[doc(hidden)]
pub async fn test_stamp_identity(&self, peer_id: &str, identity: PeerIdentity) -> bool {
let mut map = self.peers.write().await;
if !map.contains_key(peer_id) {
return false;
}
let mut by_dev = self.by_device.write().await;
let outcomes = apply_identity(&mut map, &mut by_dev, peer_id, identity);
emit_identity_outcomes(&self.event_tx, outcomes);
true
}
#[doc(hidden)]
pub async fn test_by_device(&self, device_id: &str) -> Option<String> {
self.by_device.read().await.get(device_id).cloned()
}
pub async fn disconnect(&self, peer_id: &str) {
let handle = {
let mut conns = self.ws_connections.write().await;
conns.remove(peer_id)
};
if let Some(handle) = handle {
let _ = handle.close_tx.send(()).await;
}
{
let mut map = self.peers.write().await;
if let Some(state) = map.get_mut(peer_id) {
state.ws_connected = false;
}
}
let _ = self
.event_tx
.send(PeerEvent::WsDisconnected(peer_id.to_string()));
}
pub async fn shutdown(&self) {
for handle in self.background_tasks.lock().unwrap().drain(..) {
handle.abort();
}
let handles: Vec<(String, WsConnectionHandle)> = {
let mut conns = self.ws_connections.write().await;
conns.drain().collect()
};
for (peer_id, handle) in handles {
let _ = handle.close_tx.send(()).await;
let _ = self.event_tx.send(PeerEvent::WsDisconnected(peer_id));
}
let mut map = self.peers.write().await;
for state in map.values_mut() {
state.ws_connected = false;
}
}
}
fn spawn_connection_task(
stream: WsFramedStream,
peer_id: String,
ws_connections: Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peers: Arc<RwLock<HashMap<String, PeerState>>>,
event_tx: broadcast::Sender<PeerEvent>,
incoming_tx: broadcast::Sender<IncomingMessage>,
) -> WsConnectionHandle {
let (send_tx, mut send_rx) = mpsc::channel::<Vec<u8>>(256);
let (close_tx, mut close_rx) = mpsc::channel::<()>(1);
let handle = WsConnectionHandle {
send_tx: send_tx.clone(),
close_tx: close_tx.clone(),
peer_id: peer_id.clone(),
connected_at: Instant::now(),
};
let from_tailscale_id = peer_id.clone();
tokio::spawn(async move {
let mut stream = stream;
let mut closed = false;
loop {
tokio::select! {
Some(data) = send_rx.recv() => {
if let Err(e) = stream.send(&data).await {
tracing::warn!(
peer_id = %peer_id,
error = %e,
"session: WS send error"
);
break;
}
}
result = stream.recv() => {
match result {
Ok(Some(data)) => {
let msg = IncomingMessage {
from: from_tailscale_id.clone(),
data,
received_at: Instant::now(),
};
let _ = incoming_tx.send(msg);
}
Ok(None) => {
tracing::info!(
peer_id = %peer_id,
"session: WS stream closed"
);
break;
}
Err(e) => {
tracing::warn!(
peer_id = %peer_id,
error = %e,
"session: WS recv error"
);
break;
}
}
}
_ = close_rx.recv() => {
tracing::info!(
peer_id = %peer_id,
"session: connection close requested"
);
closed = true;
let _ = stream.close().await;
break;
}
}
}
if !closed {
{
let mut conns = ws_connections.write().await;
conns.remove(&peer_id);
}
{
let mut map = peers.write().await;
if let Some(state) = map.get_mut(&peer_id) {
state.ws_connected = false;
}
}
let _ = event_tx.send(PeerEvent::WsDisconnected(peer_id));
}
});
handle
}
fn eager_jitter_delay(peer_id: &str, window_ms: u64) -> Duration {
if window_ms == 0 {
return Duration::ZERO;
}
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
peer_id.hash(&mut h);
Duration::from_millis(h.finish() % window_ms)
}
struct EagerScheduleCtx<N: NetworkProvider + 'static> {
eager_identity: bool,
peers: Arc<RwLock<HashMap<String, PeerState>>>,
by_device: Arc<RwLock<HashMap<String, String>>>,
ws_connections: Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peer_backoffs: Arc<RwLock<HashMap<String, ReconnectBackoff>>>,
connecting: Arc<RwLock<HashSet<String>>>,
event_tx: broadcast::Sender<PeerEvent>,
incoming_tx: broadcast::Sender<IncomingMessage>,
ws_transport: Arc<WebSocketTransport<N>>,
network: Arc<N>,
eager_identity_sem: Arc<Semaphore>,
eager_identity_jitter_ms: u64,
identity_inflight: Arc<AsyncMutex<HashSet<String>>>,
}
impl<N: NetworkProvider + 'static> EagerScheduleCtx<N> {
fn schedule(&self, peer_id: String) {
if !self.eager_identity {
return;
}
{
if let Ok(mut inflight) = self.identity_inflight.try_lock() {
if !inflight.insert(peer_id.clone()) {
return;
}
}
}
let peers = self.peers.clone();
let by_device = self.by_device.clone();
let ws_connections = self.ws_connections.clone();
let peer_backoffs = self.peer_backoffs.clone();
let connecting = self.connecting.clone();
let event_tx = self.event_tx.clone();
let incoming_tx = self.incoming_tx.clone();
let ws_transport = self.ws_transport.clone();
let _network = self.network.clone();
let sem = self.eager_identity_sem.clone();
let jitter_ms = self.eager_identity_jitter_ms;
let inflight = self.identity_inflight.clone();
tokio::spawn(async move {
{
let mut set = inflight.lock().await;
set.insert(peer_id.clone());
}
let delay = eager_jitter_delay(&peer_id, jitter_ms);
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
let permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => {
let mut set = inflight.lock().await;
set.remove(&peer_id);
return;
}
};
let needs = {
let map = peers.read().await;
match map.get(&peer_id) {
Some(s) => s.online && s.published_device_id().is_none(),
None => false,
}
};
if needs {
if let Err(e) = eager_connect_ws(
&peer_id,
&peers,
&by_device,
&ws_connections,
&peer_backoffs,
&connecting,
&event_tx,
&incoming_tx,
&ws_transport,
)
.await
{
tracing::debug!(
peer_id = %peer_id,
error = %e,
"session: eager identity dial failed"
);
}
}
drop(permit);
let mut set = inflight.lock().await;
set.remove(&peer_id);
});
}
}
async fn eager_connect_ws<N: NetworkProvider + 'static>(
peer_id: &str,
peers: &Arc<RwLock<HashMap<String, PeerState>>>,
by_device: &Arc<RwLock<HashMap<String, String>>>,
ws_connections: &Arc<RwLock<HashMap<String, WsConnectionHandle>>>,
peer_backoffs: &Arc<RwLock<HashMap<String, ReconnectBackoff>>>,
connecting: &Arc<RwLock<HashSet<String>>>,
event_tx: &broadcast::Sender<PeerEvent>,
incoming_tx: &broadcast::Sender<IncomingMessage>,
ws_transport: &Arc<WebSocketTransport<N>>,
) -> Result<(), SessionError> {
{
let conns = ws_connections.read().await;
if conns.contains_key(peer_id) {
return Ok(());
}
}
let peer_addr = {
let map = peers.read().await;
let state = map
.get(peer_id)
.ok_or_else(|| SessionError::UnknownPeer(peer_id.to_string()))?;
if !state.online {
return Err(SessionError::PeerOffline(peer_id.to_string()));
}
if state.published_device_id().is_some() {
return Ok(());
}
PeerAddr {
ip: Some(state.ip),
hostname: state.name.clone(),
dns_name: None,
}
};
{
let backoffs = peer_backoffs.read().await;
if let Some(backoff) = backoffs.get(peer_id) {
if backoff.should_retry().is_none() {
return Err(SessionError::ReconnectBackoff {
retry_after: backoff.retry_after(),
});
}
}
}
{
let mut connecting_g = connecting.write().await;
if connecting_g.contains(peer_id) {
return Err(SessionError::ConnectFailed(
"connection already in progress".to_string(),
));
}
connecting_g.insert(peer_id.to_string());
}
tracing::info!(peer_id = %peer_id, "session: eager identity connecting WS");
let connect_result = ws_transport.connect(&peer_addr).await;
{
let mut connecting_g = connecting.write().await;
connecting_g.remove(peer_id);
}
let ws_stream = match connect_result {
Ok(stream) => {
let mut backoffs = peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.success();
stream
}
Err(e) => {
let mut backoffs = peer_backoffs.write().await;
backoffs
.entry(peer_id.to_string())
.or_insert_with(ReconnectBackoff::new)
.failure();
return Err(SessionError::ConnectFailed(e.to_string()));
}
};
if let Some(claimed) = ws_stream.remote_identity() {
if claimed.tailscale_id != peer_id {
tracing::warn!(
peer_id = %peer_id,
claimed = %claimed.tailscale_id,
"session: eager dial answered as a different tailscale_id; dropping connection"
);
return Err(SessionError::ConnectFailed(format!(
"hello identity mismatch: dialed {peer_id}, answerer claims {}",
claimed.tailscale_id
)));
}
}
let remote_identity = ws_stream.remote_identity().cloned();
let handle = spawn_connection_task(
ws_stream,
peer_id.to_string(),
ws_connections.clone(),
peers.clone(),
event_tx.clone(),
incoming_tx.clone(),
);
{
let mut conns = ws_connections.write().await;
conns.insert(peer_id.to_string(), handle);
}
{
let mut map = peers.write().await;
let mut by_dev = by_device.write().await;
if let Some(state) = map.get_mut(peer_id) {
state.ws_connected = true;
}
if let Some(identity) = remote_identity {
let outcomes = apply_identity(&mut map, &mut by_dev, peer_id, identity);
emit_identity_outcomes(event_tx, outcomes);
}
}
let _ = event_tx.send(PeerEvent::WsConnected(peer_id.to_string()));
Ok(())
}
fn network_peer_to_state(peer: &NetworkPeer, generation: u64) -> PeerState {
let connection_type = if let Some(ref relay) = peer.relay {
format!("relay:{relay}")
} else if peer.cur_addr.is_some() {
"direct".to_string()
} else {
"unknown".to_string()
};
PeerState {
id: peer.id.clone(),
generation,
name: peer.hostname.clone(),
ip: peer.ip,
online: peer.online,
ws_connected: false,
connection_type,
os: peer.os.clone(),
last_seen: peer.last_seen.clone(),
identity: None,
identity_suppressed: false,
}
}
#[derive(Debug)]
enum IdentityOutcome {
Identity(PeerState),
GhostLeft(PeerState),
Updated(PeerState),
}
fn apply_identity(
peers: &mut HashMap<String, PeerState>,
by_device: &mut HashMap<String, String>,
ts_id: &str,
identity: PeerIdentity,
) -> Vec<IdentityOutcome> {
let mut outcomes = Vec::new();
let uid = identity.device_id.clone();
let Some(state) = peers.get(ts_id) else {
return outcomes;
};
if let Some(prev) = state.published_device_id() {
if prev != uid.as_str() {
by_device.remove(prev);
} else if !state.identity_suppressed {
if let Some(s) = peers.get_mut(ts_id) {
let changed = s.identity.as_ref() != Some(&identity);
s.identity = Some(identity);
s.identity_suppressed = false;
if changed {
outcomes.push(IdentityOutcome::Updated(s.clone()));
}
}
return outcomes;
}
}
match by_device.get(&uid).cloned() {
Some(holder) if holder == ts_id => {
if let Some(s) = peers.get_mut(ts_id) {
s.identity = Some(identity);
s.identity_suppressed = false;
outcomes.push(IdentityOutcome::Identity(s.clone()));
}
}
Some(holder) => {
let holder_online = peers.get(&holder).map(|p| p.online).unwrap_or(false);
if holder_online {
tracing::warn!(
device_id = %uid,
holder = %holder,
claimant = %ts_id,
"session: duplicate-device-id — first-wins, suppressing claimant"
);
if let Some(s) = peers.get_mut(ts_id) {
s.identity = Some(identity);
s.identity_suppressed = true;
}
} else {
if let Some(mut ghost) = peers.remove(&holder) {
ghost.online = false;
ghost.ws_connected = false;
outcomes.push(IdentityOutcome::GhostLeft(ghost));
}
by_device.insert(uid.clone(), ts_id.to_string());
if let Some(s) = peers.get_mut(ts_id) {
s.identity = Some(identity);
s.identity_suppressed = false;
outcomes.push(IdentityOutcome::Identity(s.clone()));
}
}
}
None => {
by_device.insert(uid, ts_id.to_string());
if let Some(s) = peers.get_mut(ts_id) {
s.identity = Some(identity);
s.identity_suppressed = false;
outcomes.push(IdentityOutcome::Identity(s.clone()));
}
}
}
outcomes
}
fn emit_identity_outcomes(event_tx: &broadcast::Sender<PeerEvent>, outcomes: Vec<IdentityOutcome>) {
for o in outcomes {
match o {
IdentityOutcome::GhostLeft(state) => {
let _ = event_tx.send(PeerEvent::Left(state));
}
IdentityOutcome::Identity(state) => {
let _ = event_tx.send(PeerEvent::Identity(state));
}
IdentityOutcome::Updated(state) => {
let _ = event_tx.send(PeerEvent::Updated(state));
}
}
}
}