kaspa_connectionmanager/
lib.rs

1use std::{
2    cmp::min,
3    collections::{HashMap, HashSet},
4    net::{IpAddr, SocketAddr, ToSocketAddrs},
5    sync::Arc,
6    time::{Duration, SystemTime},
7};
8
9use duration_string::DurationString;
10use futures_util::future::{join_all, try_join_all};
11use itertools::Itertools;
12use kaspa_addressmanager::{AddressManager, NetAddress};
13use kaspa_core::{debug, info, warn};
14use kaspa_p2p_lib::{common::ProtocolError, ConnectionError, Peer};
15use kaspa_utils::triggers::SingleTrigger;
16use parking_lot::Mutex as ParkingLotMutex;
17use rand::{seq::SliceRandom, thread_rng};
18use tokio::{
19    select,
20    sync::{
21        mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
22        Mutex as TokioMutex,
23    },
24    time::{interval, MissedTickBehavior},
25};
26
27pub struct ConnectionManager {
28    p2p_adaptor: Arc<kaspa_p2p_lib::Adaptor>,
29    outbound_target: usize,
30    inbound_limit: usize,
31    dns_seeders: &'static [&'static str],
32    default_port: u16,
33    address_manager: Arc<ParkingLotMutex<AddressManager>>,
34    connection_requests: TokioMutex<HashMap<SocketAddr, ConnectionRequest>>,
35    force_next_iteration: UnboundedSender<()>,
36    shutdown_signal: SingleTrigger,
37}
38
39#[derive(Clone, Debug)]
40struct ConnectionRequest {
41    next_attempt: SystemTime,
42    is_permanent: bool,
43    attempts: u32,
44}
45
46impl ConnectionRequest {
47    fn new(is_permanent: bool) -> Self {
48        Self { next_attempt: SystemTime::now(), is_permanent, attempts: 0 }
49    }
50}
51
52impl ConnectionManager {
53    pub fn new(
54        p2p_adaptor: Arc<kaspa_p2p_lib::Adaptor>,
55        outbound_target: usize,
56        inbound_limit: usize,
57        dns_seeders: &'static [&'static str],
58        default_port: u16,
59        address_manager: Arc<ParkingLotMutex<AddressManager>>,
60    ) -> Arc<Self> {
61        let (tx, rx) = unbounded_channel::<()>();
62        let manager = Arc::new(Self {
63            p2p_adaptor,
64            outbound_target,
65            inbound_limit,
66            address_manager,
67            connection_requests: Default::default(),
68            force_next_iteration: tx,
69            shutdown_signal: SingleTrigger::new(),
70            dns_seeders,
71            default_port,
72        });
73        manager.clone().start_event_loop(rx);
74        manager.force_next_iteration.send(()).unwrap();
75        manager
76    }
77
78    fn start_event_loop(self: Arc<Self>, mut rx: UnboundedReceiver<()>) {
79        let mut ticker = interval(Duration::from_secs(30));
80        ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
81        tokio::spawn(async move {
82            loop {
83                if self.shutdown_signal.trigger.is_triggered() {
84                    break;
85                }
86                select! {
87                    _ = rx.recv() => self.clone().handle_event().await,
88                    _ = ticker.tick() => self.clone().handle_event().await,
89                    _ = self.shutdown_signal.listener.clone() => break,
90                }
91            }
92            debug!("Connection manager event loop exiting");
93        });
94    }
95
96    async fn handle_event(self: Arc<Self>) {
97        debug!("Starting connection loop iteration");
98        let peers = self.p2p_adaptor.active_peers();
99        let peer_by_address: HashMap<SocketAddr, Peer> = peers.into_iter().map(|peer| (peer.net_address(), peer)).collect();
100
101        self.handle_connection_requests(&peer_by_address).await;
102        self.handle_outbound_connections(&peer_by_address).await;
103        self.handle_inbound_connections(&peer_by_address).await;
104    }
105
106    pub async fn add_connection_request(&self, address: SocketAddr, is_permanent: bool) {
107        // If the request already exists, it resets the attempts count and overrides the `is_permanent` setting.
108        self.connection_requests.lock().await.insert(address, ConnectionRequest::new(is_permanent));
109        self.force_next_iteration.send(()).unwrap(); // We force the next iteration of the connection loop.
110    }
111
112    pub async fn stop(&self) {
113        self.shutdown_signal.trigger.trigger()
114    }
115
116    async fn handle_connection_requests(self: &Arc<Self>, peer_by_address: &HashMap<SocketAddr, Peer>) {
117        let mut requests = self.connection_requests.lock().await;
118        let mut new_requests = HashMap::with_capacity(requests.len());
119        for (address, request) in requests.iter() {
120            let address = *address;
121            let request = request.clone();
122            let is_connected = peer_by_address.contains_key(&address);
123            if is_connected && !request.is_permanent {
124                // The peer is connected and the request is not permanent - no need to keep the request
125                continue;
126            }
127
128            if !is_connected && request.next_attempt <= SystemTime::now() {
129                debug!("Connecting to peer request {}", address);
130                match self.p2p_adaptor.connect_peer(address.to_string()).await {
131                    Err(err) => {
132                        debug!("Failed connecting to peer request: {}, {}", address, err);
133                        if request.is_permanent {
134                            const MAX_ACCOUNTABLE_ATTEMPTS: u32 = 4;
135                            let retry_duration =
136                                Duration::from_secs(30u64 * 2u64.pow(min(request.attempts, MAX_ACCOUNTABLE_ATTEMPTS)));
137                            debug!("Will retry peer request {} in {}", address, DurationString::from(retry_duration));
138                            new_requests.insert(
139                                address,
140                                ConnectionRequest {
141                                    next_attempt: SystemTime::now() + retry_duration,
142                                    attempts: request.attempts + 1,
143                                    is_permanent: true,
144                                },
145                            );
146                        }
147                    }
148                    Ok(_) if request.is_permanent => {
149                        // Permanent requests are kept forever
150                        new_requests.insert(address, ConnectionRequest::new(true));
151                    }
152                    Ok(_) => {}
153                }
154            } else {
155                new_requests.insert(address, request);
156            }
157        }
158
159        *requests = new_requests;
160    }
161
162    async fn handle_outbound_connections(self: &Arc<Self>, peer_by_address: &HashMap<SocketAddr, Peer>) {
163        let active_outbound: HashSet<kaspa_addressmanager::NetAddress> =
164            peer_by_address.values().filter(|peer| peer.is_outbound()).map(|peer| peer.net_address().into()).collect();
165        if active_outbound.len() >= self.outbound_target {
166            return;
167        }
168
169        let mut missing_connections = self.outbound_target - active_outbound.len();
170        let mut addr_iter = self.address_manager.lock().iterate_prioritized_random_addresses(active_outbound);
171
172        let mut progressing = true;
173        let mut connecting = true;
174        while connecting && missing_connections > 0 {
175            if self.shutdown_signal.trigger.is_triggered() {
176                return;
177            }
178            let mut addrs_to_connect = Vec::with_capacity(missing_connections);
179            let mut jobs = Vec::with_capacity(missing_connections);
180            for _ in 0..missing_connections {
181                let Some(net_addr) = addr_iter.next() else {
182                    connecting = false;
183                    break;
184                };
185                let socket_addr = SocketAddr::new(net_addr.ip.into(), net_addr.port).to_string();
186                debug!("Connecting to {}", &socket_addr);
187                addrs_to_connect.push(net_addr);
188                jobs.push(self.p2p_adaptor.connect_peer(socket_addr.clone()));
189            }
190
191            if progressing && !jobs.is_empty() {
192                // Log only if progress was made
193                info!(
194                    "Connection manager: has {}/{} outgoing P2P connections, trying to obtain {} additional connection(s)...",
195                    self.outbound_target - missing_connections,
196                    self.outbound_target,
197                    jobs.len(),
198                );
199                progressing = false;
200            } else {
201                debug!(
202                    "Connection manager: outgoing: {}/{} , connecting: {}, iterator: {}",
203                    self.outbound_target - missing_connections,
204                    self.outbound_target,
205                    jobs.len(),
206                    addr_iter.len(),
207                );
208            }
209
210            for (res, net_addr) in (join_all(jobs).await).into_iter().zip(addrs_to_connect) {
211                match res {
212                    Ok(_) => {
213                        self.address_manager.lock().mark_connection_success(net_addr);
214                        missing_connections -= 1;
215                        progressing = true;
216                    }
217                    Err(ConnectionError::ProtocolError(ProtocolError::PeerAlreadyExists(_))) => {
218                        // We avoid marking the existing connection as connection failure
219                        debug!("Failed connecting to {:?}, peer already exists", net_addr);
220                    }
221                    Err(err) => {
222                        debug!("Failed connecting to {:?}, err: {}", net_addr, err);
223                        self.address_manager.lock().mark_connection_failure(net_addr);
224                    }
225                }
226            }
227        }
228
229        if missing_connections > 0 && !self.dns_seeders.is_empty() {
230            if missing_connections > self.outbound_target / 2 {
231                // If we are missing more than half of our target, query all in parallel.
232                // This will always be the case on new node start-up and is the most resilient strategy in such a case.
233                self.dns_seed_many(self.dns_seeders.len()).await;
234            } else {
235                // Try to obtain at least twice the number of missing connections
236                self.dns_seed_with_address_target(2 * missing_connections).await;
237            }
238        }
239    }
240
241    async fn handle_inbound_connections(self: &Arc<Self>, peer_by_address: &HashMap<SocketAddr, Peer>) {
242        let active_inbound = peer_by_address.values().filter(|peer| !peer.is_outbound()).collect_vec();
243        let active_inbound_len = active_inbound.len();
244        if self.inbound_limit >= active_inbound_len {
245            return;
246        }
247
248        let mut futures = Vec::with_capacity(active_inbound_len - self.inbound_limit);
249        for peer in active_inbound.choose_multiple(&mut thread_rng(), active_inbound_len - self.inbound_limit) {
250            debug!("Disconnecting from {} because we're above the inbound limit", peer.net_address());
251            futures.push(self.p2p_adaptor.terminate(peer.key()));
252        }
253        join_all(futures).await;
254    }
255
256    /// Queries DNS seeders in random order, one after the other, until obtaining `min_addresses_to_fetch` addresses
257    async fn dns_seed_with_address_target(self: &Arc<Self>, min_addresses_to_fetch: usize) {
258        let cmgr = self.clone();
259        tokio::task::spawn_blocking(move || cmgr.dns_seed_with_address_target_blocking(min_addresses_to_fetch)).await.unwrap();
260    }
261
262    fn dns_seed_with_address_target_blocking(self: &Arc<Self>, mut min_addresses_to_fetch: usize) {
263        let shuffled_dns_seeders = self.dns_seeders.choose_multiple(&mut thread_rng(), self.dns_seeders.len());
264        for &seeder in shuffled_dns_seeders {
265            // Query seeders sequentially until reaching the desired number of addresses
266            let addrs_len = self.dns_seed_single(seeder);
267            if addrs_len >= min_addresses_to_fetch {
268                break;
269            } else {
270                min_addresses_to_fetch -= addrs_len;
271            }
272        }
273    }
274
275    /// Queries `num_seeders_to_query` random DNS seeders in parallel
276    async fn dns_seed_many(self: &Arc<Self>, num_seeders_to_query: usize) -> usize {
277        info!("Querying {} DNS seeders", num_seeders_to_query);
278        let shuffled_dns_seeders = self.dns_seeders.choose_multiple(&mut thread_rng(), num_seeders_to_query);
279        let jobs = shuffled_dns_seeders.map(|seeder| {
280            let cmgr = self.clone();
281            tokio::task::spawn_blocking(move || cmgr.dns_seed_single(seeder))
282        });
283        try_join_all(jobs).await.unwrap().into_iter().sum()
284    }
285
286    /// Query a single DNS seeder and add the obtained addresses to the address manager.
287    ///
288    /// DNS lookup is a blocking i/o operation so this function is assumed to be called
289    /// from a blocking execution context.
290    fn dns_seed_single(self: &Arc<Self>, seeder: &str) -> usize {
291        info!("Querying DNS seeder {}", seeder);
292        // Since the DNS lookup protocol doesn't come with a port, we must assume that the default port is used.
293        let addrs = match (seeder, self.default_port).to_socket_addrs() {
294            Ok(addrs) => addrs,
295            Err(e) => {
296                warn!("Error connecting to DNS seeder {}: {}", seeder, e);
297                return 0;
298            }
299        };
300
301        let addrs_len = addrs.len();
302        info!("Retrieved {} addresses from DNS seeder {}", addrs_len, seeder);
303        let mut amgr_lock = self.address_manager.lock();
304        for addr in addrs {
305            amgr_lock.add_address(NetAddress::new(addr.ip().into(), addr.port()));
306        }
307
308        addrs_len
309    }
310
311    /// Bans the given IP and disconnects from all the peers with that IP.
312    ///
313    /// _GO-KASPAD: BanByIP_
314    pub async fn ban(&self, ip: IpAddr) {
315        if self.ip_has_permanent_connection(ip).await {
316            return;
317        }
318        for peer in self.p2p_adaptor.active_peers() {
319            if peer.net_address().ip() == ip {
320                self.p2p_adaptor.terminate(peer.key()).await;
321            }
322        }
323        self.address_manager.lock().ban(ip.into());
324    }
325
326    /// Returns whether the given address is banned.
327    pub async fn is_banned(&self, address: &SocketAddr) -> bool {
328        !self.is_permanent(address).await && self.address_manager.lock().is_banned(address.ip().into())
329    }
330
331    /// Returns whether the given address is a permanent request.
332    pub async fn is_permanent(&self, address: &SocketAddr) -> bool {
333        self.connection_requests.lock().await.contains_key(address)
334    }
335
336    /// Returns whether the given IP has some permanent request.
337    pub async fn ip_has_permanent_connection(&self, ip: IpAddr) -> bool {
338        self.connection_requests.lock().await.iter().any(|(address, request)| request.is_permanent && address.ip() == ip)
339    }
340}