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}
118
119impl DnatConfig {
120    /// iptables comment used to identify this DNAT rule's rules.
121    pub fn rule_comment(&self) -> String {
122        format!("natmap:dnat:{}:{}", self.ext_ip, self.ports)
123    }
124}
125
126/// A static SNAT (source NAT) rule configuration.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct SnatConfig {
129    /// Internal source IP address.
130    pub int_ip: String,
131    /// External (masquerade) IP address.
132    pub ext_ip: String,
133    /// External network interface.
134    pub ext_if: String,
135}
136
137impl SnatConfig {
138    /// iptables comment used to identify this SNAT rule's rules.
139    pub fn rule_comment(&self) -> String {
140        format!("natmap:snat:{}:{}", self.int_ip, self.ext_ip)
141    }
142}
143
144/// A static hairpin NAT rule configuration.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct HairpinConfig {
147    /// External IP address.
148    pub ext_ip: String,
149    /// Internal IP address.
150    pub int_ip: String,
151    /// Comma-separated list of ports.
152    pub ports: String,
153    /// Transport protocol.
154    pub proto: TransportProtocol,
155}
156
157impl HairpinConfig {
158    /// iptables comment used to identify this hairpin rule's rules.
159    pub fn rule_comment(&self) -> String {
160        format!(
161            "natmap:hairpin:{}:{}:{}",
162            self.ext_ip, self.int_ip, self.ports
163        )
164    }
165}
166
167// --- API request types ---
168
169/// JSON body for creating or deleting a DNAT rule.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct DnatRequest {
172    pub ext_ip: String,
173    pub int_ip: String,
174    pub ports: String,
175    pub proto: TransportProtocol,
176    pub ext_if: Option<String>,
177}
178
179/// JSON body for creating or deleting an SNAT rule.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct SnatRequest {
182    pub int_ip: String,
183    pub ext_ip: String,
184    pub ext_if: String,
185}
186
187/// JSON body for creating or deleting a hairpin rule.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct HairpinRequest {
190    pub ext_ip: String,
191    pub int_ip: String,
192    pub ports: String,
193    pub proto: TransportProtocol,
194}
195
196// --- Persisted daemon state ---
197
198/// The complete persisted state of the natmap daemon.
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct DaemonState {
201    /// Docker container port mappings, keyed by container ID.
202    pub mapping: HashMap<String, Vec<DockerPortMap>>,
203    /// Static DNAT rule configurations.
204    pub dnats: Vec<DnatConfig>,
205    /// Static SNAT rule configurations.
206    pub snats: Vec<SnatConfig>,
207    /// Static hairpin rule configurations.
208    pub hairpins: Vec<HairpinConfig>,
209}
210
211/// Response returned by the `GET /mappings` endpoint.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ListResponse {
214    pub docker: Vec<DockerPortMap>,
215    pub dnats: Vec<DnatConfig>,
216    pub snats: Vec<SnatConfig>,
217    pub hairpins: Vec<HairpinConfig>,
218}