Skip to main content

fips_core/gateway/
net.rs

1//! Network setup for the gateway.
2//!
3//! Manages proxy NDP entries and routes for the virtual IP range.
4//! Checks IP forwarding prerequisites.
5
6use std::net::Ipv6Addr;
7use tracing::{debug, error, info, warn};
8
9/// Check if IPv6 forwarding is enabled.
10///
11/// The gateway is completely non-functional without forwarding — packets
12/// cannot traverse the NAT pipeline. Exits the process on failure.
13pub fn check_ipv6_forwarding() {
14    match std::fs::read_to_string("/proc/sys/net/ipv6/conf/all/forwarding") {
15        Ok(val) if val.trim() == "1" => {
16            debug!("IPv6 forwarding is enabled");
17        }
18        Ok(_) => {
19            error!(
20                "IPv6 forwarding is disabled. Enable with: \
21                 sysctl -w net.ipv6.conf.all.forwarding=1"
22            );
23            std::process::exit(1);
24        }
25        Err(e) => {
26            error!(error = %e, "Could not check IPv6 forwarding state");
27            std::process::exit(1);
28        }
29    }
30}
31
32/// Check that a network interface exists using rtnetlink.
33pub async fn check_interface_exists(name: &str) -> Result<u32, std::io::Error> {
34    let index = rustables::iface_index(name)
35        .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()))?;
36    debug!(interface = %name, index, "Interface found");
37    Ok(index)
38}
39
40/// Manages proxy NDP entries and routes for gateway virtual IPs.
41pub struct NetSetup {
42    lan_interface: String,
43    /// Proxy NDP entries added during this run (for cleanup).
44    proxy_entries: Vec<Ipv6Addr>,
45    /// Whether a route was added for the pool range.
46    route_added: bool,
47    pool_cidr: String,
48}
49
50impl NetSetup {
51    /// Create a new network setup manager.
52    pub fn new(lan_interface: String, pool_cidr: String) -> Self {
53        Self {
54            lan_interface,
55            proxy_entries: Vec::new(),
56            route_added: false,
57            pool_cidr,
58        }
59    }
60
61    /// Add a local route for the virtual IP pool range.
62    ///
63    /// The `local` route tells the kernel to accept packets destined for
64    /// addresses in the pool as locally-owned, enabling NAT processing.
65    /// Uses `dev lo` because local routes don't need to reference the LAN
66    /// interface — the kernel matches on the routing table regardless of
67    /// which interface the packet arrives on.
68    pub async fn add_pool_route(&mut self) -> Result<(), std::io::Error> {
69        let output = tokio::process::Command::new("ip")
70            .args(["-6", "route", "add", "local", &self.pool_cidr, "dev", "lo"])
71            .output()
72            .await?;
73
74        if !output.status.success() {
75            let stderr = String::from_utf8_lossy(&output.stderr);
76            // "File exists" means route already present — not an error
77            if stderr.contains("File exists") {
78                debug!(cidr = %self.pool_cidr, "Pool route already exists");
79                return Ok(());
80            }
81            return Err(std::io::Error::other(format!(
82                "Failed to add pool route: {stderr}"
83            )));
84        }
85
86        self.route_added = true;
87        info!(cidr = %self.pool_cidr, "Added local pool route");
88        Ok(())
89    }
90
91    /// Add a proxy NDP entry for a virtual IP on the LAN interface.
92    pub async fn add_proxy_ndp(&mut self, addr: Ipv6Addr) -> Result<(), std::io::Error> {
93        let addr_str = addr.to_string();
94        let output = tokio::process::Command::new("ip")
95            .args([
96                "-6",
97                "neigh",
98                "add",
99                "proxy",
100                &addr_str,
101                "dev",
102                &self.lan_interface,
103            ])
104            .output()
105            .await?;
106
107        if !output.status.success() {
108            let stderr = String::from_utf8_lossy(&output.stderr);
109            if stderr.contains("File exists") {
110                debug!(addr = %addr, "Proxy NDP entry already exists");
111                return Ok(());
112            }
113            return Err(std::io::Error::other(format!(
114                "Failed to add proxy NDP: {stderr}"
115            )));
116        }
117
118        self.proxy_entries.push(addr);
119        debug!(addr = %addr, iface = %self.lan_interface, "Added proxy NDP entry");
120        Ok(())
121    }
122
123    /// Remove a proxy NDP entry.
124    pub async fn remove_proxy_ndp(&mut self, addr: Ipv6Addr) -> Result<(), std::io::Error> {
125        let addr_str = addr.to_string();
126        let output = tokio::process::Command::new("ip")
127            .args([
128                "-6",
129                "neigh",
130                "del",
131                "proxy",
132                &addr_str,
133                "dev",
134                &self.lan_interface,
135            ])
136            .output()
137            .await?;
138
139        if !output.status.success() {
140            let stderr = String::from_utf8_lossy(&output.stderr);
141            // Silently ignore "No such file" — entry may have been cleaned already
142            if !stderr.contains("No such file") {
143                warn!(addr = %addr, error = %stderr.trim(), "Failed to remove proxy NDP");
144            }
145        }
146
147        self.proxy_entries.retain(|a| *a != addr);
148        Ok(())
149    }
150
151    /// Clean up all proxy NDP entries and routes added during this run.
152    pub async fn cleanup(&mut self) {
153        // Remove proxy NDP entries
154        let entries: Vec<Ipv6Addr> = self.proxy_entries.clone();
155        for addr in entries {
156            let _ = self.remove_proxy_ndp(addr).await;
157        }
158
159        // Remove pool route
160        if self.route_added {
161            let output = tokio::process::Command::new("ip")
162                .args(["-6", "route", "del", "local", &self.pool_cidr, "dev", "lo"])
163                .output()
164                .await;
165
166            match output {
167                Ok(o) if o.status.success() => {
168                    info!(cidr = %self.pool_cidr, "Removed pool route");
169                }
170                Ok(o) => {
171                    let stderr = String::from_utf8_lossy(&o.stderr);
172                    warn!(error = %stderr.trim(), "Failed to remove pool route");
173                }
174                Err(e) => {
175                    warn!(error = %e, "Failed to run ip route del");
176                }
177            }
178            self.route_added = false;
179        }
180    }
181}