Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authority-discovery: Ignore multi-addresses without IP from DHT records #6545

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a0fca05
authority-discovery: Filter out empty addresses after removing `/p2p/..`
lexnv Nov 19, 2024
3e3e461
authority-discovery: Ignore empty addresses from DHT
lexnv Nov 19, 2024
0724372
authority-discovery/tests: Add test for is empty address
lexnv Nov 19, 2024
7e5aa7b
litep2p: Ignore empty addresses
lexnv Nov 19, 2024
5e71861
litep2p/discovery: Validate empty / no-IP multiaddresses
lexnv Nov 19, 2024
f36d749
libp2p/peer_info: Confirm address only if it is valid
lexnv Nov 19, 2024
9e7c867
Add PRdoc
lexnv Nov 19, 2024
109bb1c
peer_info: Downgrade warn to debug logs
lexnv Nov 20, 2024
281e1eb
network/types: Move verification of external addr
lexnv Nov 21, 2024
f09e25c
network/types: Ensure peerID functionality
lexnv Nov 21, 2024
3906c57
Use crate::type::Multiaddr shared functionality
lexnv Nov 21, 2024
9061359
authority-discovery: Use is_valid_external_address method
lexnv Nov 22, 2024
52acb47
network/types: Enhance address validation by TCP / UDP protocols
lexnv Nov 22, 2024
6389be9
network: Rename is_valid_external_address
lexnv Nov 22, 2024
d109dd7
types/tests: Check empty addresses
lexnv Nov 22, 2024
d6da5d4
types/tests: Extensive tests to check the address is valid
lexnv Nov 22, 2024
6372b5f
litep2p: Use new multiaddr functionality
lexnv Nov 22, 2024
0144119
Merge remote-tracking branch 'origin/master' into lexnv/authorit-empt…
lexnv Nov 22, 2024
335e0e8
authority-discovery: Extra checks before publishing addresses
lexnv Nov 22, 2024
19a3c4e
authority-discovery/tests: Test are moved to network/types
lexnv Nov 22, 2024
a0243b7
network/types: Move is_global directly on the multiaddr
lexnv Nov 22, 2024
9fe8cff
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 25, 2024
bb5653e
authorit-discovery: Add more logs
lexnv Nov 25, 2024
36ff0c7
types: Do not check peerID implicitely
lexnv Nov 25, 2024
457b4f1
types/tests: Adjust testing
lexnv Nov 25, 2024
45b02e1
Update prdoc
lexnv Nov 25, 2024
7fca2c4
Merge remote-tracking branch 'origin/master' into lexnv/authorit-empt…
lexnv Nov 25, 2024
65ca987
Update PR doc
lexnv Nov 25, 2024
dd8e5ec
litep2p: Downgrade warns to debug
lexnv Nov 25, 2024
7ea83f4
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 26, 2024
a5e756d
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 26, 2024
86cab78
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 27, 2024
66d47b9
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 27, 2024
40ebbda
Update from lexnv running command 'prdoc --audience node_dev --bump p…
actions-user Nov 27, 2024
d075120
Force CI
lexnv Nov 27, 2024
fd1a4b4
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 27, 2024
d22d23f
Merge branch 'master' into lexnv/authorit-empty-addr
lexnv Nov 28, 2024
711f1c3
Merge branch 'master' into lexnv/authorit-empty-addr
bkchr Dec 23, 2024
e87378d
Update substrate/client/authority-discovery/Cargo.toml
bkchr Dec 23, 2024
b8285bc
Update substrate/client/network/src/litep2p/mod.rs
bkchr Dec 23, 2024
fa01496
Update substrate/client/network/src/litep2p/mod.rs
bkchr Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions prdoc/pr_6545.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
title: Ignore multi-addresses without IP from DHT records

doc:
- audience: [ Node Dev, Node Operator ]
description: |
This PR ensures that multiaddresses without an IP protocol are not published or utilized from DHT records.

crates:
- name: sc-network
bump: patch
- name: sc-authority-discovery
bump: patch
52 changes: 48 additions & 4 deletions substrate/client/authority-discovery/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ where
config
.public_addresses
.into_iter()
.map(|address| AddressType::PublicAddress(address).without_p2p(local_peer_id))
.filter_map(|address| {
AddressType::PublicAddress(address).without_p2p(local_peer_id)
})
.collect()
};

Expand Down Expand Up @@ -399,6 +401,7 @@ where
.filter_map(|address| {
address_is_global(&address)
.then(|| AddressType::GlobalListenAddress(address).without_p2p(local_peer_id))
.flatten()
})
.take(MAX_GLOBAL_LISTEN_ADDRESSES)
.peekable();
Expand All @@ -411,6 +414,7 @@ where
.filter_map(|address| {
(publish_non_global_ips || address_is_global(&address))
.then(|| AddressType::ExternalAddress(address).without_p2p(local_peer_id))
.flatten()
})
.peekable();

Expand Down Expand Up @@ -846,7 +850,9 @@ where
// Ignore [`Multiaddr`]s without [`PeerId`] or with own addresses.
let addresses: Vec<Multiaddr> = addresses
.into_iter()
.filter(|a| get_peer_id(&a).filter(|p| *p != local_peer_id).is_some())
.filter(|a| {
!address_is_empty(a) && get_peer_id(&a).filter(|p| *p != local_peer_id).is_some()
})
.collect();

let remote_peer_id = single(addresses.iter().map(|a| get_peer_id(&a)))
Expand Down Expand Up @@ -998,7 +1004,9 @@ impl AddressType {
///
/// In case the peer id in the address does not match the local peer id, an error is logged for
/// `ExternalAddress` and `GlobalListenAddress`.
fn without_p2p(self, local_peer_id: PeerId) -> Multiaddr {
///
/// Returns `None` if the address is empty after removing the `/p2p/..` part.
fn without_p2p(self, local_peer_id: PeerId) -> Option<Multiaddr> {
// Get the address and the source str for logging.
let (mut address, source) = match self {
AddressType::PublicAddress(address) => (address, "public address"),
Expand All @@ -1016,10 +1024,26 @@ impl AddressType {
}
address.pop();
}
address

// Empty addresses cannot be published.
if address.is_empty() {
None
} else {
Some(address)
}
}
}

/// Checks if an authority-discovery address is empty.
///
/// An address is considered empty if:
/// - The multiaddr is empty.
/// - The multiaddr contains only the `/p2p/..` protocol.
fn address_is_empty(address: &Multiaddr) -> bool {
address.is_empty() ||
address.iter().all(|protocol| matches!(protocol, multiaddr::Protocol::P2p(_)))
}

/// NetworkProvider provides [`Worker`] with all necessary hooks into the
/// underlying Substrate networking. Using this trait abstraction instead of
/// `sc_network::NetworkService` directly is necessary to unit test [`Worker`].
Expand Down Expand Up @@ -1199,3 +1223,23 @@ impl<Block: BlockT, Client, DhtEventStream> Worker<Client, Block, DhtEventStream
self.addr_cache.insert(authority, addresses);
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn check_empty_address() {
// Plain empty address.
let empty_address = Multiaddr::empty();
assert!(address_is_empty(&empty_address));

// Address is still unusable.
let address_with_p2p = Multiaddr::from(multiaddr::Protocol::P2p(PeerId::random().into()));
assert!(address_is_empty(&address_with_p2p));

// Address is not empty.
let valid_address: Multiaddr = "/dns/domain1.com/tcp/30333".parse().unwrap();
assert!(!address_is_empty(&valid_address));
}
}
68 changes: 54 additions & 14 deletions substrate/client/network/src/litep2p/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,59 @@ impl Discovery {

(false, None)
}

/// Verify the external address discovered by the identify protocol.
///
/// This ensures that:
/// - the address is not empty
/// - the address contains a valid IP address
/// - the address is for the local peer ID
/// - the address always contains `/p2p/<local_peer_id>` protocol.
///
/// The provided peer ID is only used for logging purposes.
fn verify_external_address(&mut self, address: Multiaddr, peer: PeerId) -> Option<Multiaddr> {
lexnv marked this conversation as resolved.
Show resolved Hide resolved
if address.is_empty() {
log::warn!(
target: LOG_TARGET,
"🔍 Discovered empty address from {peer:?}",
);
return None;
};

// Expect the address to contain a valid IP address.
if std::matches!(
address.iter().next(),
Some(
Protocol::Dns(_) |
Protocol::Dns4(_) |
Protocol::Dns6(_) |
Protocol::Ip6(_) |
Protocol::Ip4(_),
)
) {
log::warn!(
target: LOG_TARGET,
"🔍 Discovered external address from {peer:?} does not contain a valid IP address: {address}",
);
return None;
};

if let Some(Protocol::P2p(peer_id)) = address.iter().last() {
// Invalid address if the reported peer ID is not the local peer ID.
if peer_id != *self.local_peer_id.as_ref() {
log::warn!(
target: LOG_TARGET,
"🔍 Discovered external address from {peer:?} that is not us: {address}",
);
return None
}

return Some(address)
}

// Ensure the address contains the local peer ID.
Some(address.with(Protocol::P2p(self.local_peer_id.into())))
}
}

impl Stream for Discovery {
Expand Down Expand Up @@ -596,20 +649,7 @@ impl Stream for Discovery {
observed_address,
..
})) => {
let observed_address =
if let Some(Protocol::P2p(peer_id)) = observed_address.iter().last() {
if peer_id != *this.local_peer_id.as_ref() {
log::warn!(
target: LOG_TARGET,
"Discovered external address for a peer that is not us: {observed_address}",
);
None
} else {
Some(observed_address)
}
} else {
Some(observed_address.with(Protocol::P2p(this.local_peer_id.into())))
};
let observed_address = this.verify_external_address(observed_address, peer);

// Ensure that an external address with a different peer ID does not have
// side effects of evicting other external addresses via `ExternalAddressExpired`.
Expand Down
8 changes: 8 additions & 0 deletions substrate/client/network/src/litep2p/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,14 @@ impl<B: BlockT + 'static, H: ExHashT> NetworkBackend<B, H> for Litep2pNetworkBac
NetworkServiceCommand::AddKnownAddress { peer, address } => {
let mut address: Multiaddr = address.into();

if address.is_empty() {
log::debug!(
target: LOG_TARGET,
"Ignoring empty address for {peer:?}",
);
continue
}

if !address.iter().any(|protocol| std::matches!(protocol, Protocol::P2p(_))) {
address.push(Protocol::P2p(litep2p::PeerId::from(peer).into()));
}
Expand Down
78 changes: 67 additions & 11 deletions substrate/client/network/src/peer_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use libp2p::{
Info as IdentifyInfo,
},
identity::PublicKey,
multiaddr::Protocol,
ping::{Behaviour as Ping, Config as PingConfig, Event as PingEvent},
swarm::{
behaviour::{
Expand Down Expand Up @@ -66,6 +67,9 @@ pub struct PeerInfoBehaviour {
ping: Ping,
/// Periodically identifies the remote and responds to incoming requests.
identify: Identify,
/// Identity of our local node.
local_peer_id: PeerId,

/// Information that we know about all nodes.
nodes_info: FnvHashMap<PeerId, NodeInfo>,
/// Interval at which we perform garbage collection in `nodes_info`.
Expand Down Expand Up @@ -123,6 +127,7 @@ impl PeerInfoBehaviour {
local_public_key: PublicKey,
external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
) -> Self {
let local_peer_id = local_public_key.to_peer_id();
let identify = {
let cfg = IdentifyConfig::new("/substrate/1.0".to_string(), local_public_key)
.with_agent_version(user_agent)
Expand All @@ -134,6 +139,7 @@ impl PeerInfoBehaviour {
Self {
ping: Ping::new(PingConfig::new()),
identify,
local_peer_id,
nodes_info: FnvHashMap::default(),
garbage_collect: Box::pin(interval(GARBAGE_COLLECT_INTERVAL)),
external_addresses: ExternalAddresses { addresses: external_addresses },
Expand Down Expand Up @@ -178,6 +184,53 @@ impl PeerInfoBehaviour {
"Received pong from node we're not connected to {:?}", peer_id);
}
}

/// Verify the external address discovered by the identify protocol.
///
/// This ensures that:
/// - the address is not empty
/// - the address contains a valid IP address
/// - the address is for the local peer ID
fn verify_external_address(&mut self, address: &Multiaddr) -> bool {
if address.is_empty() {
log::warn!(
lexnv marked this conversation as resolved.
Show resolved Hide resolved
target: "sub-libp2p",
"🔍 Discovered empty address",
);
return false;
};

// Expect the address to contain a valid IP address.
if std::matches!(
address.iter().next(),
Some(
Protocol::Dns(_) |
Protocol::Dns4(_) |
Protocol::Dns6(_) |
Protocol::Ip6(_) |
Protocol::Ip4(_),
)
) {
log::warn!(
target: "sub-libp2p",
"🔍 Discovered external address does not contain a valid IP address: {address}",
);
return false;
};

if let Some(Protocol::P2p(peer_id)) = address.iter().last() {
// Invalid address if the reported peer ID is not the local peer ID.
if peer_id != self.local_peer_id {
log::warn!(
target: "sub-libp2p",
"🔍 Discovered external address that is not us: {address}",
);
return false
}
}

true
}
}

/// Gives access to the information about a node.
Expand Down Expand Up @@ -401,17 +454,20 @@ impl NetworkBehaviour for PeerInfoBehaviour {
self.external_addresses.remove(e.addr);
},
FromSwarm::NewExternalAddrCandidate(e @ NewExternalAddrCandidate { addr }) => {
self.ping.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
self.identify.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));

// Manually confirm all external address candidates.
// TODO: consider adding [AutoNAT protocol](https://docs.rs/libp2p/0.52.3/libp2p/autonat/index.html)
// (must go through the polkadot protocol spec) or implemeting heuristics for
// approving external address candidates. This can be done, for example, by
// approving only addresses reported by multiple peers.
// See also https://github.com/libp2p/rust-libp2p/pull/4721 introduced
// in libp2p v0.53 for heuristics approach.
self.pending_actions.push_back(ToSwarm::ExternalAddrConfirmed(addr.clone()));
// Verify the external address is valid.
if self.verify_external_address(addr) {
self.ping.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
self.identify.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));

// Manually confirm all external address candidates.
// TODO: consider adding [AutoNAT protocol](https://docs.rs/libp2p/0.52.3/libp2p/autonat/index.html)
// (must go through the polkadot protocol spec) or implemeting heuristics for
// approving external address candidates. This can be done, for example, by
// approving only addresses reported by multiple peers.
// See also https://github.com/libp2p/rust-libp2p/pull/4721 introduced
// in libp2p v0.53 for heuristics approach.
self.pending_actions.push_back(ToSwarm::ExternalAddrConfirmed(addr.clone()));
}
},
FromSwarm::ExternalAddrConfirmed(e @ ExternalAddrConfirmed { addr }) => {
self.ping.on_swarm_event(FromSwarm::ExternalAddrConfirmed(e));
Expand Down
5 changes: 5 additions & 0 deletions substrate/client/network/types/src/multiaddr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ pub struct Multiaddr {
}

impl Multiaddr {
/// Returns `true` if this multiaddress is empty.
pub fn is_empty(&self) -> bool {
self.multiaddr.is_empty()
}

/// Create a new, empty multiaddress.
pub fn empty() -> Self {
Self { multiaddr: LiteP2pMultiaddr::empty() }
Expand Down
Loading