Service: drop unresponsive peers

Use pings to check connection health with network peers and drop them if
they prove unresponsive.

Signed-off-by: Slack Coder <slackcoder@server.ky>
This commit is contained in:
Slack Coder 2022-09-27 10:46:26 -05:00
parent 240825387d
commit 14690d1951
7 changed files with 278 additions and 10 deletions

View File

@ -33,12 +33,12 @@ use crate::node;
use crate::service::config::ProjectTracking; use crate::service::config::ProjectTracking;
use crate::service::message::{Address, Announcement, AnnouncementMessage}; use crate::service::message::{Address, Announcement, AnnouncementMessage};
use crate::service::message::{NodeAnnouncement, RefsAnnouncement}; use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
use crate::service::peer::{SessionError, SessionState}; use crate::service::peer::{PingState, SessionError, SessionState};
use crate::storage; use crate::storage;
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage}; use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage};
pub use crate::service::config::{Config, Network}; pub use crate::service::config::{Config, Network};
pub use crate::service::message::{Envelope, Message}; pub use crate::service::message::{Envelope, Message, ZeroBytes};
pub use crate::service::peer::Session; pub use crate::service::peer::Session;
use self::gossip::Gossip; use self::gossip::Gossip;
@ -53,6 +53,11 @@ pub const ANNOUNCE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60); pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60);
pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30); pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30);
pub const MAX_CONNECTION_ATTEMPTS: usize = 3; pub const MAX_CONNECTION_ATTEMPTS: usize = 3;
/// Duration to wait on an unresponsive peer before dropping its connection.
pub const STALE_CONNECTION_TIMEOUT: LocalDuration = LocalDuration::from_secs(60);
pub const KEEP_ALIVE_DELTA: LocalDuration = LocalDuration::from_secs(30);
pub const MAX_TIME_DELTA: LocalDuration = LocalDuration::from_mins(60); pub const MAX_TIME_DELTA: LocalDuration = LocalDuration::from_mins(60);
/// Network node identifier. /// Network node identifier.
@ -362,6 +367,8 @@ where
if now - self.last_idle >= IDLE_INTERVAL { if now - self.last_idle >= IDLE_INTERVAL {
debug!("Running 'idle' task..."); debug!("Running 'idle' task...");
self.keep_alive(&now);
self.disconnect_unresponsive_peers(&now);
self.maintain_connections(); self.maintain_connections();
self.reactor.wakeup(IDLE_INTERVAL); self.reactor.wakeup(IDLE_INTERVAL);
self.last_idle = now; self.last_idle = now;
@ -485,7 +492,7 @@ where
let peer = self let peer = self
.sessions .sessions
.entry(ip) .entry(ip)
.or_insert_with(|| Session::new(*addr, Link::Outbound, persistent)); .or_insert_with(|| Session::new(*addr, Link::Outbound, persistent, &self.rng));
peer.attempted(); peer.attempted();
} }
@ -522,7 +529,12 @@ where
} else { } else {
self.sessions.insert( self.sessions.insert(
ip, ip,
Session::new(addr, Link::Inbound, self.config.is_persistent(&address)), Session::new(
addr,
Link::Inbound,
self.config.is_persistent(&address),
&self.rng,
),
); );
} }
} }
@ -724,13 +736,14 @@ where
} else { } else {
return Err(SessionError::NotFound(remote.ip())); return Err(SessionError::NotFound(remote.ip()));
}; };
peer.last_active = self.clock.local_time();
if envelope.magic != self.config.network.magic() { if envelope.magic != self.config.network.magic() {
return Err(SessionError::WrongMagic(envelope.magic)); return Err(SessionError::WrongMagic(envelope.magic));
} }
debug!("Received {:?} from {}", &envelope.msg, peer.ip()); debug!("Received {:?} from {}", &envelope.msg, peer.ip());
match (&peer.state, envelope.msg) { match (&mut peer.state, envelope.msg) {
( (
SessionState::Initial, SessionState::Initial,
Message::Initialize { Message::Initialize {
@ -764,6 +777,7 @@ where
since: self.clock.local_time(), since: self.clock.local_time(),
addrs, addrs,
git, git,
ping: Default::default(),
}; };
} }
(SessionState::Initial, _) => { (SessionState::Initial, _) => {
@ -812,6 +826,19 @@ where
); );
return Err(SessionError::Misbehavior); return Err(SessionError::Misbehavior);
} }
(SessionState::Negotiated { .. }, Message::Ping { ponglen, .. }) => {
let resp = Message::Pong {
zeroes: ZeroBytes::new(ponglen),
};
self.reactor.write(peer.addr, resp);
}
(SessionState::Negotiated { ping, .. }, Message::Pong { zeroes }) => {
if let PingState::AwaitingResponse(ponglen) = *ping {
if (ponglen as usize) == zeroes.len() {
*ping = PingState::Ok;
}
}
}
(SessionState::Disconnected { .. }, msg) => { (SessionState::Disconnected { .. }, msg) => {
debug!("Ignoring {:?} from disconnected peer {}", msg, peer.ip()); debug!("Ignoring {:?} from disconnected peer {}", msg, peer.ip());
} }
@ -883,6 +910,29 @@ where
Ok(()) Ok(())
} }
fn disconnect_unresponsive_peers(&mut self, now: &LocalTime) {
let stale = self
.sessions
.negotiated()
.filter(|(_, _, session)| session.last_active < *now - STALE_CONNECTION_TIMEOUT);
for (_, _, session) in stale {
self.reactor
.disconnect(session.addr, DisconnectReason::Error(SessionError::Timeout));
}
}
/// Ensure connection health by pinging connected peers.
fn keep_alive(&mut self, now: &LocalTime) {
let inactive_sessions = self
.sessions
.negotiated_mut()
.filter(|(_, session)| session.last_active < *now - KEEP_ALIVE_DELTA)
.map(|(_, session)| session);
for session in inactive_sessions {
session.ping(&mut self.reactor, &self.rng).ok();
}
}
fn maintain_connections(&mut self) { fn maintain_connections(&mut self) {
// TODO: Connect to all potential seeds. // TODO: Connect to all potential seeds.
if self.sessions.len() < TARGET_OUTBOUND_PEERS { if self.sessions.len() < TARGET_OUTBOUND_PEERS {
@ -1080,6 +1130,11 @@ impl Sessions {
_ => None, _ => None,
}) })
} }
/// Iterator over mutable fully negotiated peers.
pub fn negotiated_mut(&mut self) -> impl Iterator<Item = (&IpAddr, &mut Session)> {
self.0.iter_mut().filter(move |(_, p)| p.is_negotiated())
}
} }
impl Deref for Sessions { impl Deref for Sessions {

View File

@ -315,6 +315,23 @@ pub enum Message {
/// Gossip announcement. These messages are relayed to peers, and filtered /// Gossip announcement. These messages are relayed to peers, and filtered
/// using [`Message::Subscribe`]. /// using [`Message::Subscribe`].
Announcement(Announcement), Announcement(Announcement),
/// Ask a connected peer for a Pong.
///
/// Use to check if the remote peer is responsive or a side-effect free way to keep a
/// connection alive.
Ping {
/// The desired response length
ponglen: u16,
/// The ping payload.
zeroes: ZeroBytes,
},
/// Response to `Ping` message.
Pong {
/// The pong payload.
zeroes: ZeroBytes,
},
} }
impl Message { impl Message {
@ -373,10 +390,29 @@ impl fmt::Debug for Message {
Self::Announcement(Announcement { node, message, .. }) => { Self::Announcement(Announcement { node, message, .. }) => {
write!(f, "Announcement({}, {:?})", node, message) write!(f, "Announcement({}, {:?})", node, message)
} }
Self::Ping { ponglen, zeroes } => write!(f, "Ping({ponglen}, {:?})", zeroes),
Self::Pong { zeroes } => write!(f, "Pong({:?})", zeroes),
} }
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZeroBytes(u16);
impl ZeroBytes {
pub fn new(arg: u16) -> Self {
ZeroBytes(arg)
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn len(&self) -> usize {
self.0.into()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,5 +1,19 @@
use crate::service::message::*; use crate::service::message::*;
use crate::service::*; use crate::service::*;
use crate::wire;
use std::mem::size_of;
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub enum PingState {
#[default]
/// The peer has not been sent a ping.
None,
/// A ping has been sent and is waiting on the peer's response.
AwaitingResponse(u16),
/// The peer was successfully pinged.
Ok,
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@ -18,6 +32,7 @@ pub enum SessionState {
/// Addresses this peer is reachable on. /// Addresses this peer is reachable on.
addrs: Vec<Address>, addrs: Vec<Address>,
git: Url, git: Url,
ping: PingState,
}, },
/// When a peer is disconnected. /// When a peer is disconnected.
Disconnected { since: LocalTime }, Disconnected { since: LocalTime },
@ -37,6 +52,8 @@ pub enum SessionError {
VerificationFailed(#[from] storage::VerifyError), VerificationFailed(#[from] storage::VerifyError),
#[error("peer misbehaved")] #[error("peer misbehaved")]
Misbehavior, Misbehavior,
#[error("peer timed out")]
Timeout,
} }
/// A peer session. Each connected peer will have one session. /// A peer session. Each connected peer will have one session.
@ -53,22 +70,29 @@ pub struct Session {
pub state: SessionState, pub state: SessionState,
/// Peer subscription. /// Peer subscription.
pub subscribe: Option<Subscribe>, pub subscribe: Option<Subscribe>,
/// Last time a message was received from the peer.
pub last_active: LocalTime,
/// Connection attempts. For persistent peers, Tracks /// Connection attempts. For persistent peers, Tracks
/// how many times we've attempted to connect. We reset this to zero /// how many times we've attempted to connect. We reset this to zero
/// upon successful connection. /// upon successful connection.
attempts: usize, attempts: usize,
/// Source of entropy.
rng: Rng,
} }
impl Session { impl Session {
pub fn new(addr: net::SocketAddr, link: Link, persistent: bool) -> Self { pub fn new(addr: net::SocketAddr, link: Link, persistent: bool, rng: &Rng) -> Self {
Self { Self {
addr, addr,
state: SessionState::default(), state: SessionState::default(),
link, link,
subscribe: None, subscribe: None,
persistent, persistent,
last_active: LocalTime::default(),
attempts: 0, attempts: 0,
rng: rng.clone(),
} }
} }
@ -91,4 +115,20 @@ impl Session {
pub fn connected(&mut self, _link: Link) { pub fn connected(&mut self, _link: Link) {
self.attempts = 0; self.attempts = 0;
} }
pub fn ping(&mut self, reactor: &mut Reactor, rng: &Rng) -> Result<(), SessionError> {
if let SessionState::Negotiated { ping, .. } = &mut self.state {
let ponglen = rng.u16(0..wire::message::MAX_PAYLOAD_SIZE_BYTES);
let msg = Message::Ping {
ponglen,
zeroes: message::ZeroBytes::new(
rng.u16(0..(wire::message::MAX_PAYLOAD_SIZE_BYTES - (size_of::<u16>() as u16))),
),
};
reactor.write(self.addr, msg);
*ping = PingState::AwaitingResponse(ponglen);
}
Ok(())
}
} }

View File

@ -8,7 +8,7 @@ use crate::prelude::{Id, NodeId, Refs, Timestamp};
use crate::service::filter::{Filter, FILTER_SIZE_L, FILTER_SIZE_M, FILTER_SIZE_S}; use crate::service::filter::{Filter, FILTER_SIZE_L, FILTER_SIZE_M, FILTER_SIZE_S};
use crate::service::message::{ use crate::service::message::{
Address, Announcement, Envelope, InventoryAnnouncement, Message, NodeAnnouncement, Address, Announcement, Envelope, InventoryAnnouncement, Message, NodeAnnouncement,
RefsAnnouncement, Subscribe, RefsAnnouncement, Subscribe, ZeroBytes,
}; };
use crate::wire::message::MessageType; use crate::wire::message::MessageType;
@ -45,6 +45,8 @@ impl Arbitrary for Message {
MessageType::NodeAnnouncement, MessageType::NodeAnnouncement,
MessageType::RefsAnnouncement, MessageType::RefsAnnouncement,
MessageType::Subscribe, MessageType::Subscribe,
MessageType::Ping,
MessageType::Pong,
]) ])
.unwrap(); .unwrap();
@ -93,6 +95,13 @@ impl Arbitrary for Message {
since: Timestamp::arbitrary(g), since: Timestamp::arbitrary(g),
until: Timestamp::arbitrary(g), until: Timestamp::arbitrary(g),
}), }),
MessageType::Ping => Self::Ping {
ponglen: u16::arbitrary(g),
zeroes: ZeroBytes::arbitrary(g),
},
MessageType::Pong => Self::Pong {
zeroes: ZeroBytes::arbitrary(g),
},
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -115,3 +124,9 @@ impl Arbitrary for Address {
} }
} }
} }
impl Arbitrary for ZeroBytes {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
ZeroBytes::new(u16::arbitrary(g))
}
}

View File

@ -22,7 +22,7 @@ use crate::storage::WriteStorage;
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::signer::MockSigner; use crate::test::signer::MockSigner;
use crate::test::simulator; use crate::test::simulator;
use crate::{Link, LocalTime}; use crate::{Link, LocalDuration, LocalTime};
/// Service instantiation used for testing. /// Service instantiation used for testing.
pub type Service<S, G> = service::Service<routing::Table, address::Book, S, G>; pub type Service<S, G> = service::Service<routing::Table, address::Book, S, G>;
@ -251,6 +251,11 @@ where
); );
} }
pub fn elapse(&mut self, duration: LocalDuration) {
self.clock().elapse(duration);
self.service.wake();
}
/// Drain outgoing messages sent from this peer to the remote address. /// Drain outgoing messages sent from this peer to the remote address.
pub fn messages(&mut self, remote: &net::SocketAddr) -> impl Iterator<Item = Message> { pub fn messages(&mut self, remote: &net::SocketAddr) -> impl Iterator<Item = Message> {
let mut msgs = Vec::new(); let mut msgs = Vec::new();

View File

@ -36,6 +36,70 @@ use crate::{client, git, identity, rad, service, test};
// //
// You may then run the test with eg. `cargo test -- --nocapture` to always show output. // You may then run the test with eg. `cargo test -- --nocapture` to always show output.
#[test]
fn test_ping_response() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
alice.connect_to(&bob);
alice.receive(
&bob.addr(),
Message::Ping {
ponglen: 21,
zeroes: ZeroBytes::new(42),
},
);
assert_matches!(
alice.messages(&bob.addr()).next(),
Some(Message::Pong { zeroes }) if zeroes.len() == 21,
"respond with correctly formatted pong",
);
}
#[test]
fn test_disconnecting_unresponsive_peer() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
alice.connect_to(&bob);
assert_eq!(1, alice.sessions().negotiated().count(), "bob connects");
alice.elapse(STALE_CONNECTION_TIMEOUT + LocalDuration::from_secs(1));
alice
.outbox()
.find(|m| matches!(m, &Io::Disconnect(addr, _) if addr == bob.addr()))
.expect("disconnect an unresponsive bob");
}
#[test]
fn test_connection_kept_alive() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
let mut bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
let mut sim = Simulation::new(
LocalTime::now(),
alice.rng.clone(),
simulator::Options::default(),
)
.initialize([&mut alice, &mut bob]);
alice.command(service::Command::Connect(bob.addr()));
sim.run_while([&mut alice, &mut bob], |s| !s.is_settled());
assert_eq!(1, alice.sessions().negotiated().count(), "bob connects");
let mut elapsed: LocalDuration = LocalDuration::from_secs(0);
let step: LocalDuration = STALE_CONNECTION_TIMEOUT / 10;
while elapsed < STALE_CONNECTION_TIMEOUT + step {
alice.elapse(step);
bob.elapse(step);
sim.run_while([&mut alice, &mut bob], |s| !s.is_settled());
elapsed = elapsed + step;
}
assert_eq!(1, alice.sessions().len(), "alice remains connected to Bob");
assert_eq!(1, bob.sessions().len(), "bob remains connected to Alice");
}
#[test] #[test]
fn test_outbound_connection() { fn test_outbound_connection() {
let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty()); let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
@ -596,8 +660,6 @@ fn test_persistent_peer_reconnect() {
#[test] #[test]
fn test_push_and_pull() { fn test_push_and_pull() {
logger::init(log::Level::Debug);
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let storage_alice = Storage::open(tempdir.path().join("alice").join("storage")).unwrap(); let storage_alice = Storage::open(tempdir.path().join("alice").join("storage")).unwrap();

View File

@ -7,6 +7,11 @@ use crate::prelude::*;
use crate::service::message::*; use crate::service::message::*;
use crate::wire; use crate::wire;
use std::mem::size_of;
/// The maximum supported message size in bytes.
pub const MAX_PAYLOAD_SIZE_BYTES: u16 = u16::MAX - (size_of::<MessageType>() as u16);
/// Message type. /// Message type.
#[repr(u16)] #[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -16,6 +21,8 @@ pub enum MessageType {
InventoryAnnouncement = 4, InventoryAnnouncement = 4,
RefsAnnouncement = 6, RefsAnnouncement = 6,
Subscribe = 8, Subscribe = 8,
Ping = 10,
Pong = 12,
} }
impl From<MessageType> for u16 { impl From<MessageType> for u16 {
@ -34,6 +41,8 @@ impl TryFrom<u16> for MessageType {
4 => Ok(MessageType::InventoryAnnouncement), 4 => Ok(MessageType::InventoryAnnouncement),
6 => Ok(MessageType::RefsAnnouncement), 6 => Ok(MessageType::RefsAnnouncement),
8 => Ok(MessageType::Subscribe), 8 => Ok(MessageType::Subscribe),
10 => Ok(MessageType::Ping),
12 => Ok(MessageType::Pong),
_ => Err(other), _ => Err(other),
} }
} }
@ -49,6 +58,8 @@ impl Message {
AnnouncementMessage::Inventory(_) => MessageType::InventoryAnnouncement, AnnouncementMessage::Inventory(_) => MessageType::InventoryAnnouncement,
AnnouncementMessage::Refs(_) => MessageType::RefsAnnouncement, AnnouncementMessage::Refs(_) => MessageType::RefsAnnouncement,
}, },
Self::Ping { .. } => MessageType::Ping,
Self::Pong { .. } => MessageType::Pong,
} }
.into() .into()
} }
@ -188,6 +199,13 @@ impl wire::Encode for Message {
n += message.encode(writer)?; n += message.encode(writer)?;
n += signature.encode(writer)?; n += signature.encode(writer)?;
} }
Self::Ping { ponglen, zeroes } => {
n += ponglen.encode(writer)?;
n += zeroes.encode(writer)?;
}
Self::Pong { zeroes } => {
n += zeroes.encode(writer)?;
}
} }
Ok(n) Ok(n)
} }
@ -258,6 +276,15 @@ impl wire::Decode for Message {
} }
.into()) .into())
} }
Ok(MessageType::Ping) => {
let ponglen = u16::decode(reader)?;
let zeroes = ZeroBytes::decode(reader)?;
Ok(Self::Ping { ponglen, zeroes })
}
Ok(MessageType::Pong) => {
let zeroes = ZeroBytes::decode(reader)?;
Ok(Self::Pong { zeroes })
}
Err(other) => Err(wire::Error::UnknownMessageType(other)), Err(other) => Err(wire::Error::UnknownMessageType(other)),
} }
} }
@ -335,6 +362,26 @@ impl wire::Decode for Address {
} }
} }
impl wire::Encode for ZeroBytes {
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
let mut n = (self.len() as u16).encode(writer)?;
for _ in 0..self.len() {
n += 0u8.encode(writer)?
}
Ok(n)
}
}
impl wire::Decode for ZeroBytes {
fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
let zeroes = u16::decode(reader)?;
for _ in 0..zeroes {
_ = u8::decode(reader)?;
}
Ok(ZeroBytes::new(zeroes))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -377,6 +424,14 @@ mod tests {
.quickcheck(property as fn(items: Vec<Envelope>)); .quickcheck(property as fn(items: Vec<Envelope>));
} }
#[quickcheck]
fn prop_zero_bytes_encode_decode(zeroes: ZeroBytes) {
assert_eq!(
wire::deserialize::<ZeroBytes>(&wire::serialize(&zeroes)).unwrap(),
zeroes
);
}
#[quickcheck] #[quickcheck]
fn prop_addr(addr: Address) { fn prop_addr(addr: Address) {
assert_eq!( assert_eq!(