herolib-mos 0.3.13

Mycelium Operating System (MOS) - Network and VM abstraction layer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
use super::error::{NetworkError, Result};
use std::process::Command;

/// Enable IP forwarding in the kernel
pub fn enable_ip_forwarding() -> Result<()> {
    // Enable IPv4 forwarding
    let output = Command::new("sysctl")
        .args(["-w", "net.ipv4.ip_forward=1"])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to enable IPv4 forwarding: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to enable IPv4 forwarding: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Enable IPv6 forwarding
    let output = Command::new("sysctl")
        .args(["-w", "net.ipv6.conf.all.forwarding=1"])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to enable IPv6 forwarding: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to enable IPv6 forwarding: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}


/// Add iptables FORWARD rule to allow traffic within a bridge
/// This is needed when FORWARD policy is DROP (e.g., Docker)
pub fn add_bridge_forward_rule(bridge: &str) -> Result<()> {
    let check = Command::new("iptables")
        .args(["-C", "FORWARD", "-i", bridge, "-o", bridge, "-j", "ACCEPT"])
        .output();
    
    if check.is_err() || !check.unwrap().status.success() {
        let output = Command::new("iptables")
            .args(["-I", "FORWARD", "-i", bridge, "-o", bridge, "-j", "ACCEPT"])
            .output()
            .map_err(|e| NetworkError::Route(format!("Failed to add bridge forward rule: {}", e)))?;

        if !output.status.success() {
            log::warn!(
                "Failed to add bridge-internal forward rule for {}: {}",
                bridge,
                String::from_utf8_lossy(&output.stderr)
            );
        }
    }
    Ok(())
}

/// Add iptables masquerading rule for outbound traffic from bridge
pub fn add_masquerade_rule(bridge: &str, outbound_interface: &str) -> Result<()> {
    add_bridge_forward_rule(bridge)?;

    // add POSTROUTING rule change source address to outbound interface
    let output = Command::new("iptables")
        .args([
            "-t", "nat",
            "-A", "POSTROUTING",
            "-o", outbound_interface,
            "-s", &format!("{}.0/24", get_bridge_network(bridge)?),
            "-j", "MASQUERADE"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to add masquerade rule: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to add masquerade rule: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Add FORWARD rule to allow traffic from bridge to outbound
    let output = Command::new("iptables")
        .args([
            "-A", "FORWARD",
            "-i", bridge,
            "-o", outbound_interface,
            "-j", "ACCEPT"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to add forward rule: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to add forward rule: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Add FORWARD rule for established/related traffic
    let output = Command::new("iptables")
        .args([
            "-A", "FORWARD",
            "-i", outbound_interface,
            "-o", bridge,
            "-m", "state", "--state", "ESTABLISHED,RELATED",
            "-j", "ACCEPT"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to add forward rule: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to add forward rule: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

/// Delete iptables masquerading rule
pub fn delete_masquerade_rule(bridge: &str, outbound_interface: &str) -> Result<()> {
    // Try to get bridge network, but don't fail if it doesn't have IPv4
    let bridge_network = match get_bridge_network(bridge) {
        Ok(network) => network,
        Err(_) => {
            // Bridge likely doesn't have IPv4 (e.g., Mycelium bridge)
            // Just try to delete any existing rules without specifying source
            return Ok(());
        }
    };
    
    // Delete POSTROUTING rule
    let output = Command::new("iptables")
        .args([
            "-t", "nat",
            "-D", "POSTROUTING",
            "-o", outbound_interface,
            "-s", &format!("{}.0/24", bridge_network),
            "-j", "MASQUERADE"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to delete masquerade rule: {}", e)))?;

    let stderr = String::from_utf8_lossy(&output.stderr);
    if !output.status.success() && !stderr.contains("No chain/target/match") {
        return Err(NetworkError::Route(format!(
            "Failed to delete masquerade rule: {}",
            stderr
        )));
    }

    // Delete FORWARD rules
    let _ = Command::new("iptables")
        .args(["-D", "FORWARD", "-i", bridge, "-j", "ACCEPT"])
        .output();

    let _ = Command::new("iptables")
        .args([
            "-D", "FORWARD",
            "-i", outbound_interface,
            "-o", bridge,
            "-m", "state", "--state", "ESTABLISHED,RELATED",
            "-j", "ACCEPT"
        ])
        .output();

    Ok(())
}

fn get_bridge_network(bridge: &str) -> Result<String> {
    // Get the IP address of the bridge
    let output = Command::new("ip")
        .args(["addr", "show", bridge])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to get bridge IP: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to get bridge IP: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    let output_str = String::from_utf8_lossy(&output.stdout);
    
    // Parse the IP address from the output
    // Look for "inet <ip>/<mask>" line
    let ip_network = output_str
        .lines()
        .find(|line| line.trim().starts_with("inet "))
        .and_then(|line| {
            line.split_whitespace()
                .nth(1) // Get the second element after "inet"
        })
        .and_then(|addr| {
            // Split on / to get just the network part
            addr.split('/').next()
        });

    match ip_network {
        Some(ip) => {
            // Convert IP to network address by zeroing the host part
            // For IPv4: if IP is 10.200.0.1, return 10.200.0
            let parts: Vec<&str> = ip.split('.').collect();
            if parts.len() == 4 {
                Ok(format!("{}.{}.{}", parts[0], parts[1], parts[2]))
            } else {
                Err(NetworkError::Route(format!(
                    "Invalid IP address format for bridge {}: {}", 
                    bridge, ip
                )))
            }
        }
        None => Err(NetworkError::Route(format!(
            "No IP address found for bridge {}", bridge
        ))),
    }
}

/// Add iptables PREROUTING rule for port forwarding
pub fn add_port_forward_rule(
    external_port: u16,
    dest_ip: &str,
    dest_port: u16,
) -> Result<()> {
    // Add PREROUTING rule to redirect port
    let output = Command::new("iptables")
        .args([
            "-t", "nat",
            "-A", "PREROUTING",
            "-p", "tcp",
            "--dport", &external_port.to_string(),
            "-j", "DNAT",
            "--to-destination", &format!("{}:{}", dest_ip, dest_port)
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to add port forward rule: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to add port forward rule: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Add FORWARD rule to allow forwarded traffic
    let output = Command::new("iptables")
        .args([
            "-A", "FORWARD",
            "-p", "tcp",
            "-d", dest_ip,
            "--dport", &dest_port.to_string(),
            "-j", "ACCEPT"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to add forward rule for port: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to add forward rule for port: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

/// Delete iptables PREROUTING rule for port forwarding
pub fn delete_port_forward_rule(
    external_port: u16,
    dest_ip: &str,
    dest_port: u16,
) -> Result<()> {
    // Delete PREROUTING rule
    let output = Command::new("iptables")
        .args([
            "-t", "nat",
            "-D", "PREROUTING",
            "-p", "tcp",
            "--dport", &external_port.to_string(),
            "-j", "DNAT",
            "--to-destination", &format!("{}:{}", dest_ip, dest_port)
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to delete port forward rule: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to delete port forward rule: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Delete FORWARD rule
    let output = Command::new("iptables")
        .args([
            "-D", "FORWARD",
            "-p", "tcp",
            "-d", dest_ip,
            "--dport", &dest_port.to_string(),
            "-j", "ACCEPT"
        ])
        .output()
        .map_err(|e| NetworkError::Route(format!("Failed to delete forward rule for port: {}", e)))?;

    if !output.status.success() {
        return Err(NetworkError::Route(format!(
            "Failed to delete forward rule for port: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_enable_ip_forwarding() {
        // This test requires root privileges to actually enable forwarding
        // To run with root: sudo cargo test test_enable_ip_forwarding -- --ignored

        let result = enable_ip_forwarding();
        assert!(result.is_ok(), "IP forwarding should be enabled");
    }
    
    // Integration tests - require root privileges
    #[cfg(target_os = "linux")]
    #[test]
    #[ignore]
    fn test_add_masquerade_rule() {
        // Test adding masquerade rule
        // This test requires root privileges
        // To run: sudo cargo test test_add_masquerade_rule -- --ignored
        
        // Create bridge first
        use crate::network::bridge;
        let _bridge = bridge::create_bridge("test-br").unwrap();
        
        // Set bridge IP
        let _ = std::process::Command::new("ip")
            .args(["addr", "add", "10.200.0.1/24", "dev", "test-br"])
            .output();
        
        let result = add_masquerade_rule("test-br", "eth0");
        assert!(result.is_ok(), "Masquerade rule addition should succeed");
        
        // Clean up
        let _ = delete_masquerade_rule("test-br", "eth0");
        let _ = bridge::delete_bridge("test-br");
    }
    
    #[cfg(target_os = "linux")]
    #[test]
    #[ignore]
    fn test_delete_masquerade_rule() {
        // Test deleting masquerade rule
        // This test requires root privileges
        // To run: sudo cargo test test_delete_masquerade_rule -- --ignored
        
        // Create bridge first
        use crate::network::bridge;
        let bridge_name = "test-br-del"; // Unique name
        let _bridge = bridge::create_bridge(bridge_name).unwrap();
        
        // Set bridge IP
        let _ = std::process::Command::new("ip")
            .args(["addr", "add", "10.200.0.1/24", "dev", bridge_name])
            .output();
        
        // Add first
        let _ = add_masquerade_rule(bridge_name, "eth0");
        
        // Then delete
        let result = delete_masquerade_rule(bridge_name, "eth0");
        assert!(result.is_ok(), "Masquerade rule deletion should succeed");
        
        // Clean up bridge
        let _ = bridge::delete_bridge(bridge_name);
    }
    
    #[cfg(target_os = "linux")]
    #[test]
    #[ignore]
    fn test_add_port_forward_rule() {
        // Test adding port forward rule
        // This test requires root privileges
        // To run: sudo cargo test test_add_port_forward_rule -- --ignored
        
        let result = add_port_forward_rule(8080, "10.200.0.10", 80);
        assert!(result.is_ok(), "Port forward rule addition should succeed");
        
        // Clean up
        let _ = delete_port_forward_rule(8080, "10.200.0.10", 80);
    }
    
    #[cfg(target_os = "linux")]
    #[test]
    #[ignore]
    fn test_delete_port_forward_rule() {
        // Test deleting port forward rule
        // This test requires root privileges
        // To run: sudo cargo test test_delete_port_forward_rule -- --ignored
        
        // Add first
        let _ = add_port_forward_rule(8080, "10.200.0.10", 80);
        
        // Then delete
        let result = delete_port_forward_rule(8080, "10.200.0.10", 80);
        assert!(result.is_ok(), "Port forward rule deletion should succeed");
    }
}