freenet_test_network/
remote.rs

1//! Remote peer support via SSH
2
3use crate::{Error, Result};
4use ssh2::Session;
5use std::io::Read;
6use std::net::TcpStream;
7use std::path::PathBuf;
8
9/// Configuration for a remote Linux machine accessible via SSH
10#[derive(Debug, Clone)]
11pub struct RemoteMachine {
12    /// SSH hostname or IP address
13    pub host: String,
14
15    /// SSH username (defaults to current user if None)
16    pub user: Option<String>,
17
18    /// SSH port (defaults to 22 if None)
19    pub port: Option<u16>,
20
21    /// Path to SSH identity file (private key)
22    pub identity_file: Option<PathBuf>,
23
24    /// Path to freenet binary on remote machine (if pre-installed)
25    /// If None, binary will be deployed via SCP
26    pub freenet_binary: Option<PathBuf>,
27
28    /// Working directory for peer data on remote machine
29    /// Defaults to /tmp/freenet-test-network if None
30    pub work_dir: Option<PathBuf>,
31}
32
33impl RemoteMachine {
34    /// Create a new remote machine configuration
35    pub fn new(host: impl Into<String>) -> Self {
36        Self {
37            host: host.into(),
38            user: None,
39            port: None,
40            identity_file: None,
41            freenet_binary: None,
42            work_dir: None,
43        }
44    }
45
46    /// Set the SSH username
47    pub fn user(mut self, user: impl Into<String>) -> Self {
48        self.user = Some(user.into());
49        self
50    }
51
52    /// Set the SSH port
53    pub fn port(mut self, port: u16) -> Self {
54        self.port = Some(port);
55        self
56    }
57
58    /// Set the SSH identity file path
59    pub fn identity_file(mut self, path: impl Into<PathBuf>) -> Self {
60        self.identity_file = Some(path.into());
61        self
62    }
63
64    /// Set the remote freenet binary path
65    pub fn freenet_binary(mut self, path: impl Into<PathBuf>) -> Self {
66        self.freenet_binary = Some(path.into());
67        self
68    }
69
70    /// Set the remote working directory
71    pub fn work_dir(mut self, path: impl Into<PathBuf>) -> Self {
72        self.work_dir = Some(path.into());
73        self
74    }
75
76    /// Get the SSH port (22 if not specified)
77    pub fn ssh_port(&self) -> u16 {
78        self.port.unwrap_or(22)
79    }
80
81    /// Get the SSH username (current user if not specified)
82    pub fn ssh_user(&self) -> String {
83        self.user
84            .clone()
85            .unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "root".to_string()))
86    }
87
88    /// Get the remote work directory
89    pub fn remote_work_dir(&self) -> PathBuf {
90        self.work_dir
91            .clone()
92            .unwrap_or_else(|| PathBuf::from("/tmp/freenet-test-network"))
93    }
94
95    /// Establish an SSH connection to this machine
96    pub fn connect(&self) -> Result<Session> {
97        let addr = format!("{}:{}", self.host, self.ssh_port());
98        let tcp = TcpStream::connect(&addr).map_err(|e| {
99            Error::PeerStartupFailed(format!("Failed to connect to {}: {}", addr, e))
100        })?;
101
102        let mut session = Session::new().map_err(|e| {
103            Error::PeerStartupFailed(format!("Failed to create SSH session: {}", e))
104        })?;
105
106        session.set_tcp_stream(tcp);
107        session
108            .handshake()
109            .map_err(|e| Error::PeerStartupFailed(format!("SSH handshake failed: {}", e)))?;
110
111        // Authenticate
112        let username = self.ssh_user();
113        if let Some(identity) = &self.identity_file {
114            session
115                .userauth_pubkey_file(&username, None, identity, None)
116                .map_err(|e| {
117                    Error::PeerStartupFailed(format!("SSH key authentication failed: {}", e))
118                })?;
119        } else {
120            // Try agent authentication
121            session.userauth_agent(&username).map_err(|e| {
122                Error::PeerStartupFailed(format!("SSH agent authentication failed: {}", e))
123            })?;
124        }
125
126        if !session.authenticated() {
127            return Err(Error::PeerStartupFailed(
128                "SSH authentication failed".to_string(),
129            ));
130        }
131
132        Ok(session)
133    }
134
135    /// Execute a command on the remote machine and return output
136    pub fn exec(&self, command: &str) -> Result<String> {
137        let session = self.connect()?;
138        let mut channel = session
139            .channel_session()
140            .map_err(|e| Error::PeerStartupFailed(format!("Failed to open SSH channel: {}", e)))?;
141
142        channel
143            .exec(command)
144            .map_err(|e| Error::PeerStartupFailed(format!("Failed to execute command: {}", e)))?;
145
146        let mut output = String::new();
147        channel.read_to_string(&mut output).map_err(|e| {
148            Error::PeerStartupFailed(format!("Failed to read command output: {}", e))
149        })?;
150
151        channel.wait_close().ok();
152        let exit_status = channel
153            .exit_status()
154            .map_err(|e| Error::PeerStartupFailed(format!("Failed to get exit status: {}", e)))?;
155
156        if exit_status != 0 {
157            return Err(Error::PeerStartupFailed(format!(
158                "Command failed with exit code {}: {}",
159                exit_status, output
160            )));
161        }
162
163        Ok(output.trim().to_string())
164    }
165
166    /// Copy a local file to the remote machine via SCP
167    pub fn scp_upload(&self, local_path: &std::path::Path, remote_path: &str) -> Result<()> {
168        let session = self.connect()?;
169
170        let local_file = std::fs::File::open(local_path)
171            .map_err(|e| Error::PeerStartupFailed(format!("Failed to open local file: {}", e)))?;
172
173        let metadata = local_file
174            .metadata()
175            .map_err(|e| Error::PeerStartupFailed(format!("Failed to get file metadata: {}", e)))?;
176
177        let mut remote_file = session
178            .scp_send(
179                std::path::Path::new(remote_path),
180                0o755, // executable permissions
181                metadata.len(),
182                None,
183            )
184            .map_err(|e| {
185                Error::PeerStartupFailed(format!("Failed to initiate SCP upload: {}", e))
186            })?;
187
188        std::io::copy(
189            &mut std::fs::File::open(local_path).unwrap(),
190            &mut remote_file,
191        )
192        .map_err(|e| Error::PeerStartupFailed(format!("Failed to upload file: {}", e)))?;
193
194        remote_file.send_eof().ok();
195        remote_file.wait_eof().ok();
196        remote_file.close().ok();
197        remote_file.wait_close().ok();
198
199        Ok(())
200    }
201
202    /// Copy a remote file to the local machine via SCP
203    pub fn scp_download(&self, remote_path: &str, local_path: &std::path::Path) -> Result<()> {
204        let session = self.connect()?;
205
206        let (mut remote_file, _stat) = session
207            .scp_recv(std::path::Path::new(remote_path))
208            .map_err(|e| {
209                Error::PeerStartupFailed(format!("Failed to initiate SCP download: {}", e))
210            })?;
211
212        let mut local_file = std::fs::File::create(local_path)
213            .map_err(|e| Error::PeerStartupFailed(format!("Failed to create local file: {}", e)))?;
214
215        std::io::copy(&mut remote_file, &mut local_file)
216            .map_err(|e| Error::PeerStartupFailed(format!("Failed to download file: {}", e)))?;
217
218        remote_file.send_eof().ok();
219        remote_file.wait_eof().ok();
220        remote_file.close().ok();
221        remote_file.wait_close().ok();
222
223        Ok(())
224    }
225
226    /// Discover the public IP address of the remote machine
227    pub fn discover_public_address(&self) -> Result<String> {
228        // Try multiple methods to discover the public IP
229
230        // Method 1: Use ip route to find the default interface's IP
231        if let Ok(addr) = self.exec("ip route get 8.8.8.8 | awk '{print $7; exit}'") {
232            if !addr.is_empty() && addr != "127.0.0.1" {
233                return Ok(addr);
234            }
235        }
236
237        // Method 2: Use hostname -I to get all IPs and filter
238        if let Ok(output) = self.exec("hostname -I") {
239            for addr in output.split_whitespace() {
240                if addr.starts_with("192.168.")
241                    || addr.starts_with("10.")
242                    || addr.starts_with("172.")
243                {
244                    return Ok(addr.to_string());
245                }
246            }
247        }
248
249        // Fallback: use the SSH host address
250        Ok(self.host.clone())
251    }
252}
253
254/// Specifies where a peer should be spawned
255#[derive(Debug, Clone)]
256pub enum PeerLocation {
257    /// Spawn the peer on the local machine
258    Local,
259
260    /// Spawn the peer on a remote machine via SSH
261    Remote(RemoteMachine),
262}
263
264impl Default for PeerLocation {
265    fn default() -> Self {
266        PeerLocation::Local
267    }
268}