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    /// Optional LAN source CIDR. When set, only traffic from this subnet is
158    /// MASQUERADEd (instead of all sources with `0.0.0.0/0`), and the
159    /// PREROUTING DNAT rule is skipped. Used for `preserve_src_ip` hairpin.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub lan_cidr: Option<String>,
162}
163
164impl HairpinConfig {
165    /// iptables comment used to identify this hairpin rule's rules.
166    pub fn rule_comment(&self) -> String {
167        format!(
168            "natmap:hairpin:{}:{}:{}",
169            self.ext_ip, self.int_ip, self.ports
170        )
171    }
172}
173
174// --- API request types ---
175
176/// JSON body for creating or deleting a DNAT rule.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct DnatRequest {
179    pub ext_ip: String,
180    pub int_ip: String,
181    pub ports: String,
182    pub proto: TransportProtocol,
183    pub ext_if: Option<String>,
184    #[serde(default)]
185    pub no_masquerade: bool,
186}
187
188/// JSON body for creating or deleting an SNAT rule.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SnatRequest {
191    pub int_ip: String,
192    pub ext_ip: String,
193    pub ext_if: String,
194}
195
196/// JSON body for creating or deleting a hairpin rule.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct HairpinRequest {
199    pub ext_ip: String,
200    pub int_ip: String,
201    pub ports: String,
202    pub proto: TransportProtocol,
203    #[serde(default)]
204    pub lan_cidr: Option<String>,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct PolicyRouteConfig {
209    pub src_ip: String,
210    pub via: String,
211    pub table: u32,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct PolicyRouteRequest {
216    pub src_ip: String,
217    pub via: String,
218    pub table: u32,
219}
220
221// --- Persisted daemon state ---
222
223/// The complete persisted state of the natmap daemon.
224#[derive(Debug, Clone, Serialize, Deserialize, Default)]
225pub struct DaemonState {
226    /// Docker container port mappings, keyed by container ID.
227    pub mapping: HashMap<String, Vec<DockerPortMap>>,
228    /// Static DNAT rule configurations.
229    pub dnats: Vec<DnatConfig>,
230    /// Static SNAT rule configurations.
231    pub snats: Vec<SnatConfig>,
232    /// Static hairpin rule configurations.
233    pub hairpins: Vec<HairpinConfig>,
234    /// Static policy routing configurations.
235    #[serde(default)]
236    pub policy_routes: Vec<PolicyRouteConfig>,
237}
238
239/// Response returned by the `GET /mappings` endpoint.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ListResponse {
242    pub docker: Vec<DockerPortMap>,
243    pub dnats: Vec<DnatConfig>,
244    pub snats: Vec<SnatConfig>,
245    pub hairpins: Vec<HairpinConfig>,
246    pub policy_routes: Vec<PolicyRouteConfig>,
247}