ant_node_manager/cmd/
nat_detection.rs

1// Copyright (C) 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::{
10    config::get_node_registry_path, helpers::download_and_extract_release, VerbosityLevel,
11};
12use ant_bootstrap::ContactsFetcher;
13use ant_releases::{AntReleaseRepoActions, ReleaseType};
14use ant_service_management::{NatDetectionStatus, NodeRegistryManager};
15use color_eyre::eyre::{bail, Context, Result};
16use libp2p::Multiaddr;
17use rand::seq::SliceRandom;
18use std::{
19    io::{BufRead, BufReader},
20    path::PathBuf,
21    process::{Command, Stdio},
22    time::Duration,
23};
24use tokio::{task, time::timeout};
25pub const NAT_DETECTION_TIMEOUT_SECS: u64 = 180;
26
27const NAT_DETECTION_SERVERS_LIST_URL: &str =
28    "https://sn-testnet.s3.eu-west-2.amazonaws.com/nat-detection-servers";
29pub async fn run_nat_detection(
30    servers: Option<Vec<Multiaddr>>,
31    force_run: bool,
32    path: Option<PathBuf>,
33    url: Option<String>,
34    version: Option<String>,
35    verbosity: VerbosityLevel,
36) -> Result<()> {
37    let servers = match servers {
38        Some(servers) => servers,
39        None => {
40            let mut contacts_fetcher = ContactsFetcher::new()?;
41            contacts_fetcher.ignore_peer_id(true);
42            contacts_fetcher.insert_endpoint(NAT_DETECTION_SERVERS_LIST_URL.parse()?);
43
44            let fetched = contacts_fetcher.fetch_addrs().await?;
45            fetched
46                .choose_multiple(&mut rand::thread_rng(), 10)
47                .cloned()
48                .collect::<Vec<_>>()
49        }
50    };
51    info!("Running nat detection with servers: {servers:?}");
52
53    let node_registry = NodeRegistryManager::load(&get_node_registry_path()?).await?;
54
55    if !force_run {
56        if let Some(status) = node_registry.nat_status.read().await.as_ref() {
57            if verbosity != VerbosityLevel::Minimal {
58                println!("NAT status has already been set as: {status:?}");
59            }
60            debug!("NAT status already set as: {status:?}, skipping.");
61            return Ok(());
62        }
63    }
64
65    let nat_detection_path = if let Some(path) = path {
66        path
67    } else {
68        let release_repo = <dyn AntReleaseRepoActions>::default_config();
69        let (path, _) = download_and_extract_release(
70            ReleaseType::NatDetection,
71            url,
72            version,
73            &*release_repo,
74            verbosity,
75            None,
76        )
77        .await?;
78        path
79    };
80
81    if verbosity != VerbosityLevel::Minimal {
82        println!("Running NAT detection. This can take a while..");
83    }
84    debug!(
85        "Running NAT detection with binary path: {:?}",
86        nat_detection_path
87    );
88
89    let servers_arg = servers
90        .iter()
91        .map(|a| a.to_string())
92        .collect::<Vec<_>>()
93        .join(",");
94    let trace_enabled = tracing::level_enabled!(tracing::Level::TRACE);
95    let timeout_duration = Duration::from_secs(NAT_DETECTION_TIMEOUT_SECS);
96    debug!(
97        "NAT detection timeout set to {} seconds",
98        NAT_DETECTION_TIMEOUT_SECS
99    );
100
101    let output = timeout(
102        timeout_duration,
103        task::spawn_blocking(move || -> Result<i32> {
104            let mut command = Command::new(&nat_detection_path);
105            command.stdout(Stdio::piped()).stderr(Stdio::null());
106            command.arg(servers_arg);
107            if trace_enabled {
108                command.arg("-vvvv");
109            }
110
111            let mut child = command
112                .spawn()
113                .wrap_err("Failed to spawn NAT detection process")?;
114
115            if trace_enabled {
116                if let Some(ref mut stdout) = child.stdout {
117                    let reader = BufReader::new(stdout);
118                    for line in reader.lines() {
119                        let line = line?;
120                        let clean_line = strip_ansi_escapes(&line);
121                        trace!("{clean_line}");
122                    }
123                }
124            }
125
126            let status = child
127                .wait()
128                .wrap_err("Failed to wait on NAT detection process")?;
129            Ok(status.code().unwrap_or(-1))
130        }),
131    )
132    .await;
133
134    let exit_code = match output {
135        Ok(Ok(code)) => code,
136        Ok(Err(e)) => bail!("Failed to detect NAT status, exit code: {:?}", e),
137        Err(_) => {
138            debug!(
139                "NAT detection timed out after {} seconds",
140                NAT_DETECTION_TIMEOUT_SECS
141            );
142            bail!(
143                "NAT detection timed out after {} seconds",
144                NAT_DETECTION_TIMEOUT_SECS
145            );
146        }
147    };
148
149    let status = match exit_code {
150        Ok(10) => NatDetectionStatus::Public,
151        Ok(11) => NatDetectionStatus::UPnP,
152        Ok(12) => NatDetectionStatus::Private,
153        code => bail!("Failed to detect NAT status, exit code: {:?}", code),
154    };
155
156    if verbosity != VerbosityLevel::Minimal {
157        println!("NAT status has been found to be: {status:?}");
158    }
159
160    *node_registry.nat_status.write().await = Some(status);
161    node_registry.save().await?;
162
163    Ok(())
164}
165
166fn strip_ansi_escapes(input: &str) -> String {
167    let mut output = String::new();
168    let mut chars = input.chars();
169    while let Some(c) = chars.next() {
170        if c == '\x1b' {
171            for next_char in chars.by_ref() {
172                if next_char.is_ascii_lowercase() || next_char.is_ascii_uppercase() {
173                    break;
174                }
175            }
176        } else {
177            output.push(c);
178        }
179    }
180    output
181}