Skip to main content

lab_ops_natmap/
models.rs

1//! Data models for the natmap daemon and its API.
2//!
3//! Defines request/response types, persisted state structures, and shared
4//! enums used across the CLI, daemon, and iptables modules.
5
6use std::collections::HashMap;
7use std::net::SocketAddr;
8
9pub use lab_ops_lab_lib::TransportProtocol;
10use serde::Deserialize;
11use serde::Serialize;
12
13/// Describes the desired port mapping between a host and a container.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct DockerPortMapRequest {
16    pub host_addr: SocketAddr,
17    pub container_addr: SocketAddr,
18    pub proto: TransportProtocol,
19}
20
21impl DockerPortMapRequest {
22    /// Returns whether the host address is an IPv6 address.
23    pub fn is_ipv6(&self) -> bool {
24        self.host_addr.is_ipv6()
25    }
26}
27
28/// An active port mapping that has been installed in iptables.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct DockerPortMap {
31    /// Unique numeric ID assigned by the daemon.
32    pub id: u64,
33    /// The mapping request that was fulfilled.
34    pub request: DockerPortMapRequest,
35    /// Docker container ID.
36    pub container_id: String,
37    /// Docker container name.
38    pub container_name: String,
39    /// iptables comment used to identify this mapping's rules.
40    pub rule_comment: String,
41}
42
43impl DockerPortMap {
44    /// Creates a new [`DockerPortMap`] with a generated rule comment.
45    ///
46    /// The comment format is `natmap:<container_id>:<host_port>`.
47    pub fn new(
48        id: u64,
49        request: DockerPortMapRequest,
50        container_id: String,
51        container_name: String,
52    ) -> Self {
53        let rule_comment = format!("natmap:{}:{}", container_id, request.host_addr.port());
54        Self {
55            id,
56            request,
57            container_id,
58            container_name,
59            rule_comment,
60        }
61    }
62}
63
64/// Request to remap a host port for an existing container.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DockerRemapRequest {
67    pub host_port: u16,
68    pub new_host_port: u16,
69}
70
71/// Request to add a new port mapping.
72///
73/// For Docker containers, `container_id` in the URL path identifies the container
74/// and its IP is resolved via `docker inspect`. For local (non-Docker) services,
75/// set `target_ip` to skip Docker inspection entirely.
76#[derive(Debug, Clone, Serialize, Deserialize, Default)]
77pub struct DockerAddMapRequest {
78    /// Host IP to bind to (defaults to `0.0.0.0`).
79    #[serde(default = "default_host_ip")]
80    pub host_ip: String,
81    /// Port on the host.
82    pub host_port: u16,
83    /// Port on the target (container or local service).
84    pub container_port: u16,
85    /// Optional target IP override. When set, skips Docker inspect and uses
86    /// this IP directly — useful for local (non-Docker) services.
87    #[serde(default)]
88    pub target_ip: Option<String>,
89    /// Transport protocol (`tcp` or `udp`, defaults to `tcp`).
90    #[serde(default = "default_proto")]
91    pub proto: TransportProtocol,
92}
93
94fn default_host_ip() -> String {
95    "0.0.0.0".to_string()
96}
97
98fn default_proto() -> TransportProtocol {
99    TransportProtocol::default()
100}
101
102// --- Static NAT configs (persisted to state.json) ---
103
104/// A static DNAT (destination NAT) rule configuration.
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct DnatConfig {
107    /// External (public) IP address.
108    pub ext_ip: String,
109    /// Internal (private) destination IP address.
110    pub int_ip: String,
111    /// Comma-separated list of ports.
112    pub ports: String,
113    /// Transport protocol.
114    pub proto: TransportProtocol,
115    /// Optional external network interface.
116    pub ext_if: Option<String>,
117    #[serde(default)]
118    pub no_masquerade: bool,
119}
120
121impl DnatConfig {
122    /// iptables comment used to identify this DNAT rule's rules.
123    pub fn rule_comment(&self) -> String {
124        format!("natmap:dnat:{}:{}", self.ext_ip, self.ports)
125    }
126}
127
128/// A static SNAT (source NAT) rule configuration.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct SnatConfig {
131    /// Internal source IP address.
132    pub int_ip: String,
133    /// External (masquerade) IP address.
134    pub ext_ip: String,
135    /// External network interface.
136    pub ext_if: String,
137}
138
139impl SnatConfig {
140    /// iptables comment used to identify this SNAT rule's rules.
141    pub fn rule_comment(&self) -> String {
142        format!("natmap:snat:{}:{}", self.int_ip, self.ext_ip)
143    }
144}
145
146/// A static hairpin NAT rule configuration.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct HairpinConfig {
149    /// External IP address.
150    pub ext_ip: String,
151    /// Internal IP address.
152    pub int_ip: String,
153    /// Comma-separated list of ports.
154    pub ports: String,
155    /// Transport protocol.
156    pub proto: TransportProtocol,
157}
158
159impl HairpinConfig {
160    /// iptables comment used to identify this hairpin rule's rules.
161    pub fn rule_comment(&self) -> String {
162        format!(
163            "natmap:hairpin:{}:{}:{}",
164            self.ext_ip, self.int_ip, self.ports
165        )
166    }
167}
168
169// --- API request types ---
170
171/// JSON body for creating or deleting a DNAT rule.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct DnatRequest {
174    pub ext_ip: String,
175    pub int_ip: String,
176    pub ports: String,
177    pub proto: TransportProtocol,
178    pub ext_if: Option<String>,
179    #[serde(default)]
180    pub no_masquerade: bool,
181}
182
183/// JSON body for creating or deleting an SNAT rule.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct SnatRequest {
186    pub int_ip: String,
187    pub ext_ip: String,
188    pub ext_if: String,
189}
190
191/// JSON body for creating or deleting a hairpin rule.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct HairpinRequest {
194    pub ext_ip: String,
195    pub int_ip: String,
196    pub ports: String,
197    pub proto: TransportProtocol,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct PolicyRouteConfig {
202    pub src_ip: String,
203    pub via: String,
204    pub table: u32,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct PolicyRouteRequest {
209    pub src_ip: String,
210    pub via: String,
211    pub table: u32,
212}
213
214// --- Persisted daemon state ---
215
216/// The complete persisted state of the natmap daemon.
217#[derive(Debug, Clone, Serialize, Deserialize, Default)]
218pub struct DaemonState {
219    /// Docker container port mappings, keyed by container ID.
220    pub mapping: HashMap<String, Vec<DockerPortMap>>,
221    /// Static DNAT rule configurations.
222    pub dnats: Vec<DnatConfig>,
223    /// Static SNAT rule configurations.
224    pub snats: Vec<SnatConfig>,
225    /// Static hairpin rule configurations.
226    pub hairpins: Vec<HairpinConfig>,
227    /// Static policy routing configurations.
228    #[serde(default)]
229    pub policy_routes: Vec<PolicyRouteConfig>,
230}
231
232/// Response returned by the `GET /mappings` endpoint.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct ListResponse {
235    pub docker: Vec<DockerPortMap>,
236    pub dnats: Vec<DnatConfig>,
237    pub snats: Vec<SnatConfig>,
238    pub hairpins: Vec<HairpinConfig>,
239    pub policy_routes: Vec<PolicyRouteConfig>,
240}