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// ── Firewall Group types ───────────────────────────────────────────
286
287/// Type of firewall group.
288///
289/// `--from-file` payloads can use either kebab-case (`"port-group"`,
290/// matching the CLI `--type` flag and the controller wire format) or
291/// PascalCase (`"PortGroup"`, the Rust variant names).
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "kebab-case")]
294pub enum FirewallGroupType {
295    #[serde(alias = "PortGroup")]
296    PortGroup,
297    #[serde(alias = "AddressGroup")]
298    AddressGroup,
299    #[serde(alias = "Ipv6AddressGroup")]
300    Ipv6AddressGroup,
301}
302
303impl std::fmt::Display for FirewallGroupType {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        match self {
306            Self::PortGroup => write!(f, "port-group"),
307            Self::AddressGroup => write!(f, "address-group"),
308            Self::Ipv6AddressGroup => write!(f, "ipv6-address-group"),
309        }
310    }
311}
312
313/// Firewall Group -- port group, address group, or IPv6 address group
314/// managed via the legacy Session API `rest/firewallgroup`.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct FirewallGroup {
317    pub id: EntityId,
318    pub external_id: Option<String>,
319    pub name: String,
320    pub group_type: FirewallGroupType,
321    pub group_members: Vec<String>,
322
323    #[serde(skip)]
324    #[allow(dead_code)]
325    pub(crate) source: DataSource,
326}
327
328/// ACL Rule action.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330pub enum AclAction {
331    Allow,
332    Block,
333}
334
335/// ACL Rule type.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub enum AclRuleType {
338    Ipv4,
339    Mac,
340}
341
342/// ACL Rule.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct AclRule {
345    pub id: EntityId,
346    pub name: String,
347    pub enabled: bool,
348    pub rule_type: AclRuleType,
349    pub action: AclAction,
350    pub source_summary: Option<String>,
351    pub destination_summary: Option<String>,
352    pub origin: Option<EntityOrigin>,
353
354    #[serde(skip)]
355    #[allow(dead_code)]
356    pub(crate) source: DataSource,
357}