darkfi/net/
hosts.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19//! The main interface for interacting with the hostlist. Contains the following:
20//!
21//! `Hosts`: the main parent class that manages HostRegistry and HostContainer. It is also
22//!  responsible for filtering addresses before writing to the hostlist.
23//!
24//! `HostRegistry`: A locked HashMap that maps peer addresses onto mutually exclusive
25//!  states (`HostState`). Prevents race conditions by dictating a strict flow of logically
26//!  acceptable states.
27//!
28//! `HostContainer`: A wrapper for the hostlists. Each hostlist is represented by a `HostColor`,
29//!  which can be Grey, White, Gold or Black. Exposes a common interface for hostlist queries and
30//!  utilities.
31//!
32//! `HostColor`:
33//! > `White`: Hosts that have passed the `GreylistRefinery` successfully.
34//!
35//! > `Gold`: Hosts we have been able to establish a connection to in `OutboundSession`.
36//!
37//! > `Grey`: Recently received hosts that are checked by the `GreylistRefinery` and
38//! > upgraded to the whitelist if valid. If they're inaccessible by the Refinery
39//! > they will be deleted.
40//!
41//! > `Black`: hostile hosts that are strictly avoided for the duration of the program.
42//!
43//! > `Dark`: hosts that do not match our transports, but that we continue to share with
44//! > other peers. We do not keep darklist entries that are older than one day.
45//! > This is to avoid peers propagating nodes that may be faulty. We assume that
46//! > within the one day period, the nodes will be picked up by peers that accept
47//! > the transports and can refine them to remove inactive peers. Dark list hosts
48//! > are otherwise ignored.
49//!
50//! `HostState`: a set of mutually exclusive states that can be Insert, Refine, Connect, Suspend
51//!  or Connected. The state is `None` when the corresponding host has been removed from the
52//!  HostRegistry.
53
54//TODO: Use HostState::Free `age` variable to implement a pruning logic that deletes peers from
55//the registry once they have bypassed a certain age threshold.
56
57use rand::{prelude::IteratorRandom, rngs::OsRng, Rng};
58use smol::lock::RwLock as AsyncRwLock;
59use std::{
60    collections::HashMap,
61    fmt, fs,
62    fs::File,
63    net::{IpAddr, Ipv4Addr, Ipv6Addr},
64    sync::{
65        atomic::{AtomicBool, Ordering},
66        Arc, Mutex as SyncMutex, RwLock,
67    },
68    time::{Instant, UNIX_EPOCH},
69};
70use tracing::{debug, error, trace, warn};
71use url::{Host, Url};
72
73use super::{
74    session::{SESSION_REFINE, SESSION_SEED},
75    settings::Settings,
76    ChannelPtr,
77};
78use crate::{
79    system::{Publisher, PublisherPtr, Subscription},
80    util::{
81        file::{load_file, save_file},
82        logger::verbose,
83        most_frequent_or_any,
84        path::expand_path,
85        ringbuffer::RingBuffer,
86    },
87    Error, Result,
88};
89
90// An array containing all possible local host strings
91// TODO: This could perhaps be more exhaustive?
92pub const LOCAL_HOST_STRS: [&str; 2] = ["localhost", "localhost.localdomain"];
93const WHITELIST_MAX_LEN: usize = 5000;
94const GREYLIST_MAX_LEN: usize = 2000;
95const DARKLIST_MAX_LEN: usize = 1000;
96
97/// Atomic pointer to hosts object
98pub type HostsPtr = Arc<Hosts>;
99
100/// Keeps track of hosts and their current state. Prevents race conditions
101/// where multiple threads are simultaneously trying to change the state of
102/// a given host.
103pub(in crate::net) type HostRegistry = SyncMutex<HashMap<Url, HostState>>;
104
105/// HostState is a set of mutually exclusive states that can be Insert,
106/// Refine, Move, Connect, Suspend or Connected or Free.
107/// ```
108///                +------+
109///                | free |
110///                +------+
111///                   ^
112///                   |
113///                   v
114///                +------+      +---------+
115///       +------> | move | ---> | suspend |
116///       |        +------+      +---------+
117///       |           |               |        +--------+
118///       |           |               v        | insert |
119///  +---------+      |          +--------+    +--------+
120///  | connect |      |          | refine |        ^
121///  +---------+      |          +--------+        |
122///       |           v               |            v
123///       |     +-----------+         |         +------+
124///       +---> | connected | <-------+-------> | free |
125///             +-----------+                   +------+
126///                   ^
127///                   |
128///                   v
129///                +------+
130///                | free |
131///                +------+
132///
133/// ```
134/* NOTE: Currently if a user loses connectivity, they will be deleted from
135our hostlist by the refinery process and forgotten about until they regain
136connectivity and share their external address with the p2p network again.
137
138We may want to keep nodes with patchy connections in a `Red` list
139and periodically try to connect to them in Outbound Session, rather
140than sending them to the refinery (which will delete them if they are
141offline) as we do using `Suspend`. The current design favors reliability
142of connections but this may come at a risk for security since an attacker
143is likely to have good uptime. We want to insure that users with patchy
144connections or on mobile are still likely to be connected to.*/
145
146#[derive(Clone, Debug)]
147pub(in crate::net) enum HostState {
148    /// Hosts that are currently being inserting into the hostlist.
149    Insert,
150    /// Hosts that are migrating from the greylist to the whitelist or being
151    /// removed from the greylist, as defined in `refinery.rs`.
152    Refine,
153    /// Hosts that are being connected to in Outbound and Manual Session.
154    Connect,
155    /// Hosts that we have just failed to connect to. Marking a host as
156    /// Suspend effectively sends this host to refinery, since Suspend->
157    /// Refine is an acceptable state transition. Being marked as Suspend does
158    /// not increase a host's probability of being refined, since the refinery
159    /// selects its subjects randomly (with the caveat that we cannot refine
160    /// nodes marked as Connect, Connected, Insert or Move). It does however
161    /// mean this host cannot be connected to unless it passes through the
162    /// refinery successfully.
163    Suspend,
164    /// Hosts that have been successfully connected to.
165    Connected(ChannelPtr),
166
167    /// Host that are moving between hostlists, implemented in
168    /// store::move_host().
169    Move,
170
171    /// Free up a peer for any future operation.
172    Free(u64),
173}
174
175impl HostState {
176    // Try to change state to Insert. Only possible if we are not yet
177    // tracking this host in the HostRegistry, or if this host is marked
178    // as Free.
179    fn try_insert(&self) -> Result<Self> {
180        let start = self.to_string();
181        let end = HostState::Insert.to_string();
182        match self {
183            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
184            HostState::Refine => Err(Error::HostStateBlocked(start, end)),
185            HostState::Connect => Err(Error::HostStateBlocked(start, end)),
186            HostState::Suspend => Err(Error::HostStateBlocked(start, end)),
187            HostState::Connected(_) => Err(Error::HostStateBlocked(start, end)),
188            HostState::Move => Err(Error::HostStateBlocked(start, end)),
189            HostState::Free(_) => Ok(HostState::Insert),
190        }
191    }
192
193    // Try to change state to Refine. Only possible if the peer is marked
194    // as Free, or Suspend i.e. we have failed to connect to it.
195    fn try_refine(&self) -> Result<Self> {
196        let start = self.to_string();
197        let end = HostState::Refine.to_string();
198        match self {
199            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
200            HostState::Refine => Err(Error::HostStateBlocked(start, end)),
201            HostState::Connect => Err(Error::HostStateBlocked(start, end)),
202            HostState::Suspend => Ok(HostState::Refine),
203            HostState::Connected(_) => Err(Error::HostStateBlocked(start, end)),
204            HostState::Move => Err(Error::HostStateBlocked(start, end)),
205            HostState::Free(_) => Ok(HostState::Refine),
206        }
207    }
208
209    // Try to change state to Connect. Only possible if this peer is marked
210    // as Free.
211    fn try_connect(&self) -> Result<Self> {
212        let start = self.to_string();
213        let end = HostState::Connect.to_string();
214        match self {
215            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
216            HostState::Refine => Err(Error::HostStateBlocked(start, end)),
217            HostState::Connect => Err(Error::HostStateBlocked(start, end)),
218            HostState::Suspend => Err(Error::HostStateBlocked(start, end)),
219            HostState::Connected(_) => Err(Error::HostStateBlocked(start, end)),
220            HostState::Move => Err(Error::HostStateBlocked(start, end)),
221            HostState::Free(_) => Ok(HostState::Connect),
222        }
223    }
224
225    // Try to change state to Connected. Possible if this peer's state
226    // is currently Connect, Refine, Move, or Free. Refine is necessary since the
227    // refinery process requires us to establish a connection to a peer.
228    // Move is necessary due to the upgrade to Gold sequence in
229    // `session::perform_handshake_protocols`. Free is necessary since
230    // this could be a peer we previously recognize from inbound sessions.
231    fn try_connected(&self, channel: ChannelPtr) -> Result<Self> {
232        let start = self.to_string();
233        let end = HostState::Connected(channel.clone()).to_string();
234        match self {
235            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
236            HostState::Refine => Ok(HostState::Connected(channel)),
237            HostState::Connect => Ok(HostState::Connected(channel)),
238            HostState::Suspend => Err(Error::HostStateBlocked(start, end)),
239            HostState::Connected(_) => Err(Error::HostStateBlocked(start, end)),
240            HostState::Move => Ok(HostState::Connected(channel)),
241            HostState::Free(_) => Ok(HostState::Connected(channel)),
242        }
243    }
244
245    // Try to change state to Move. Possibly if this host is currently
246    // Connect i.e. it is being connected to, if we are currently Connected
247    // to this peer (due to host Downgrade sequence in `session::remove_sub_on_stop`),
248    // or if this node is Free (since we might recognize this peer from a previous
249    // inbound session).
250    fn try_move(&self) -> Result<Self> {
251        let start = self.to_string();
252        let end = HostState::Move.to_string();
253        match self {
254            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
255            HostState::Refine => Ok(HostState::Move),
256            HostState::Connect => Ok(HostState::Move),
257            HostState::Suspend => Err(Error::HostStateBlocked(start, end)),
258            HostState::Connected(_) => Ok(HostState::Move),
259            HostState::Move => Err(Error::HostStateBlocked(start, end)),
260            HostState::Free(_) => Ok(HostState::Move),
261        }
262    }
263
264    // Try to change the state to Suspend. Only possible when we are
265    // currently moving this host, since we suspend a host after failing
266    // to connect to it in `outbound_session::try_connect` and then downgrading
267    // in `hosts::move_host`.
268    fn try_suspend(&self) -> Result<Self> {
269        let start = self.to_string();
270        let end = HostState::Suspend.to_string();
271        match self {
272            HostState::Insert => Err(Error::HostStateBlocked(start, end)),
273            HostState::Refine => Err(Error::HostStateBlocked(start, end)),
274            HostState::Connect => Err(Error::HostStateBlocked(start, end)),
275            HostState::Suspend => Err(Error::HostStateBlocked(start, end)),
276            HostState::Connected(_) => Err(Error::HostStateBlocked(start, end)),
277            HostState::Move => Ok(HostState::Suspend),
278            HostState::Free(_) => Err(Error::HostStateBlocked(start, end)),
279        }
280    }
281
282    // Free up this host to be used by the HostRegistry. The most permissive
283    // state that allows every state transition.
284    // This is preferable to simply deleting hosts from the HostRegistry since
285    // it is less likely to result in race conditions.
286    fn try_free(&self, age: u64) -> Result<Self> {
287        match self {
288            HostState::Insert => Ok(HostState::Free(age)),
289            HostState::Refine => Ok(HostState::Free(age)),
290            HostState::Connect => Ok(HostState::Free(age)),
291            HostState::Suspend => Ok(HostState::Free(age)),
292            HostState::Connected(_) => Ok(HostState::Free(age)),
293            HostState::Move => Ok(HostState::Free(age)),
294            HostState::Free(age) => Ok(HostState::Free(*age)),
295        }
296    }
297}
298
299impl fmt::Display for HostState {
300    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
301        fmt::Debug::fmt(self, f)
302    }
303}
304
305#[repr(u8)]
306#[derive(Clone, Debug)]
307pub enum HostColor {
308    /// Intermediary nodes that are periodically probed and updated
309    /// to White.
310    Grey = 0,
311    /// Recently seen hosts. Shared with other nodes.
312    White = 1,
313    /// Nodes to which we have already been able to establish a
314    /// connection.
315    Gold = 2,
316    /// Hostile peers that can neither be connected to nor establish
317    /// connections to us for the duration of the program.
318    Black = 3,
319    /// Peers that do not match our accepted transports. We are blind
320    /// to these nodes (we do not use them) but we send them around
321    /// the network anyway to ensure all transports are propagated.
322    /// Cleared once daily.
323    Dark = 4,
324}
325
326impl TryFrom<usize> for HostColor {
327    type Error = Error;
328
329    fn try_from(value: usize) -> Result<Self> {
330        match value {
331            0 => Ok(HostColor::Grey),
332            1 => Ok(HostColor::White),
333            2 => Ok(HostColor::Gold),
334            3 => Ok(HostColor::Black),
335            4 => Ok(HostColor::Dark),
336            _ => Err(Error::InvalidHostColor),
337        }
338    }
339}
340
341/// A Container for managing Grey, White, Gold and Black hostlists. Exposes
342/// a common interface for writing to and querying hostlists.
343// TODO: Benchmark hostlist operations when the hostlist is at max size.
344pub struct HostContainer {
345    pub(in crate::net) hostlists: [RwLock<Vec<(Url, u64)>>; 5],
346}
347
348impl HostContainer {
349    fn new() -> Self {
350        let hostlists: [RwLock<Vec<(Url, u64)>>; 5] = [
351            RwLock::new(Vec::new()),
352            RwLock::new(Vec::new()),
353            RwLock::new(Vec::new()),
354            RwLock::new(Vec::new()),
355            RwLock::new(Vec::new()),
356        ];
357
358        Self { hostlists }
359    }
360
361    /// Append host to a hostlist. Called when initalizing the hostlist in load_hosts().
362    fn store(&self, color: usize, addr: Url, last_seen: u64) {
363        trace!(target: "net::hosts::store", "[START] list={:?}",
364        HostColor::try_from(color).unwrap());
365
366        let mut list = self.hostlists[color].write().unwrap();
367        list.push((addr.clone(), last_seen));
368        debug!(target: "net::hosts::store", "Added [{addr}] to {:?} list",
369               HostColor::try_from(color).unwrap());
370
371        trace!(target: "net::hosts::store", "[END] list={:?}",
372               HostColor::try_from(color).unwrap());
373    }
374
375    /// Stores an address on a hostlist or updates its last_seen field if
376    /// we already have the address.
377    fn store_or_update(&self, color: HostColor, addr: Url, last_seen: u64) {
378        trace!(target: "net::hosts::store_or_update", "[START]");
379        let color_code = color.clone() as usize;
380        let mut list = self.hostlists[color_code].write().unwrap();
381        if let Some(entry) = list.iter_mut().find(|(u, _)| *u == addr) {
382            entry.1 = last_seen;
383            debug!(target: "net::hosts::store_or_update", "Updated [{addr}] entry on {:?} list",
384                color.clone());
385        } else {
386            list.push((addr.clone(), last_seen));
387            debug!(target: "net::hosts::store_or_update", "Added [{addr}] to {color:?} list");
388        }
389        trace!(target: "net::hosts::store_or_update", "[STOP]");
390    }
391
392    /// Update the last_seen field of a peer on a hostlist.
393    pub fn update_last_seen(&self, color: usize, addr: Url, last_seen: u64) {
394        trace!(target: "net::hosts::update_last_seen", "[START] list={:?}",
395        HostColor::try_from(color).unwrap());
396
397        let mut list = self.hostlists[color].write().unwrap();
398        if let Some(entry) = list.iter_mut().find(|(u, _)| *u == addr) {
399            entry.1 = last_seen;
400        }
401        trace!(target: "net::hosts::update_last_seen", "[END] list={:?}",
402               HostColor::try_from(color).unwrap());
403    }
404
405    /// Return all known hosts on a hostlist.
406    pub fn fetch_all(&self, color: HostColor) -> Vec<(Url, u64)> {
407        self.hostlists[color as usize].read().unwrap().iter().cloned().collect()
408    }
409
410    /// Get the oldest entry from a hostlist.
411    pub fn fetch_last(&self, color: HostColor) -> Option<(Url, u64)> {
412        let list = self.hostlists[color as usize].read().unwrap();
413        list.last().cloned()
414    }
415
416    /// Get up to limit peers that match the given transport schemes from
417    /// a hostlist.  If limit was not provided, return all matching peers.
418    pub(in crate::net) fn fetch_with_schemes(
419        &self,
420        color: usize,
421        schemes: &[String],
422        limit: Option<usize>,
423    ) -> Vec<(Url, u64)> {
424        trace!(target: "net::hosts::fetch_with_schemes", "[START] {:?}",
425               HostColor::try_from(color).unwrap());
426
427        let list = self.hostlists[color].read().unwrap();
428
429        let mut limit = match limit {
430            Some(l) => l.min(list.len()),
431            None => list.len(),
432        };
433        let mut ret = vec![];
434
435        if limit == 0 {
436            return ret
437        }
438
439        for (addr, last_seen) in list.iter() {
440            if schemes.contains(&addr.scheme().to_string()) {
441                ret.push((addr.clone(), *last_seen));
442                limit -= 1;
443                if limit == 0 {
444                    debug!(target: "net::hosts::fetch_with_schemes",
445                           "Found matching addr on list={:?}, returning {} addresses",
446                           HostColor::try_from(color).unwrap(), ret.len());
447                    return ret
448                }
449            }
450        }
451
452        if ret.is_empty() {
453            debug!(target: "net::hosts::fetch_with_schemes",
454                   "No matching schemes found on list={:?}!", HostColor::try_from(color).unwrap())
455        }
456
457        ret
458    }
459
460    /// Get up to limit peers that don't match the given transport schemes
461    /// from a hostlist.  If limit was not provided, return all matching
462    /// peers.
463    fn fetch_excluding_schemes(
464        &self,
465        color: usize,
466        schemes: &[String],
467        limit: Option<usize>,
468    ) -> Vec<(Url, u64)> {
469        trace!(target: "net::hosts::fetch_with_schemes", "[START] {:?}",
470               HostColor::try_from(color).unwrap());
471
472        let list = self.hostlists[color].read().unwrap();
473
474        let mut limit = match limit {
475            Some(l) => l.min(list.len()),
476            None => list.len(),
477        };
478        let mut ret = vec![];
479
480        if limit == 0 {
481            return ret
482        }
483
484        for (addr, last_seen) in list.iter() {
485            if !schemes.contains(&addr.scheme().to_string()) {
486                ret.push((addr.clone(), *last_seen));
487                limit -= 1;
488                if limit == 0 {
489                    return ret
490                }
491            }
492        }
493
494        if ret.is_empty() {
495            debug!(target: "net::hosts::fetch_excluding_schemes", "No such schemes found!");
496        }
497
498        ret
499    }
500
501    /// Get a random peer from a hostlist that matches the given transport
502    /// schemes.
503    pub(in crate::net) fn fetch_random_with_schemes(
504        &self,
505        color: HostColor,
506        schemes: &[String],
507    ) -> Option<((Url, u64), usize)> {
508        // Retrieve all peers corresponding to that transport schemes
509        trace!(target: "net::hosts::fetch_random_with_schemes", "[START] {color:?}");
510        let list = self.fetch_with_schemes(color as usize, schemes, None);
511
512        if list.is_empty() {
513            return None
514        }
515
516        let position = rand::thread_rng().gen_range(0..list.len());
517        let entry = &list[position];
518        Some((entry.clone(), position))
519    }
520
521    /// Get up to n random peers. Schemes are not taken into account.
522    pub(in crate::net) fn fetch_n_random(&self, color: HostColor, n: u32) -> Vec<(Url, u64)> {
523        trace!(target: "net::hosts::fetch_n_random", "[START] {color:?}");
524        let n = n as usize;
525        if n == 0 {
526            return vec![]
527        }
528        let mut hosts = vec![];
529
530        let list = self.hostlists[color as usize].read().unwrap();
531
532        for (addr, last_seen) in list.iter() {
533            hosts.push((addr.clone(), *last_seen));
534        }
535
536        if hosts.is_empty() {
537            debug!(target: "net::hosts::fetch_n_random", "No entries found!");
538            return hosts
539        }
540
541        // Grab random ones
542        let urls = hosts.iter().choose_multiple(&mut OsRng, n.min(hosts.len()));
543        urls.iter().map(|&url| url.clone()).collect()
544    }
545
546    /// Get up to n random peers that match the given transport schemes.
547    pub(in crate::net) fn fetch_n_random_with_schemes(
548        &self,
549        color: HostColor,
550        schemes: &[String],
551        n: u32,
552    ) -> Vec<(Url, u64)> {
553        trace!(target: "net::hosts::fetch_n_random_with_schemes", "[START] {color:?}");
554        let index = color as usize;
555        let n = n as usize;
556        if n == 0 {
557            return vec![]
558        }
559
560        // Retrieve all peers corresponding to that transport schemes
561        let hosts = self.fetch_with_schemes(index, schemes, None);
562        if hosts.is_empty() {
563            debug!(target: "net::hosts::fetch_n_random_with_schemes",
564                  "No such schemes found!");
565            return hosts
566        }
567
568        // Grab random ones
569        let urls = hosts.iter().choose_multiple(&mut OsRng, n.min(hosts.len()));
570        urls.iter().map(|&url| url.clone()).collect()
571    }
572
573    /// Get up to n random peers that don't match the given transport schemes
574    /// from a hostlist.
575    pub(in crate::net) fn fetch_n_random_excluding_schemes(
576        &self,
577        color: HostColor,
578        schemes: &[String],
579        n: u32,
580    ) -> Vec<(Url, u64)> {
581        trace!(target: "net::hosts::fetch_excluding_schemes", "[START] {color:?}");
582        let index = color as usize;
583        let n = n as usize;
584        if n == 0 {
585            return vec![]
586        }
587        // Retrieve all peers not corresponding to that transport schemes
588        let hosts = self.fetch_excluding_schemes(index, schemes, None);
589
590        if hosts.is_empty() {
591            debug!(target: "net::hosts::fetch_n_random_excluding_schemes",
592            "No such schemes found!");
593            return hosts
594        }
595
596        // Grab random ones
597        let urls = hosts.iter().choose_multiple(&mut OsRng, n.min(hosts.len()));
598        urls.iter().map(|&url| url.clone()).collect()
599    }
600
601    /// Remove an entry from a hostlist if it exists.
602    pub fn remove_if_exists(&self, color: HostColor, addr: &Url) {
603        let color_code = color.clone() as usize;
604        let mut list = self.hostlists[color_code].write().unwrap();
605        if let Some(position) = list.iter().position(|(u, _)| u == addr) {
606            debug!(target: "net::hosts::remove_if_exists", "Removing addr={addr} list={color:?}");
607            list.remove(position);
608        }
609    }
610
611    /// Check if a hostlist is empty.
612    pub fn is_empty(&self, color: HostColor) -> bool {
613        self.hostlists[color as usize].read().unwrap().is_empty()
614    }
615
616    /// Check if host is in a hostlist
617    pub fn contains(&self, color: usize, addr: &Url) -> bool {
618        self.hostlists[color].read().unwrap().iter().any(|(u, _t)| u == addr)
619    }
620
621    /// Get the index for a given addr on a hostlist.
622    pub fn get_index_at_addr(&self, color: usize, addr: Url) -> Option<usize> {
623        self.hostlists[color].read().unwrap().iter().position(|a| a.0 == addr)
624    }
625
626    /// Get the last_seen field for a given entry on a hostlist.
627    pub fn get_last_seen(&self, color: usize, addr: &Url) -> Option<u64> {
628        self.hostlists[color]
629            .read()
630            .unwrap()
631            .iter()
632            .find(|(url, _)| url == addr)
633            .map(|(_, last_seen)| *last_seen)
634    }
635
636    /// Sort a hostlist by last_seen.
637    fn sort_by_last_seen(&self, color: usize) {
638        let mut list = self.hostlists[color].write().unwrap();
639        list.sort_by_key(|entry| entry.1);
640        list.reverse();
641    }
642
643    /// Remove the last item on a hostlist if it reaches max size.
644    fn resize(&self, color: HostColor) {
645        let list = self.hostlists[color.clone() as usize].read().unwrap();
646        let size = list.len();
647
648        // Immediately drop the read lock.
649        drop(list);
650
651        match color {
652            HostColor::Grey | HostColor::White | HostColor::Dark => {
653                let max_size = match color {
654                    HostColor::Grey => GREYLIST_MAX_LEN,
655                    HostColor::White => WHITELIST_MAX_LEN,
656                    HostColor::Dark => DARKLIST_MAX_LEN,
657                    _ => {
658                        unreachable!()
659                    }
660                };
661                if size == max_size {
662                    let mut list = self.hostlists[color.clone() as usize].write().unwrap();
663                    let last_entry = list.pop().unwrap();
664
665                    debug!(
666                        target: "net::hosts::resize",
667                        "{color:?}list reached max size. Removed {last_entry:?}"
668                    );
669                }
670            }
671            // Gold and Black list do not have a max size.
672            HostColor::Gold | HostColor::Black => (),
673        }
674    }
675
676    /// Delete items from a hostlist that are older than a specified maximum.
677    /// Maximum should be specified in seconds.
678    fn refresh(&self, color: HostColor, max_age: u64) {
679        let now = UNIX_EPOCH.elapsed().unwrap().as_secs();
680        let mut old_items = vec![];
681
682        let darklist = self.fetch_all(HostColor::Dark);
683        for (addr, last_seen) in darklist {
684            // Skip if last_seen comes from the future.
685            //
686            // We do this to avoid an overflow, which can happen if
687            // our system clock is behind or if other nodes are
688            // misreporting the last_seen field.
689            if now < last_seen {
690                debug!(target: "net::hosts::refresh",
691                "last_seen [{now}] is newer than current system time [{last_seen}]. Skipping");
692                continue
693            }
694            if (now - last_seen) > max_age {
695                old_items.push(addr);
696            }
697        }
698
699        for item in old_items {
700            debug!(target: "net::hosts::refresh", "Removing {item:?}");
701            self.remove_if_exists(color.clone(), &item);
702        }
703    }
704
705    /// Load the hostlists from a file.
706    pub(in crate::net) fn load_all(&self, path: &str) -> Result<()> {
707        let path = expand_path(path)?;
708
709        if !path.exists() {
710            if let Some(parent) = path.parent() {
711                fs::create_dir_all(parent)?;
712            }
713
714            File::create(path.clone())?;
715        }
716
717        let contents = load_file(&path);
718        if let Err(e) = contents {
719            warn!(target: "net::hosts::load_hosts", "Failed retrieving saved hosts: {e}");
720            return Ok(())
721        }
722
723        for line in contents.unwrap().lines() {
724            let data: Vec<&str> = line.split('\t').collect();
725
726            let url = match Url::parse(data[1]) {
727                Ok(u) => u,
728                Err(e) => {
729                    debug!(target: "net::hosts::load_hosts", "Skipping malformed URL {e}");
730                    continue
731                }
732            };
733
734            let last_seen = match data[2].parse::<u64>() {
735                Ok(t) => t,
736                Err(e) => {
737                    debug!(target: "net::hosts::load_hosts", "Skipping malformed last seen {e}");
738                    continue
739                }
740            };
741
742            match data[0] {
743                "gold" => {
744                    self.store(HostColor::Gold as usize, url, last_seen);
745                    self.sort_by_last_seen(HostColor::Gold as usize);
746                }
747                "white" => {
748                    self.store(HostColor::White as usize, url, last_seen);
749                    self.sort_by_last_seen(HostColor::White as usize);
750                    self.resize(HostColor::White);
751                }
752                "grey" => {
753                    self.store(HostColor::Grey as usize, url, last_seen);
754                    self.sort_by_last_seen(HostColor::Grey as usize);
755                    self.resize(HostColor::Grey);
756                }
757                "dark" => {
758                    self.store(HostColor::Dark as usize, url, last_seen);
759                    self.sort_by_last_seen(HostColor::Dark as usize);
760                    self.resize(HostColor::Dark);
761
762                    // Delete darklist entries that are older than one day.
763                    let day = 86400;
764                    self.refresh(HostColor::Dark, day);
765                }
766                _ => {
767                    debug!(target: "net::hosts::load_hosts", "Malformed list name...");
768                }
769            }
770        }
771
772        Ok(())
773    }
774
775    /// Save the hostlist to a file.
776    pub(in crate::net) fn save_all(&self, path: &str) -> Result<()> {
777        let path = expand_path(path)?;
778
779        let mut tsv = String::new();
780        let mut hostlist: HashMap<String, Vec<(Url, u64)>> = HashMap::new();
781
782        hostlist.insert("dark".to_string(), self.fetch_all(HostColor::Dark));
783        hostlist.insert("grey".to_string(), self.fetch_all(HostColor::Grey));
784        hostlist.insert("white".to_string(), self.fetch_all(HostColor::White));
785        hostlist.insert("gold".to_string(), self.fetch_all(HostColor::Gold));
786
787        for (name, list) in hostlist {
788            for (url, last_seen) in list {
789                tsv.push_str(&format!("{name}\t{url}\t{last_seen}\n"));
790            }
791        }
792
793        if !tsv.is_empty() {
794            verbose!(target: "net::hosts::save_hosts", "Saving hosts to: {path:?}");
795            if let Err(e) = save_file(&path, &tsv) {
796                error!(target: "net::hosts::save_hosts", "Failed saving hosts: {e}");
797            }
798        }
799
800        Ok(())
801    }
802
803    /// Performs transport mixing for an url returning a list of addresses
804    /// with mixed transports.
805    /// For example we're allowed to use tor:// to connect to tcp:// and tor+tls://
806    /// to connect to tcp+tls:// or socks5:// to connect to tor://.
807    /// However, **do not** mix tor:// and tcp+tls://, nor tor+tls:// and tcp://.
808    pub(in crate::net) fn mix_host(
809        addr: &Url,
810        transports: &[String],
811        mixed_transports: &[String],
812        tor_socks5_proxy: &Option<Url>,
813        nym_socks5_proxy: &Option<Url>,
814    ) -> Vec<Url> {
815        let mut hosts = vec![];
816
817        if !mixed_transports.contains(&addr.scheme().to_string()) {
818            return hosts;
819        }
820
821        macro_rules! mix_transport {
822            ($a:expr, $b:expr) => {
823                if transports.contains(&$a.to_string()) && addr.scheme() == $b {
824                    let mut url = addr.clone();
825                    url.set_scheme($a).unwrap();
826                    hosts.push(url);
827                }
828            };
829        }
830        macro_rules! mix_socks5_transport {
831            ($a:expr, $b:expr, $proxies:expr) => {
832                if transports.contains(&$a.to_string()) && addr.scheme() == $b {
833                    for proxy in $proxies {
834                        if let Some(mut endpoint) = proxy {
835                            endpoint.set_path(&format!(
836                                "{}:{}",
837                                addr.host().unwrap(),
838                                addr.port().unwrap()
839                            ));
840                            endpoint.set_scheme($a).unwrap();
841                            hosts.push(endpoint);
842                        }
843                    }
844                }
845            };
846        }
847
848        mix_transport!("tor", "tcp");
849        mix_transport!("tor+tls", "tcp+tls");
850        mix_transport!("nym", "tcp");
851        mix_transport!("nym+tls", "tcp+tls");
852        mix_socks5_transport!(
853            "socks5",
854            "tcp",
855            [tor_socks5_proxy.clone(), nym_socks5_proxy.clone()]
856        );
857        mix_socks5_transport!(
858            "socks5+tls",
859            "tcp+tls",
860            [tor_socks5_proxy.clone(), nym_socks5_proxy.clone()]
861        );
862        mix_socks5_transport!("socks5", "tor", [tor_socks5_proxy.clone()]);
863        mix_socks5_transport!("socks5+tls", "tor+tls", [tor_socks5_proxy.clone()]);
864
865        hosts
866    }
867}
868
869/// Main parent class for the management and manipulation of
870/// hostlists.
871///
872/// Keeps track of hosts and their current state via the HostRegistry,
873/// and stores hostlists and associated methods in the HostContainer.
874/// Also operates two publishers to notify other parts of the code base
875/// when new channels have been created or new hosts have been added to
876/// the hostlist.
877pub struct Hosts {
878    /// A registry that tracks hosts and their current state.
879    registry: HostRegistry,
880
881    /// Hostlists and associated methods.
882    pub container: HostContainer,
883
884    /// Publisher listening for store updates
885    store_publisher: PublisherPtr<usize>,
886
887    /// Publisher for notifications of new channels
888    pub(in crate::net) channel_publisher: PublisherPtr<Result<ChannelPtr>>,
889
890    /// Publisher listening for network disconnects
891    pub(in crate::net) disconnect_publisher: PublisherPtr<Error>,
892
893    /// Keeps track of the last time a connection was made.
894    pub(in crate::net) last_connection: SyncMutex<Instant>,
895
896    /// Marker for IPv6 availability
897    pub(in crate::net) ipv6_available: AtomicBool,
898
899    /// Auto self discovered addresses. Used for filtering self connections.
900    auto_self_addrs: SyncMutex<RingBuffer<Ipv6Addr, 20>>,
901
902    /// Pointer to configured P2P settings
903    settings: Arc<AsyncRwLock<Settings>>,
904}
905
906impl Hosts {
907    /// Create a new hosts list
908    pub(in crate::net) fn new(settings: Arc<AsyncRwLock<Settings>>) -> HostsPtr {
909        Arc::new(Self {
910            registry: SyncMutex::new(HashMap::new()),
911            container: HostContainer::new(),
912            store_publisher: Publisher::new(),
913            channel_publisher: Publisher::new(),
914            disconnect_publisher: Publisher::new(),
915            last_connection: SyncMutex::new(Instant::now()),
916            ipv6_available: AtomicBool::new(true),
917            auto_self_addrs: SyncMutex::new(RingBuffer::new()),
918            settings,
919        })
920    }
921
922    /// Safely insert into the HostContainer. Filters the addresses first before storing and
923    /// notifies the publisher. Must be called when first receiving greylist addresses.
924    pub(in crate::net) async fn insert(&self, color: HostColor, addrs: &[(Url, u64)]) {
925        trace!(target: "net::hosts:insert", "[START]");
926
927        // First filter these address to ensure this peer doesn't exist in our black, gold or
928        // whitelist and apply transport filtering. If we don't support this transport,
929        // store the peer on our dark list to broadcast to other nodes.
930        let filtered_addrs = self.filter_addresses(addrs).await;
931        let mut addrs_len = 0;
932
933        if filtered_addrs.is_empty() {
934            debug!(target: "net::hosts::insert", "Filtered out all addresses");
935        }
936
937        // Then ensure we aren't currently trying to add this peer to the hostlist.
938        for (i, (addr, last_seen)) in filtered_addrs.iter().enumerate() {
939            if let Err(e) = self.try_register(addr.clone(), HostState::Insert) {
940                debug!(target: "net::hosts::insert", "Cannot insert addr={}, err={e}",
941                       addr.clone());
942
943                continue
944            }
945
946            addrs_len += i + 1;
947
948            self.container.store_or_update(color.clone(), addr.clone(), *last_seen);
949            self.container.sort_by_last_seen(color.clone() as usize);
950            self.container.resize(color.clone());
951
952            if let Err(e) = self.unregister(addr) {
953                warn!(target: "net::hosts::insert", "Error while unregistering addr={addr}, err={e}");
954            }
955        }
956
957        self.store_publisher.notify(addrs_len).await;
958        trace!(target: "net::hosts:insert", "[END]");
959    }
960
961    /// Check whether a peer is available to be refined currently. Returns true
962    /// if available, false otherwise.
963    pub fn refinable(&self, addr: Url) -> bool {
964        self.try_register(addr.clone(), HostState::Refine).is_ok()
965    }
966
967    /// Try to update the registry. If the host already exists, try to update its state.
968    /// Otherwise add the host to the registry along with its state.
969    pub(in crate::net) fn try_register(
970        &self,
971        addr: Url,
972        new_state: HostState,
973    ) -> Result<HostState> {
974        let mut registry = self.registry.lock().unwrap();
975
976        trace!(target: "net::hosts::try_update_registry", "Try register addr={addr}, state={}",
977               &new_state);
978
979        if registry.contains_key(&addr) {
980            let current_state = registry.get(&addr).unwrap().clone();
981
982            let result: Result<HostState> = match new_state {
983                HostState::Insert => current_state.try_insert(),
984                HostState::Refine => current_state.try_refine(),
985                HostState::Connect => current_state.try_connect(),
986                HostState::Suspend => current_state.try_suspend(),
987                HostState::Connected(c) => current_state.try_connected(c),
988                HostState::Move => current_state.try_move(),
989                HostState::Free(a) => current_state.try_free(a),
990            };
991
992            if let Ok(state) = &result {
993                registry.insert(addr.clone(), state.clone());
994            }
995
996            trace!(target: "net::hosts::try_update_registry", "Returning result {result:?}");
997
998            result
999        } else {
1000            // We don't know this peer. We can safely update the state.
1001            debug!(target: "net::hosts::try_update_registry", "Inserting addr={addr}, state={}",
1002                   &new_state);
1003
1004            registry.insert(addr.clone(), new_state.clone());
1005
1006            Ok(new_state)
1007        }
1008    }
1009
1010    // Loop through hosts selected by Outbound Session and see if any of them are
1011    // free to connect to.
1012    pub(in crate::net) async fn check_addrs(&self, hosts: Vec<(Url, u64)>) -> Option<(Url, u64)> {
1013        trace!(target: "net::hosts::check_addrs", "[START]");
1014
1015        let seeds = self.settings.read().await.seeds.clone();
1016        let external_addrs = self.external_addrs().await;
1017
1018        for (host, last_seen) in hosts {
1019            // Print a warning if we are trying to connect to a seed node in
1020            // Outbound session. This shouldn't happen as we reject configured
1021            // seed nodes from entering our hostlist in filter_addrs().
1022            if seeds.contains(&host) {
1023                warn!(
1024                    target: "net::hosts::check_addrs",
1025                    "Seed addr={} has entered the hostlist! Skipping", host.clone(),
1026                );
1027                continue
1028            }
1029
1030            if external_addrs.contains(&host) {
1031                warn!(
1032                    target: "net::hosts::check_addrs",
1033                    "External addr={} has entered the hostlist! Skipping", host.clone(),
1034                );
1035                continue
1036            }
1037
1038            if let Err(e) = self.try_register(host.clone(), HostState::Connect) {
1039                trace!(
1040                    target: "net::hosts::check_addrs",
1041                    "Skipping addr={}, err={e}", host.clone(),
1042                );
1043                continue
1044            }
1045
1046            debug!(target: "net::hosts::check_addrs", "Found valid host {host}");
1047            return Some((host.clone(), last_seen))
1048        }
1049
1050        None
1051    }
1052
1053    /// Mark as host as Free which frees it up for most future operations.
1054    pub(in crate::net) fn unregister(&self, addr: &Url) -> Result<()> {
1055        let age = UNIX_EPOCH.elapsed().unwrap().as_secs();
1056        self.try_register(addr.clone(), HostState::Free(age))?;
1057        debug!(target: "net::hosts::unregister", "Unregistered: {}", &addr);
1058        Ok(())
1059    }
1060
1061    /// Return the list of all connected channels, including seed and
1062    /// refinery connections.
1063    pub fn channels(&self) -> Vec<ChannelPtr> {
1064        let registry = self.registry.lock().unwrap();
1065        let mut channels = Vec::new();
1066
1067        for (_, state) in registry.iter() {
1068            if let HostState::Connected(c) = state {
1069                channels.push(c.clone());
1070            }
1071        }
1072        channels
1073    }
1074
1075    /// Grab the channel pointer of provided channel ID, if it exists.
1076    pub fn get_channel(&self, id: u32) -> Option<ChannelPtr> {
1077        let mut channel = None;
1078
1079        let channels = self.channels();
1080        for c in channels {
1081            if c.info.id == id {
1082                channel = Some(c.clone());
1083                break
1084            }
1085        }
1086
1087        channel
1088    }
1089
1090    /// Return the list of connected peers. Seed and refinery connections
1091    /// are not taken into account.
1092    pub fn peers(&self) -> Vec<ChannelPtr> {
1093        let registry = self.registry.lock().unwrap();
1094        let mut channels = Vec::new();
1095
1096        for (_, state) in registry.iter() {
1097            if let HostState::Connected(c) = state {
1098                // Skip this channel is it's a seed or refine session.
1099                if c.session_type_id() & (SESSION_SEED | SESSION_REFINE) != 0 {
1100                    continue
1101                }
1102                channels.push(c.clone());
1103            }
1104        }
1105        channels
1106    }
1107
1108    /// Returns the list of suspended channels.
1109    pub(in crate::net) fn suspended(&self) -> Vec<Url> {
1110        let registry = self.registry.lock().unwrap();
1111        let mut addrs = Vec::new();
1112
1113        for (url, state) in registry.iter() {
1114            if let HostState::Suspend = state {
1115                addrs.push(url.clone());
1116            }
1117        }
1118        addrs
1119    }
1120
1121    /// Retrieve a random connected channel
1122    pub fn random_channel(&self) -> ChannelPtr {
1123        let channels = self.channels();
1124        let position = rand::thread_rng().gen_range(0..channels.len());
1125        channels[position].clone()
1126    }
1127
1128    /// Add a channel to the set of connected channels
1129    pub(in crate::net) async fn register_channel(&self, channel: ChannelPtr) {
1130        let address = channel.address().clone();
1131
1132        // This is an attempt to skip any Tor (and similar-behaving) inbound connections
1133        if channel.p2p().settings().read().await.inbound_addrs.contains(&address) {
1134            return
1135        }
1136
1137        // This will error if we are already connected to this peer, this peer
1138        // is suspended, or this peer is currently being inserted into the hostlist.
1139        // None of these scenarios should ever happen.
1140        if let Err(e) = self.try_register(address, HostState::Connected(channel.clone())) {
1141            warn!(target: "net::hosts::register_channel", "Error while registering channel {channel:?}: {e:?}");
1142            return
1143        }
1144
1145        // Notify that channel processing was successful
1146        self.channel_publisher.notify(Ok(channel.clone())).await;
1147
1148        let mut last_online = self.last_connection.lock().unwrap();
1149        *last_online = Instant::now();
1150    }
1151
1152    /// Get notified when new hosts have been inserted into a hostlist.
1153    pub async fn subscribe_store(&self) -> Subscription<usize> {
1154        self.store_publisher.clone().subscribe().await
1155    }
1156
1157    /// Get notified when a new channel has been created
1158    pub async fn subscribe_channel(&self) -> Subscription<Result<ChannelPtr>> {
1159        self.channel_publisher.clone().subscribe().await
1160    }
1161
1162    /// Get notified when a node has no active connections (is disconnected)
1163    pub async fn subscribe_disconnect(&self) -> Subscription<Error> {
1164        self.disconnect_publisher.clone().subscribe().await
1165    }
1166
1167    // Verify whether a URL is local.
1168    // NOTE: This function is stateless and not specific to
1169    // `Hosts`. For this reason, it might make more sense
1170    // to move this function to a more appropriate location
1171    // in the codebase.
1172    /// Check whether a URL is local host
1173    pub fn is_local_host(&self, url: &Url) -> bool {
1174        // Reject Urls without host strings.
1175        if url.host_str().is_none() {
1176            return false
1177        }
1178
1179        // Filter private IP ranges
1180        match url.host().unwrap() {
1181            url::Host::Ipv4(ip) => {
1182                if !ip.unstable_is_global() {
1183                    return true
1184                }
1185            }
1186            url::Host::Ipv6(ip) => {
1187                if !ip.unstable_is_global() {
1188                    return true
1189                }
1190            }
1191            url::Host::Domain(d) => {
1192                if LOCAL_HOST_STRS.contains(&d) {
1193                    return true
1194                }
1195            }
1196        }
1197        false
1198    }
1199
1200    /// Check whether a URL is IPV6
1201    pub fn is_ipv6(&self, url: &Url) -> bool {
1202        // Reject Urls without host strings.
1203        if url.host_str().is_none() {
1204            return false
1205        }
1206
1207        if let url::Host::Ipv6(_) = url.host().unwrap() {
1208            return true
1209        }
1210        false
1211    }
1212
1213    /// Import blacklisted peers specified in the config file.
1214    pub(in crate::net) async fn import_blacklist(&self) -> Result<()> {
1215        for (hostname, schemes, ports) in self.settings.read().await.blacklist.clone() {
1216            // If schemes are not set use default tcp+tls.
1217            let schemes = if schemes.is_empty() { vec!["tcp+tls".to_string()] } else { schemes };
1218
1219            // If ports are not set block all ports.
1220            let ports = if ports.is_empty() { vec![0] } else { ports };
1221
1222            for scheme in schemes {
1223                for &port in &ports {
1224                    let url_string = if port == 0 {
1225                        format!("{scheme}://{hostname}")
1226                    } else {
1227                        format!("{scheme}://{hostname}:{port}")
1228                    };
1229
1230                    if let Ok(url) = Url::parse(&url_string) {
1231                        self.container.store(HostColor::Black as usize, url, 0);
1232                    }
1233                }
1234            }
1235        }
1236        Ok(())
1237    }
1238
1239    /// To block a peer trying to access by all ports, simply store its
1240    /// hostname in the blacklist. This method will check if a host is
1241    /// stored in the blacklist without a port, and if so, it will return
1242    /// true.
1243    pub(in crate::net) fn block_all_ports(&self, url: &Url) -> bool {
1244        let host = url.host();
1245        if host.is_none() {
1246            // the url is a unix socket or an invalid address so it won't be in hostlist
1247            return false
1248        }
1249
1250        let host = host.unwrap();
1251        self.container.hostlists[HostColor::Black as usize]
1252            .read()
1253            .unwrap()
1254            .iter()
1255            .any(|(u, _t)| u.host().unwrap() == host && u.port().is_none())
1256    }
1257
1258    /// Filter given addresses based on certain rulesets and validity. Strictly called only on
1259    /// the first time learning of new peers.
1260    async fn filter_addresses(&self, addrs: &[(Url, u64)]) -> Vec<(Url, u64)> {
1261        debug!(target: "net::hosts::filter_addresses", "Filtering addrs: {addrs:?}");
1262        let mut ret = vec![];
1263
1264        // Acquire read lock on P2P settings. Dropped when this function finishes.
1265        let settings = self.settings.read().await;
1266
1267        'addr_loop: for (addr_, last_seen) in addrs {
1268            // Validate that the format is `scheme://host_str:port`
1269            if addr_.host_str().is_none() || addr_.port().is_none() || addr_.cannot_be_a_base() {
1270                debug!(
1271                    target: "net::hosts::filter_addresses",
1272                    "[{addr_}] has invalid addr format. Skipping"
1273                );
1274                continue
1275            }
1276
1277            // Configured seeds should never enter the hostlist.
1278            if settings.seeds.contains(addr_) {
1279                debug!(
1280                    target: "net::hosts::filter_addresses",
1281                    "[{addr_}] is a configured seed. Skipping"
1282                );
1283                continue
1284            }
1285
1286            // Configured peers should not enter the hostlist.
1287            if settings.peers.contains(addr_) {
1288                debug!(
1289                    target: "net::hosts::filter_addresses",
1290                    "[{addr_}] is a configured peer. Skipping"
1291                );
1292                continue
1293            }
1294
1295            // Blacklist peers should never enter the hostlist.
1296            if self.container.contains(HostColor::Black as usize, addr_) ||
1297                self.block_all_ports(addr_)
1298            {
1299                debug!(
1300                    target: "net::hosts::filter_addresses",
1301                    "[{addr_}] is blacklisted"
1302                );
1303                continue
1304            }
1305
1306            let host = addr_.host().unwrap();
1307            let host_str = addr_.host_str().unwrap();
1308
1309            if !settings.localnet {
1310                // Our own external addresses should never enter the hosts set.
1311                for ext in self.external_addrs().await {
1312                    if host == ext.host().unwrap() {
1313                        debug!(
1314                            target: "net::hosts::filter_addresses",
1315                            "[{addr_}] is our own external addr. Skipping"
1316                        );
1317                        continue 'addr_loop
1318                    }
1319                }
1320            } else {
1321                // On localnet, make sure ours ports don't enter the host set.
1322                for ext in &settings.external_addrs {
1323                    if addr_.port() == ext.port() {
1324                        debug!(
1325                            target: "net::hosts::filter_addresses",
1326                            "[{addr_}] is our own localnet port. Skipping"
1327                        );
1328                        continue 'addr_loop
1329                    }
1330                }
1331            }
1332
1333            // Filter non-global ranges if we're not allowing localnet.
1334            // Should never be allowed in production, so we don't really care
1335            // about some of them (e.g. 0.0.0.0, or broadcast, etc.).
1336            if !settings.localnet && self.is_local_host(addr_) {
1337                debug!(
1338                    target: "net::hosts::filter_addresses",
1339                    "[{addr_}] Filtering non-global ranges"
1340                );
1341                continue
1342            }
1343
1344            match addr_.scheme() {
1345                // Validate that the address is an actual onion.
1346                #[cfg(feature = "p2p-tor")]
1347                "tor" | "tor+tls" => {
1348                    use std::str::FromStr;
1349                    if tor_hscrypto::pk::HsId::from_str(host_str).is_err() {
1350                        continue
1351                    }
1352                    trace!(
1353                        target: "net::hosts::filter_addresses",
1354                        "[Tor] Valid: {host_str}"
1355                    );
1356                }
1357
1358                #[cfg(feature = "p2p-nym")]
1359                "nym" | "nym+tls" => continue, // <-- Temp skip
1360
1361                "tcp" | "tcp+tls" => {
1362                    trace!(
1363                        target: "net::hosts::filter_addresses",
1364                        "[TCP] Valid: {host_str}"
1365                    );
1366                }
1367
1368                #[cfg(feature = "p2p-i2p")]
1369                "i2p" | "i2p+tls" => {
1370                    if !Self::is_i2p_host(host_str) {
1371                        continue
1372                    }
1373                    trace!(
1374                        target: "net::hosts::filter_addresses",
1375                        "[I2p] Valid: {host_str}"
1376                    );
1377                }
1378
1379                _ => continue,
1380            }
1381
1382            // Store this peer on Dark list if we do not support this transport
1383            // or if this peer is IPV6 and we do not support IPV6.
1384            // We will personally ignore this peer but still send it to others in
1385            // Protocol Addr to ensure all transports get propagated.
1386            if !settings.active_profiles.contains(&addr_.scheme().to_string()) ||
1387                (!self.ipv6_available.load(Ordering::SeqCst) && self.is_ipv6(addr_))
1388            {
1389                self.container.store_or_update(HostColor::Dark, addr_.clone(), *last_seen);
1390                self.container.sort_by_last_seen(HostColor::Dark as usize);
1391                self.container.resize(HostColor::Dark);
1392
1393                // Delete darklist entries that are older than one day.
1394                let day = 86400;
1395                self.container.refresh(HostColor::Dark, day);
1396
1397                // If the scheme is not found in mixed_profiles we can not connect to this host
1398                if !settings.mixed_profiles.contains(&addr_.scheme().to_string()) {
1399                    continue;
1400                }
1401            }
1402
1403            // Reject this peer if it's already stored on the Gold, White or Grey list.
1404            //
1405            // We do this last since it is the most expensive operation.
1406            if self.container.contains(HostColor::Gold as usize, addr_) ||
1407                self.container.contains(HostColor::White as usize, addr_) ||
1408                self.container.contains(HostColor::Grey as usize, addr_)
1409            {
1410                debug!(target: "net::hosts::filter_addresses", "[{addr_}] exists! Skipping");
1411                continue
1412            }
1413
1414            ret.push((addr_.clone(), *last_seen));
1415        }
1416
1417        ret
1418    }
1419
1420    /// Method to fetch the last_seen field for a give address when we do
1421    /// not know what hostlist it is on.
1422    pub fn fetch_last_seen(&self, addr: &Url) -> Option<u64> {
1423        if self.container.contains(HostColor::Gold as usize, addr) {
1424            self.container.get_last_seen(HostColor::Gold as usize, addr)
1425        } else if self.container.contains(HostColor::White as usize, addr) {
1426            self.container.get_last_seen(HostColor::White as usize, addr)
1427        } else if self.container.contains(HostColor::Grey as usize, addr) {
1428            self.container.get_last_seen(HostColor::Grey as usize, addr)
1429        } else {
1430            None
1431        }
1432    }
1433
1434    /// Downgrade host to Greylist, remove from Gold or White list.
1435    pub async fn greylist_host(&self, addr: &Url, last_seen: u64) -> Result<()> {
1436        debug!(target: "net::hosts:greylist_host", "Downgrading addr={addr}");
1437        self.move_host(addr, last_seen, HostColor::Grey).await?;
1438
1439        // Free up this addr for future operations.
1440        self.unregister(addr)
1441    }
1442
1443    pub async fn whitelist_host(&self, addr: &Url, last_seen: u64) -> Result<()> {
1444        debug!(target: "net::hosts:whitelist_host", "Upgrading addr={addr}");
1445        self.move_host(addr, last_seen, HostColor::White).await?;
1446
1447        // Free up this addr for future operations.
1448        self.unregister(addr)
1449    }
1450
1451    /// A single function for moving hosts between hostlists. Called on the following occasions:
1452    ///
1453    /// * When we cannot connect to a peer: move to grey, remove from white and gold.
1454    /// * When a peer disconnects from us: move to grey, remove from white and gold.
1455    /// * When the refinery passes successfully: move to white, remove from greylist.
1456    /// * When we connect to a peer, move to gold, remove from white or grey.
1457    /// * When we add a peer to the black list: move to black, remove from all other lists.
1458    ///
1459    /// Note that this method puts a given Url into the "Move" state but does not reset the
1460    /// state afterwards. This is because the next state will differ depending on its usage.
1461    /// The state transition from `Move` to `Connected` or `Suspend` are both valid operations.
1462    /// In some cases, `unregister()` can be called after `move_host()` to explicitly mark
1463    /// the host state as `Free`.
1464    pub(in crate::net) async fn move_host(
1465        &self,
1466        addr: &Url,
1467        last_seen: u64,
1468        destination: HostColor,
1469    ) -> Result<()> {
1470        debug!(target: "net::hosts::move_host", "Trying to move addr={addr} destination={destination:?}");
1471
1472        // If we cannot register this address as move, this will simply return here.
1473        self.try_register(addr.clone(), HostState::Move)?;
1474
1475        debug!(target: "net::hosts::move_host", "Moving addr={} destination={destination:?}",
1476            addr.clone());
1477
1478        match destination {
1479            // Downgrade to grey. Remove from white and gold.
1480            HostColor::Grey => {
1481                self.container.remove_if_exists(HostColor::Gold, addr);
1482                self.container.remove_if_exists(HostColor::White, addr);
1483
1484                self.container.store_or_update(HostColor::Grey, addr.clone(), last_seen);
1485                self.container.sort_by_last_seen(HostColor::Grey as usize);
1486                self.container.resize(HostColor::Grey);
1487            }
1488
1489            // Remove from Greylist, add to Whitelist. Called by the Refinery.
1490            HostColor::White => {
1491                self.container.remove_if_exists(HostColor::Grey, addr);
1492
1493                self.container.store_or_update(HostColor::White, addr.clone(), last_seen);
1494                self.container.sort_by_last_seen(HostColor::White as usize);
1495                self.container.resize(HostColor::White);
1496            }
1497
1498            // Upgrade to gold. Remove from white or grey.
1499            HostColor::Gold => {
1500                self.container.remove_if_exists(HostColor::Grey, addr);
1501                self.container.remove_if_exists(HostColor::White, addr);
1502
1503                self.container.store_or_update(HostColor::Gold, addr.clone(), last_seen);
1504                self.container.sort_by_last_seen(HostColor::Gold as usize);
1505            }
1506
1507            // Move to black. Remove from all other lists.
1508            HostColor::Black => {
1509                // We ignore UNIX sockets here so we will just work
1510                // with stuff that has host_str().
1511                if addr.host_str().is_some() {
1512                    // Localhost connections should never enter the blacklist
1513                    // This however allows any Tor, Nym and I2p connections.
1514                    if !self.settings.read().await.localnet && self.is_local_host(addr) {
1515                        return Ok(());
1516                    }
1517
1518                    self.container.remove_if_exists(HostColor::Grey, addr);
1519                    self.container.remove_if_exists(HostColor::White, addr);
1520                    self.container.remove_if_exists(HostColor::Gold, addr);
1521
1522                    self.container.store_or_update(HostColor::Black, addr.clone(), last_seen);
1523                }
1524            }
1525
1526            HostColor::Dark => return Err(Error::InvalidHostColor),
1527        }
1528
1529        Ok(())
1530    }
1531
1532    /// Upon version exchange, the node reports our external network address to us.
1533    /// Accumulate them here in a ring buffer.
1534    pub(in crate::net) fn add_auto_addr(&self, addr: Ipv6Addr) {
1535        let mut auto_addrs = self.auto_self_addrs.lock().unwrap();
1536        auto_addrs.push(addr);
1537    }
1538
1539    /// Pick the most frequent occuring reported external address from other nodes as
1540    /// our auto ipv6 address.
1541    pub fn guess_auto_addr(&self) -> Option<Ipv6Addr> {
1542        let mut auto_addrs = self.auto_self_addrs.lock().unwrap();
1543        let items = auto_addrs.make_contiguous();
1544        most_frequent_or_any(items)
1545    }
1546
1547    /// The external_addrs is set by the user but we need the actual addresses.
1548    /// If the external_addr is set to `[::]` (unspecified), then replace it with the
1549    /// the best guess from `guess_auto_addr()`.
1550    /// Also if the port is 0, we lookup the port from the `InboundSession`.
1551    pub async fn external_addrs(&self) -> Vec<Url> {
1552        let mut external_addrs = self.settings.read().await.external_addrs.clone();
1553        for ext_addr in &mut external_addrs {
1554            // We must patch the port first since InboundSession hashmap used to lookup
1555            // the port number uses the inbound address.
1556            let _ = self.patch_port(ext_addr);
1557            let _ = self.patch_auto_addr(ext_addr);
1558        }
1559        external_addrs
1560    }
1561
1562    /// Make a best effort guess from the most frequently reported ipv6 auto address
1563    /// to set any unspecified ipv6 addrs: `external_addrs = ["tcp://[::]:1365"]`.
1564    fn patch_auto_addr(&self, ext_addr: &mut Url) -> Option<()> {
1565        if ext_addr.scheme() != "tcp" && ext_addr.scheme() != "tcp+tls" {
1566            return None
1567        }
1568
1569        let ext_host = ext_addr.host()?;
1570        // Is it an Ipv6 listener?
1571        let Host::Ipv6(ext_ip) = ext_host else { return None };
1572        // We are only interested if it's [::]
1573        if !ext_ip.is_unspecified() {
1574            return None
1575        }
1576
1577        // Get our auto-discovered IP
1578        let auto_addr = self.guess_auto_addr()?;
1579
1580        // Do the actual replacement of the host part of the URL
1581        ext_addr.set_ip_host(IpAddr::V6(auto_addr)).ok()?;
1582        Some(())
1583    }
1584
1585    /// If the port number specified is 0, then replace it with whatever the OS has assigned
1586    /// as a port for that inbound.
1587    fn patch_port(&self, ext_addr: &mut Url) -> Option<()> {
1588        // Only patch URLs with port set to 0.
1589        if ext_addr.port()? != 0 {
1590            return None
1591        }
1592
1593        // TODO:
1594        // InboundSession needs a HashMap: Url listen addr -> u16 port numbers.
1595        // Lookup the external_addr from InboundSession to get the port number
1596        //
1597        // ext_addr.set_port(my_new_port_number);
1598        //
1599
1600        None
1601    }
1602
1603    // Checks if we have successful connection with a host on any port
1604    pub fn has_existing_connection(&self, url: &Url) -> bool {
1605        let host = url.host().unwrap();
1606        let colors = [HostColor::Gold, HostColor::White];
1607        colors.iter().any(|color| {
1608            self.container.hostlists[color.clone() as usize]
1609                .read()
1610                .unwrap()
1611                .iter()
1612                .any(|(u, _t)| u.host().unwrap() == host)
1613        })
1614    }
1615
1616    #[cfg(feature = "p2p-i2p")]
1617    fn is_i2p_host(host: &str) -> bool {
1618        if !host.ends_with(".i2p") {
1619            return false
1620        }
1621
1622        // Two kinds of address
1623        // 1. wvbtv6i6njxdtxwsgsr3d4xejdtsy6n7s3d2paqgigjkv3fv5imq.b32.i2p
1624        // 2. node.darkfi.i2p
1625        let name = host.trim_end_matches(".i2p");
1626
1627        if name.ends_with(".b32") {
1628            let b32 = name.trim_end_matches(".b32");
1629            let decoded = crate::util::encoding::base32::decode(b32);
1630            // decoded should be a SHA256 hash
1631            return decoded.is_some() && decoded.unwrap().len() == 32
1632        }
1633
1634        name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
1635    }
1636}
1637
1638/// We need a convenience method from Rust's unstable feature "ip".
1639/// When <https://github.com/rust-lang/rust/issues/27709> is stablized we can remove this.
1640trait UnstableFeatureIp {
1641    fn unstable_is_global(&self) -> bool;
1642}
1643
1644impl UnstableFeatureIp for Ipv4Addr {
1645    // Copied from: https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L839
1646    #[inline]
1647    fn unstable_is_global(&self) -> bool {
1648        !(self.octets()[0] == 0 // "This network"
1649            || self.is_private()
1650            // is_shared https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L875
1651            || self.octets()[0] == 100 && (self.octets()[1] & 0b1100_0000 == 0b0100_0000)
1652            || self.is_loopback()
1653            || self.is_link_local()
1654            // addresses reserved for future protocols (`192.0.0.0/24`)
1655            // .9 and .10 are documented as globally reachable so they're excluded
1656            || (
1657                self.octets()[0] == 192 && self.octets()[1] == 0 && self.octets()[2] == 0
1658                && self.octets()[3] != 9 && self.octets()[3] != 10
1659            )
1660            || self.is_documentation()
1661            // is_benchmarking https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L902
1662            || self.octets()[0] == 198 && (self.octets()[1] & 0xfe) == 18
1663            // is_reserved https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L938
1664            || self.octets()[0] & 240 == 240 && !self.is_broadcast()
1665            || self.is_broadcast())
1666    }
1667}
1668
1669impl UnstableFeatureIp for Ipv6Addr {
1670    // Copied from: https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L1598
1671    #[inline]
1672    fn unstable_is_global(&self) -> bool {
1673        !(self.is_unspecified()
1674            || self.is_loopback()
1675            // IPv4-mapped Address (`::ffff:0:0/96`)
1676            || matches!(self.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
1677            // IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
1678            || matches!(self.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
1679            // Discard-Only Address Block (`100::/64`)
1680            || matches!(self.segments(), [0x100, 0, 0, 0, _, _, _, _])
1681            // IETF Protocol Assignments (`2001::/23`)
1682            || (matches!(self.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
1683                && !(
1684                    // Port Control Protocol Anycast (`2001:1::1`)
1685                    u128::from_be_bytes(self.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
1686                    // Traversal Using Relays around NAT Anycast (`2001:1::2`)
1687                    || u128::from_be_bytes(self.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
1688                    // AMT (`2001:3::/32`)
1689                    || matches!(self.segments(), [0x2001, 3, _, _, _, _, _, _])
1690                    // AS112-v6 (`2001:4:112::/48`)
1691                    || matches!(self.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
1692                    // ORCHIDv2 (`2001:20::/28`)
1693                    // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)`
1694                    || matches!(self.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x3F).contains(&b))
1695                ))
1696            // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable,
1697            // IANA says N/A.
1698            || matches!(self.segments(), [0x2002, _, _, _, _, _, _, _])
1699            // is_documentation https://github.com/rust-lang/rust/blob/ea99e81485ff5d82cabba9af5d1c21293737cc16/library/core/src/net/ip_addr.rs#L1754
1700            || matches!(self.segments(), [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..])
1701            // Segment Routing (SRv6) SIDs (`5f00::/16`)
1702            || matches!(self.segments(), [0x5f00, ..])
1703            || self.is_unique_local()
1704            || self.is_unicast_link_local())
1705    }
1706}
1707
1708#[cfg(test)]
1709mod tests {
1710    use super::*;
1711    use crate::system::sleep;
1712
1713    #[test]
1714    fn test_is_local_host() {
1715        let settings = Settings {
1716            localnet: false,
1717            external_addrs: vec![
1718                Url::parse("tcp://foo.bar:123").unwrap(),
1719                Url::parse("tcp://lol.cat:321").unwrap(),
1720            ],
1721            ..Default::default()
1722        };
1723        let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1724
1725        let local_hosts: Vec<Url> = vec![
1726            Url::parse("tcp://localhost").unwrap(),
1727            Url::parse("tcp://127.0.0.1").unwrap(),
1728            Url::parse("tcp+tls://[::1]").unwrap(),
1729            Url::parse("tcp://localhost.localdomain").unwrap(),
1730            Url::parse("tcp://192.168.10.65").unwrap(),
1731        ];
1732        for host in local_hosts {
1733            eprintln!("{host}");
1734            assert!(hosts.is_local_host(&host));
1735        }
1736        let remote_hosts: Vec<Url> = vec![
1737            Url::parse("https://dyne.org").unwrap(),
1738            Url::parse("tcp://77.168.10.65:2222").unwrap(),
1739            Url::parse("tcp://[2345:0425:2CA1:0000:0000:0567:5673:23b5]").unwrap(),
1740            Url::parse("http://eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion")
1741                .unwrap(),
1742        ];
1743        for host in remote_hosts {
1744            assert!(!hosts.is_local_host(&host))
1745        }
1746    }
1747
1748    #[test]
1749    fn test_is_ipv6() {
1750        let settings = Settings { ..Default::default() };
1751        let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1752
1753        let ipv6_hosts: Vec<Url> = vec![
1754            Url::parse("tcp+tls://[::1]").unwrap(),
1755            Url::parse("tcp://[2001:0000:130F:0000:0000:09C0:876A:130B]").unwrap(),
1756            Url::parse("tcp://[2345:0425:2CA1:0000:0000:0567:5673:23b5]").unwrap(),
1757        ];
1758
1759        let ipv4_hosts: Vec<Url> = vec![
1760            Url::parse("tcp://192.168.10.65").unwrap(),
1761            Url::parse("https://dyne.org").unwrap(),
1762            Url::parse("tcp+tls://agorism.xyz").unwrap(),
1763        ];
1764
1765        for host in ipv6_hosts {
1766            assert!(hosts.is_ipv6(&host))
1767        }
1768
1769        for host in ipv4_hosts {
1770            assert!(!hosts.is_ipv6(&host))
1771        }
1772    }
1773
1774    #[test]
1775    fn test_block_all_ports() {
1776        let settings = Settings { ..Default::default() };
1777        let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1778
1779        let blacklist1 = Url::parse("tcp+tls://nietzsche.king:333").unwrap();
1780        let blacklist2 = Url::parse("tcp+tls://agorism.xyz").unwrap();
1781
1782        hosts.container.store(HostColor::Black as usize, blacklist1.clone(), 0);
1783        hosts.container.store(HostColor::Black as usize, blacklist2.clone(), 0);
1784
1785        assert!(hosts.block_all_ports(&blacklist2));
1786        assert!(!hosts.block_all_ports(&blacklist1));
1787    }
1788
1789    #[test]
1790    fn test_store() {
1791        let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
1792
1793        let settings = Settings { ..Default::default() };
1794        let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1795
1796        let grey_hosts = vec![
1797            Url::parse("tcp://localhost:3921").unwrap(),
1798            Url::parse("tor://[::1]:21481").unwrap(),
1799            Url::parse("tcp://192.168.10.65:311").unwrap(),
1800            Url::parse("tcp+tls://0.0.0.0:2312").unwrap(),
1801            Url::parse("tcp://255.255.255.255:2131").unwrap(),
1802        ];
1803
1804        for addr in &grey_hosts {
1805            hosts.container.store(HostColor::Grey as usize, addr.clone(), last_seen);
1806        }
1807        assert!(!hosts.container.is_empty(HostColor::Grey));
1808
1809        let white_hosts = vec![
1810            Url::parse("tcp://localhost:3921").unwrap(),
1811            Url::parse("tor://[::1]:21481").unwrap(),
1812            Url::parse("tcp://192.168.10.65:311").unwrap(),
1813            Url::parse("tcp+tls://0.0.0.0:2312").unwrap(),
1814            Url::parse("tcp://255.255.255.255:2131").unwrap(),
1815        ];
1816
1817        for host in &white_hosts {
1818            hosts.container.store(HostColor::White as usize, host.clone(), last_seen);
1819        }
1820        assert!(!hosts.container.is_empty(HostColor::White));
1821
1822        let gold_hosts = vec![
1823            Url::parse("tcp://dark.fi:80").unwrap(),
1824            Url::parse("tcp://http.cat:401").unwrap(),
1825            Url::parse("tcp://foo.bar:111").unwrap(),
1826        ];
1827
1828        for host in &gold_hosts {
1829            hosts.container.store(HostColor::Gold as usize, host.clone(), last_seen);
1830        }
1831
1832        assert!(hosts.container.contains(HostColor::Grey as usize, &grey_hosts[0]));
1833        assert!(hosts.container.contains(HostColor::White as usize, &white_hosts[1]));
1834        assert!(hosts.container.contains(HostColor::Gold as usize, &gold_hosts[2]));
1835    }
1836
1837    #[test]
1838    fn test_refresh() {
1839        smol::block_on(async {
1840            let settings = Settings { ..Default::default() };
1841            let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1842            let old_timestamp = 1720000000;
1843
1844            // Insert 5 items into the darklist with an old timestamp.
1845            for i in 0..5 {
1846                let last_seen = old_timestamp + i;
1847                let url = Url::parse(&format!("tcp://old_darklist{i}:123")).unwrap();
1848                hosts.container.store(HostColor::Dark as usize, url.clone(), last_seen);
1849            }
1850
1851            // Insert another 5 items into the darklist with a recent timestamp.
1852            for i in 0..5 {
1853                let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
1854                let url = Url::parse(&format!("tcp://new_darklist{i}:123")).unwrap();
1855                hosts.container.store(HostColor::Dark as usize, url.clone(), last_seen);
1856            }
1857
1858            // Delete all items that are older than a day.
1859            let day = 86400;
1860            hosts.container.refresh(HostColor::Dark, day);
1861
1862            let darklist = hosts.container.hostlists[HostColor::Dark as usize].read().unwrap();
1863            assert!(darklist.len() == 5);
1864
1865            for (_, last_seen) in darklist.iter() {
1866                assert!(*last_seen > old_timestamp);
1867            }
1868
1869            drop(darklist);
1870            let now = UNIX_EPOCH.elapsed().unwrap().as_secs();
1871            let future_timestamp = now + 100;
1872
1873            // Insert another 5 items into the darklist with a timestamp from the future.
1874            for i in 0..5 {
1875                let last_seen = future_timestamp;
1876                let url = Url::parse(&format!("tcp://future_darklist{i}:123")).unwrap();
1877                hosts.container.store(HostColor::Dark as usize, url.clone(), last_seen);
1878            }
1879
1880            hosts.container.refresh(HostColor::Dark, day);
1881
1882            // Darklist length should be 5 + 5 (new entries + future entries)
1883            let darklist = hosts.container.hostlists[HostColor::Dark as usize].read().unwrap();
1884            assert!(darklist.len() == 10);
1885        });
1886    }
1887
1888    #[test]
1889    fn test_get_last() {
1890        smol::block_on(async {
1891            let settings = Settings { ..Default::default() };
1892            let hosts = Hosts::new(Arc::new(AsyncRwLock::new(settings)));
1893
1894            // Build up a hostlist
1895            for i in 0..10 {
1896                sleep(1).await;
1897                let last_seen = UNIX_EPOCH.elapsed().unwrap().as_secs();
1898                let url = Url::parse(&format!("tcp://whitelist{i}:123")).unwrap();
1899                hosts.container.store(HostColor::White as usize, url.clone(), last_seen);
1900            }
1901
1902            for (url, last_seen) in
1903                hosts.container.hostlists[HostColor::White as usize].read().unwrap().iter()
1904            {
1905                println!("{url} {last_seen}");
1906            }
1907
1908            let entry = hosts.container.fetch_last(HostColor::White).unwrap();
1909            println!("last entry: {} {}", entry.0, entry.1);
1910        });
1911    }
1912
1913    #[test]
1914    fn test_is_p2p_host() {
1915        assert!(Hosts::is_i2p_host("tm7bz5qfh73id33yjpshxmesrqedoz2ckghd3levktqywcrramwq.b32.i2p"));
1916        assert!(!Hosts::is_i2p_host("randomstring.b32.i2p"));
1917        assert!(Hosts::is_i2p_host("node.dark.fi.i2p"));
1918        assert!(!Hosts::is_i2p_host("node.dark.fi"));
1919    }
1920
1921    // Test tcp endpoint is changed to tor and tcp will not be used to
1922    // connect to any host directly
1923    #[test]
1924    fn test_transport_tor_mixed_with_tcp() {
1925        let mixed_hosts = HostContainer::mix_host(
1926            &Url::parse("tcp://dark.fi:28880").unwrap(),
1927            &["tor+tls".to_string(), "tcp".to_string(), "tor".to_string()],
1928            &["tcp".to_string()],
1929            &Url::parse("socks5://127.0.0.1:9050").ok(),
1930            &None,
1931        );
1932
1933        assert_eq!(mixed_hosts.len(), 1);
1934        assert_eq!(mixed_hosts[0].to_string(), "tor://dark.fi:28880/");
1935    }
1936
1937    // Test when both tor_socks5_proxy and nym_socks5_proxy are passed
1938    // tcp+tls endpoint is changed to socks5+tls and the endpoint is changed to two
1939    // endpoints where one is routed through tor and another through nym
1940    #[test]
1941    fn test_transport_socks5_mixed_with_tcp_through_tor_and_nym_proxy() {
1942        let tor_socks5_proxy_url = Url::parse("socks5://127.0.0.1:9050").ok();
1943        let nym_socks5_proxy_url = Url::parse("socks5://127.0.0.1:1080").ok();
1944
1945        let fetched_hosts = HostContainer::mix_host(
1946            &Url::parse("tcp+tls://dark.fi:28880").unwrap(),
1947            &["socks5".to_string(), "socks5+tls".to_string()],
1948            &["tcp+tls".to_string()],
1949            &tor_socks5_proxy_url,
1950            &nym_socks5_proxy_url,
1951        );
1952
1953        assert_eq!(fetched_hosts.len(), 2);
1954        assert!(
1955            fetched_hosts[0].scheme() == "socks5+tls" && fetched_hosts[1].scheme() == "socks5+tls"
1956        );
1957        assert_eq!(
1958            fetched_hosts
1959                .iter()
1960                .filter(|h| h.port() == tor_socks5_proxy_url.as_ref().unwrap().port())
1961                .count(),
1962            1
1963        );
1964        assert_eq!(
1965            fetched_hosts
1966                .iter()
1967                .filter(|h| h.port() == nym_socks5_proxy_url.as_ref().unwrap().port())
1968                .count(),
1969            1
1970        );
1971    }
1972
1973    // Test tor endpoint is changed to socks5 and tor will not be used to
1974    // connect to any host directly and tor endpoints are not routed through nym
1975    #[test]
1976    fn test_transport_socks5_mixed_with_tor() {
1977        let addr = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion:23330";
1978        let tor_socks5_proxy_url = Url::parse("socks5://127.0.0.1:9050").ok();
1979        let nym_socks5_proxy_url = Url::parse("socks5://127.0.0.1:1080").ok();
1980
1981        let fetched_hosts = HostContainer::mix_host(
1982            &Url::parse(&format!("tor://{addr}")).unwrap(),
1983            &["socks5".to_string(), "socks5+tls".to_string(), "tor".to_string()],
1984            &["tor".to_string()],
1985            &tor_socks5_proxy_url,
1986            &nym_socks5_proxy_url,
1987        );
1988
1989        assert_eq!(fetched_hosts.len(), 1);
1990        let mixed_url = fetched_hosts[0].clone();
1991        assert_eq!(mixed_url.scheme(), tor_socks5_proxy_url.as_ref().unwrap().scheme());
1992        assert_eq!(mixed_url.host(), tor_socks5_proxy_url.as_ref().unwrap().host());
1993        assert_eq!(mixed_url.port(), tor_socks5_proxy_url.as_ref().unwrap().port());
1994        assert_eq!(mixed_url.path_segments().unwrap().next(), Some(addr));
1995    }
1996
1997    // Test the tcp endpoint is changed to two endpoints socks5 and tor.
1998    #[test]
1999    fn test_transport_tor_and_socks5_mixed_with_tcp() {
2000        let fetched_hosts = HostContainer::mix_host(
2001            &Url::parse("tcp://dark.fi:28880").unwrap(),
2002            &[
2003                "tor".to_string(),
2004                "tor+tls".to_string(),
2005                "socks5".to_string(),
2006                "socks5+tls".to_string(),
2007            ],
2008            &["tcp".to_string()],
2009            &Url::parse("socks5://127.0.0.1:9050").ok(),
2010            &None,
2011        );
2012
2013        assert_eq!(fetched_hosts.len(), 2);
2014        let endpoints: Vec<_> = fetched_hosts.iter().map(|item| item.scheme()).collect();
2015        assert!(endpoints.iter().all(|&scheme| scheme == "tor" || scheme == "socks5"));
2016    }
2017}