use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use wsb::peer::identity::{MachineIdentity, PairedPeer, PairingCode, PAIRING_WINDOW};
use wsb::peer::model::{PeerId, PeerState};
use wsb::peer::transport::{self, Expect, Role, Session};
use super::store::MachineStore;
pub use wsb::peer::DEFAULT_PEER_PORT;
#[derive(Debug, Clone)]
pub struct PairingOffer {
pub code: PairingCode,
pub opened_at: SystemTime,
}
impl PairingOffer {
fn live(&self) -> bool {
self.opened_at
.elapsed()
.map(|since| since < PAIRING_WINDOW)
.unwrap_or(false)
}
}
pub struct PeerService {
identity: Arc<MachineIdentity>,
store: Arc<MachineStore>,
offer: Mutex<Option<PairingOffer>>,
live: Mutex<std::collections::BTreeMap<PeerId, Liveness>>,
peer_port: u16,
listen: IpAddr,
}
#[derive(Debug, Clone)]
pub struct Liveness {
pub state: PeerState,
pub detail: Option<String>,
pub since: SystemTime,
}
impl Liveness {
pub fn held_for(&self) -> u64 {
self.since.elapsed().map(|held| held.as_secs()).unwrap_or(0)
}
}
impl PeerService {
pub fn new(
identity: Arc<MachineIdentity>,
store: Arc<MachineStore>,
listen: IpAddr,
peer_port: u16,
) -> Self {
Self {
identity,
store,
offer: Mutex::new(None),
live: Mutex::new(std::collections::BTreeMap::new()),
peer_port,
listen,
}
}
pub fn peer_port(&self) -> u16 {
self.peer_port
}
pub fn peer_address(&self) -> IpAddr {
self.listen
}
pub fn open_pairing(&self) -> Result<PairingCode, String> {
let code = PairingCode::mint();
let mut offer = self
.offer
.lock()
.map_err(|_| "the pairing window is unusable after a panic".to_string())?;
*offer = Some(PairingOffer {
code: code.clone(),
opened_at: SystemTime::now(),
});
Ok(code)
}
pub fn close_pairing(&self) {
if let Ok(mut offer) = self.offer.lock() {
*offer = None;
}
}
pub fn pairing_offer(&self) -> Option<PairingOffer> {
let mut offer = self.offer.lock().ok()?;
match offer.as_ref() {
Some(open) if open.live() => Some(open.clone()),
Some(_) => {
*offer = None;
None
}
None => None,
}
}
pub fn pair_with(&self, address: &str, code: &PairingCode) -> Result<PairedPeer, String> {
let mut stream = dial(address)?;
let session = transport::handshake_announcing(
(stream.try_clone().map_err(|e| e.to_string())?, stream.peer_addr().ok()),
Role::Initiator,
&self.identity,
Expect::Pairing(code),
self.peer_port,
)?;
let peer = self.remember(&session, address)?;
session.shutdown();
let _ = &mut stream;
Ok(peer)
}
fn remember(&self, session: &Session, address: &str) -> Result<PairedPeer, String> {
let peer = PairedPeer {
id: session.peer().clone(),
name: session.peer_name().to_string(),
public_key: wsb::peer::identity::base64_encode(session.peer_key().as_bytes()),
fingerprint: wsb::peer::identity::Fingerprint::of(session.peer_key()),
address: address.to_string(),
paired_at: now(),
};
self.store.add_peer(&peer)?;
Ok(peer)
}
pub fn connect(&self, peer: &PeerId) -> Result<Session, String> {
let known = self
.store
.peer(peer)?
.ok_or_else(|| format!("{peer} is not a paired peer"))?;
let key = known.verifying_key()?;
self.mark(peer, PeerState::Connecting, None);
let stream = match dial(&known.address) {
Ok(stream) => stream,
Err(error) => {
self.mark(peer, PeerState::Offline, Some(error.clone()));
return Err(error);
}
};
let announced = (
match stream.try_clone() {
Ok(clone) => clone,
Err(error) => {
self.mark(peer, PeerState::Error, Some(error.to_string()));
return Err(error.to_string());
}
},
stream.peer_addr().ok(),
);
match transport::handshake_announcing(
announced,
Role::Initiator,
&self.identity,
Expect::Pinned(&key),
self.peer_port,
) {
Ok(session) => {
bound(&session)?;
self.mark(peer, PeerState::Connected, None);
self.store.note_seen(peer, now())?;
Ok(session)
}
Err(error) => {
self.mark(peer, PeerState::Error, Some(error.clone()));
Err(error)
}
}
}
pub fn accept(&self, stream: TcpStream) -> Result<Session, String> {
if let Some(offer) = self.pairing_offer() {
let announced = (
stream.try_clone().map_err(|e| e.to_string())?,
stream.peer_addr().ok(),
);
let session = transport::handshake_announcing(
announced,
Role::Responder,
&self.identity,
Expect::Pairing(&offer.code),
self.peer_port,
)?;
let address = session.dialable_address().ok_or_else(|| {
"the peer connected from an address this machine cannot name, so there \
is nowhere to call it back"
.to_string()
})?;
self.remember(&session, &address)?;
self.close_pairing();
self.mark(session.peer(), PeerState::Connected, None);
bound(&session)?;
return Ok(session);
}
let peers = self.store.peers()?;
let announced = (
stream.try_clone().map_err(|e| e.to_string())?,
stream.peer_addr().ok(),
);
let session = transport::handshake_announcing(
announced,
Role::Responder,
&self.identity,
Expect::Any,
self.peer_port,
)?;
let presented = wsb::peer::identity::base64_encode(session.peer_key().as_bytes());
let known = peers
.iter()
.find(|peer| peer.public_key == presented && peer.id == *session.peer());
match known {
Some(peer) => {
self.mark(&peer.id, PeerState::Connected, None);
self.store.note_seen(&peer.id, now())?;
if let Some(address) = session.dialable_address() {
if address != peer.address {
let mut moved = peer.clone();
moved.address = address;
let _ = self.store.add_peer(&moved);
}
}
bound(&session)?;
Ok(session)
}
None => Err(format!(
"a machine that is not paired with this one connected ({}); \
run `wsb peer pair` on both to introduce them",
wsb::peer::identity::Fingerprint::of(session.peer_key())
)),
}
}
pub fn listen(&self) -> Result<TcpListener, String> {
let address = SocketAddr::new(self.listen, self.peer_port);
TcpListener::bind(address).map_err(|e| {
format!(
"cannot listen for peers on {address}: {e}. Another daemon may hold it, \
or this machine may not be able to bind that address."
)
})
}
pub fn mark(&self, peer: &PeerId, state: PeerState, detail: Option<String>) {
if let Ok(mut live) = self.live.lock() {
live.insert(
peer.clone(),
Liveness {
state,
detail,
since: SystemTime::now(),
},
);
}
}
pub fn liveness(&self, peer: &PeerId) -> Liveness {
self.live
.lock()
.ok()
.and_then(|live| live.get(peer).cloned())
.unwrap_or(Liveness {
state: PeerState::Offline,
detail: None,
since: SystemTime::UNIX_EPOCH,
})
}
}
pub const PEER_IO_BOUND: Duration = Duration::from_secs(120);
fn bound(session: &Session) -> Result<(), String> {
session.set_timeout(PEER_IO_BOUND)
}
fn dial(address: &str) -> Result<TcpStream, String> {
use std::net::ToSocketAddrs;
let mut resolved = address
.to_socket_addrs()
.map_err(|e| format!("cannot resolve {address}: {e}"))?;
let target = resolved
.next()
.ok_or_else(|| format!("{address} resolves to no address"))?;
TcpStream::connect_timeout(&target, Duration::from_secs(10))
.map_err(|e| format!("cannot reach {address}: {e}"))
}
pub fn now() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|since| since.as_secs())
.unwrap_or(0)
}
pub fn spawn_accept_loop(
machine: std::sync::Arc<crate::machine::MachineState>,
listener: TcpListener,
) {
let address = listener.local_addr().ok();
std::thread::Builder::new()
.name("wsb-peer-accept".to_string())
.spawn(move || {
if let Some(address) = address {
tracing::info!(%address, "listening for peers");
}
for incoming in listener.incoming() {
let stream = match incoming {
Ok(stream) => stream,
Err(error) => {
tracing::warn!(%error, "a peer connection could not be accepted");
continue;
}
};
let machine = machine.clone();
let spawned = std::thread::Builder::new()
.name("wsb-peer-serve".to_string())
.spawn(move || serve_one(machine, stream));
if let Err(error) = spawned {
tracing::warn!(%error, "cannot serve a peer connection");
}
}
})
.expect("the peer accept thread starts");
}
fn serve_one(machine: std::sync::Arc<crate::machine::MachineState>, stream: TcpStream) {
let peer_address = stream
.peer_addr()
.map(|address| address.to_string())
.unwrap_or_else(|_| "an unknown address".to_string());
let mut session = match machine.peers.accept(stream) {
Ok(session) => session,
Err(error) => {
tracing::warn!(%error, address = %peer_address, "a peer connection was refused");
return;
}
};
let peer = session.peer().clone();
tracing::debug!(%peer, address = %peer_address, "a peer connected");
let blocking = crate::machine::BlockingMachine {
state: machine.clone(),
};
match crate::peer::connection::serve(&blocking, &mut session, &peer) {
Ok(()) => {
machine.peers.mark(&peer, PeerState::Connected, None);
tracing::debug!(%peer, "a peer connection finished");
}
Err(error) => {
machine
.peers
.mark(&peer, PeerState::Error, Some(error.clone()));
tracing::warn!(%peer, %error, "a peer connection ended badly");
}
}
}