Make `services` field in `MetaAddr` optional (#2976)
* Use `prop_assert` instead of `assert` Otherwise the test input isn't minimized. * Split long string into a multi-line string And add some newlines to try to improve readability. * Fix referenced issue number They had a typo in their number. * Make peer services optional It is unknown for initial peers. * Fix `preserve_initial_untrusted_values` test Now that it's optional, the services field can be written to if it was previously empty. * Fix formatting of property tests Run rustfmt on them. * Restore `TODO` comment Make it easy to find planned improvements in the code. Co-authored-by: teor <teor@riseup.net> * Comment on how ordering is affected Make it clear that missing services causes the peer to be chosen last. Co-authored-by: teor <teor@riseup.net> * Don't expect `services` to be available Avoid a panic by using the compiler to help enforce the handling of the case correctly. * Panic if received gossiped address has no services All received gossiped addresses have services. The only addresses that don't have services configured are the initial seed addresses. Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
9963471b7c
commit
a9f1c189d9
|
@ -150,9 +150,9 @@ pub struct MetaAddr {
|
|||
/// `services` from `NeverAttempted` peers may be invalid due to outdated
|
||||
/// records, older peer versions, or buggy or malicious peers.
|
||||
//
|
||||
// TODO: make services private and optional
|
||||
// split gossiped and handshake services? (#2234)
|
||||
pub(crate) services: PeerServices,
|
||||
// TODO: make services private
|
||||
// split gossiped and handshake services? (#2324)
|
||||
pub(crate) services: Option<PeerServices>,
|
||||
|
||||
/// The unverified "last seen time" gossiped by the remote peer that sent us
|
||||
/// this address.
|
||||
|
@ -177,8 +177,7 @@ pub struct MetaAddr {
|
|||
|
||||
/// The outcome of our most recent communication attempt with this peer.
|
||||
//
|
||||
// TODO: make services private and optional?
|
||||
// move the time and services fields into PeerAddrState?
|
||||
// TODO: move the time and services fields into PeerAddrState?
|
||||
// then some fields could be required in some states
|
||||
pub(crate) last_connection_state: PeerAddrState,
|
||||
}
|
||||
|
@ -260,7 +259,7 @@ impl MetaAddr {
|
|||
) -> MetaAddr {
|
||||
MetaAddr {
|
||||
addr: canonical_socket_addr(addr),
|
||||
services: untrusted_services,
|
||||
services: Some(untrusted_services),
|
||||
untrusted_last_seen: Some(untrusted_last_seen),
|
||||
last_response: None,
|
||||
last_attempt: None,
|
||||
|
@ -270,15 +269,19 @@ impl MetaAddr {
|
|||
}
|
||||
|
||||
/// Returns a [`MetaAddrChange::NewGossiped`], based on a gossiped peer
|
||||
/// `MetaAddr`.
|
||||
pub fn new_gossiped_change(self) -> MetaAddrChange {
|
||||
NewGossiped {
|
||||
/// [`MetaAddr`].
|
||||
///
|
||||
/// Returns [`None`] if the gossiped peer is missing the untrusted services field.
|
||||
pub fn new_gossiped_change(self) -> Option<MetaAddrChange> {
|
||||
let untrusted_services = self.services?;
|
||||
|
||||
Some(NewGossiped {
|
||||
addr: canonical_socket_addr(self.addr),
|
||||
untrusted_services: self.services,
|
||||
untrusted_services,
|
||||
untrusted_last_seen: self
|
||||
.untrusted_last_seen
|
||||
.expect("unexpected missing last seen"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a [`MetaAddrChange::UpdateResponded`] for a peer that has just
|
||||
|
@ -502,7 +505,12 @@ impl MetaAddr {
|
|||
/// - reject `NeverAttempted...` [`MetaAddrChange`]s, and
|
||||
/// - temporarily stop outbound connections to a [`MetaAddr`].
|
||||
pub fn last_known_info_is_valid_for_outbound(&self) -> bool {
|
||||
self.services.contains(PeerServices::NODE_NETWORK) && self.address_is_valid_for_outbound()
|
||||
let is_node = match self.services {
|
||||
Some(services) => services.contains(PeerServices::NODE_NETWORK),
|
||||
None => true,
|
||||
};
|
||||
|
||||
is_node && self.address_is_valid_for_outbound()
|
||||
}
|
||||
|
||||
/// Return a sanitized version of this `MetaAddr`, for sending to a remote peer.
|
||||
|
@ -524,9 +532,10 @@ impl MetaAddr {
|
|||
|
||||
Some(MetaAddr {
|
||||
addr: canonical_socket_addr(self.addr),
|
||||
// initial peers are sanitized assuming they are `NODE_NETWORK`
|
||||
// TODO: split untrusted and direct services
|
||||
// sanitize untrusted services to NODE_NETWORK only? (#2234)
|
||||
services: self.services,
|
||||
// consider sanitizing untrusted services to NODE_NETWORK (#2324)
|
||||
services: self.services.or(Some(PeerServices::NODE_NETWORK)),
|
||||
// only put the last seen time in the untrusted field,
|
||||
// this matches deserialization, and avoids leaking internal state
|
||||
untrusted_last_seen: Some(last_seen),
|
||||
|
@ -586,10 +595,10 @@ impl MetaAddrChange {
|
|||
NewAlternate {
|
||||
untrusted_services, ..
|
||||
} => Some(*untrusted_services),
|
||||
// TODO: create a "services implemented by Zebra" constant (#2234)
|
||||
// TODO: create a "services implemented by Zebra" constant (#2324)
|
||||
NewLocal { .. } => Some(PeerServices::NODE_NETWORK),
|
||||
UpdateAttempt { .. } => None,
|
||||
// TODO: split untrusted and direct services (#2234)
|
||||
// TODO: split untrusted and direct services (#2324)
|
||||
UpdateResponded { services, .. } => Some(*services),
|
||||
UpdateFailed { services, .. } => *services,
|
||||
}
|
||||
|
@ -678,10 +687,7 @@ impl MetaAddrChange {
|
|||
match self {
|
||||
NewGossiped { .. } | NewAlternate { .. } | NewLocal { .. } => Some(MetaAddr {
|
||||
addr: self.addr(),
|
||||
// TODO: make services optional when we add a DNS seeder change and state
|
||||
services: self
|
||||
.untrusted_services()
|
||||
.expect("unexpected missing services"),
|
||||
services: self.untrusted_services(),
|
||||
untrusted_last_seen: self.untrusted_last_seen(),
|
||||
last_response: None,
|
||||
last_attempt: None,
|
||||
|
@ -727,8 +733,7 @@ impl MetaAddrChange {
|
|||
// so malicious peers can't keep changing our peer connection order.
|
||||
Some(MetaAddr {
|
||||
addr: self.addr(),
|
||||
// TODO: or(self.untrusted_services()) when services become optional (#2234)
|
||||
services: previous.services,
|
||||
services: previous.services.or_else(|| self.untrusted_services()),
|
||||
untrusted_last_seen: previous
|
||||
.untrusted_last_seen
|
||||
.or_else(|| self.untrusted_last_seen()),
|
||||
|
@ -752,7 +757,7 @@ impl MetaAddrChange {
|
|||
addr: self.addr(),
|
||||
// We want up-to-date services, even if they have fewer bits,
|
||||
// or they are applied out of order.
|
||||
services: self.untrusted_services().unwrap_or(previous.services),
|
||||
services: self.untrusted_services().or(previous.services),
|
||||
// Only NeverAttempted changes can modify the last seen field
|
||||
untrusted_last_seen: previous.untrusted_last_seen,
|
||||
// Since Some(time) is always greater than None, `max` prefers:
|
||||
|
@ -831,7 +836,9 @@ impl Ord for MetaAddr {
|
|||
// So this comparison will have no impact until Zebra implements
|
||||
// more service features.
|
||||
//
|
||||
// TODO: order services by usefulness, not bit pattern values (#2234)
|
||||
// None is less than Some(T), so peers with missing services are chosen last.
|
||||
//
|
||||
// TODO: order services by usefulness, not bit pattern values (#2324)
|
||||
// Security: split gossiped and direct services
|
||||
let larger_services = self.services.cmp(&other.services);
|
||||
|
||||
|
@ -879,10 +886,23 @@ impl Eq for MetaAddr {}
|
|||
impl ZcashSerialize for MetaAddr {
|
||||
fn zcash_serialize<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
|
||||
self.last_seen()
|
||||
.expect("unexpected MetaAddr with missing last seen time: MetaAddrs should be sanitized before serialization")
|
||||
.expect(
|
||||
"unexpected MetaAddr with missing last seen time: MetaAddrs should be sanitized \
|
||||
before serialization",
|
||||
)
|
||||
.zcash_serialize(&mut writer)?;
|
||||
writer.write_u64::<LittleEndian>(self.services.bits())?;
|
||||
|
||||
writer.write_u64::<LittleEndian>(
|
||||
self.services
|
||||
.expect(
|
||||
"unexpected MetaAddr with missing peer services: MetaAddrs should be \
|
||||
sanitized before serialization",
|
||||
)
|
||||
.bits(),
|
||||
)?;
|
||||
|
||||
writer.write_socket_addr(self.addr)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,8 +70,9 @@ pub(crate) fn sanitize_avoids_leaks(original: &MetaAddr, sanitized: &MetaAddr) {
|
|||
// Sanitize to the the default state, even though it's not serialized
|
||||
assert_eq!(sanitized.last_connection_state, Default::default());
|
||||
// Sanitize to known flags
|
||||
let sanitized_peer_services = original.services & PeerServices::all();
|
||||
assert_eq!(sanitized.services, sanitized_peer_services);
|
||||
let sanitized_peer_services =
|
||||
original.services.unwrap_or(PeerServices::NODE_NETWORK) & PeerServices::all();
|
||||
assert_eq!(sanitized.services, Some(sanitized_peer_services));
|
||||
|
||||
// Remove IPv6 scope ID and flow information
|
||||
let sanitized_socket_addr = SocketAddr::new(original.addr.ip(), original.addr.port());
|
||||
|
|
|
@ -52,7 +52,10 @@ proptest! {
|
|||
// also check the address, port, and services individually
|
||||
prop_assert!(!addr.addr.ip().is_unspecified());
|
||||
prop_assert_ne!(addr.addr.port(), 0);
|
||||
prop_assert!(addr.services.contains(PeerServices::NODE_NETWORK));
|
||||
|
||||
if let Some(services) = addr.services {
|
||||
prop_assert!(services.contains(PeerServices::NODE_NETWORK));
|
||||
}
|
||||
|
||||
check::sanitize_avoids_leaks(&addr, &sanitized);
|
||||
}
|
||||
|
@ -60,9 +63,7 @@ proptest! {
|
|||
|
||||
/// Test round-trip serialization for gossiped MetaAddrs
|
||||
#[test]
|
||||
fn gossiped_roundtrip(
|
||||
gossiped_addr in MetaAddr::gossiped_strategy()
|
||||
) {
|
||||
fn gossiped_roundtrip(gossiped_addr in MetaAddr::gossiped_strategy()) {
|
||||
zebra_test::init();
|
||||
|
||||
// We require sanitization before serialization
|
||||
|
@ -121,14 +122,11 @@ proptest! {
|
|||
deserialized_addr,
|
||||
hex::encode(&addr_bytes2),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/// Test round-trip serialization for all MetaAddr variants after sanitization
|
||||
#[test]
|
||||
fn sanitized_roundtrip(
|
||||
addr in any::<MetaAddr>()
|
||||
) {
|
||||
fn sanitized_roundtrip(addr in any::<MetaAddr>()) {
|
||||
zebra_test::init();
|
||||
|
||||
// We require sanitization before serialization,
|
||||
|
@ -194,14 +192,15 @@ proptest! {
|
|||
deserialized_addr,
|
||||
hex::encode(&addr_bytes2),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/// Make sure that [`MetaAddrChange`]s:
|
||||
/// - do not modify the last seen time, unless it was None, and
|
||||
/// - only modify the services after a response or failure.
|
||||
#[test]
|
||||
fn preserve_initial_untrusted_values((mut addr, changes) in MetaAddrChange::addr_changes_strategy(MAX_ADDR_CHANGE)) {
|
||||
fn preserve_initial_untrusted_values(
|
||||
(mut addr, changes) in MetaAddrChange::addr_changes_strategy(MAX_ADDR_CHANGE),
|
||||
) {
|
||||
zebra_test::init();
|
||||
|
||||
for change in changes {
|
||||
|
@ -211,14 +210,19 @@ proptest! {
|
|||
if addr.untrusted_last_seen.is_some() {
|
||||
prop_assert_eq!(changed_addr.untrusted_last_seen, addr.untrusted_last_seen);
|
||||
} else {
|
||||
prop_assert_eq!(changed_addr.untrusted_last_seen, change.untrusted_last_seen());
|
||||
prop_assert_eq!(
|
||||
changed_addr.untrusted_last_seen,
|
||||
change.untrusted_last_seen()
|
||||
);
|
||||
}
|
||||
|
||||
// services:
|
||||
// check that we only change if there was a handshake
|
||||
if changed_addr.last_connection_state.is_never_attempted()
|
||||
|| changed_addr.last_connection_state == AttemptPending
|
||||
|| change.untrusted_services().is_none() {
|
||||
if addr.services.is_some()
|
||||
&& (changed_addr.last_connection_state.is_never_attempted()
|
||||
|| changed_addr.last_connection_state == AttemptPending
|
||||
|| change.untrusted_services().is_none())
|
||||
{
|
||||
prop_assert_eq!(changed_addr.services, addr.services);
|
||||
}
|
||||
|
||||
|
@ -256,7 +260,7 @@ proptest! {
|
|||
|
||||
// If `change` is invalid for the current MetaAddr state, skip it.
|
||||
if let Some(changed_addr) = change.apply_to_meta_addr(addr) {
|
||||
assert_eq!(changed_addr.addr, addr.addr);
|
||||
prop_assert_eq!(changed_addr.addr, addr.addr);
|
||||
addr = changed_addr;
|
||||
}
|
||||
}
|
||||
|
@ -276,16 +280,15 @@ proptest! {
|
|||
) {
|
||||
zebra_test::init();
|
||||
|
||||
let address_book = AddressBook::new_with_addrs(
|
||||
local_listener,
|
||||
Span::none(),
|
||||
address_book_addrs
|
||||
);
|
||||
let address_book =
|
||||
AddressBook::new_with_addrs(local_listener, Span::none(), address_book_addrs);
|
||||
let sanitized_addrs = address_book.sanitized();
|
||||
|
||||
let expected_local_listener = address_book.local_listener_meta_addr();
|
||||
let canonical_local_listener = canonical_socket_addr(local_listener);
|
||||
let book_sanitized_local_listener = sanitized_addrs.iter().find(|meta_addr| meta_addr.addr == canonical_local_listener );
|
||||
let book_sanitized_local_listener = sanitized_addrs
|
||||
.iter()
|
||||
.find(|meta_addr| meta_addr.addr == canonical_local_listener);
|
||||
|
||||
// invalid addresses should be removed by sanitization,
|
||||
// regardless of where they have come from
|
||||
|
@ -325,9 +328,10 @@ proptest! {
|
|||
// Run the test for this much simulated time
|
||||
let overall_test_time: Duration = MIN_PEER_RECONNECTION_DELAY * LIVE_PEER_INTERVALS;
|
||||
// Advance the clock by this much for every peer change
|
||||
let peer_change_interval: Duration = overall_test_time / MAX_ADDR_CHANGE.try_into().unwrap();
|
||||
let peer_change_interval: Duration =
|
||||
overall_test_time / MAX_ADDR_CHANGE.try_into().unwrap();
|
||||
|
||||
assert!(
|
||||
prop_assert!(
|
||||
u32::try_from(MAX_ADDR_CHANGE).unwrap() >= 3 * LIVE_PEER_INTERVALS,
|
||||
"there are enough changes for good test coverage",
|
||||
);
|
||||
|
@ -365,10 +369,10 @@ proptest! {
|
|||
|
||||
for (i, change) in changes.into_iter().enumerate() {
|
||||
while let Some(candidate_addr) = candidate_set.next().await {
|
||||
assert_eq!(candidate_addr.addr, addr.addr);
|
||||
prop_assert_eq!(candidate_addr.addr, addr.addr);
|
||||
|
||||
attempt_count += 1;
|
||||
assert!(
|
||||
prop_assert!(
|
||||
attempt_count <= 1,
|
||||
"candidate: {:?}, change: {}, now: {:?}, earliest next attempt: {:?}, \
|
||||
attempts: {}, live peer interval limit: {}, test time limit: {:?}, \
|
||||
|
@ -395,7 +399,9 @@ proptest! {
|
|||
attempt_count = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
/// Make sure that all disconnected [`MetaAddr`]s are retried once, before
|
||||
|
@ -420,9 +426,10 @@ proptest! {
|
|||
// Run the test for this much simulated time
|
||||
let overall_test_time: Duration = MIN_PEER_RECONNECTION_DELAY * LIVE_PEER_INTERVALS;
|
||||
// Advance the clock by this much for every peer change
|
||||
let peer_change_interval: Duration = overall_test_time / MAX_ADDR_CHANGE.try_into().unwrap();
|
||||
let peer_change_interval: Duration =
|
||||
overall_test_time / MAX_ADDR_CHANGE.try_into().unwrap();
|
||||
|
||||
assert!(
|
||||
prop_assert!(
|
||||
u32::try_from(MAX_ADDR_CHANGE).unwrap() >= 3 * LIVE_PEER_INTERVALS,
|
||||
"there are enough changes for good test coverage",
|
||||
);
|
||||
|
@ -449,7 +456,9 @@ proptest! {
|
|||
|
||||
while addr.is_ready_for_connection_attempt() {
|
||||
*attempt_counts.entry(addr.addr).or_default() += 1;
|
||||
assert!(*attempt_counts.get(&addr.addr).unwrap() <= LIVE_PEER_INTERVALS + 1);
|
||||
prop_assert!(
|
||||
*attempt_counts.get(&addr.addr).unwrap() <= LIVE_PEER_INTERVALS + 1
|
||||
);
|
||||
|
||||
// Simulate an attempt
|
||||
*addr = MetaAddr::new_reconnect(&addr.addr)
|
||||
|
@ -461,17 +470,18 @@ proptest! {
|
|||
// If we've run out of changes for this addr, do nothing.
|
||||
if let Some(changed_addr) = change
|
||||
.map(|change| change.apply_to_meta_addr(*addr))
|
||||
.flatten() {
|
||||
assert_eq!(changed_addr.addr, addr.addr);
|
||||
*addr = changed_addr;
|
||||
}
|
||||
.flatten()
|
||||
{
|
||||
prop_assert_eq!(changed_addr.addr, addr.addr);
|
||||
*addr = changed_addr;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::advance(peer_change_interval).await;
|
||||
}
|
||||
|
||||
attempt_counts
|
||||
});
|
||||
Ok(attempt_counts)
|
||||
})?;
|
||||
|
||||
let min_attempts = attempt_counts.values().min();
|
||||
let max_attempts = attempt_counts.values().max();
|
||||
|
|
|
@ -267,7 +267,12 @@ where
|
|||
|
||||
/// Add new `addrs` to the address book.
|
||||
fn send_addrs(&self, addrs: impl IntoIterator<Item = MetaAddr>) {
|
||||
let addrs = addrs.into_iter().map(MetaAddr::new_gossiped_change);
|
||||
let addrs = addrs
|
||||
.into_iter()
|
||||
.map(MetaAddr::new_gossiped_change)
|
||||
.map(|maybe_addr| {
|
||||
maybe_addr.expect("Received gossiped peers always have services set")
|
||||
});
|
||||
|
||||
// # Correctness
|
||||
//
|
||||
|
|
|
@ -1260,7 +1260,9 @@ where
|
|||
let addr =
|
||||
MetaAddr::new_gossiped_meta_addr(addr, PeerServices::NODE_NETWORK, DateTime32::now());
|
||||
fake_peer = Some(addr);
|
||||
let addr = addr.new_gossiped_change();
|
||||
let addr = addr
|
||||
.new_gossiped_change()
|
||||
.expect("created MetaAddr contains enough information to represent a gossiped address");
|
||||
|
||||
address_book.update(addr);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue