Skip to main content

unifly_api/model/
firewall.rs

1// ── Firewall domain types ──
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{DataSource, EntityOrigin};
6use super::entity_id::EntityId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
9pub enum FirewallAction {
10    Allow,
11    Block,
12    Reject,
13}
14
15impl<'de> Deserialize<'de> for FirewallAction {
16    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
17    where
18        D: serde::Deserializer<'de>,
19    {
20        let s = String::deserialize(deserializer)?;
21        match s.to_lowercase().as_str() {
22            "allow" => Ok(Self::Allow),
23            "block" => Ok(Self::Block),
24            "reject" => Ok(Self::Reject),
25            _ => Err(serde::de::Error::unknown_variant(
26                &s,
27                &["allow", "block", "reject"],
28            )),
29        }
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum IpVersion {
35    Ipv4,
36    Ipv6,
37    Both,
38}
39
40/// Firewall Zone -- container for networks, policies operate between zones.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FirewallZone {
43    pub id: EntityId,
44    pub name: String,
45    pub network_ids: Vec<EntityId>,
46    pub origin: Option<EntityOrigin>,
47
48    #[serde(skip)]
49    #[allow(dead_code)]
50    pub(crate) source: DataSource,
51}
52
53// ── Traffic filter types ─────────────────────────────────────────
54
55/// Source endpoint with zone and optional traffic filter.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PolicyEndpoint {
58    pub zone_id: Option<EntityId>,
59    pub filter: Option<TrafficFilter>,
60}
61
62/// Traffic filter applied to a source or destination.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "kind", rename_all = "snake_case")]
65pub enum TrafficFilter {
66    Network {
67        network_ids: Vec<EntityId>,
68        match_opposite: bool,
69        #[serde(default, skip_serializing_if = "Vec::is_empty")]
70        mac_addresses: Vec<String>,
71        #[serde(default, skip_serializing_if = "Option::is_none")]
72        ports: Option<PortSpec>,
73    },
74    IpAddress {
75        addresses: Vec<IpSpec>,
76        match_opposite: bool,
77        #[serde(default, skip_serializing_if = "Vec::is_empty")]
78        mac_addresses: Vec<String>,
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        ports: Option<PortSpec>,
81    },
82    MacAddress {
83        mac_addresses: Vec<String>,
84        #[serde(default, skip_serializing_if = "Option::is_none")]
85        ports: Option<PortSpec>,
86    },
87    Port {
88        ports: PortSpec,
89    },
90    Region {
91        regions: Vec<String>,
92        #[serde(default, skip_serializing_if = "Option::is_none")]
93        ports: Option<PortSpec>,
94    },
95    Application {
96        application_ids: Vec<i64>,
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        ports: Option<PortSpec>,
99    },
100    ApplicationCategory {
101        category_ids: Vec<i64>,
102        #[serde(default, skip_serializing_if = "Option::is_none")]
103        ports: Option<PortSpec>,
104    },
105    Domain {
106        domains: Vec<String>,
107        #[serde(default, skip_serializing_if = "Option::is_none")]
108        ports: Option<PortSpec>,
109    },
110    /// Catch-all for filter types not yet modeled.
111    Other {
112        raw_type: String,
113    },
114}
115
116/// Port specification.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(tag = "kind", rename_all = "snake_case")]
119pub enum PortSpec {
120    Values {
121        items: Vec<String>,
122        match_opposite: bool,
123    },
124    MatchingList {
125        list_id: EntityId,
126        match_opposite: bool,
127    },
128}
129
130/// IP address specification.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(tag = "kind", rename_all = "snake_case")]
133pub enum IpSpec {
134    Address { value: String },
135    Range { start: String, stop: String },
136    Subnet { value: String },
137    MatchingList { list_id: EntityId },
138}
139
140impl TrafficFilter {
141    /// Human-readable summary for table display.
142    pub fn summary(&self) -> String {
143        match self {
144            Self::Network {
145                network_ids,
146                match_opposite,
147                ..
148            } => {
149                let prefix = if *match_opposite { "NOT " } else { "" };
150                format!("{prefix}net({} networks)", network_ids.len())
151            }
152            Self::IpAddress {
153                addresses,
154                match_opposite,
155                ..
156            } => {
157                let prefix = if *match_opposite { "NOT " } else { "" };
158                let items: Vec<String> = addresses
159                    .iter()
160                    .map(|a| match a {
161                        IpSpec::Address { value } | IpSpec::Subnet { value } => value.clone(),
162                        IpSpec::Range { start, stop } => format!("{start}-{stop}"),
163                        IpSpec::MatchingList { list_id } => format!("list:{list_id}"),
164                    })
165                    .collect();
166                let display = if items.len() <= 2 {
167                    items.join(", ")
168                } else {
169                    format!("{}, {} +{} more", items[0], items[1], items.len() - 2)
170                };
171                format!("{prefix}ip({display})")
172            }
173            Self::MacAddress { mac_addresses, .. } => {
174                format!("mac({})", mac_addresses.len())
175            }
176            Self::Port { ports } => summarize_ports(ports),
177            Self::Region { regions, .. } => format!("region({})", regions.join(",")),
178            Self::Application {
179                application_ids, ..
180            } => {
181                format!("app({} apps)", application_ids.len())
182            }
183            Self::ApplicationCategory { category_ids, .. } => {
184                format!("cat({} categories)", category_ids.len())
185            }
186            Self::Domain { domains, .. } => {
187                if domains.len() <= 2 {
188                    format!("domain({})", domains.join(", "))
189                } else {
190                    format!("domain({} +{} more)", domains[0], domains.len() - 1)
191                }
192            }
193            Self::Other { raw_type } => format!("({raw_type})"),
194        }
195    }
196}
197
198fn summarize_ports(spec: &PortSpec) -> String {
199    match spec {
200        PortSpec::Values {
201            items,
202            match_opposite,
203        } => {
204            let prefix = if *match_opposite { "NOT " } else { "" };
205            format!("{prefix}port({})", items.join(","))
206        }
207        PortSpec::MatchingList {
208            list_id,
209            match_opposite,
210        } => {
211            let prefix = if *match_opposite { "NOT " } else { "" };
212            format!("{prefix}port(list:{list_id})")
213        }
214    }
215}
216
217/// Firewall Policy -- a rule between two zones.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct FirewallPolicy {
220    pub id: EntityId,
221    pub name: String,
222    pub description: Option<String>,
223    pub enabled: bool,
224    pub index: Option<i32>,
225
226    pub action: FirewallAction,
227    pub ip_version: IpVersion,
228
229    // Structured source/destination with traffic filters
230    pub source: PolicyEndpoint,
231    pub destination: PolicyEndpoint,
232
233    // Human-readable summaries (computed from filters)
234    pub source_summary: Option<String>,
235    pub destination_summary: Option<String>,
236
237    // Protocol and schedule display fields
238    pub protocol_summary: Option<String>,
239    pub schedule: Option<String>,
240    pub ipsec_mode: Option<String>,
241
242    pub connection_states: Vec<String>,
243    pub logging_enabled: bool,
244
245    pub origin: Option<EntityOrigin>,
246
247    #[serde(skip)]
248    #[allow(dead_code)]
249    pub(crate) data_source: DataSource,
250}
251
252// ── NAT Policy types ────────────────────────────────────────────────
253
254/// NAT policy type.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub enum NatType {
257    Masquerade,
258    Source,
259    Destination,
260}
261
262/// NAT Policy -- masquerade, source NAT, or destination NAT rule.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct NatPolicy {
265    pub id: EntityId,
266    pub name: String,
267    pub description: Option<String>,
268    pub enabled: bool,
269    pub nat_type: NatType,
270    pub interface_id: Option<EntityId>,
271    pub protocol: Option<String>,
272    pub src_address: Option<String>,
273    pub src_port: Option<String>,
274    pub dst_address: Option<String>,
275    pub dst_port: Option<String>,
276    pub translated_address: Option<String>,
277    pub translated_port: Option<String>,
278    pub origin: Option<EntityOrigin>,
279
280    #[serde(skip)]
281    #[allow(dead_code)]
282    pub(crate) data_source: DataSource,
283}
284
285/// ACL Rule action.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287pub enum AclAction {
288    Allow,
289    Block,
290}
291
292/// ACL Rule type.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum AclRuleType {
295    Ipv4,
296    Mac,
297}
298
299/// ACL Rule.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct AclRule {
302    pub id: EntityId,
303    pub name: String,
304    pub enabled: bool,
305    pub rule_type: AclRuleType,
306    pub action: AclAction,
307    pub source_summary: Option<String>,
308    pub destination_summary: Option<String>,
309    pub origin: Option<EntityOrigin>,
310
311    #[serde(skip)]
312    #[allow(dead_code)]
313    pub(crate) source: DataSource,
314}